返回值不是挺简单的吗?有什么好研究的。
其实返回值不简单,下面就让我们来看看返回值有什么好研究的。
在操作系统中(以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.c
- struct 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就可以了。下面是示例
- //示例1:返回值为char
- /*C代码*/
- char f(){ char a = 'a'; return a;}int main(){ char b = f(); return 0;}
- /*汇编代码*/
- .file "char.c"
- .text
- .globl f
- f:
- pushl %ebp
- movl %esp, %ebp
- subl $16, %esp
- movb $97, -1(%ebp)
- movsbl -1(%ebp),%eax //符号扩展
- 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
- movb %al, -5(%ebp)
- movl $0, %eax
- addl $16, %esp
- popl %ecx
- popl %ebp
- leal -4(%ecx), %esp
- ret
从汇编代码中可以看出,调用完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)
- movl -8(%ebp), %eax
- movl -4(%ebp), %edx
- 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
- movl %eax, -16(%ebp)
- movl %edx, -12(%ebp)
- 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)
- flds -4(%ebp) //把结果压到浮点寄存器栈顶
- 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
- fstps -8(%ebp) //从浮点寄存器栈顶取数
- movl $0, %eax
- addl $16, %esp
- popl %ecx
- popl %ebp
- leal -4(%ecx), %esp
- ret
关于浮点寄存器及浮点运算指令,可参考:
如果返回值为指针?那肯定是用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
- movl $f, %eax
- 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
- call whatisthis
- movl %eax, -12(%ebp)
- movl -12(%ebp), %eax
- call *%eax
- 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
- movl 8(%ebp), %edx //取出地址,放入edx
- movb $57, -1(%ebp)
- movzbl -1(%ebp), %eax //'9'放到 al
- movb %al, (%edx) //将al内容写到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 -21(%ebp), %eax //地址放到eax
- movl %eax, (%esp) //地址压入栈中
- call f
- subl $4, %esp //没有取返回值的指令了
- movzbl -21(%ebp), %eax//因为已经写到目的地址了
- movb %al, -5(%ebp)
- movl $0, %eax
- movl -4(%ebp), %ecx
- leave
- leal -4(%ecx), %esp
- ret
我们再来看个复杂点的例子
- /*示例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
- movl 8(%ebp), %edx //取地址
- 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
- movl $1, 4(%esp) //先压入参数
- movl %eax, (%esp) //再压入返回值地址
- call f
- subl $4, %esp
- movl $0, %eax
- movl -4(%ebp), %ecx
- leave
- leal -4(%ecx), %esp
- ret
进入被调用函数后的堆栈情况
它会到假定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这个宏用到了上述内容,所以我稍微研究了下。
维基百科讲的比较详细,)