第2章 最简函数

返回预定常量的函数,已经算得上是最简单的函数了。

本章围绕下列函数进行演示:

指令清单2.1 C/C++ 代码

int f() 
{
         return 123;
}

2.1 x86

在开启优化功能之后,GCC编译器产生的汇编指令,如下所示。

指令清单2.2 Optimizing GCC/MSVC(汇编输出)

f:
        mov   eax, 123
        ret

MSVC编译的程序和上述指令完全一致。

这个函数仅由两条指令构成:第一条指令把数值123存放在EAX寄存器里;根据函数调用约定[1],后面一条指令会把EAX的值当作返回值传递给调用者函数,而调用者函数(caller)会从EAX寄存器里取值,把它当作返回结果。

2.2 ARM

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)。

2.3 MIPS

在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指令集与寄存器名称

习惯上,MIPS领域中的寄存器名称和指令名称都使用小写字母书写。但是为了在排版风格上与其他指令集架构的程序保持一致,本书采用大写字母进行排版。


[1] Calling Convention,又称为函数的调用协定、调用规范。