在刚接触C语言编程时,无论是前辈还是教科书,都反复告诫我们两件事:
①函数的参数是值传递(意味着在函数中对参数本身的修改无法“传回”);
②不要返回函数体内局部变量的地址,因为函数结束时栈会回收,局部变量也随之销毁(如果局部变量为类对象,其析构函数会被自动调用),但可以返回局部变量本身。
那么如果一个函数的返回值为结构体类型,其返回值是如何“返回”的呢?
是通过“值传递”吗?我们知道函数参数的值传递,就是将参数入栈(因此参数有自己的一份拷贝,函数中对参数的操作其实操作的就是这份拷贝)。而函数结束时要回收堆栈,那么返回值如何进行“值传递”呢?
是通过寄存器吗?我们知道,对X86来说,一般情况下,函数的返回值通过eax(或者以及edx)返回,但结构体的size太大,无法通过寄存器来返回。
这两种方法都行不通,那只能曲线救国了。
下文通过分析div()函数的汇编代码,来看看如何曲线救国。
1.函数原型
div_t div(int num,int denom);
返回值为结构体类型(包含商和余数)
typedef struct {
int quot;
int rem;
} div_t;
div()函数的实现如下:
div_t div(int num, int denom)
{
div_t r;
r.quot =num / denom;
r.rem =num % denom;
if (num>= 0 && r.rem < 0) {
r.quot++;
r.rem-= denom;
}
return(r);
}
2.汇编代码分析
我们来写一段代码,调用div()函数,如下:
void testdiv()
{
...
int iMinutes = 1000;
const intMinutesPerHour = 60;
div_t HourAndMinutes = div(iMinutes, MinutesPerHour);
...
}
来看看对应的汇编代码:
;...
mov [esp+1CH], 3E8h ;[esp+1CH] = 1000 即iMinutes
mov [esp+18H], 3Ch ;[esp+18H] =60 即MinutesPerHour
lea eax, [esp+1Ch] ;eax = esp + 1CH
mov [esp+8h], 3Ch ;[esp + 8] = 3CH 第3个参数 即60
mov edx, [esp+2Ch+var_10] ;edx = [esp + 1CH] 即1000
mov [esp+2Ch+denom], edx ;[esp + 4] = 1000 即第2个参数 iMinutes
mov [esp+2Ch+numer], eax ;[esp] = esp + 1CH 即存放结果结构体的地址
call _div
sub esp, 4 ;div通过retn 4返回,栈平衡被破坏,所以这里调整下esp
;...
函数的参数从右至左依次入栈。在call _div指令之前,[esp]中存放函数的第一参数,但这里并不是iMinutes(1000)而是testdiv()函数堆栈中某个地址,而原本是第一参数的iMinutes和第二个参数的MinutesPerHour分别变成了第二个([esp + 4])和第三个参数([esp + 8])了。可见原本只有两个参数的div()函数,编译后有三个参数,第一个参数是编译器附加的,为一指针,指向“存放返回的结构体”的地址,且该地址是在caller的堆栈上分配的。
调用call _div之后,为什么要将esp减4呢?看完div()函数的汇编代码后再回过头来分析。总结下调用div()函数的过程,用伪代码表示如下:
拨动esp为div_t结构体HourAndMinutes在栈上分配空间;
MinutesPerHour入栈; //原本第2个参数变成第3个参数
iMinutes入栈; //原本第1个参数变成第2个参数
HourAndMinutes在栈上的地址入栈; //“附加”一个指针参数
call div;
sub esp, 4; //堆栈调整
div()函数对应的汇编代码如下:
public div
arg_0 = dword ptr 8
arg_4 = dword ptr 0Ch
arg_8 = dword ptr 10h
push ebp
mov ebp, esp
lea esp, [esp-8]
mov [esp], esi
mov [esp+4], edi
mov esi, [ebp+arg_4]
mov edi, [ebp+arg_8]
mov edx, esi
mov eax, esi
sar edx, 1Fh
mov ecx, [ebp+arg_0] ;第一个参数即结构体的地址赋给ecx
idiv edi
test esi, esi
js short loc_2A61D
test edx, edx
jns short loc_2A61D
lea eax, [eax+1]
sub edx, edi
loc_2A61D:
mov [ecx], eax ;与下一行一起将计算结果保存到结构体对应的地址空间中
mov [ecx+4], edx ;
mov eax, ecx ;函数的返回值由eax返回,可见返回值其实为结构体的地址
mov esi, [esp]
mov edi, [esp+4]
mov esp, ebp
pop ebp
retn 4 ;因为div函数原型中只有两个参数,编译成汇编之后有3个参数,编译器有点
;过意不去,在返回时除了将返回地址出栈,第一个参数也出栈,这样该函数返
;回后,在调用者的堆栈中,编译器附加的第一个参数(结构体的地址)便失
;效了,这就是retn后面的4的作用。如此一来,函数调用者的堆栈可能就不
;平衡了,需要重新调整
div endp
参考注释。可见div()函数的返回值(通过eax返回)其实为结构体的地址;而其语义上的返回值(返回结构体对象)却是通过附加的指针参数“回传”的。
我们知道,函数一般通过ret指令返回。因为call指令暗含了“将函数的返回地址入栈的操作”,因此对应的ret指令暗含了“弹出函数返回地址的操作”,这一入栈、一弹出栈就平衡了。但div()函数却是通过“retn 4”返回,即在“弹出函数的返回地址”之后又弹出4个字节,将div()函数的第一个参数即附加的参数也弹出了。至于为什么要这么干,注释中的说明有些戏虐的成分,应该是与调用规范有关。总之,通过“retn 4”返回到caller中(testdiv()函数中),由于多执行一次“出栈”操作,导致caller的栈不平衡了,因此在caller中通过“sub esp,4”又将堆栈扩展4个字节,恢复栈平衡。
3.结论
可见,函数的返回值为结构体类型,其返回值既不是“值传递”也不是通过“寄存器”回传。编译器在编译此类函数时,为其附加了一个指针参数(指向的地址在caller的堆栈上),且作为函数的第一个参数(函数本身的参数依次后移),函数语义上的返回值通过该附加的指针参数回传,而函数真正的返回值就是该指针。
————————————————
版权声明:本文为CSDN博主「stillvxx」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/stillvxx/article/details/41409949