C语言返回值深入研究

来源:互联网 发布:淘宝去边框代码 编辑:程序博客网 时间:2024/06/01 10:38

<span style="background-color: rgb(255, 255, 255); font-family: Arial, Helvetica, sans-serif;">转载于:http://nxlhero.blog.51cto.com/962631/703953</span>

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

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

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

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


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

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


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

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

int f()  {        return 100;  }  void main()  {        f();  } 

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

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

//file:haha.cstruct xxx{       int a[50];};struct xxx main(){       struct xxx haha;       return haha;}  

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


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

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

然后是结构类型struct。

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

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

<span style="color:#555555;">//示例1:返回值为char  /*C代码*/char f(){        char a = 'a';        return a;}int main(){        char b = f();        return 0;}/*汇编代码*/         .file   "char.c"         .text  .globl ff:pushl   %ebpmovl    %esp, %ebpsubl    $16, %esp       </span><span style="color:#ff6666;"> movb    $97, -1(%ebp)          movsbl  -1(%ebp),%eax  //符号扩展</span><span style="color:#555555;">leaveret.globl main          main:          leal    4(%esp), %ecx          andl    $-16, %esp          pushl   -4(%ecx)          pushl   %ebp          movl    %esp, %ebp          pushl   %ecx        </span><span style="color:#ff6666;">  subl    $16, %esp          call    f          movb    %al, -5(%ebp)  </span><span style="color:#555555;">        movl    $0, %eax          addl    $16, %esp          popl    %ecx          popl    %ebp          leal    -4(%ecx), %esp          ret  </span>

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

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

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

/*示例2:32位系统上返回64位整数*/ /*C代码*/ long long f()  {          long long a = 5;          return a;  }  int main()  {          long long b;           b=f();          return 0;  }  /*汇编代码*/        .file   "longint.c"         .text  .globl f  f:          pushl   %ebp          movl    %esp, %ebp          subl    $16, %esp          movl    $5, -8(%ebp)          movl    $0, -4(%ebp)          <span style="color:#ff6666;">movl    -8(%ebp), %eax          movl    -4(%ebp), %edx  </span>        leave          ret  .globl main  main:          leal    4(%esp), %ecx          andl    $-16, %esp          pushl   -4(%ecx)          pushl   %ebp          movl    %esp, %ebp          pushl   %ecx          subl    $20, %esp          call    f          <span style="color:#ff6666;">movl    %eax, -16(%ebp)          movl    %edx, -12(%ebp) </span>         movl    $0, %eax          addl    $20, %esp          popl    %ecx          popl    %ebp          leal    -4(%ecx), %esp          ret  

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

下面是示例。

/*示例3:返回值为浮点数*  /*C代码*/  float f()  {          return 0.1;  }  int main()  {          float a = f();          return 0;  }  /*汇编代码*/          .file   "float.c"          .text  .globl f  f:          pushl   %ebp          movl    %esp, %ebp          subl    $4, %esp          movl    $0x3dcccccd, %eax          movl    %eax, -4(%ebp)          <span style="color:#ff6666;">flds    -4(%ebp)  //把结果压到浮点寄存器栈顶  </span>        leave          ret  .globl main  main:          leal    4(%esp), %ecx          andl    $-16, %esp          pushl   -4(%ecx)          pushl   %ebp          movl    %esp, %ebp          pushl   %ecx          subl    $16, %esp          call    f          <span style="color:#ff6666;">fstps   -8(%ebp) //从浮点寄存器栈顶取数  </span>        movl    $0, %eax          addl    $16, %esp          popl    %ecx          popl    %ebp          leal    -4(%ecx), %esp          ret 

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

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

/*示例4:返回值为指针*/ /*C代码*/ int f()  {          return 5;  }  int (*whatisthis()) ()  //这个函数的返回类型是函数指针{          return f;  }  int main()  {          int (*a) ();          int b;          a = whatisthis();          b = a();          printf("%d\n",b);          return 0;  }  /*汇编代码*/         .file   "ret_fun.c"         .text  .globl f  f:          pushl   %ebp          movl    %esp, %ebp          movl    $5, %eax          popl    %ebp          ret   .globl whatisthis  whatisthis:          pushl   %ebp          movl    %esp, %ebp          <span style="color:#ff6666;">movl    $f, %eax  </span>        popl    %ebp          ret   .LC0:          .string "%d\n"         .text   .globl main  main:          leal    4(%esp), %ecx          andl    $-16, %esp          pushl   -4(%ecx)          pushl   %ebp          movl    %esp, %ebp          pushl   %ecx          subl    $36, %esp          <span style="color:#ff6666;">call    whatisthis          movl    %eax, -12(%ebp)          movl    -12(%ebp), %eax          call    *%eax    </span>                movl    %eax, -8(%ebp)          movl    -8(%ebp), %eax          movl    %eax, 4(%esp)          movl    $.LC0, (%esp)          call    printf          movl    $0, %eax          addl    $36, %esp          popl    %ecx          popl    %ebp          leal    -4(%ecx), %esp          ret  

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

函数1   int f(int,char)

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

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

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

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

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

/*示例5:struct只有一个字节*/ /*C代码*/ struct xxx{          char a;  };  struct xxx  f()  {          struct xxx x;          x.a = '9';          return x;  }  int main()  {          struct xxx y = f();          return 0;  }  /*汇编代码*/         .file   "struct_char.c"         .text  .globl f  f:          pushl   %ebp          movl    %esp, %ebp          subl    $16, %esp          <span style="color:#ff6666;">movl    8(%ebp), %edx //取出地址,放入edx  </span>        movb    $57, -1(%ebp)            <span style="color:#ff6666;">movzbl  -1(%ebp), %eax //'9'放到 al          movb    %al, (%edx) //将al内容写到edx指向的地址  </span>        movl    %edx, %eax          leave          ret     $4   .globl main  main:          leal    4(%esp), %ecx          andl    $-16, %esp          pushl   -4(%ecx)          pushl   %ebp          movl    %esp, %ebp          pushl   %ecx          subl    $24, %esp          <span style="color:#ff6666;">leal    -21(%ebp), %eax //地址放到eax          movl    %eax, (%esp) //地址压入栈中  </span>        call    f           <span style="color:#ff6666;">subl    $4, %esp    //没有取返回值的指令了          movzbl  -21(%ebp), %eax//因为已经写到目的地址了  </span>        movb    %al, -5(%ebp)          movl    $0, %eax          movl    -4(%ebp), %ecx          leave          leal    -4(%ecx), %esp          ret  
我们再来看个复杂点的例子

<span style="color:#555555;">/*示例6: struct较大*/ /*C代码*/ struct xxx {          char a[10];  };  struct xxx f(int a)  {          struct xxx t;          t.a[9] = 1;          return t;  }  int main()  {          struct xxx m=f(1);          return 0;  }  /*汇编代码*/         .file   "struct.c"         .text  .globl f  f:          pushl   %ebp          movl    %esp, %ebp          subl    $16, %esp       </span><span style="color:#ff6666;">   movl    8(%ebp), %edx   //取地址</span><span style="color:#555555;">        movb    $1, -1(%ebp)          movl    -10(%ebp), %eax          movl    %eax, (%edx)          movl    -6(%ebp), %eax          movl    %eax, 4(%edx)          movzwl  -2(%ebp), %eax          movw    %ax, 8(%edx)          movl    %edx, %eax          leave          ret     $4   .globl main  main:          leal    4(%esp), %ecx          andl    $-16, %esp          pushl   -4(%ecx)          pushl   %ebp          movl    %esp, %ebp          pushl   %ecx          subl    $24, %esp          leal    -14(%ebp), %eax         </span><span style="color:#ff6666;"> movl    $1, 4(%esp)      //先压入参数          movl    %eax, (%esp)     //再压入返回值地址  </span><span style="color:#555555;">        call    f          subl    $4, %esp          movl    $0, %eax          movl    -4(%ebp), %ecx          leave          leal    -4(%ecx), %esp          ret  </span>
进入被调用函数后的堆栈情况



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

下面这个程序

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

比如

struct xxx {          char a[10];  };  struct xxx f(int a)  {          struct xxx t;          t.a[9] = 1;          return t;  }  int main()  {          f(1);          return 0;  }  

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

补充:

gcc支持代码块有返回值

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

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

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

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

下面代码可正常运行:

int main()  {          int a;          long long a1;          double a2;          a  = {int b = 5; printf("xxx\n");;};          a1  = {int b = 5;int c = 2; 3-4;b-c;};          a2  = {int b = 5;int c = 2; 10-8;};          printf("%d\n",a);          printf("%ld\n",a1);          printf("%lf\n",a2);          return 0;  }  
上面代码中的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)

0 0
原创粉丝点击