返回预定常量的函数,已经算得上是最简单的函数了。
本章围绕下列函数进行演示:
指令清单2.1 C/C++ 代码
int f()
{
return 123;
}
在开启优化功能之后,GCC编译器产生的汇编指令,如下所示。
指令清单2.2 Optimizing GCC/MSVC(汇编输出)
f:
mov eax, 123
ret
MSVC编译的程序和上述指令完全一致。
这个函数仅由两条指令构成:第一条指令把数值123存放在EAX寄存器里;根据函数调用约定[1],后面一条指令会把EAX的值当作返回值传递给调用者函数,而调用者函数(caller)会从EAX寄存器里取值,把它当作返回结果。
ARM模式是什么情况?
指令清单2.3 Optimizing Keil 6/2013 (ARM模式)
f PROC
MOV r0,#0x7b ; 123
BX lr
ENDP
ARM程序使用R0寄存器传递函数返回值,所以指令把数值123赋值给R0。
ARM程序使用LR寄存器(Link Register)存储函数结束之后的返回地址(RA/ Return Address)。x86程序使用“栈”结构存储上述返回地址。可见,BX LR指令的作用是跳转到返回地址,即返回到调用者函数,然后继续执行调用体caller的后续指令。
如您所见,x86和ARM指令集的MOV指令确实和对应单词“move”没有什么瓜葛。它的作用是复制(copy),而非移动(move)。
在MIPS指令里,寄存器有两种命名方式。一种是以数字命名($0~$31),另一种则是以伪名称(pseudoname)命名($V0~VA0,依此类推)。在GCC编译器生成的汇编指令中,寄存器都采用数字方式命名。
指令清单2.4 Optimizing GCC 4.4.5(汇编输出)
j $31
li $2,123 # 0x7b
然而IDA则会显示寄存器的伪名称。
指令清单2.5 Optimizing GCC 4.4.5(IDA)
jr $ra
li $v0, 0x7B
根据伪名称和寄存器数字编号的关系可知,存储函数返回值的寄存器都是$2(即$V0)。此处LI指令是英文词组“Load Immediate(加载立即数)”的缩写。
其中,J和JR指令都属于跳转指令,它们把执行流递交给调用者函数,跳转到$31即$RA寄存器中的地址。这个寄存器相当于的ARM平台的LR寄存器。
此外,为什么赋值指令LI和转移指令J/JR的位置反过来了?这属于RISC精简指令集的特性之一——分支(转移)指令延迟槽 (Branch delay slot)的现象。简单地说,不管分支(转移)发生与否,位于分支指令后面的一条指令(在延时槽里的指令),总是被先于分支指令提交。这是RISC精简指令集的一种特例,我们不必在此处深究。总之,转移指令后面的这条赋值指令实际上是在转移指令之前运行的。
习惯上,MIPS领域中的寄存器名称和指令名称都使用小写字母书写。但是为了在排版风格上与其他指令集架构的程序保持一致,本书采用大写字母进行排版。
[1] Calling Convention,又称为函数的调用协定、调用规范。