这段看似加了两个指针的基本汇编代码怎么理解呢?

How to understand this basic Assembly Code that seems to be adding two pointers?

根据下列MIPS指令构造一个C语言语句。
(var f -> $s0, 数组A和B的起始地址 -> $s6, $s7)


addi $t0, $s6, 4        //$t0 = &A[1]
add  $t1, $s6, [=10=]       //$t1 = &A[0]
sw   $t1, 0($t0)        //A[1] = &A[0]
lw   $t0, 0($t0)        //$t0 = &A[0]
add  $s0, $t1, $t0      //f = &A[0] + &A[0]

左边是说明,右边是我看不懂的评论
我得到的最终答案是 f = &A[0] + &A[0],但这似乎不对。我哪里错了?

你没疯,代码真的很奇怪!

添加两个指针基本上没有意义,所以这是一个技巧问题。
等效的 C 确实看起来不对/疯狂:

intptr_t *A = ...;  // in $s6

 A[1] = (intptr_t)&A[0];
 f = A[1] + (intptr_t)&A[0];

请注意,有符号溢出在 C 中是未定义的行为,因此将其编译为 MIPS add 是合法的,这将捕获有符号溢出。如果我们使用 uintptr_t,所需的溢出语义将是包装/截断,add 没有实现。

(MIPS 的真实世界 C 编译器总是使用 addu / addiu,而不是 add,即使是有符号的 int,因为未定义的行为意味着任何事情都是允许的,包括包装。如果您使用 gcc -fwrapv 进行编译,它甚至是必需的。由于 MIPS 是一个 2 的补码机器,adduadd 是相同的二进制操作,它的不同之处仅在于不捕获有符号溢出:当输入符号相同,但输出符号不同。)


就 C 而言,它将编译回更接近给定 asm 的东西,或者至少用 C 临时变量表​​示每个 asm 操作:

我使用 GNU C register-global variables 而不是函数参数,因此函数主体将使用实际正确的寄存器(并且不会用 save/restore 和初始化这些寄存器的额外指令使 asm 混乱)。所以这让我让 GCC 制作一个 asm 块,其中 s 寄存器作为输入和输出,而不是正常的调用约定。

#include <stdint.h>

register intptr_t *A  asm("s6");
// register char  *B  asm("s7");    // unused, no idea what type makes sense
register intptr_t f asm("s0");

void foo()
{
  volatile intptr_t *t0_ptr = A+1;  // volatile forces store and reload
  intptr_t t1 = (intptr_t)A;

  *t0_ptr = t1;                  //sw   $t1, 0($t0)       //A[1] = &A[0]
  intptr_t t0_int = *t0_ptr;     //lw   $t0, 0($t0)       //$t0 = &A[0]
  f = t0_int + t1;               //add  $s0, $t1, $t0     //f = &A[0] + &A[0]
  //return f;
}

请注意,$t0 在这里用于两种不同的事物,具有不同的类型:一种是指向数组的指针,另一种是数组中的值。我用两个不同的 C 变量表达了这一点,因为事情通常是这样的。 (当一个寄存器在另一个变量之前/之前“死”时,编译器将为不同的变量重用相同的寄存器。)

MIPS 的 GCC5.4 生成的汇编,options to make MARS-compatible asm: -O2 -march=mips3 -fno-delayed-branch. MIPS3 means no load delay slots, like the code in the question which uses the lw result in the instruction after the load. (Godbolt compiler explorer)

foo:
        move    ,         # $v0, $s6   pointless copy into $v0
        sw      ,4()      # A[1] = A
        lw      ,4()      # v1 = A[1]
        addu    ,,     # $s6 = (intptr_t)A + A[1]
        j       
        nop                                  # branch-delay slot

(GCC 使用数字寄存器名称,而不是 ABI 名称,例如 $s? 用于调用保留,$t? 用于调用破坏的临时寄存器等。http://www.cs.uwm.edu/classes/cs315/Bacon/Lecture/HTML/ch05s03.html 有一个 table.)

另一种写法,不太严谨:重要的区别是缺少 volatile 来强制编译器重新加载。

void bar() {
  A[1] = &A[0];
  f = A[1] + (intptr_t)&A[0];
}
bar:
        move    ,          # still a useless copy
        sw      ,4()
        sll     ,,1       # 2 * (intptr_t)A;   no reload, just CSE the store value.
        j       
        nop

当然还有其他表达方式,例如使用 A 作为指针数组,而不是 intptr_tintint32_t.

的数组

我选择整数是因为 C 指针类型在您进行指针加法时神奇地按类型宽度缩放。