您的位置:首页 > 编程语言 > C语言/C++

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(),有的编译器会报错,有的会警告,如果编译过了,运行时一般没问题。

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)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息