C语言返回值深入研究 推荐
2011-11-02 12:27
363 查看
返回值不是挺简单的吗?有什么好研究的。
其实返回值不简单,下面就让我们来看看返回值有什么好研究的。
在操作系统中(以linux为例),每个程序都需要有一个返回值,返回给操作系统.
在shell中,可以利用echo $?查看程序的返回值
可以看到,not_exist不存在,返回2,main.c存在,返回0,一般返回0表示成功,而返回非0表示失败或者其他意义。
其实这个返回值是存放在eax中的,c规范要求main必须返回int,而int和eax长度是一的(32位系统)。
这个汇编程序只有一条指令,将4存到eax,检测返回值发现是4。
如果你的程序用void main(),有的编译器会报错,有的会警告,如果编译过了,运行时一般没问题。
函数f把返回值放到eax了,main函数什么都没做,所以返回值还是100。
但是我们来看另外一个例子
为什么会出现段错误?我们后面会研究它。
我们先把返回值进行分类:
首先是基本类型,void,char,short,long,long long,float,double,指针
然后是结构类型struct。
对于void类型,没有返回值,不做讨论。
char只有1个字节,eax有4个字节,怎么存?只用低8位al就可以了。下面是示例
从汇编代码中可以看出,调用完f后,main函数从al中找返回值。
同样,对于short,int,分别把返回值存放到ax,eax,假如在64位系统里,那么long long 返回值是存到rax的,它的长度为64位,在32位系统里是怎么存的呢?
在32位系统里返回64位数,是通过edx和eax联合实现的,edx存高32位,eax存低32位。
对于浮点类型,虽然运算过程中会存放在eax等普通寄存器中,但是作为返回值时,不会用eax,edx等,即使运算结果已经存到了eax中,也要再压到浮点数寄存器堆栈中,在主调函数中,会认为返回结果存到浮点数寄存器了,当然,如果你要手动优化汇编代码也是没问题的。
下面是示例。
关于浮点寄存器及浮点运算指令,可参考:http://www.diybl.com/course/3_program/hb/hbjs/2007124/89946.html
如果返回值为指针?那肯定是用eax(32bit)或者rax(64bit)了。不管是什么类型的指针,都一样,我们来看一个奇怪的程序。
一个函数的返回值可以是函数指针,定义一个这样的函数如下:
函数1 int f(int,char)
函数2 返回值为上面函数的类型的指针,假如函数名为g,参数为float
那么g的定义为 int (* g(float x) ) (int,char)
基本类型讨论完了,那么struct类型呢?struct可大可小,怎么存到寄存器里呢?
答案是:主调函数会把被赋值对象的地址传给被调用函数。你可能会说这不是传引用吗,其实传引用传值什么的都是浮云。
还有一个问题就是,对于struct xxx { char a; };这样的结构也要传地址吗?答案是肯定的,gcc是这样做的,其它编译器可能不这样,当然也可以手动修改汇编代码。
我们再来看个复杂点的例子
进入被调用函数后的堆栈情况
它会到假定8(%ebp)处存放着返回值的地址。这也是为什么main的返回值为struct时会引起段错误,main函数认为这个地方存着返回值的地址,实际上这个地方是操作系统写入的特定值,把这个当作返回值的地址乱写,肯定会引起段错误。
下面这个程序
假如对于struct,有返回值的函数却不赋值怎么办?
比如
对于上述程序,主调用函数需要开辟垃圾空间作为返回值空间,感兴趣的可以验证下看看。
补充:
gcc支持代码块有返回值
比如a = { int b = 2; int c = 3; c-b;} 最终a = 1;
根据我的测试:代码块里必须有除了变量声明的其他语句,否则不对,不能有return;
另外,只能对基本类型赋值,struct类型不能赋值。
最后的结果是:代码块执行结束后,取出eax的值,检查要赋值的变量类型,如果是char,取al,如果是int,取eax,如果是long long,符号扩展,如果是float或者double,将eax强制转换成浮点数。
下面代码可正常运行:
上面代码中的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)
其实返回值不简单,下面就让我们来看看返回值有什么好研究的。
在操作系统中(以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就可以了。下面是示例
//示例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
关于浮点寄存器及浮点运算指令,可参考: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 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这个宏用到了上述内容,所以我稍微研究了下。
维基百科讲的比较详细,http://zh.wikipedia.org/wiki/%E5%9D%97_(C%E8%AF%AD%E8%A8%80%E6%89%A9%E5%B1%95)
相关文章推荐
- C语言返回值深入研究
- <C语言>关于函数返回值是指针,取不到值的问题研究
- 《java入门第一季》之面向对象(形式参数和返回值问题的深入研究3)
- ADO.NET深入研究(1)[特别推荐]
- ADO.NET深入研究(2)[特别推荐]
- ADO.NET深入研究(2)[特别推荐]
- 形式参数和返回值的问题深入研究
- [转]推荐:深入研究ITL阻塞与ITL死锁
- 深入讲解main()返回值研究
- 深入研究C语言 第二篇
- 推荐android几本研究深入的书籍(对开发人员很有帮助)
- ADO.NET深入研究(1)[特别推荐]
- ADO.NET深入研究(1)[特别推荐]
- ADO.NET深入研究(2)[特别推荐]
- ADO.NET深入研究(2)[特别推荐]
- 深入研究C语言 第一篇(续)
- 【推荐】Spring 3 MVC深入研究
- 深入研究C语言 第二篇(续)
- [程序代写推荐]spring Scurity终于测试OK了,复杂的功能还待深入研究!发布出来一起探讨吧!
- ADO.NET深入研究(1)[特别推荐]