«

»

30

C++中自定义结构作为函数参数的内部实现

C++自定义结构可以作为函数参数或返回值,从高级语言的角度来看这是很自然的,不过从汇编的角度想我就有点想不明白,一个复杂的 自定义结构,在作为参数进行函数调用时,参数是怎么传递的呢?以前在用C的时候就考虑过这个问题,不过一直也没深究,现在把这个问题拿出来解决一下。

自定义结构体作为函数参数
传递较为复杂的参数,比较高效的方法是使用指针,但倘若我们的代码里并没有用指针来实现,硬是把结构体作为参数类型,那么C++编译器内部是怎么做的?难道也会隐式地使用指针吗?为了进行调试,我先用C++写了一个小程序,代码如下:
 

#include <iostream>
#include <string>

 

using namespace std;

struct Student {
    char Name[10];
    unsigned short Age;
    int Score;
};

void print_stu_info(Student stu)
{
    cout << "Student Card\n"
            "Name: " << stu.Name << endl <<
            "Age: " << stu.Age << endl <<
            "Score: " << stu.Score << endl << endl;
}

int main(void)
{
    Student monitor = {"Jason", 18, 100};

    print_stu_info(monitor);

    cin.get();

    return 0;
}


这个简短的演示程序涉及到了自定义结构作为函数参数,将它编译后我们从汇编的角度来解读它。
先来看main()的反汇编代码:

int main(void)
{
00851670  push        ebp  
00851671  mov         ebp,esp  
00851673  sub         esp,0DCh  
00851679  push        ebx  
0085167A  push        esi  
0085167B  push        edi                            ;先保存寄存器原来的值
0085167C  lea         edi,[ebp-0DCh]                 ;为变量分配一段栈空间
00851682  mov         ecx,37h  
00851687  mov         eax,0CCCCCCCCh  
0085168C  rep stos    dword ptr es:[edi]             ;用0xCC(int 3)初始化这段内存
0085168E  mov         eax,dword ptr [___security_cookie (859000h)]  
00851693  xor         eax,ebp  
00851695  mov         dword ptr [ebp-4],eax  
    
Student monitor = {"Jason", 18, 100};            ;下面是对monitor对象的初始化操作
00851698  mov         eax,dword ptr [string "Jason" (85785Ch)]  
0085169D  mov         dword ptr [ebp-18h],eax        ;"Jaso" 4字节写入[ebp-18h]
008516A0  mov         cx,word ptr ds:[857860h]
008516A7  mov         word ptr [ebp-14h],cx          ;"n\0"  2字节写入[ebp-14h]
008516AB  xor         eax,eax  
008516AD  mov         dword ptr [ebp-12h],eax        ;[ebp-12h]两字节清零
008516B0  mov         eax,12h  
008516B5  mov         word ptr [ebp-0Eh],ax          ;设置Age
008516B9  mov         dword ptr [ebp-0Ch],64h        ;设置Score

 

    print_stu_info(monitor);                         ;下面的代码展示了传递参数的步骤
008516C0  sub         esp,10h  
008516C3  mov         eax,esp                        ;eax为栈顶指针
008516C5  mov         ecx,dword ptr [ebp-18h]        ;依次地将16个字节的内容拷贝到[esp]~[esp+10h]的内存中,
008516C8  mov         dword ptr [eax],ecx            ;其实跟传递其他类型参数一样,只不过这里没有用到push,
008516CA  mov         edx,dword ptr [ebp-14h]        ;并非隐式地使用指针
008516CD  mov         dword ptr [eax+4],edx
008516D0  mov         ecx,dword ptr [ebp-10h]  
008516D3  mov         dword ptr [eax+8],ecx  
008516D6  mov         edx,dword ptr [ebp-0Ch]  
008516D9  mov         dword ptr [eax+0Ch],edx  
008516DC  call        print_stu_info (8511EFh)  
008516E1  add         esp,10h  

    cin.get();
008516E4  mov         esi,esp  
008516E6  mov         ecx,dword ptr [__imp_std::cin (85A324h)]  
008516EC  call        dword ptr [__imp_std::basic_istream<char,std::char_traits<char> >::get (85A328h)]  
008516F2  cmp         esi,esp  
008516F4  call        @ILT+405(__RTC_CheckEsp) (85119Ah)  

    return 0;
008516F9  xor         eax,eax  
}


虽然没有用到push pop等操作栈的指令,但实际上仍是将整个结构体包含的内容复制到栈空间中,完毕后esp指向栈顶同时也指向结构体的首地址,再调用call,便跟常规的函数调用无异了。

print_stu_info对参数的引用即是通过[ebp+xx]来实现的:

void print_stu_info(Student stu)
{
008514B0  push        ebp  
008514B1  mov         ebp,esp  
008514B3  sub         esp,0DCh  
008514B9  push        ebx  
008514BA  push        esi  
008514BB  push        edi  
008514BC  lea         edi,[ebp-0DCh]  
008514C2  mov         ecx,37h  
008514C7  mov         eax,0CCCCCCCCh  
008514CC  rep stos    dword ptr es:[edi]  
    
cout << "Student Card\n"
            "Name: " << stu.Name << endl <<
            "Age: " << stu.Age << endl <<
            "Score: " << stu.Score << endl << endl;
008514CE  mov         esi,esp  
008514D0  mov         eax,dword ptr [__imp_std::endl (85A32Ch)]  
008514D5  push        eax  
008514D6  mov         edi,esp  
008514D8  mov         ecx,dword ptr [__imp_std::endl (85A32Ch)]  
008514DE  push        ecx  
008514DF  mov         ebx,esp  
008514E1  mov         edx,dword ptr [ebp+14h]  
008514E4  push        edx  
008514E5  push        offset string "Score: " (857850h)  
008514EA  mov         eax,esp  
008514EC  mov         ecx,dword ptr [__imp_std::endl (85A32Ch)]  
008514F2  push        ecx  
008514F3  mov         ecx,esp  
008514F5  movzx       edx,word ptr [ebp+12h]  
008514F9  push        edx  
008514FA  push        offset string "Age: " (857848h)  
008514FF  mov         edx,esp  
00851501  mov         dword ptr [ebp-0D0h],esi  
00851507  mov         esi,dword ptr [__imp_std::endl (85A32Ch)]  
0085150D  push        esi  
0085150E  lea         esi,[stu]  
00851511  push        esi  
00851512  push        offset string "Student Card\nName: " (857830h)  
········
}

自定义结构体作为函数返回值
 下面将代码修改一下以便研究返回结构体的实现方式,C++代码如下:

#include <iostream>
#include <string>

 

using namespace std;

struct Student {
    char Name[10];
    unsigned short Age;
    int Score;
};

Student init_stu_info()
{
    Student monitor = {"Jason", 18, 100};
    return monitor;
}

int main(void)
{
    Student stu = init_stu_info();

    cout << "Student Card\n"
            "Name: " << stu.Name << endl <<
            "Age: " << stu.Age << endl <<
            "Score: " << stu.Score << endl << endl;

    cin.get();

    return 0;
}

 看了一眼main()里调用init_stu_info的代码,顿时豁然开朗~

int main(void)
{
00BF1560  push        ebp  
00BF1561  mov         ebp,esp  
00BF1563  sub         esp,11Ch  
00BF1569  push        ebx  
00BF156A  push        esi  
00BF156B  push        edi  
00BF156C  lea         edi,[ebp-11Ch]  
00BF1572  mov         ecx,47h  
00BF1577  mov         eax,0CCCCCCCCh  
00BF157C  rep stos    dword ptr es:[edi]  
00BF157E  mov         eax,dword ptr [___security_cookie (0BF9000h)]  
00BF1583  xor         eax,ebp  
00BF1585  mov         dword ptr [ebp-4],eax  
    
Student stu = init_stu_info();
00BF1588  lea         eax,[ebp-108h]                   ;申请一块足够大的栈空间用以保存返回值
00BF158E  push        eax  
00BF158F  call        init_stu_info (0BF1253h)         ;init_stu_info会把返回的结构体保存到上述栈空间里
00BF1594  add         esp,4                            ;之后主函数main()直接通过[eax+xx]来引用返回的内容
00BF1597  mov         ecx,dword ptr [eax]  
00BF1599  mov         dword ptr [ebp-0F0h],ecx  
00BF159F  mov         edx,dword ptr [eax+4]  
00BF15A2  mov         dword ptr [ebp-0ECh],edx  
00BF15A8  mov         ecx,dword ptr [eax+8]  
00BF15AB  mov         dword ptr [ebp-0E8h],ecx  
00BF15B1  mov         edx,dword ptr [eax+0Ch]  
00BF15B4  mov         dword ptr [ebp-0E4h],edx  
00BF15BA  mov         eax,dword ptr [ebp-0F0h]  
00BF15C0  mov         dword ptr [ebp-18h],eax  
00BF15C3  mov         ecx,dword ptr [ebp-0ECh]  
00BF15C9  mov         dword ptr [ebp-14h],ecx  
00BF15CC  mov         edx,dword ptr [ebp-0E8h]  
00BF15D2  mov         dword ptr [ebp-10h],edx  
00BF15D5  mov         eax,dword ptr [ebp-0E4h]  
00BF15DB  mov         dword ptr [ebp-0Ch],eax  
········
}

 一样还是通过栈来保存/传递数据

总结:
1.函数传参的本质就是将参数压栈,对于自定义的结构体,压栈不一定使用push,但都是规规矩矩地通过栈来实现参数传递。并不会隐式地传递指针(虽然这样做效率会更高,但可见编译器还是严格按照开发者的代码来按部就班地完成任务)。

2. 结构体作为函数返回值时,其值并不一定保存在eax中,而是调用者事先在栈空间分配一段内存以供保存返回的数据,被调用者对该段内存进行修改,返回后调用 者直接引用这段内存,由此实现了数据的“传递”。这里eax通常不用来保存返回值(因为大多数情况下4bytes存不下咯)。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>