返回值不是挺简单的吗?有什么好研究的。

其实返回值不简单,下面就让我们来看看返回值有什么好研究的。

在操作系统中(以linux为例),每个程序都需要有一个返回值,返回给操作系统.

在shell中,可以利用echo $?查看程序的返回值

可以看到,not_exist不存在,返回2,main.c存在,返回0,一般返回0表示成功,而返回非0表示失败或者其他意义。

其实这个返回值是存放在eax中的,c规范要求main必须返回int,而int和eax长度是一的(32位系统)。

 

这个汇编程序只有一条指令,将4存到eax,检测返回值发现是4。

如果你的程序用void main(),有的编译器会报错,有的会警告,如果编译过了,运行时一般没问题。

  1. int f()  
  2. {  
  3.       return 100;  
  4. }  
  5. void main()  
  6. {  
  7.       f();  

 

函数f把返回值放到eax了,main函数什么都没做,所以返回值还是100。

但是我们来看另外一个例子

    //file:haha.c
  1. struct xxx{
  2. int a[50];
  3. };
  4. struct xxx main()
  5. {
  6. struct xxx haha;
  7. return haha;
  8. }  

为什么会出现段错误?我们后面会研究它。

 

我们先把返回值进行分类:

首先是基本类型,void,char,short,long,long long,float,double,指针

然后是结构类型struct。

对于void类型,没有返回值,不做讨论。

char只有1个字节,eax有4个字节,怎么存?只用低8位al就可以了。下面是示例

  1. //示例1:返回值为char  
  2.  
  3. /*C代码*/
  4. char f()
    {
            char a = 'a';
            return a;
    }
    int main()
    {
            char b = f();
            return 0;
    }
     
  5.  
  6. /*汇编代码*/
  7. .file "char.c"
  8. .text
  9. .globl f
  10. f:
  11. pushl %ebp
  12. movl %esp, %ebp
  13. subl $16, %esp
  14. movb $97, -1(%ebp)
  15. movsbl -1(%ebp),%eax //符号扩展
  16. leave
  17. ret
  18. .globl main
  19. main:
  20. leal 4(%esp), %ecx
  21. andl $-16, %esp
  22. pushl -4(%ecx)
  23. pushl %ebp
  24. movl %esp, %ebp
  25. pushl %ecx
  26. subl $16, %esp
  27. call f
  28. movb %al, -5(%ebp)
  29. movl $0, %eax
  30. addl $16, %esp
  31. popl %ecx
  32. popl %ebp
  33. leal -4(%ecx), %esp
  34. ret

从汇编代码中可以看出,调用完f后,main函数从al中找返回值。

同样,对于short,int,分别把返回值存放到ax,eax,假如在64位系统里,那么long long 返回值是存到rax的,它的长度为64位,在32位系统里是怎么存的呢?

在32位系统里返回64位数,是通过edx和eax联合实现的,edx存高32位,eax存低32位。

  1. /*示例2:32位系统上返回64位整数*/ 
  2. /*C代码*/ 
  3. long long f()  
  4. {  
  5.         long long a = 5;  
  6.         return a;  
  7. }  
  8. int main()  
  9. {  
  10.         long long b;  
  11.          b=f();  
  12.         return 0;  
  13. }  
  14. /*汇编代码*/ 
  15.        .file   "longint.c" 
  16.         .text  
  17. .globl f  
  18. f:  
  19.         pushl   %ebp  
  20.         movl    %esp, %ebp  
  21.         subl    $16, %esp  
  22.         movl    $5, -8(%ebp)  
  23.         movl    $0, -4(%ebp)  
  24.         movl    -8(%ebp), %eax  
  25.         movl    -4(%ebp), %edx  
  26.         leave  
  27.         ret  
  28. .globl main  
  29. main:  
  30.         leal    4(%esp), %ecx  
  31.         andl    $-16, %esp  
  32.         pushl   -4(%ecx)  
  33.         pushl   %ebp  
  34.         movl    %esp, %ebp  
  35.         pushl   %ecx  
  36.         subl    $20, %esp  
  37.         call    f  
  38.         movl    %eax, -16(%ebp)  
  39.         movl    %edx, -12(%ebp)  
  40.         movl    $0, %eax  
  41.         addl    $20, %esp  
  42.         popl    %ecx  
  43.         popl    %ebp  
  44.         leal    -4(%ecx), %esp  
  45.         ret  

对于浮点类型,虽然运算过程中会存放在eax等普通寄存器中,但是作为返回值时,不会用eax,edx等,即使运算结果已经存到了eax中,也要再压到浮点数寄存器堆栈中,在主调函数中,会认为返回结果存到浮点数寄存器了,当然,如果你要手动优化汇编代码也是没问题的。

下面是示例。

  1. /*示例3:返回值为浮点数*  
  2. /*C代码*/  
  3. float f()  
  4. {  
  5.         return 0.1;  
  6. }  
  7. int main()  
  8. {  
  9.         float a = f();  
  10.         return 0;  
  11. }  
  12. /*汇编代码*/  
  13.         .file   "float.c"  
  14.         .text  
  15. .globl f  
  16. f:  
  17.         pushl   %ebp  
  18.         movl    %esp, %ebp  
  19.         subl    $4, %esp  
  20.         movl    $0x3dcccccd, %eax  
  21.         movl    %eax, -4(%ebp)  
  22.         flds    -4(%ebp)  //把结果压到浮点寄存器栈顶  
  23.         leave  
  24.         ret  
  25. .globl main  
  26. main:  
  27.         leal    4(%esp), %ecx  
  28.         andl    $-16, %esp  
  29.         pushl   -4(%ecx)  
  30.         pushl   %ebp  
  31.         movl    %esp, %ebp  
  32.         pushl   %ecx  
  33.         subl    $16, %esp  
  34.         call    f  
  35.         fstps   -8(%ebp) //从浮点寄存器栈顶取数  
  36.         movl    $0, %eax  
  37.         addl    $16, %esp  
  38.         popl    %ecx  
  39.         popl    %ebp  
  40.         leal    -4(%ecx), %esp  
  41.         ret 

关于浮点寄存器及浮点运算指令,可参考:http://www.diybl.com/course/3_program/hb/hbjs/2007124/89946.html

如果返回值为指针?那肯定是用eax(32bit)或者rax(64bit)了。不管是什么类型的指针,都一样,我们来看一个奇怪的程序。

  1. /*示例4:返回值为指针*/ 
  2. /*C代码*/ 
  3. int f()  
  4. {  
  5.         return 5;  
  6. }  
  7. int (*whatisthis()) ()  //这个函数的返回类型是函数指针
  8. {  
  9.         return f;  
  10. }  
  11. int main()  
  12. {  
  13.         int (*a) ();  
  14.         int b;  
  15.         a = whatisthis();  
  16.         b = a();  
  17.         printf("%d\n",b);  
  18.         return 0;  
  19. }  
  20. /*汇编代码*/ 
  21.         .file   "ret_fun.c" 
  22.         .text  
  23. .globl f  
  24. f:  
  25.         pushl   %ebp  
  26.         movl    %esp, %ebp  
  27.         movl    $5, %eax  
  28.         popl    %ebp  
  29.         ret  
  30.  
  31. .globl whatisthis  
  32. whatisthis:  
  33.         pushl   %ebp  
  34.         movl    %esp, %ebp  
  35.         movl    $f, %eax  
  36.         popl    %ebp  
  37.         ret  
  38.  
  39. .LC0:  
  40.         .string "%d\n" 
  41.         .text  
  42.  
  43. .globl main  
  44. main:  
  45.         leal    4(%esp), %ecx  
  46.         andl    $-16, %esp  
  47.         pushl   -4(%ecx)  
  48.         pushl   %ebp  
  49.         movl    %esp, %ebp  
  50.         pushl   %ecx  
  51.         subl    $36, %esp  
  52.         call    whatisthis  
  53.         movl    %eax, -12(%ebp)  
  54.         movl    -12(%ebp), %eax  
  55.         call    *%eax            
  56.         movl    %eax, -8(%ebp)  
  57.         movl    -8(%ebp), %eax  
  58.         movl    %eax, 4(%esp)  
  59.         movl    $.LC0, (%esp)  
  60.         call    printf  
  61.         movl    $0, %eax  
  62.         addl    $36, %esp  
  63.         popl    %ecx  
  64.         popl    %ebp  
  65.         leal    -4(%ecx), %esp  
  66.         ret  

一个函数的返回值可以是函数指针,定义一个这样的函数如下:

函数1   int f(int,char)

函数2   返回值为上面函数的类型的指针,假如函数名为g,参数为float

那么g的定义为     int   (* g(float x)  )    (int,char)

基本类型讨论完了,那么struct类型呢?struct可大可小,怎么存到寄存器里呢?

答案是:主调函数会把被赋值对象的地址传给被调用函数。你可能会说这不是传引用吗,其实传引用传值什么的都是浮云。

还有一个问题就是,对于struct xxx { char a; };这样的结构也要传地址吗?答案是肯定的,gcc是这样做的,其它编译器可能不这样,当然也可以手动修改汇编代码。

  1. /*示例5:struct只有一个字节*/ 
  2. /*C代码*/ 
  3. struct xxx{  
  4.         char a;  
  5. };  
  6. struct xxx  f()  
  7. {  
  8.         struct xxx x;  
  9.         x.a = '9';  
  10.         return x;  
  11. }  
  12. int main()  
  13. {  
  14.         struct xxx y = f();  
  15.         return 0;  
  16. }  
  17. /*汇编代码*/ 
  18.         .file   "struct_char.c" 
  19.         .text  
  20. .globl f  
  21. f:  
  22.         pushl   %ebp  
  23.         movl    %esp, %ebp  
  24.         subl    $16, %esp  
  25.         movl    8(%ebp), %edx //取出地址,放入edx  
  26.         movb    $57, -1(%ebp)    
  27.         movzbl  -1(%ebp), %eax //'9'放到 al  
  28.         movb    %al, (%edx) //将al内容写到edx指向的地址  
  29.         movl    %edx, %eax  
  30.         leave  
  31.         ret     $4  
  32.  
  33. .globl main  
  34. main:  
  35.         leal    4(%esp), %ecx  
  36.         andl    $-16, %esp  
  37.         pushl   -4(%ecx)  
  38.         pushl   %ebp  
  39.         movl    %esp, %ebp  
  40.         pushl   %ecx  
  41.         subl    $24, %esp  
  42.         leal    -21(%ebp), %eax //地址放到eax  
  43.         movl    %eax, (%esp) //地址压入栈中  
  44.         call    f   
  45.         subl    $4, %esp    //没有取返回值的指令了  
  46.         movzbl  -21(%ebp), %eax//因为已经写到目的地址了  
  47.         movb    %al, -5(%ebp)  
  48.         movl    $0, %eax  
  49.         movl    -4(%ebp), %ecx  
  50.         leave  
  51.         leal    -4(%ecx), %esp  
  52.         ret  

我们再来看个复杂点的例子

  1. /*示例6: struct较大*/ 
  2. /*C代码*/ 
  3. struct xxx {  
  4.         char a[10];  
  5. };  
  6. struct xxx f(int a)  
  7. {  
  8.         struct xxx t;  
  9.         t.a[9] = 1;  
  10.         return t;  
  11. }  
  12. int main()  
  13. {  
  14.         struct xxx m=f(1);  
  15.         return 0;  
  16. }  
  17. /*汇编代码*/ 
  18.         .file   "struct.c" 
  19.         .text  
  20. .globl f  
  21. f:  
  22.         pushl   %ebp  
  23.         movl    %esp, %ebp  
  24.         subl    $16, %esp  
  25.         movl    8(%ebp), %edx   //取地址
  26.         movb    $1, -1(%ebp)  
  27.         movl    -10(%ebp), %eax  
  28.         movl    %eax, (%edx)  
  29.         movl    -6(%ebp), %eax  
  30.         movl    %eax, 4(%edx)  
  31.         movzwl  -2(%ebp), %eax  
  32.         movw    %ax, 8(%edx)  
  33.         movl    %edx, %eax  
  34.         leave  
  35.         ret     $4  
  36.  
  37. .globl main  
  38. main:  
  39.         leal    4(%esp), %ecx  
  40.         andl    $-16, %esp  
  41.         pushl   -4(%ecx)  
  42.         pushl   %ebp  
  43.         movl    %esp, %ebp  
  44.         pushl   %ecx  
  45.         subl    $24, %esp  
  46.         leal    -14(%ebp), %eax  
  47.         movl    $1, 4(%esp)      //先压入参数  
  48.         movl    %eax, (%esp)     //再压入返回值地址  
  49.         call    f  
  50.         subl    $4, %esp  
  51.         movl    $0, %eax  
  52.         movl    -4(%ebp), %ecx  
  53.         leave  
  54.         leal    -4(%ecx), %esp  
  55.         ret  

进入被调用函数后的堆栈情况

 

它会到假定8(%ebp)处存放着返回值的地址。这也是为什么main的返回值为struct时会引起段错误,main函数认为这个地方存着返回值的地址,实际上这个地方是操作系统写入的特定值,把这个当作返回值的地址乱写,肯定会引起段错误。

下面这个程序

假如对于struct,有返回值的函数却不赋值怎么办?

比如

  1. struct xxx {  
  2.         char a[10];  
  3. };  
  4. struct xxx f(int a)  
  5. {  
  6.         struct xxx t;  
  7.         t.a[9] = 1;  
  8.         return t;  
  9. }  
  10. int main()  
  11. {  
  12.         f(1);  
  13.         return 0;  
  14. }  

对于上述程序,主调用函数需要开辟垃圾空间作为返回值空间,感兴趣的可以验证下看看。

补充:

gcc支持代码块有返回值

比如a = { int b = 2; int c = 3; c-b;} 最终a = 1;

根据我的测试:代码块里必须有除了变量声明的其他语句,否则不对,不能有return;

另外,只能对基本类型赋值,struct类型不能赋值。

最后的结果是:代码块执行结束后,取出eax的值,检查要赋值的变量类型,如果是char,取al,如果是int,取eax,如果是long long,符号扩展,如果是float或者double,将eax强制转换成浮点数。

下面代码可正常运行:

  1. int main()  
  2. {  
  3.         int a;  
  4.         long long a1;  
  5.         double a2;  
  6.         a  = {int b = 5; printf("xxx\n");;};  
  7.         a1  = {int b = 5;int c = 2; 3-4;b-c;};  
  8.         a2  = {int b = 5;int c = 2; 10-8;};  
  9.         printf("%d\n",a);  
  10.         printf("%ld\n",a1);  
  11.         printf("%lf\n",a2);  
  12.         return 0;  
  13. }  

上面代码中的3-4会被忽略,因为没有用,而10-8不会被忽略,因为它在代码块最后,但是不是执行sub指令,直接movl $2, %eax;

这东西有用吗?没用我就不去研究它了,确实用到了,在Linux内核里,contain_of这个宏用到了上述内容,所以我稍微研究了下。

维基百科讲的比较详细,http://zh.wikipedia.org/wiki/%E5%9D%97_(C%E8%AF%AD%E8%A8%80%E6%89%A9%E5%B1%95)