在x86系统里,被调用方函数通常通过EAX寄存器返回运算结果。[1]若返回值属于byte或char类型数据,返回值将存储于EAX寄存器的低8位——AL寄存器存储返回值。如果返回值是浮点float型数据,那么返回值将存储在FPU的ST(0)寄存器里。ARM系统的情况相对简单一些,它通常使用R0寄存器回传返回值。
主函数main()的数据类型通常是void而不是int,程序如何处理返回值呢?
调用main()函数的有关代码大体会是这样的:
push envp
push argv
push argc
call main
push eax
call exit
将其转换为源代码,也就是:
exit(main(argc,argv,envp));
如果声明main()的数据类型是void,则main()函数不会明确返回任何值(没有return指令)。不过在main()函数退出时,EAX寄存器还会存有数据,EAX寄存器保存的数据会被传递给exit()函数、成为后者的输入参数。通常EAX寄存器的值会是被调用方函数残留的确定数据,所以void类型函数的返回值、也就是主函数退出代码往往属于伪随机数(pseudorandom)。
在我们进行相应的演示之前,请注意main()函数返回值是void型:
#include <stdio.h>
void main()
{
printf ("Hello, world!\n");
};
然后使用Linux系统编译。
3.4.3节处介绍过GCC 4.8.1会使用puts()替换printf(),而且puts()函数会返回它所输出的字符的总数。我们将充分利用这点,观察main()函数的返回值。请注意在main()函数结束时,EAX寄存器的值不会是零;也就是说,此时EAX寄存器存储的值应当是上一个函数——puts()函数的返回值。
指令清单9.1 GCC 4.8.1
.LC0:
.string "Hello, world!"
main:
push ebp
mov ebp, esp
and esp, -16
sub esp, 16
mov DWORD PTR [esp], OFFSET FLAT:.LC0
call puts
leave
ret
我们再通过一段bash脚本程序,观察程序的退出状态(返回值)。
指令清单9.2 tst.sh
#!/bin/sh
./hello_world
echo $?
执行上述脚本之后,我们将会看到:
$ ./tst.sh
Hello, world!
14
这个“14”就是puts()函数输出的字符的总数。
printf() 函数的返回值为打印的字符的总数,但是很少有程序会使用这个返回值。实际上,确实有调用运算函数、却不使用运算结果的程序:
int f()
{
// skip first 3 random values
rand();
rand();
rand();
// and use 4th
return rand();
};
上述四个rand()函数都会把运算结果存储到EAX寄存器里。但是前三个rand()函数留在EAX寄存器的运算结果都被抛弃了。
我们继续讨论使用EAX寄存器存储函数返回值的案例。函数只能够使用EAX这1个寄存器回传返回值。因为这种局限,过去的C编译器无法编译返回值超过EAX容量(一般来说,就是int型数据)的函数。那个时候,如果要让返回多个返回值,那么只能用函数返回一个值、再通过指针传递其余的返回值。现在的C编译器已经没有这种短板了,return指令甚至可以返回结构体型的数据,只是时下很少有人会这么做。如果函数的返回值是大型结构的数据,那么应由调用方函数(caller)负责分配空间,给结构体分配指针,再把指针作为第一个参数传递给被调用方函数。现在的编译器已经能够替程序员自动完成这种复杂的操作了,其处理方式相当于上述几个步骤,只是编译器隐藏了有关操作。
我们来看:
struct s
{
int a;
int b;
int c;
};
struct s get_some_values (int a)
{
struct s rt;
rt.a=a+1;
rt.b=a+2;
rt.c=a+3;
return rt;
};
使用MSVC 2010(启用优化选项/Ox)编译,可得到:
$T3853 = 8 ; size = 4
_a$ = 12 ; size = 4
?get_some_values@@YA?AUs@@H@Z PROC ; get_some_values
mov ecx, DWORD PTR _a$[esp-4]
mov eax, DWORD PTR $T3853[esp-4]
lea edx, DWORD PTR [ecx+1]
mov DWORD PTR [eax], edx
lea edx, DWORD PTR [ecx+2]
add ecx, 3
mov DWORD PTR [eax+4], edx
mov DWORD PTR [eax+8], ecx
ret 0
?get_some_values@@YA?AUs@@H@Z ENDP ; get_some_values
在程序内部传递结构体的指针就是$T3853。
如果使用C99 扩展语法来写,刚才的程序就是:
struct s
{
int a;
int b;
int c;
};
struct s get_some_values (int a)
{
return (struct s){.a=a+1, .b=a+2, .c=a+3};
};
经GCC 4.8.1编译上述程序,可得到如下所示的指令。
指令清单9.3 GCC 4.8.1
_get_some_values proc near
ptr_to_struct = dword ptr 4
a = dword ptr 8
  mov edx, [esp+a]
mov eax, [esp+ptr_to_struct]
lea ecx, [edx+1]
mov [eax], ecx
lea ecx, [edx+2]
add edx, 3
mov [eax+4], ecx
mov [eax+8], edx
retn
_get_some_values endp
可见,调用方函数(caller)创建了数据结构、分配了数据空间,被调用的函数仅向结构体填充数据。其效果等同于返回结构体。这种处理方法并不会影响程序性能。
[1] 请参见MSDN: Return Values (C++): http://msdn.microsoft.com/en-us/library/7572ztz4.aspx。