第二章
最简单的函数
最简单的函数可能只需要返回一个常数值。 这有个例子: 清单 2.1: C/C++ 代码
int f()
{
return 123;
};
让我们编译一下!
2.1 x86
下面是带优化的GCC和MSVC编译器在x86平台上的输出: 清单 2.2: 带优化的 GCC/MSVC (汇编输出)
f:
mov eax, 123
ret
这里只有两个函数:第一个把123放入EAX
寄存器里,EAX
通常被用作存放函数的返回值。第二个是RET
,RET
把控制权交给主调函数。
主调函数会从EAX
里取出返回值。
2.2 ARM
在ARM平台上会有一点点区别。
清单 2.3: 带优化的 Keil 6/2013 (ARM mode) ASM 输出
f PROC
MOV r0,#0x7b ; 123
BX lr
ENDP
ARM 用R0
来储存函数的返回值,所以123被复制进R0
.
在ARM构架里返回值的地址不是保存在局部堆栈里,而是放在链接寄存器里,所以BX LR
指令转跳到那个地址,这有效地把控制权转交给了主调函数。
值得注意的是,对于x86和ARM构架来说,MOV
是一个容易令人误解性的名称。数据事实上没有被移动,而是被复制了。
MIPS
在MIPS的世界里有两种寄存器命名的形式:用数字(从$0
到 $31
)或者用别名($V0
, $A0
, 等等)。
GCC汇编输出中会像下面列表中那样用数字表示寄存器:
清单 2.4: 带优化的 GCC 4.4.5 (汇编输出)
j $31
li $2,123 # 0x7b
而IDA会把它转换成别名: 清单 2.5: 带优化的 GCC 4.4.5 (IDA)
jr $ra
li $v0, 0x7B
$2
(或 $V0
)被用来储存函数的返回值。LI
代表“立即加载”,这也是MIPS里MOV
的一个的等价用法。
剩下的指令是转跳指令(J
或 JR
),它把控制权交给主调函数,并转跳到$2
(或 $V0
)寄存器里的地址。
这个寄存器类似于ARM
里的LR寄存器。
你可能想知道为什么加载指令(LI
)和转跳指令(J
或 JR
)的位置被交换了。这都是由于RISC中被称为“分支延迟槽”的特性。
对于这种现象发生的原因有个借口:这是一些MIPS构架编译器的一个怪癖。但这对我们的目的来说并不重要,我们只需记住在MIPS里是这样的:在转跳指令之后的指令会先比转跳指令本身先执行。
2.3.1 关于MIPS指令/寄存器命名的一点
在MIPS的世界里,寄存器和指令名习惯上使用小写。但为了一致性,我们坚持使用大写,并在这本书里,把这点当做一个其他编译器都遵守的约定。