函数返回结构体的内幕

在刚接触C语言编程时,无论是前辈还是教科书,都反复告诫我们两件事:

①函数的参数是值传递(意味着在函数中对参数本身的修改无法“传回”);

②不要返回函数体内局部变量的地址,因为函数结束时栈会回收,局部变量也随之销毁(如果局部变量为类对象,其析构函数会被自动调用),但可以返回局部变量本身。

 

那么如果一个函数的返回值为结构体类型,其返回值是如何“返回”的呢?

是通过“值传递”吗?我们知道函数参数的值传递,就是将参数入栈(因此参数有自己的一份拷贝,函数中对参数的操作其实操作的就是这份拷贝)。而函数结束时要回收堆栈,那么返回值如何进行“值传递”呢?

是通过寄存器吗?我们知道,对X86来说,一般情况下,函数的返回值通过eax(或者以及edx)返回,但结构体的size太大,无法通过寄存器来返回。

 

这两种方法都行不通,那只能曲线救国了。

下文通过分析div()函数的汇编代码,来看看如何曲线救国。

 

1.函数原型

<stdlib.h>
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之后,为什么要将esp4呢?看完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的堆栈上),且作为函数的第一个参数(函数本身的参数依次后移),函数语义上的返回值通过该附加的指针参数回传,而函数真正的返回值就是该指针。

  • 11
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值