第9章 返回值

在x86系统里,被调用方函数通常通过EAX寄存器返回运算结果。[1]若返回值属于byte或char类型数据,返回值将存储于EAX寄存器的低8位——AL寄存器存储返回值。如果返回值是浮点float型数据,那么返回值将存储在FPU的ST(0)寄存器里。ARM系统的情况相对简单一些,它通常使用R0寄存器回传返回值。

9.1 void型函数的返回值

主函数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()函数输出的字符的总数。

9.2 函数返回值不被调用的情况

printf() 函数的返回值为打印的字符的总数,但是很少有程序会使用这个返回值。实际上,确实有调用运算函数、却不使用运算结果的程序:

int f() 
{
     // skip first 3 random values
     rand();
     rand();
     rand();
     // and use 4th
     return rand();
};

上述四个rand()函数都会把运算结果存储到EAX寄存器里。但是前三个rand()函数留在EAX寄存器的运算结果都被抛弃了。

9.3 返回值为结构体型数据

我们继续讨论使用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。