C++自定义结构可以作为函数参数或返回值,从高级语言的角度来看这是很自然的,不过从汇编的角度想我就有点想不明白,一个复杂的 自定义结构,在作为参数进行函数调用时,参数是怎么传递的呢?以前在用C的时候就考虑过这个问题,不过一直也没深究,现在把这个问题拿出来解决一下。
自定义结构体作为函数参数
传递较为复杂的参数,比较高效的方法是使用指针,但倘若我们的代码里并没有用指针来实现,硬是把结构体作为参数类型,那么C++编译器内部是怎么做的?难道也会隐式地使用指针吗?为了进行调试,我先用C++写了一个小程序,代码如下:
#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()的反汇编代码:
{
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]来实现的:
{
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 <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的代码,顿时豁然开朗~
{
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存不下咯)。