第二章

最简单的函数

最简单的函数可能只需要返回一个常数值。 这有个例子: 清单 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的一个的等价用法。

剩下的指令是转跳指令(JJR),它把控制权交给主调函数,并转跳到$2 (或 $V0)寄存器里的地址。

这个寄存器类似于ARM里的LR寄存器。

你可能想知道为什么加载指令(LI)和转跳指令(JJR)的位置被交换了。这都是由于RISC中被称为“分支延迟槽”的特性。

对于这种现象发生的原因有个借口:这是一些MIPS构架编译器的一个怪癖。但这对我们的目的来说并不重要,我们只需记住在MIPS里是这样的:在转跳指令之后的指令会先比转跳指令本身先执行。

2.3.1 关于MIPS指令/寄存器命名的一点

在MIPS的世界里,寄存器和指令名习惯上使用小写。但为了一致性,我们坚持使用大写,并在这本书里,把这点当做一个其他编译器都遵守的约定。

results matching ""

    No results matching ""