第6章 printf()函数与参数传递

现在我们对Hello, world! 程序稍做修改,演示它的参数传递的过程。

#include <stdio.h>

int main() 
{
printf("a=%d; b=%d; c=%d", 1, 2, 3);
return 0; 
};

6.1 x86

6.1.1 x86:传递3个参数

MSVC

使用MSVC 2010 express编译上述程序,可得到下列汇编指令:

$SG3830 DB    'a=%d; b=%d; c=%d’, 00H
...
     push  3
     push  2
     push  1
     push  OFFSET $SG3830
     call  _printf
     add   esp, 16  ; 00000010H

这与最初的Hello World程序相差不多。我们看到printf()函数的参数以逆序存入栈里,第一个参数在最后入栈。

在32位环境下,32位地址指针和int类型数据都占据32 位/4字节空间。所以,我们这里的四个参数总共占用4×4=16(字节)的存储空间。

在调用函数之后,“ADD ESP, X”指令修正ESP寄存器中的栈指针。通常情况下,我们可以通过call之后的这条指令判断参数的数量:变量总数=X÷4。

这种判断方法仅适用于调用约定为cdecl的程序。本书将在第64章详细介绍各种函数约定[1]

如果某个程序连续地调用多个函数,且调用函数的指令之间不夹杂其他指令,那么编译器可能把释放参数存储空间的“ADD ESP,X”指令进行合并,一次性地释放所有空间。例如:

push a1
push a2
call ...
...
push a1
call ...
... 
push a1
push a2
push a3
call ...
add esp, 24

如下是一个规定中的例子。

指令清单6.1 x86

.text:100113E7 push 3
.text:100113E9 call sub_100018B0 ; takes one argument (3)
.text:100113EE call sub_100019D0 ; takes no arguments at all
.text:100113F3 call sub_10006A90 ; takes no arguments at all
.text:100113F8 push 1
.text:100113FA call sub_100018B0 ; takes one argument (1)
.text:100113FF add esp, 8     ; drops two arguments from stack at once

使用OllyDbg调试MSVC编译出的程序

现在我们在OllyDbg中加载这个范例。OllyDbg是非常受欢迎的user-land(用户空间)win32 debugger。在使用MSVC 2012编译这个样本程序的时候,启用/MD选项,可使可执行程序与MSVCR*.DLL建立动态链接。这样,我们就能在debugger里清楚地观察到程序从标准库里调用函数的过程。

在OllyDbg里调用可执行文件,将第一个断点设为ntdll.dll,然后按F9键执行。然后把第二个断点设置在CRT-code里。我们应该能够找到main()主函数。

因为MSVC将main()函数分配到代码段的开始处,所以只要向下滚屏就能够在底部找到main()函数体。如图6.1所示。

..\TU\0601.tif{}

图6.1 使用OllyDbg:查看main()函数的启动部分

单击PUSH EBP指令,按F2设置断点,再按F9键运行。这么做是为了跳过CRT-code,因为我们的目的不是分析CRT代码。

而后按6次F8键,就是说跳过6条指令。如图6.2所示。

..\TU\0602.tif{}

图6.2 使用OllyDbg:定位到调用printf()之前的指令

此时,指令指针PC指向CALL printf指令。与其他主流的调试器相同,OllyDbg调试器会加亮显示所有发生过变化的寄存器。每按一次F8键,EIP的值都会发生变化,所以这个寄存器一直被高亮显示。在这个例子中,ESP同样会被OllyDbg高亮显示,因为在这几步调试中ESP寄存器的值也发生了变化。

栈里的数值在哪呢?我们看一下debugger的右下角,如图6.3所示。

..\TU\0603.tif

图6.3 使用OllyDbg:观察PUSH指令造成的栈值变化(红色方块内)

此处的内容分为3列:栈地址列、数值列及OllyDbg的注释列。OllyDbg能够识别printf()这样的指令字符串,按照其指令的形式把它所引用的三个值进行了整理。

我们还可以使用鼠标的右键单击printf()的格式化字符串、单击下拉菜单中的“Follow in dump”,这样屏幕左下方将会显示出格式化字符串的输出结果。调试器左下方的这个区域用于显示内存中的部分数据。我们同样可以编辑这些值。如果修改格式化字符串,那么程序的输出结果就会发生变化。在这个例子中,我们还用不上这样的功能,但是我们可以练练手,从而进一步熟悉调试器。

按下F8键,单步执行(Step Over)。

我们将会看到图6.4所示的输出结果。

..\TU\0604.tif

图6.4 printf() 函数的输出结果

寄存器和栈状态的变化过程如图6.5所示。

..\TU\0605.tif{}

图6.5 使用OllyDbg:观察printf()运行后的数据栈

现在,EAX寄存器的值是0xD(即13)。这个值是printf()函数所打印的字符总数,所以这个值没有问题。EIP寄存器的值发生了变化,实际上它是在执行CALL printf指令后的PC。与此同时,ECX和EDX寄存器的值也发生了变化;显然printf()函数在运行过程中会使用这两个寄存器。

这里最重要的现象是ESP的值和栈里的参数都没有发生变化。我们可以观察到数据栈里的、传递给printf()函数的字符串和其他3个参数的值原封未动。这是cdecl调用约定的特征:被调用方函数不负责恢复ESP的状态;调用方函数(caller function)负责还原参数所用的栈空间。

继续按F8键,执行“ADD ESP, 10”指令,如图6.6所示。

..\TU\0606.tif{}

图6.6 使用OllyDbg:观察“ADD ESP,10”指令运行之后的状态

ESP寄存器的值有变化,但是栈中的数据还在那里。因为程序没有把原有栈的数据进行置零(也没有必要清除这些值),所以保留在栈指针SP之上的(原有地址)参数值就成为了噪音(noise)或者脏数据(garbage),失去了使用的价值。另外,清除全部噪音的操作十分耗时,程序员完全没有必要刻意地去这么做。

GCC

现在我们使用GCC 4.4.1 编译这个程序,并使用IDA打开可执行文件:

main   proc near

var_10   = dword ptr -10h
var_C    = dword ptr -0Ch
var_8    = dword ptr -8
var_4    = dword ptr -4

 push   ebp
 mov    ebp, esp
 and    esp, 0FFFFFFF0h
 sub    esp, 10h
 mov    eax, offset aADBDCD ; "a=%d; b=%d; c=%d"
 mov    [esp+10h+var_4], 3
 mov    [esp+10h+var_8], 2
 mov    [esp+10h+var_C], 1
 mov    [esp+10h+var_10], eax
 call   _printf
 mov    eax, 0
 leave
 retn
main     endp

与MSVC生成的程序相比,GCC生成的程序仅在参数入栈的方式上有所区别。在这个例子中,GCC没有使用PUSH/POP指令,而是直接对栈进行了操作。

GCC和GDB

GDB就是GNU debugger。

在Linux下,我们通过下述指令编译示例程序。其中,“−g”选项表示在可执行文件中生成debug信息。

$ gcc 1.c –g –o 1

接下来对可执行文件进行调试。

$ gdb 1
GNU gdb (GDB) 7.6.1-ubuntu
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "i686-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/dennis/polygon/1...done.

指令清单6.2 在printf()函数开始之前设置调试断点

(gdb) b printf
Breakpoint 1 at 0x80482f0

然后运行程序。我们的程序里没有printf() 的源代码,所以GDB也无法显示相应源代码。但是,这并不妨碍我们调试程序。

(gdb) run
Starting program: /home/dennis/polygon/1

Breakpoint 1, __printf (format=0x80484f0 "a=%d; b=%d; c=%d") at printf.c:29
29   printf.c: No such file or directory.

继而令GDB显示栈里的10个数据,其中左边第一列是栈地址。

(gdb) x/10w $esp
0xbffff11c: 0x0804844a 0x080484f0 0x00000001 0x00000002
0xbffff12c: 0x00000003 0x08048460 0x00000000 0x00000000
0xbffff13c: 0xb7e29905 0x00000001

第一个元素就是返回地址RA(0x0804844a)。为了验证这点,我们让GDB显示出这个地址开始的指令:

(gdb) x/5i 0x0804844a
  0x804844a <main+45>: mov  $0x0,%eax
  0x804844f <main+50>: leave
  0x8048450 <main+51>: ret
  0x8048451:  xchg  %ax,%ax
  0x8048453:  xchg  %ax,%ax

ret之后的XCHG指令与NOP指令等效。x86平台并没有专用的NOP指令,实际上,多数采用RISC的CPU的指令集里也没有专用的NOP指令。

栈中的第二个数据(0x080484f0)是字符串格式的内存地址(指针):

(gdb) x/s 0x080484f0
0x80484f0:   "a=%d; b=%d; c=%d"

紧接其后的三个数据,是传递给printf()函数的余下的三个参数。栈中其余的数据,可能是脏数据,也可能是其他函数的数据或局部变量等数据。没有必要进行深究。

此后,我们执行“finish”指令,令调试器“继续执行余下的指令、直到函数结束为止”。在我们的例子里,这条指令将引导GDB执行printf()函数,到它退出为止。

(gdb) finish
Run till exit from #0 __printf (format=0x80484f0 "a=%d; b=%d; c=%d") at printf.c:29
main () at 1.c:6
6    return 0;
Value returned is $2 = 13

GDB 显示:在printf()退出时,EAX寄存器的值为13。这个值是函数打印的字符的总数,其结果与OllyDbg的结果相同。

GDB显示出了程序的源代码“return 0;”,它是1.c文件里第六行的指令。实际上1.c文件就在当前目录里,GDB在源文件里找到了汇编指令对应的源代码。GDB又是如何知道当前的指令对应源代码的哪一行呢?这就要归功于编译器所生成的调试信息了。如果启用了保存调试信息的选项,那么编译器在编译程序的时候,会生成源代码的行号与对应的指令地址之间的对应关系表,把它一并保存在可执行文件里。

我们一起检查下EAX寄存器里到底是不是13:

(gdb) info registers
eax  0xd      13
ecx  0x0      0
edx  0x0      0
ebx  0xb7fc0000    -1208221696
esp  0xbffff120    0xbffff120
ebp  0xbffff138    0xbffff138
esi  0x0      0
edi  0x0      0
eip  0x804844a     0x804844a <main+45>
...

使用下述指令反汇编当前的指令。图中箭头指向的是接下来将要运行的指令。

(gdb) disas
Dump of assembler code for function main:
  0x0804841d <+0>:     push  %ebp
  0x0804841e <+1>:     mov   %esp,%ebp
  0x08048420 <+3>:     and   $0xfffffff0,%esp
  0x08048423 <+6>:     sub   $0x10,%esp
  0x08048426 <+9>:     movl  $0x3,0xc(%esp)
  0x0804842e <+17>:    movl  $0x2,0x8(%esp)
  0x08048436 <+25>:    movl  $0x1,0x4(%esp)
  0x0804843e <+33>:    movl  $0x80484f0,(%esp)
  0x08048445 <+40>:    call  0x80482f0 <printf@plt>
=> 0x0804844a <+45>:    mov   $0x0,%eax
  0x0804844f <+50>:    leave
  0x08048450 <+51>:    ret
End of assembler dump.

默认情况下,GDB会以AT&T语体显示汇编指令。我们可以通过下述指令,令GDB采用Intel语体:

(gdb) set disassembly-flavor intel
(gdb) disas
Dump of assembler code for function main:
  0x0804841d <+0>:   push  ebp
  0x0804841e <+1>:   mov   ebp, esp
  0x08048420 <+3>:   and   esp , 0xfffffff0
  0x08048423 <+6>:   sub   esp, 0x10
  0x08048426 <+9>:   mov   DWORD PTR [esp+0xc],0x3
  0x0804842e <+17>:   mov   DWORD PTR [esp+0x8],0x2
  0x08048436 <+25>:   mov   DWORD PTR[esp+0x4],0x1
  0x0804843e <+33>:   mov   DWORD PTR[esp], 0x80484f0
  0x08048445 <+40>:   call  0x80482f0 <printf@plt>
=> 0x0804844a <+45>:   mov   eax, 0x0
  0x0804844f <+50>:   leave
  0x08048450 <+51>:   ret
End of assembler dump.

单步调试。此处,GDB用大括号表示这里是函数的结束部分。

(gdb) step
7    };

在执行过“MOV EAX, 0”指令之后,EAX会被置零。我们对此进行验证:

(gdb) info registers

eax  0x0    0
ecx  0x0    0
edx  0x0    0
ebx  0xb7fc0000   -1208221696
esp  0xbffff120   0xbffff120
ebp  0xbffff138   0xbffff138
esi  0x0    0
edi  0x0    0
eip  0x804844f    0x804844f <main+50>
...
6.1.2 x64:传递9个参数

为了便于演示,我们再次修改代码,将参数的数量增加到9个。其中8个参数是int整型变量,另有1个用于控制输出格式的格式化字符串。

#include <stdio.h>
int main() 
{
printf("a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d\n", 1, 2, 3, 4, 5, 6, 7, 8);
return 0;
};

MSVC

前文介绍过,Win64使用RCX、RDX、R8、R9寄存器传递前4个参数,使用栈来传递其余的参数。我们将在本例中观察到这个现象。在下面的例子里,编译器使用了MOV指令对栈地址进行直接操作而没有使用PUSH指令。

指令清单6.3 MSVC 2012 x64

$SG2923 DB   'a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d’, 0aH, 00H

main   PROC
sub   rsp, 88

mov   DWORD PTR [rsp+64], 8
mov   DWORD PTR [rsp+56], 7
mov   DWORD PTR [rsp+48], 6
mov   DWORD PTR [rsp+40], 5
mov   DWORD PTR [rsp+32], 4
mov   r9d, 3
mov   r8d, 2
mov   edx, 1
lea   rcx, OFFSET FLAT:$SG2923
call   printf

; return 0
xor   eax, eax

add   rsp, 88
ret   0
main    ENDP
_TEXT   ENDS
END

在64位系统中,整型数据只占用4字节空间。那么,为什么编译器给整型数据分配了8个字节?其实,即使数据的存储空间不足64位,编译器还是会给它分配8字节的存储空间。这不仅是为了方便系统对每个参数进行内存寻址,而且编译器都会进行地址对齐。所以,64位系统为所有类型的数据都保留8字节空间。同理,32位系统也为所有类型的数据都保留4字节空间。

GCC

x86-64的*NIX系统采用与Win64类似的方法传递参数。它优先使用 RDI、RSI、RDX、RCX、R8、R9寄存器传递前六个参数,然后利用栈传递其余的参数。在生成汇编代码时,GCC把字符串指针存储到了EDI寄存器、而非完整的RDI寄存器。在前面的3.2.2节中,64位GCC生成的汇编代码里也出现过这个现象。

在3.2.2节的那个例子里,程序在调用printf()函数之前清空了EAX寄存器。本例中存在相同的操作。

指令清单6.4 Optimizing GCC 4.4.6 x64

.LC0:
    .string "a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d\n"

main:
    sub    rsp, 40

    mov    r9d, 5
    mov    r8d, 4
    mov    ecx, 3
    mov    edx, 2
    mov    esi, 1
    mov    edi, OFFSET FLAT:.LC0
    xor    eax, eax ; number of vector registers passed 
    mov    DWORD PTR [rsp+16], 8
    mov    DWORD PTR [rsp+8], 7
    mov    DWORD PTR [rsp], 6 
    call   printf

    ; return 0

    xor  eax, eax 
    add  rsp, 40 
    ret

GCC + GDB

在使用GDB调试它之前,我们首先要编译源代码:

$ gcc -g 2.c -o 2
 
$gdb 2
GNU gdb (GDB)7.6.1‐ubuntu
Copyright(C)2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version3 or later<http://gnu.org/licenses/gpl.html>
This is free software : you are free to change and redistribute it.
There is NOWARRANTY, to the extent permitted by law. Type "show copying" 
and "show warranty" for details.
This GDB was configured as"x86_64‐linux‐gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/dennis/polygon/2...done.

接下来,我们在printf()运行之前设置断点,然后运行这个程序:

(gdb) b printf
Breakpoint 1 at 0x400410
(gdb) run
Starting program: /home/dennis/polygon/2

Breakpoint 1, __printf (format=0x400628 "a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d\n") at printf.c:29
29 printf.c: No such file or directory

RSI/RDX/RCX/R8/R9寄存器的值就是传递给函数的参数。RIP寄存器的值应当是printf()函数的首地址。我们可以在GDB里查看各寄存器的值:

(gdb) info registers
rax  0x0    0
rbx  0x0    0
rcx  0x3    3
rdx  0x2    2
rsi  0x1    1
rdi  0x400628 4195880
rbp  0x7fffffffdf60   0x7fffffffdf60
rsp  0x7fffffffdf38   0x7fffffffdf38
r8   0x4    4
r9   0x5    5
r10  0x7fffffffdce0   140737488346336
r11  0x7ffff7a65f60   140737348263776
r12  0x400440 4195392
r13  0x7fffffffe040   140737488347200
r14  0x0    0
r15  0x0    0
rip  0x7ffff7a65f60   0x7ffff7a65f60 <__printf>
...

指令清单6.5 检查格式化字符串

(gdb)x/s $rdi
0x400628:    "a=%d;b=%d;c=%d;d=%d;e=%d;f=%d;g=%d;h=%d\n"

然后使用x/g指令显示栈中的数值。指令中的g代表giant words,即以64位words型数据的格式显示各数据。

(gdb)x/10g $rsp
0x7fffffffdf38: 0x0000000000400576  0x0000000000000006
0x7fffffffdf48: 0x0000000000000007  0x00007fff00000008
0x7fffffffdf58: 0x0000000000000000  0x0000000000000000
0x7fffffffdf68: 0x00007ffff7a33de5  0x0000000000000000
0x7fffffffdf78: 0x00007fffffffe048  0x0000000100000000

64位系统的栈与32位系统的栈没有太大的区别。栈里的第一个值是返回地址RA。紧接其后的,是三个保存在栈里的参数6、7、8。应该注意到数值“8”的高32位地址位没有被清零,与之对应的存储空间的值为“0x00007fff00000008”。因为int类型只占用(低)32位,所以这种存储并不会产生问题。另外,我们可以认为这个地方的高32位数据是随机的脏数据。

此后,我们通过以下指令查看printf()函数运行之后的、返回地址开始的有关指令。GDB将会显示main()函数的全部指令。

(gdb) set disassembly-flavor intel
(gdb) disas 0x0000000000400576
Dump of assembler code for function main:
  0x000000000040052d <+0>:   push  rbp
  0x000000000040052e <+1>:   mov   rbp, rsp
  0x0000000000400531 <+4>:   sub   rsp, 0x20
  0x0000000000400535 <+8>:   mov   DWORD PTR [rsp+0x10], 0x8
  0x000000000040053d <+16>:   mov   DWORD PTR [rsp+0x8], 0x7
  0x0000000000400545 <+24>:   mov   DWORD PTR [rsp], 0x6
  0x000000000040054c <+31>:   mov   r9d, 0x5
  0x0000000000400552 <+37>:   mov   r8d, 0x4
  0x0000000000400558 <+43>:   mov   ecx, 0x3
  0x000000000040055d <+48>:   mov   edx, 0x2
  0x0000000000400562 <+53>:   mov   esi, 0x1
  0x0000000000400567 <+58>:   mov   edi, 0x400628
  0x000000000040056c <+63>:   mov   eax, 0x0
  0x0000000000400571 <+68>:   call  0x400410, <printf@plt>
  0x0000000000400576 <+73>:   mov   eax, 0x0
  0x000000000040057b <+78>:   leave
  0x000000000040057c <+79>:   ret
End of assembler dump.

接下来,通过“finish”指令运行printf()之后的程序。余下的指令会清空EAX寄存器,可以注意到那时EAX已经为零。现在,RIP指针指向LEAVE指令,就是main()函数的倒数第二条指令。

(gdb) finish
Run till exit from #0__printf (format=0x400628"a=%d;b=%d;c=%d;d=%d; e=%d;f=%d;g=%d;h=%d\n") at printf.c:29
a=1;b=2;c=3;d=4;e=5;f=6;g=7;h=8
main () at2.c:6
6    return0;
Value returned is $1=39
(gdb) next
7    };
(gdb) info registers
rax  0x0   0
rbx  0x0   0
rcx  0x26  38
rdx  0x7ffff7dd59f0  140737351866864
rsi  0x7fffffd9    2147483609
rdi  0x0           0
rbp  0x7fffffffdf60  0x7fffffffdf60
rsp  0x7fffffffdf40  0x7fffffffdf40
r8   0x7ffff7dd26a0  140737351853728
r9   0x7ffff7a60134  140737348239668
r10  0x7fffffffd5b0  140737488344496
r11  0x7ffff7a95900  140737348458752
r12  0x400440  4195392
r13  0x7fffffffe040  140737488347200
r14  0x0    0
r15  0x0    0
rip  0x40057b 0x40057b <main+78>
...

6.2 ARM

6.2.1 ARM模式下传递3个参数

ARM系统在传递参数时,通常会进行拆分:把前4个参数传递给R0~R3寄存器,然后利用栈传递其余的参数。在获取(组装)参数时,遵循的函数调用约定是fastcall约定或win64约定。有关这两种约定的的详细介绍,请参照64.3节和64.5.1节。

32位ARM系统

非经优化的Keil+ARM模式

指令清单6.6 非经优化的Keil 6/2013(ARM模式)

.text:00000000 main
.text:00000000 10 40 2D E9 STMFD SP!, {R4,LR}
.text:00000004 03 30 A0 E3 MOV   R3, #3
.text:00000008 02 20 A0 E3 MOV   R2, #2
.text:0000000C 01 10 A0 E3 MOV   R1, #1
.text:00000010 08 00 8F E2 ADR   R0, aADBDCD;    "a=%d; b=%d; c=%d"
.text:00000014 06 00 00 EB BL   __2printf
.text:00000018 00 00 A0 E3 MOV   R0, #0   ; return 0
.text:0000001C 10 80 BD E8 LDMFD  SP!, {R4,PC}

可见,R0~R3寄存器依次负责传递参数。其中,R0寄存器用来传递格式化字符串,R1寄存器传递1,R2寄存器传递2,R3寄存器传递3。

0x18处的指令将R0寄存器置零,对应着源代码中的“return 0”。

这段汇编指令中规中矩。

即使开启了优化选项,Keil 6/2013生成的汇编指令也完全相同。

开启了优化选项的Keil 6/2013 (Thumb模式)

指令清单6.7 开启了优化选项的Keil 6/2013 (Thumb模式)

.text:00000000 main
.text:00000000 10 B5    PUSH  {R4, LR}
.text:00000002 03 23    MOVS  R3, #3
.text:00000004 02 22    MOVS  R2, #2
.text:00000006 01 21    MOVS  R1, #1
.text:00000008 02 A0    ADR  R0, aADBDCD;"a=%d; b=%d; c=%d"
.text:0000000A 00 F0 0D F8 BL   __2printf
.text:0000000E 00 20    MOVS  R0, #0
.text:00000010 10 BD    POP  {R4,PC}

这段代码和前面那段ARM程序没有太多差别。

开启优化选项的Keil 6/2013 (ARM模式) + 无返回值

我们对源代码略做修改,删除“return 0”的语句:

#include <stdio.h>

void main() 
{
printf("a=%d; b=%d; c=%d", 1, 2, 3);
};

相应的汇编指令就会出现显著的差别:

.text:00000014 main
.text:00000014 03 30 A0 E3  MOV  R3, #3
.text:00000018 02 20 A0 E3  MOV  R2, #2
.text:0000001C 01 10 A0 E3  MOV  R1, #1
.text:00000020 1E 0E 8F E2  ADR  R0, aADBDCD  ;"a=%d;b=%d;c=%d\n"
.text:00000024 CB 18 00 EA  B   __2printf

在其余优化选项(-O3)之后,把源代码编译为ARM模式的代码。这次,程序的最后一条指令变成了B指令,不再使用之前的BL指令。另外,与前面那个没有启用优化选项的例子相比,本例并没有出现保存R0和LR寄存器的函数序言或函数尾声。这形成了另一个显著的差异。B指令仅仅将程序跳转到另一个地址,不会根据LR寄存器的值进行返回。大体上说,它和x86平台的JMP指令非常相似。为什么编译器会如此处理呢?实际上,这些指令与前面(未启用优化选项)的运行结果相同。主要原因有两个:

(1)栈和SP(Stack Pointer)都没有发生变化。

(2)调用printf()函数是程序的最后一条指令;调用之后程序再无其他操作。

即使未启用优化选项,在完成printf()函数的作业之后,程序只是要返回到LR寄存器里存储的返回地址而已。LR的值并没有因为调用printf()函数而发生变化,而且程序也没有调用printf()函数之外的函数。因为没有指令会修改LR的值,所以程序不必保存LR的状态。另外,在调用这个函数之后,程序也没有其他操作。故而编译器进行了相应的优化。

当程序最后的语句是调用另外一个函数时,编译器通常都会进行这种优化。

本书13.1.1节的“指令清单13.2”再次出现了这种优化技术。

ARM64

非经优化的GCC (Linaro) 4.9

指令清单6.8 非经优化的GCC (Linaro) 4.9

.LC1:
.string "a=%d; b=%d; c=%d"
f2:
; save FP and LR in stack frame:
stp   x29, x30, [sp, -16]!
; set stack frame (FP=SP):
add   x29, sp, 0
adrp   x0, .LC1
add   x0, x0, :lo12:.LC1
mov   w1, 1
mov   w2, 2
mov   w3, 3
bl    printf
mov   w0, 0
; restore FP and LR
ldp   x29, x30, [sp], 16
ret

第一条STP(Store Pair)指令把FP(X29)和LR(X30)的值推送入栈。第二条“ADD X29, SP, 0”指令构成栈帧,它只是把SP的值复制给X29。

后面的“ADRP/ADD”指令对构建了字符串的指针。

在传递给printf()的格式化字符串里,%d是32位int整型数据。所以,程序使用了寄存器的32位存储后面的数据1、2、3。

即使启用了GCC (Linaro) 4.9的优化选项,它生成的指令也与此相同。

6.2.2 ARM模式下传递8个参数

我们对6.1.2节的例子稍做修改:

#include <stdio.h>

int main() 
{
     printf("a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d\n", 1, 2, 3, 4, 5, 6, 7, 8);
     return 0; 
};

Optimizing Keil 6/2013:ARM模式

.text:00000028   main
.text:00000028
.text:00000028   var_18 = -0x18
.text:00000028   var_14 = -0x14
.text:00000028   var_4 = -4
.text:00000028
.text:00000028 04 E0 2D E5 STR  LR, [SP,#var_4]!
.text:0000002C 14 D0 4D E2 SUB  SP, SP, #0x14
.text:00000030 08 30 A0 E3 MOV  R3, #8
.text:00000034 07 20 A0 E3 MOV  R2, #7
.text:00000038 06 10 A0 E3 MOV  R1, #6
.text:0000003C 05 00 A0 E3 MOV  R0, #5
.text:00000040 04 C0 8D E2 ADD  R12, SP, #0x18+var_14
.text:00000044 0F 00 8C E8 STMIA R12, {R0-R3}
.text:00000048 04 00 A0 E3 MOV  R0, #4
.text:0000004C 00 00 8D E5 STR  R0, [SP,#0x18+var_18]
.text:00000050 03 30 A0 E3 MOV  R3, #3
.text:00000054 02 20 A0 E3 MOV  R2, #2
.text:00000058 01 10 A0 E3 MOV  R1, #1
.text:0000005C 6E 0F 8F E2 ADR   R0, aADBDCDDDEDFDGD ; "a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%"...
.text:00000060 BC 18 00 EB BL   __2printf
.text:00000064 14 D0 8D E2 ADD  SP, SP, #0x14
.text:00000068 04 F0 9D E4 LDR  PC, [SP+4+var_4],#4

程序分为以下几个部分。

Optimizing Keil 6/2013: Thumb模式

.text:0000001C  printf_main2
.text:0000001C
.text:0000001C  var_18 = -0x18
.text:0000001C  var_14 = -0x14
.text:0000001C  var_8 = -8
.text:0000001C
.text:0000001C 00 B5     PUSH  {LR}
.text:0000001E 08 23     MOVS  R3, #8
.text:00000020 85 B0     SUB   SP, SP, #0x14
.text:00000022 04 93     STR   R3, [SP,#0x18+var_8]
.text:00000024 07 22     MOVS  R2, #7
.text:00000026 06 21     MOVS  R1, #6
.text:00000028 05 20     MOVS  R0, #5
.text:0000002A 01 AB     ADD   R3, SP, #0x18+var_14
.text:0000002C 07 C3     STMIA  R3!, {R0-R2}
.text:0000002E 04 20     MOVS  R0, #4
.text:00000030 00 90     STR   R0, [SP,#0x18+var_18]
.text:00000032 03 23     MOVS  R3, #3
.text:00000034 02 22     MOVS  R2, #2
.text:00000036 01 21     MOVS  R1, #1
.text:00000038 A0 A0     ADR   R0,aADBDCDDDEDFDGD ; "a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%"…
.text:0000003A 06 F0 D9 F8 BL    __2printf
.text:0000003E
.text:0000003E   loc_3E ; CODE XREF: example13_f+16
.text:0000003E 05 B0     ADD   SP, SP, #0x14
.text:00000040 00 BD     POP   {PC}

Thumb模式的代码与ARM模式的代码十分相似,但是参数入栈的顺序不同。即,Thumb模式会在第一批次将8推送入栈、第二批次将7、6、5推送入栈,而在第三批次将4推送入栈。

Optimizing Xcode 4.6.3 (LLVM):ARM模式

__text:0000290C  _printf_main2
__text:0000290C    
__text:0000290C  var_1C = -0x1C
__text:0000290C  var_C = -0xC
__text:0000290C    
__text:0000290C 80 40 2D E9  STMFD SP!, {R7,LR}
__text:00002910 0D 70 A0 E1  MOV  R7, SP
__text:00002914 14 D0 4D E2  SUB  SP, SP, #0x14
__text:00002918 70 05 01 E3  MOV  R0, #0x1570
__text:0000291C 07 C0 A0 E3  MOV  R12, #7
__text:00002920 00 00 40 E3  MOVT  R0, #0
__text:00002924 04 20 A0 E3  MOV  R2, #4
__text:00002928 00 00 8F E0  ADD  R0, PC, R0
__text:0000292C 06 30 A0 E3  MOV  R3, #6
__text:00002930 05 10 A0 E3  MOV  R1, #5
__text:00002934 00 20 8D E5  STR  R2, [SP,#0x1C+var_1C]
__text:00002938 0A 10 8D E9  STMFA SP, {R1,R3,R12}
__text:0000293C 08 90 A0 E3  MOV  R9, #8
__text:00002940 01 10 A0 E3  MOV  R1, #1
__text:00002944 02 20 A0 E3  MOV  R2, #2
__text:00002948 03 30 A0 E3  MOV  R3, #3
__text:0000294C 10 90 8D E5  STR  R9, [SP,#0x1C+var_C]
__text:00002950 A4 05 00 EB  BL   _printf
__text:00002954 07 D0 A0 E1  MOV  SP, R7
__text:00002958 80 80 BD E8  LDMFD SP!, {R7, PC}

这段汇编代码与前面的代码十分雷同,不同的是STMFA (Store Multiple Full Ascending)指令。它是STMIB (Store Multiple Increment Before)的同义词。它们首先增加SP指针的值,然后将数据推送入栈;而不是先入栈,再调整SP指针。[2]

虽然这些指令表面看来杂乱无章,但是似乎这就是xcode编译出来的程序的一种特点。例如,在地址0x2918、0x2920、0x2928处,R0寄存器的相关操作似乎可以在一处集中处理。不过,这种分散布局是编译器针对并行计算而进行的优化。通常,处理器会尝试着并行处理那些相邻的指令。以“MOVT R0, #0”和“ADD R0, PC, R0”为例——这两条指令都是操作R0寄存器的指令,若集中在一起就无法并行计算。另一方面,“MOVT R0, #0”和“MOV R2, #4”之间不存在这种资源冲突,可以同时执行。想必是出于这种设计,编译器才会尽可能地进行这种处理吧。

Optimizing Xcode 4.6.3 (LLVM):Thumb-2模式

__text:00002BA0            _printf_main2
__text:00002BA0
__text:00002BA0            var_1C = −0x1C
__text:00002BA0            var_18 = −0x18
__text:00002BA0            var_C  = −0xC
__text:00002BA0
__text:00002BA0 80 B5        PUSH      {R7,LR}
__text:00002BA2 6F 46        MOV       R7, SP
__text:00002BA4 85 B0        SUB       SP, SP, #0x14
__text:00002BA6 41 F2 D8 20  MOVW      R0, #0x12D8
__text:00002BAA 4F F0 07 0C  MOV.W     R12, #7
__text:00002BAE C0 F2 00 00  MOVT.W    R0, #0
__text:00002BB2 04 22        MOVS      R2, #4
__text:00002BB4 78 44        ADD       R0, PC ; char *
__text:00002BB6 06 23        MOVS      R3, #6
__text:00002BB8 05 21        MOVS      R1, #5
__text:00002BBA 0D F1 04 0E  ADD.W     LR, SP, #0x1C+var_18
__text:00002BBE 00 92        STR       R2, [SP,#0x1C+var_1C]
__text:00002BC0 4F F0 08 09  MOV.W     R9, #8
__text:00002BC4 8E E8 0A 10  STMIA.W   LR, {R1,R3,R12}
__text:00002BC8 01 21        MOVS      R1, #1
__text:00002BCA 02 22        MOVS      R2, #2
__text:00002BCC 03 23        MOVS      R3, #3
__text:00002BCE CD F8        STR.W     R9, [SP,#0x1C+var_C]
__text:00002BD2 01 F0 0A EA  BLX       _print5
__text:00002BD6 05 B0        ADD       SP, SP, #0x14
__text:00002BD8 80 BD        POP       {R7,PC}

与ARM模式编译出的代码相比,这段代码存在着明显的Thumb指令的特征。除此之外,ARM模式和Thumb模式编译出的代码并无实际区别。

ARM64

Non-optimizing GCC (Linaro) 4.9

指令清单6.9 Non-optimizing GCC (Linaro) 4.9

.LC2:
     .string "a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d\n"
f3:
; grab more space in stack:
        sub   sp, sp, #32
; save FP and LR in stack frame:
        stp   x29, x30, [sp,16]
; set stack frame (FP=SP):
        add   x29, sp, 16
        adrp   x0, .LC2 ; "a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d\n"
        add   x0, x0, :lo12:.LC2
        mov   w1, 8   ; 9th argument
        str   w1, [sp] ; store 9th argument in the stack
        mov   w1, 1
        mov   w2, 2
        mov   w3, 3
        mov   w4, 4
        mov   w5, 5
        mov   w6, 6
        mov   w7, 7
        bl    printf
        sub   sp, x29, #16
; restore FP and LR   
        ldp   x29, x30, [sp,16]
        add   sp, sp, 32
        ret

X-或W-寄存器传递函数的前8个参数[参见ARM13c]。字符串指针使用64位寄存器,所以它使用整个X0寄存器。所有的其他参数都属于32位整型数据,可由寄存器的低32位/即W-寄存器传递。程序使用栈来传递第九个参数(数值8)。CPU的寄存器总数有限,所以寄存器往往不足以传递全部参数。

启用优化选项之后,GCC (linaro)4.9生成的代码与此相同。

6.3 MIPS

6.3.1 传递3个参数

Optimizing GCC 4.4.5

在MIPS平台上编译“Hello, world!”程序,编译器不会使用puts()函数替代printf()函数,而且它会使用$5~$7寄存器(即$A0~$A2)传递前3个参数。

这3个寄存器都是“A-”开头的寄存器,因为它们就是负责传递参数(arguments)的寄存器。

指令清单6.10 Optimizing GCC 4.4.5 (汇编输出)

$LC0:
     .ascii  "a=%d; b=%d; c=%d\000"
main:
; function prologue:
     lui   $28,%hi(__gnu_local_gp)
     addiu  $sp,$sp,-32
     addiu  $28,$28,%lo(__gnu_local_gp)
     sw    $31,28($sp)
; load address of printf():
     lw    $25,%call16(printf)($28)
; load address of the text string and set 1st argument of printf():
     lui   $4,%hi($LC0)
     addiu  $4,$4,%lo($LC0)
; set 2nd argument of printf():
     li    $5,1 # 0x1
; set 3rd argument of printf():
     li    $6,2 # 0x2
; call printf():
     jalr   $25
; set 4th argument of printf() (branch delay slot):
     li    $7,3 # 0x3

; function epilogue:
     lw    $31,28($sp)
; set return value to 0:
     move   $2,$0
; return
     j    $31
     addiu  $sp,$sp,32 ; branch delay slot

指令清单6.11 Optimizing GCC 4.4.5 (IDA)

.text:00000000 main:
.text:00000000
.text:00000000 var_10     = -0x10 
.text:00000000 var_4      = -4
.text:00000000
; function prologue:
.text:00000000   lui  $gp, (__gnu_local_gp >> 16)
.text:00000004   addiu  $sp, -0x20
.text:00000008   la   $gp, (__gnu_local_gp & 0xFFFF)
.text:0000000C   sw   $ra, 0x20+var_4($sp)
.text:00000010   sw   $gp, 0x20+var_10($sp)
; load address of printf():
.text:00000014   lw   $t9, (printf & 0xFFFF)($gp)
; load address of the text string and set 1st argument of printf():
.text:00000018   la   $a0, $LC0   # "a=%d; b=%d; c=%d"
; set 2nd argument of printf():
.text:00000020   li   $a1, 1
; set 3rd argument of printf():
.text:00000024   li   $a2, 2
; call printf():
.text:00000028   jalr $t9
; set 4th argument of printf() (branch delay slot):
.text:0000002C   li  $a3, 3
; function epilogue:
.text:00000030   lw  $ra, 0x20+var_4($sp)
; set return value to 0:
.text:00000034   move $v0, $zero
; return
.text:00000038   jr  $ra
.text:0000003C   addiu #sp, 0x20 ; branch delay slot

IDA没有显示0x1C的指令。实际上0x18是“LUI”和“ADDIU”两条指令,IDA把它们显示为单条的伪指令,占用了8个字节。

Non-optimizing GCC 4.4.5

如果不启用编译器的优化选项,那么GCC输出的指令会详细得多。

指令清单6.12 Non-optimizing GCC 4.4.5(汇编输出)

$LC0:
     .ascii "a=%d; b=%d; c=%d\000"
main:
; function prologue:
     addiu    $sp,$sp,-32
     sw       $31,28($sp)
     sw       $fp,24($sp)
     move     $fp,$sp
     lui      $28,%hi(__gnu_local_gp)
     addiu    $28,$28,%lo(__gnu_local_gp)
; load address of the text string
     lui      $2,%hi($LC0)
     addiu    $2,$2,%lo($LC0)
; set 1st argument of printf():
     move     $4,$2
; set 2nd argument of printf():
     li       $5,1# 0x1
; set 3rd argument of printf():
     li       $6,2# 0x2
; set 4th argument of printf(): 
     li       $7,3# 0x3
; get address of printf():
     lw       $2,%call16(printf)($28)
     nop
; call printf():
     move     $25,$2
     jalr     $25
     nop

; function epilogue:
     lw       $28,16($fp)
; set return value to 0:
     move     $2,$0
     move     $sp,$fp
     lw       $31,28($sp)
     lw       $fp,24($sp)
     addiu    $sp,$sp,32
; return
     j        #31
     nop

指令清单6.13 Non-optimizing GCC 4.4.5 (IDA)

.text:00000000 main:
.text:00000000
.text:00000000 var_10      = -0x10
.text:00000000 var_8       = -8
.text:00000000 var_4       = -4
.text:00000000
; function prologue:
.text:00000000             addiu     $sp, -0x20
.text:00000004             sw        $ra, 0x20+var_4($sp)
.text:00000008             sw        $fp, 0x20+var_8($sp)
.text:0000000C             move      $fp, $sp
.text:00000010             la        $gp, __gnu_local_gp
.text:00000018             sw        $gp, 0x20+var_10($sp)
; load address of the text string:
.text:0000001C             la        $v0, aADBDCD # "a=%d; b=%d; c=%d"
; set 1st argument of printf():
.text:00000024             move      $a0, $v0
; set 2nd argument of printf():
.text:00000028             li        $a1, 1
; set 3rd argument of printf():
.text:0000002C             li        $a2, 2
; set 4th argument of printf():
.text:00000030             li        $a3, 3
; get address of printf():
.text:00000034             lw        $v0, (printf & 0xFFFF)($gp)
.text:00000038             or        $at, $zero
; call printf():
.text:0000003C             move      $t9, $v0
.text:00000040             jalr      $t9
.text:00000044             or        $at, $zero ; NOP
; function epilogue:
.text:00000048             lw        $gp, 0x20+var_10($fp)
; set return value to 0:
.text:0000004C             move      $v0, $zero
.text:00000050             move      $sp, $fp
.text:00000054             lw        $ra, 0x20+var_4($sp)
.text:00000058             lw        $fp, 0x20+var_8($sp)
.text:0000005C             addiu     $sp, 0x20
; return
.text:00000060             jr        $ra
.text:00000064             or        $at, $zero ; NOP
6.3.2 传递9个参数

我们再次使用6.1.2节中的例子,演示9个参数的传递。

#include <stdio.h>

int main() 
{
     printf("a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d\n", 1, 2, 3, 4, 5, 6, 7, 8);
     return 0; 
};

Optimizing GCC 4.4.5

在传递多个参数时,MIPS会使用$A0~$A3传递前4个参数,使用栈传递其余的参数。这种平台主要采用一种名为“O32”的函数调用约定。实际上大多数MIPS系统都采用这种约定。如果采用了其他的函数调用约定,例如N32约定,寄存器的用途则会有不同的设定。

下面指令中的“SW”是“Store Word”的缩写,用以把寄存器的值写入内存。MIPS的指令集很小,没有把数据直接写入内存地址的那类指令。当需要进行这种操作时,就不得不组合使用LI/SW指令。

指令清单6.14 Optimizing GCC 4.4.5 (汇编输出)

$LC0:
     .ascii "a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d\012\000"
main:
; function prologue:
     lui      $28,%hi(__gnu_local_gp)
     addiu    $sp,$sp,-56
     addiu    $28,$28,%lo(__gnu_local_gp)
     sw       $31,52($sp)
; pass 5th argument in stack:
     li       $2,4   # 0x4
     sw       $2,16($sp)
; pass 6th argument in stack:
     li       $2,5   # 0x5
     sw       $2,20($sp)
; pass 7th argument in stack:
     li       $2,6   # 0x6
     sw       $2,24($sp)
; pass 8th argument in stack:
     li       $2,7   # 0x7
     lw       $25,%call16(printf)($28)
     sw       $2,28($sp)
; pass 1st argument in $a0:
     lui      $4,%hi($LC0)
; pass 9th argument in stack:
     li       $2,8   # 0x8
     sw       $2,32($sp)
     addiu    $4,$4,%lo($LC0)
; pass 2nd argument in $a1:
     li       $5,1   # 0x1
; pass 3rd argument in $a2:
     li       $6,2   # 0x2
; call printf():
     jalr     $25
; pass 4th argument in $a3 (branch delay slot):
     li       $7,3   # 0x3

; function epilogue:
     lw       $31,52($sp)
; set return value to 0:
     move     $2,$0
; return
     j        $31
     addiu    $sp,$sp,56 ; branch delay slot

指令清单6.15 Optimizing GCC 4.4.5 (IDA)

.text:00000000 main:
.text:00000000
.text:00000000 var_28     = -0x28
.text:00000000 var_24     = -0x24 
.text:00000000 var_20     = -0x20 
.text:00000000 var_1C     = -0x1C
.text:00000000 var_18     = -0x18
.text:00000000 var_10     = -0x10
.text:00000000 var_4      = -4
.text:00000000  
; function prologue:
.text:00000000             lui       $gp, (__gnu_local_gp >> 16)
.text:00000004             addiu     $sp, -0x38
.text:00000008             la        $gp, (__gnu_local_gp & 0xFFFF)
.text:0000000C             sw        $ra, 0x38+var_4($sp)
.text:00000010             sw        $gp, 0x38+var_10($sp)
; pass 5th argument in stack:
.text:00000014             li        $v0, 4
.text:00000018             sw        $v0, 0x38+var_28($sp)
; pass 6th argument in stack:
.text:0000001C             li        $v0, 5
.text:00000020             sw        $v0, 0x38+var_24($sp)
; pass 7th argument in stack:
.text:00000024             li        $v0, 6
.text:00000028             sw        $v0, 0x38+var_20($sp)
; pass 8th argument in stack:
.text:0000002C             li        $v0, 7
.text:00000030             lw        $t9, (printf & 0xFFFF)($gp)
.text:00000034             sw        $v0, 0x38+var_1C($sp)
; prepare 1st argument in $a0:
.text:00000038             lui       $a0, ($LC0 >> 16) # "a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%"…
; pass 9th argument in stack:
.text:0000003C             li        $v0, 8
.text:00000040             sw        $v0, 0x38+var_18($sp)
; pass 1st argument in $a1: 
.text:00000044             la        $a0, ($LC0 & 0xFFFF) # "a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%"...
; pass 2nd argument in $a1:
.text:00000048             li        $a1, 1
; pass 3rd argument in $a2:
.text:0000004C             li        $a2, 2
; call printf():
.text:00000050             jalr      $t9
; pass 4th argument in $a3 (branch delay slot):
.text:00000054             li        $a3, 3
; function epilogue:
.text:00000058             lw        $ra, 0x38+var_4($sp)
; set return value to 0:
.text:0000005C             move      $v0, $zero
; return
.text:00000060             jr        $ra
.text:00000064             addiu     $sp, 0x38 ; branch delay slot

Non-optimizing GCC 4.4.5

关闭优化选项后,GCC会生成较为详细的指令。

指令清单6.16 Non-optimizing GCC 4.4.5(汇编输出)

$LC0:
     .ascii "a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d\012\000"
main:
; function prologue:
     addiu    $sp,$sp,-56
     sw       $31,52($sp)
     sw       $fp,48($sp)
     move     $fp,$sp
     lui      $28,%hi(__gnu_local_gp)
     addiu    $28,$28,%lo(__gnu_local_gp)
     lui      $2,%hi($LC0)
     addiu    $2,$2,%lo($LC0)
; pass 5th argument in stack:
     li       $3,4     # 0x4
     sw       $3,16($sp)
; pass 6th argument in stack:
     li       $3,5     # 0x5
     sw       $3,20($sp)
; pass 7th argument in stack:
     li       $3,6     # 0x6
     sw       $3,24($sp)
; pass 8th argument in stack:
     li       $3,7     # 0x7
     sw       $3,28($sp)
; pass 9th argument in stack:
     li       $3,8     # 0x8
     sw       $3,32($sp)
; pass 1st argument in $a0:
     move     $4,$2
; pass 2nd argument in $a1:
     li       $5,1     # 0x1
; pass 3rd argument in $a2:
     li       $6,2     # 0x2
; pass 4th argument in $a3:
     li       $7,3     # 0x3
; call printf():
     lw       $2,%call16(printf)($28)
     nop
     move     $25,$2
     jalr     $25
     nop
; function epilogue:
     lw       $28,40($fp)
; set return value to 0:
     move     $2,$0
     move     $sp,$fp
     lw       $31,52($sp)
     lw       $fp,48($sp)
     addiu    $sp,$sp,56
; return
     j        $31
     nop

指令清单6.17 Non-optimizing GCC 4.4.5 (IDA)

.text:00000000 main:
.text:00000000
.text:00000000 var_28      = -0x28
.text:00000000 var_24      = -0x24
.text:00000000 var_20      = -0x20
.text:00000000 var_1C      = -0x1C
.text:00000000 var_18      = -0x18
.text:00000000 var_10      = -0x10
.text:00000000 var_8       = -8
.text:00000000 var_4       = -4
.text:00000000
; function prologue:
.text:00000000             addiu $sp, -0x38
.text:00000004             sw    $ra, 0x38+var_4($sp)
.text:00000008             sw    $fp, 0x38+var_8($sp)
.text:0000000C             move  $fp, $sp
.text:00000010             la    $gp, __gnu_local_gp
.text:00000018             sw    $gp, 0x38+var_10($sp)
.text:0000001C             la    $v0, aADBDCDDDEDFDGD # "a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%" ...
; pass 5th argument in stack:
.text:00000024             li    $v1, 4
.text:00000028             sw    $v1, 0x38+var_28($sp)
; pass 6th argument in stack:
.text:0000002C             li    $v1, 5
.text:00000030             sw    $v1, 0x38+var_24($sp)
; pass 7th argument in stack:
.text:00000034             li    $v1, 6
.text:00000038             sw    $v1, 0x38+var_20($sp)
; pass 8th argument in stack:
.text:0000003C             li    $v1, 7
.text:00000040             sw    $v1, 0x38+var_1C($sp)
; pass 9th argument in stack:
.text:00000044             li    $v1, 8
.text:00000048             sw    $v1, 0x38+var_18($sp)
; pass 1st argument in $a0:
.text:0000004C             move  $a0, $v0
; pass 2nd argument in $a1:
.text:00000050             li    $a1, 1
; pass 3rd argument in $a2:
.text:00000054             li    $a2, 2
; pass 4th argument in $a3:
.text:00000058             li    $a3, 3
; call printf():
.text:0000005C             lw    $v0, (printf & 0xFFFF)($gp)
.text:00000060             or    $at, $zero
.text:00000064             move  $t9, $v0
.text:00000068             jalr  $t9
.text:0000006C             or    $at, $zero ; NOP
; function epilogue:
.text:00000070             lw    $gp, 0x38+var_10($fp)
; set return value to 0:
.text:00000074             move  $v0, $zero
.text:00000078             move  $sp, $fp
.text:0000007C             lw    $ra, 0x38+var_4($sp)
.text:00000080             lw    $fp, 0x38+var_8($sp)
.text:00000084             addiu $sp, 0x38
; return
.text:00000088             jr    $ra
.text:0000008C             or    $at, $zero ; NOP

6.4 总结

调用函数的时候,程序的传递过程大体如下。

指令清单6.18 x86

...
PUSH 3rd argument
PUSH 2nd argument
PUSH 1st argument
CALL function
; modify stack pointer (if needed)

指令清单6.19 x64 (MSVC)

MOV RCX, 1st argument
MOV RDX, 2nd argument
MOV R8, 3rd argument
MOV R9, 4th argument
...
PUSH 5th, 6th argument, etc (if needed)
CALL function
; modify stack pointer (if needed)

指令清单6.20 x64 (GCC)

MOV RDI, 1st argument
MOV RSI, 2nd argument
MOV RDX, 3rd argument
MOV RCX, 4th argument
MOV R8, 5th argument
MOV R9, 6th argument
...
PUSH 7th, 8th argument, etc (if needed)
CALL function
; modify stack pointer (if needed)

指令清单6.21 ARM

MOV R0, 1st argument
MOV R1, 2nd argument
MOV R2, 3rd argument
MOV R3, 4th argument
; pass 5th, 6th argument, etc, in stack (if needed)
BL function
; modify stack pointer (if needed)

指令清单6.22 ARM64

MOV X0, 1st argument
MOV X1, 2nd argument
MOV X2, 3rd argument
MOV X3, 4th argument
MOV X4, 5th argument
MOV X5, 6th argument
MOV X6, 7th argument
MOV X7, 8th argument
; pass 9th, 10th argument, etc, in stack (if needed)
BL CALL function
; modify stack pointer (if needed)

指令清单6.23 MIPS(O32 调用约定)

LI $4, 1st argument ; AKA $A0
LI $5, 2nd argument ; AKA $A1
LI $6, 3rd argument ; AKA $A2
LI $7, 4th argument ; AKA $A3
; pass 5th, 6th argument, etc, in stack (if needed)
LW temp_reg, address of function
JALR temp_reg

6.5 其他

在x86、x64、ARM和MIPS平台上,程序向函数传递参数的方法各不相同。这种差异性表明,函数间传递参数的方法与CPU的关系并不是那么密切。如果付诸努力,人们甚至可以开发出一种不依赖数据栈即可传递参数的超级编译器。

为了方便记忆,在采用O32调用约定时,MIPS平台的4号~7号寄存器也叫作$A0~$A7寄存器。实际上,编程人员完全可以使用$ZERO之外的其他寄存器传递数据,也可以采取其他的函数调用约定。

CPU完全不在乎程序使用何种调用约定。

汇编语言的编程新手可能采取五花八门的方法传递参数。虽然他们基本上都是利用寄存器传递参数,但是传递参数的顺序往往不那么讲究,甚至可能通过全局变量传递参数。不过,这样的程序也能正常运行。


[1] calling convention,又有“调用规范”“调用协定”等译法。

[2] 指针指向的地址必须有数据,这就是full stack的涵义,也是它们与empty stack的区别。