如何理解这个似乎添加两个指针的基本汇编代码? [英] 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, $0 //$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]
,但这似乎不对.我做错了什么?
On the left are the instructions given and comments on the right are me struggling to understand.
Final answer I got is f = &A[0] + &A[0]
, but that doesn't seem right. What am I getting wrong?
推荐答案
你没疯,代码真的有那么奇怪!
添加两个指针基本上没有意义,所以这是一个棘手的问题.
等效的 C 看起来确实是错误的/疯狂的:
Adding two pointers basically never makes sense, so this is kind of a trick question.
The equivalent C does look wrong / insane:
intptr_t *A = ...; // in $s6
A[1] = (intptr_t)&A[0];
f = A[1] + (intptr_t)&A[0];
请注意,有符号溢出在 C 中是未定义的行为,因此将其编译为 MIPS add
是合法的,它会捕获有符号溢出.如果我们使用 uintptr_t
,所需的溢出语义将是包装/截断,而 add
没有实现.
Note that signed overflow is undefined behaviour in C, so it's legal to compile it to a MIPS add
which will trap on signed overflow. If we'd used uintptr_t
, the required overflow semantics would be wrapping / truncation, which add
doesn't implement.
(MIPS 的真实世界 C 编译器总是使用 addu
/addiu
,而不是 add
,即使对于有符号整数,因为未定义的行为意味着任何东西都是允许的,包括包装.如果你用 gcc -fwrapv
编译,它甚至是必需的.由于 MIPS 是 2 的补码机,addu
与 是相同的二进制操作添加
,它的不同之处仅在于不捕获有符号溢出:当输入符号相同但输出符号不同时.)
(Real-world C compilers for MIPS always use addu
/ addiu
, not add
, even for signed int, because undefined behaviour means anything is allowed, including wrapping. It's even required if you compile with gcc -fwrapv
. Since MIPS is a 2's complement machine, addu
is the same binary operation as add
, it differs only in not trapping on signed overflow: when the inputs are the same sign but the output has a different sign from that.)
就 C 而言,它将编译回更接近给定 asm 的东西,或者至少用 C 临时变量表示每个 asm 操作:
In terms of C that will compile back to something closer to the given asm, or at least represent every asm operations with a C temporary var:
我使用了 GNU C register-globalvariables 而不是函数 args,因此函数体将使用实际正确的寄存器(并且不会用额外的指令来使 asm 混乱以保存/恢复和初始化这些寄存器).所以这让我让 GCC 制作一个具有 s
寄存器作为输入和输出的 asm 块,而不是正常的调用约定.
I used GNU C register-global variables instead of function args so the function body would be using the actual correct register (and without cluttering the asm with extra instructions to save/restore and init those registers). So this lets me get GCC to make a block of asm that has s
registers as inputs and outputs, instead of the normal calling convention.
#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
在这里用于 2 种不同的事物,具有不同的类型:一种是指向数组的指针,另一种是数组中的值.我用两个不同的 C 变量表达了这一点,因为事情通常是这样进行的.(当一个变量在/之前死"时,编译器将为不同的变量重用相同的寄存器,因为需要另一个变量.)
Note that $t0
gets used for 2 different things here, with different types: one being a pointer into the array, and the other a value from the array. I expressed this with two different C variables, because that's how things normally go. (Compilers will reuse the same register for a different variable when one is "dead" before / as the other one is needed.)
GCC5.4 for MIPS 的结果汇编,带有 使 MARS 兼容的 asm 的选项:-O2 -march=mips3 -fno-delayed-branch
.MIPS3 意味着没有加载延迟槽,就像问题中的代码在加载后的指令中使用 lw
结果一样.(Godbolt 编译器浏览器)
The resulting asm from GCC5.4 for MIPS, with 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 $2,$22 # $v0, $s6 pointless copy into $v0
sw $22,4($2) # A[1] = A
lw $3,4($22) # v1 = A[1]
addu $16,$22,$3 # $s6 = (intptr_t)A + A[1]
j $31
nop # branch-delay slot
(GCC 使用数字寄存器名称,而不是像 $s?
用于调用保留的 ABI 名称,$t?
用于调用破坏的暂存寄存器等.http://www.cs.uwm.edu/classes/cs315/Bacon/Lecture/HTML/ch05s03.html 有一张桌子.)
(GCC uses numeric register names, not the ABI names like $s?
for call-preserved, $t?
for call-clobbered scratch regs, etc. http://www.cs.uwm.edu/classes/cs315/Bacon/Lecture/HTML/ch05s03.html has a table.)
另一种不那么严谨的编写方式:重要的区别是缺少 volatile
来强制编译器重新加载.
Another way to write it, with less rigour: the important difference is the lack of volatile
to force the compiler to reload.
void bar() {
A[1] = &A[0];
f = A[1] + (intptr_t)&A[0];
}
bar:
move $2,$22 # still a useless copy
sw $22,4($2)
sll $16,$22,1 # 2 * (intptr_t)A; no reload, just CSE the store value.
j $31
nop
当然还有其他表达方式,例如使用 A
作为指针数组而不是 intptr_t
、int
或 int32_t
数组.
Of course there would be other ways to express this, e.g. using A
as an array of pointers instead of an array of intptr_t
, int
, or int32_t
.
我选择整数是因为当你做指针加法时,C 指针类型会神奇地按类型宽度进行缩放.
I chose integers because C pointer types magically scale by the type width when you do pointer addition.
这篇关于如何理解这个似乎添加两个指针的基本汇编代码?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!