运行机制探索
通过第2节我们学会了可变参数表的使用方法,相信喜欢抛根问底的读者还不甘心,必然想知道如下问题:
(1)为什么按照第2节的做法就可以获得可变参数并对其进行操作?
(2)C/C++在底层究竟是依靠什么来对这一语法进行支持的,为什么其它语言就不能提供可变参数表呢?
我们带着这些疑问来一步步进行摸索。
3.1 调用机制反汇编
反汇编是研究语法深层特性的终极良策,先来看看2.2节例子中主函数进行max ( 5, 5, 6 ,3 ,8 ,5)调用时的反汇编:
从上述反汇编代码中我们可以看出,C/C++函数调用的过程中:
第一步:将参数从右向左入栈(第1~6行);
第二步:调用call指令进行跳转(第7行)。
这两步包含了深刻的含义,它说明C/C++默认的调用方式为由调用者管理参数入栈的操作,且入栈的顺序为从右至左,这种调用方式称为_cdecl调用。x86系统的入栈方向为从高地址到低地址,故第1至n个参数被放在了地址递增的堆栈内。在被调用函数内部,读取这些堆栈的内容就可获得各个参数的值,让我们反汇编到max函数的内部:
分析上述反汇编代码,对于一个真正的程序员而言,将是一种很大的享受;而对于初学者,也将使其受益良多。所以请一定要赖着头皮认真研究,千万不要被吓倒!
行1~10进行执行函数内代码的准备工作,保存现场。第2行对堆栈进行移动;第3行则意味着max函数为其内部局部变量准备的堆栈空间为50h字节;第11行表示把变量n的内存空间安排在了函数内部局部栈底减8的位置(占用4个字节)。
第12~13行非常关键,对应着va_start ( ap, num ),这两行将第一个可变参数的地址赋值给了指针ap。另外,从第12行可以看出num的地址为ebp+0Ch;从第13行可以看出ap被分配在函数内部局部栈底减4的位置上(占用4个字节)。
第22~27行最为关键,对应着va_arg (ap, int)。其中,22~24行的作用为将ap指向下一可变参数(可变参数的地址间隔为4个字节,从add eax,4可以看出);25~27行则取当前可变参数的值赋给变量t。这段反汇编很奇怪,它先移动可变参数指针,再在赋值指令里面回过头来取先前的参数值赋给t(从mov edx,dword ptr [ecx-4]语句可以看出)。Visual C++同学玩得有意思,不知道碰见同样的情况Visual Basic等其它同学怎么玩?
第36~41行恢复现场和堆栈地址,执行函数返回操作。
痛苦的反汇编之旅差不多结束了,看了这段反汇编我们总算弄明白了可变参数的存放位置以及它们被读取的方式,顿觉全省轻松!
2、特殊的调用约定
除此之外,我们需要了解C/C++函数调用对参数占用空间的一些特殊约定,因为在_cdecl调用协议中,有些变量类型是按照其它变量的尺寸入栈的。
例如,字符型变量将被自动扩展为一个字的空间,因为入栈操作针对的是一个字。
参数n实际占用的空间为( ( sizeof(n) + sizeof(int) - 1 ) & ~( sizeof(int) - 1 ) ),这就是第2.1节_INTSIZEOF(v)宏的来历!
既然如此,前面给出的va_arg ( list, mode )宏为什么玩这么大的飞机就很清楚了。这个问题就留个读者您来分析
通过第2节我们学会了可变参数表的使用方法,相信喜欢抛根问底的读者还不甘心,必然想知道如下问题:
(1)为什么按照第2节的做法就可以获得可变参数并对其进行操作?
(2)C/C++在底层究竟是依靠什么来对这一语法进行支持的,为什么其它语言就不能提供可变参数表呢?
我们带着这些疑问来一步步进行摸索。
3.1 调用机制反汇编
反汇编是研究语法深层特性的终极良策,先来看看2.2节例子中主函数进行max ( 5, 5, 6 ,3 ,8 ,5)调用时的反汇编:
1. 004010C8 push 5 2. 004010CA push 8 3. 004010CC push 3 4. 004010CE push 6 5. 004010D0 push 5 6. 004010D2 push 5 7. 004010D4 call @ILT+5(max) (0040100a) |
从上述反汇编代码中我们可以看出,C/C++函数调用的过程中:
第一步:将参数从右向左入栈(第1~6行);
第二步:调用call指令进行跳转(第7行)。
这两步包含了深刻的含义,它说明C/C++默认的调用方式为由调用者管理参数入栈的操作,且入栈的顺序为从右至左,这种调用方式称为_cdecl调用。x86系统的入栈方向为从高地址到低地址,故第1至n个参数被放在了地址递增的堆栈内。在被调用函数内部,读取这些堆栈的内容就可获得各个参数的值,让我们反汇编到max函数的内部:
int max ( int num, ...) { 1. 00401020 push ebp 2. 00401021 mov ebp,esp 3. 00401023 sub esp,50h 4. 00401026 push ebx 5. 00401027 push esi 6. 00401028 push edi 7. 00401029 lea edi,[ebp-50h] 8. 0040102C mov ecx,14h 9. 00401031 mov eax,0CCCCCCCCh 10. 00401036 rep stos dword ptr [edi] va_list ap; int m = -0x7FFFFFFF; /* 32系统中最小的整数 */ 11. 00401038 mov dword ptr [ebp-8],80000001h va_start ( ap, num ); 12. 0040103F lea eax,[ebp+0Ch] 13. 00401042 mov dword ptr [ebp-4],eax for ( int i= 0; i< num; i++ ) 14. 00401045 mov dword ptr [ebp-0Ch],0 15. 0040104C jmp max+37h (00401057) 16. 0040104E mov ecx,dword ptr [ebp-0Ch] 17. 00401051 add ecx,1 18. 00401054 mov dword ptr [ebp-0Ch],ecx 19. 00401057 mov edx,dword ptr [ebp-0Ch] 20. 0040105A cmp edx,dword ptr [ebp+8] 21. 0040105D jge max+61h (00401081) { int t= va_arg (ap, int); 22. 0040105F mov eax,dword ptr [ebp-4] 23. 00401062 add eax,4 24. 00401065 mov dword ptr [ebp-4],eax 25. 00401068 mov ecx,dword ptr [ebp-4] 26. 0040106B mov edx,dword ptr [ecx-4] 27. 0040106E mov dword ptr [t],edx if ( t > m ) 28. 00401071 mov eax,dword ptr [t] 29. 00401074 cmp eax,dword ptr [ebp-8] 30. 00401077 jle max+5Fh (0040107f) m = t; 31. 00401079 mov ecx,dword ptr [t] 32. 0040107C mov dword ptr [ebp-8],ecx } 33. 0040107F jmp max+2Eh (0040104e) va_end (ap); 34. 00401081 mov dword ptr [ebp-4],0 return m; 35. 00401088 mov eax,dword ptr [ebp-8] } 36. 0040108B pop edi 37. 0040108C pop esi 38. 0040108D pop ebx 39. 0040108E mov esp,ebp 40. 00401090 pop ebp 41. 00401091 ret |
分析上述反汇编代码,对于一个真正的程序员而言,将是一种很大的享受;而对于初学者,也将使其受益良多。所以请一定要赖着头皮认真研究,千万不要被吓倒!
行1~10进行执行函数内代码的准备工作,保存现场。第2行对堆栈进行移动;第3行则意味着max函数为其内部局部变量准备的堆栈空间为50h字节;第11行表示把变量n的内存空间安排在了函数内部局部栈底减8的位置(占用4个字节)。
第12~13行非常关键,对应着va_start ( ap, num ),这两行将第一个可变参数的地址赋值给了指针ap。另外,从第12行可以看出num的地址为ebp+0Ch;从第13行可以看出ap被分配在函数内部局部栈底减4的位置上(占用4个字节)。
第22~27行最为关键,对应着va_arg (ap, int)。其中,22~24行的作用为将ap指向下一可变参数(可变参数的地址间隔为4个字节,从add eax,4可以看出);25~27行则取当前可变参数的值赋给变量t。这段反汇编很奇怪,它先移动可变参数指针,再在赋值指令里面回过头来取先前的参数值赋给t(从mov edx,dword ptr [ecx-4]语句可以看出)。Visual C++同学玩得有意思,不知道碰见同样的情况Visual Basic等其它同学怎么玩?
第36~41行恢复现场和堆栈地址,执行函数返回操作。
痛苦的反汇编之旅差不多结束了,看了这段反汇编我们总算弄明白了可变参数的存放位置以及它们被读取的方式,顿觉全省轻松!
2、特殊的调用约定
除此之外,我们需要了解C/C++函数调用对参数占用空间的一些特殊约定,因为在_cdecl调用协议中,有些变量类型是按照其它变量的尺寸入栈的。
例如,字符型变量将被自动扩展为一个字的空间,因为入栈操作针对的是一个字。
参数n实际占用的空间为( ( sizeof(n) + sizeof(int) - 1 ) & ~( sizeof(int) - 1 ) ),这就是第2.1节_INTSIZEOF(v)宏的来历!
既然如此,前面给出的va_arg ( list, mode )宏为什么玩这么大的飞机就很清楚了。这个问题就留个读者您来分析