从汇编角度查看C语言函数调用约定
2017-07-25 19:03
246 查看
为了防止出现不必要的代码影响汇编语言的查看,所以程序中不使用任何库函数,以保持汇编代码的简洁。
这里所使用的汇编是VC的MASM。
默认函数调用方式
对应汇编代码:
VC默认的函数调用方式就是
微软的
该种规约不使用任何寄存器传递参数,参数全部在栈上(从右到左依次入栈),其规则完全符合ABI规定。
对应汇编代码:
微软的
同
在寄存器保护问题上同
对应汇编代码:
微软的
同前,
该方法使用ECX和EDX传递前两个参数(从左到右),其余参数在栈上(从右到左依次入栈)。
根据ABI,调用者不需要保护ECX和EDX,因此该规约并不违反ABI。
必须注意,微软的thiscall和g++的thiscall在二进制上是不兼容的。
微软的thiscall采用ECX作为this指针,其余参数全部在栈上(从右到左依次入栈)。
而g++的thiscall就是__cdecl,它把this指针当作第一个参数,连同其他参数一起被放在栈上(从右到左依次入栈)。
显然这两种方法都不违背ABI。
微软
对于这种类型的函数,对于调用者(C/C++程序),编译器保证其生成代码是符合ABI规定的。
但是被调用函数也就是naked函数本身,编写者必须自己实现对ABI的兼容。
GNU的
在i386下这里的n取值只能是0、1、2、3。它表示该函数使用几个寄存器来传递参数。
当n=0时,所有参数都在栈上(从右到左依次入栈)。也就是
当n=1时,第1个参数在EAX,其余参数在栈上(从右到左依次入栈)。
当n=2时,第1个参数在EAX,第2个参数在EDX,其余参数在栈上(从右到左依次入栈)。
当n=3时,第1个参数在EAX,第2个参数在EDX,第3个参数在ECX,其余参数在栈上(从右到左依次入栈)。
由于EAX、EDX、ECX都是不需要被调用者保护的寄存器,所以这里也不违背ABI规定。
Linux内核(i386)的
asmlinkage被定义为
fastcall被定义为
参考链接:
维基百科-X86调用约定:https://en.wikipedia.org/wiki/X86_calling_conventions
i386 ABI之寄存器保护规则:http://blog.csdn.net/axx1611/article/details/5138618
StackOverflow-Windows头文件中MAC宏是什么:https://stackoverflow.com/questions/2376478/whats-with-ifdef-mac-in-windows-header-files
这里所使用的汇编是VC的MASM。
默认函数调用方式__cdecl
int add(int a, int b) { return a + b; } int main() { int a = 1, b = 2; return add(a,b); }
对应汇编代码:
; Listing generated by Microsoft (R) Optimizing Compiler Version 19.10.25019.0 ; 伪指令,如指定处理器指令,存储模型等 TITLE D:\C&C++\CppLearn\CLang\FunctionStack.c .686P .XMM include listing.inc .model flat ; 导入必要的静态库,如C语言运行时 INCLUDELIB MSVCRTD INCLUDELIB OLDNAMES ; 所有函数过程的声明 PUBLIC _add PUBLIC _main EXTRN __RTC_CheckEsp:PROC EXTRN __RTC_InitBase:PROC EXTRN __RTC_Shutdown:PROC ; COMDAT rtc$TMZ rtc$TMZ SEGMENT __RTC_Shutdown.rtc$TMZ DD FLAT:__RTC_Shutdown rtc$TMZ ENDS ; COMDAT rtc$IMZ rtc$IMZ SEGMENT __RTC_InitBase.rtc$IMZ DD FLAT:__RTC_InitBase rtc$IMZ ENDS _TEXT SEGMENT ; 局部变量的偏移地址 _b$ = -20 ; size = 4 _a$ = -8 ; size = 4 ; main函数过程定义 _main PROC ; COMDAT ; 函数调用前必要的初始化,如寄存器入栈以保存CPU运行现场 push ebp ; 保存原来的基栈指针 mov ebp, esp ; 将原来的栈顶指针作为新的基栈指针 sub esp, 216 ; 000000d8H,为堆栈分配内存 push ebx push esi push edi ; 对堆栈进行初始化 lea edi, DWORD PTR [ebp-216]; 获取堆栈地址存入edi寄存器中 mov ecx, 54 ; 00000036H mov eax, -858993460 ; ccccccccH rep stosd ; rep指令表示重复执行后面的指令,重复次数为ecx中的值 ; stosd表示使用eax中的值对es:[edi]指向的地址进行初始化,单位为dword ; 所以36H * 4 = d8H ; int a = 1, b = 2; 变量赋值 mov DWORD PTR _a$[ebp], 1 mov DWORD PTR _b$[ebp], 2 ; return add(a,b); ; 可以看出函数调用的入栈顺序:从右到左 mov eax, DWORD PTR _b$[ebp] push eax ;函数参数入栈 mov ecx, DWORD PTR _a$[ebp] push ecx ;函数参数入栈 call _add ;调用add函数 add esp, 8 ;直接还原栈顶指针,不需要出栈数据 ; 函数执行完成后,寄存器出栈,恢复执行现场 pop edi pop esi pop ebx add esp, 216 ; 000000d8H,回收堆栈内存 cmp ebp, esp ; 比较基栈指针与栈顶指针是否相等 call __RTC_CheckEsp ; 堆栈检测,防止栈溢出 mov esp, ebp ; 恢复栈顶指针 pop ebp ; 恢复基栈指针 ret 0 _main ENDP _TEXT ENDS _TEXT SEGMENT ; 局部变量的偏移地址 _a$ = 8 ; size = 4 _b$ = 12 ; size = 4 ; add函数过程定义 _add PROC ; COMDAT ; 函数调用前必要的初始化,如寄存器入栈以保存CPU运行现场 push ebp ; 保存原来的基栈指针 mov ebp, esp ; 将原来的栈顶指针作为新的基栈指针 sub esp, 192 ; 000000c0H,为堆栈分配内存 push ebx push esi push edi ; 对堆栈进行初始化 lea edi, DWORD PTR [ebp-192] mov ecx, 48 ; 00000030H mov eax, -858993460 ; ccccccccH rep stosd ; rep指令表示重复执行后面的指令,重复次数为ecx中的值 ; stosd表示使用eax中的值对es:[edi]指向的地址进行初始化,单位为dword ; 所以30H * 4 = c0H ; return a + b; mov eax, DWORD PTR _a$[ebp] add eax, DWORD PTR _b$[ebp] ; eax作为返回值 ; 函数执行完成后,寄存器出栈,恢复执行现场 pop edi pop esi pop ebx mov esp, ebp ; 恢复栈顶指针 pop ebp ; 恢复基栈指针 ret 0 ; 函数返回 _add ENDP _TEXT ENDS END
VC默认的函数调用方式就是
__cdecl。
微软的
__cdecl与GNU的
__attribute__ ((cdecl))
__cdecl想必大家都不陌生了,gcc出于兼容,可以使用
__attribute__ ((cdecl))达到同样的效果。
该种规约不使用任何寄存器传递参数,参数全部在栈上(从右到左依次入栈),其规则完全符合ABI规定。
__stdcall函数调用方式
int __stdcall add(int a, int b) { return a + b; } int main() { int a = 1, b = 2; return add(a,b); }
对应汇编代码:
; 省略前面的若干伪指令 ... ; COMDAT _main _TEXT SEGMENT _b$ = -20 ; size = 4 _a$ = -8 ; size = 4 _main PROC ; COMDAT ; 函数调用前必要的初始化,如寄存器入栈以保存CPU运行现场 push ebp mov ebp, esp sub esp, 216 ; 000000d8H push ebx push esi push edi lea edi, DWORD PTR [ebp-216] mov ecx, 54 ; 00000036H mov eax, -858993460 ; ccccccccH rep stosd ; int a = 1, b = 2; mov DWORD PTR _a$[ebp], 1 mov DWORD PTR _b$[ebp], 2 ; return add(a,b); ; 可以看出函数调用的入栈顺序:从右到左 mov eax, DWORD PTR _b$[ebp] push eax mov ecx, DWORD PTR _a$[ebp] push ecx call _add@8 ; 不用自己还原堆栈,在函数执行ret指令时还原堆栈状态 pop edi pop esi pop ebx add esp, 216 ; 000000d8H cmp ebp, esp call __RTC_CheckEsp mov esp, ebp pop ebp ret 0 _main ENDP _TEXT ENDS ; COMDAT _add@8 _TEXT SEGMENT _a$ = 8 ; size = 4 _b$ = 12 ; size = 4 _add@8 PROC ; COMDAT ; 函数调用前必要的初始化,如寄存器入栈以保存CPU运行现场 push ebp mov ebp, esp sub esp, 192 ; 000000c0H push ebx push esi push edi lea edi, DWORD PTR [ebp-192] mov ecx, 48 ; 00000030H mov eax, -858993460 ; ccccccccH rep stosd ; return a + b; mov eax, DWORD PTR _a$[ebp] add eax, DWORD PTR _b$[ebp] pop edi pop esi pop ebx mov esp, ebp pop ebp ret 8 ; 返回时,清除堆栈 _add@8 ENDP _TEXT ENDS END
微软的
__stdcall与GNU的
__attribute__ ((stdcall))
同
__attribute__ ((cdecl)),
__attribute__ ((stdcall))也是用来兼容微软的。
在寄存器保护问题上同
__cdecl,不使用任何寄存器传递参数且符合ABI。
__fastcall函数调用方式
int __fastcall add(int a, int b) { return a + b; } int main() { int a = 1, b = 2; return add(a,b); }
对应汇编代码:
; 省略前面的若干伪指令 ... ; COMDAT _main _TEXT SEGMENT _b$ = -20 ; size = 4 _a$ = -8 ; size = 4 _main PROC ; COMDAT push ebp mov ebp, esp sub esp, 216 ; 000000d8H push ebx push esi push edi lea edi, DWORD PTR [ebp-216] mov ecx, 54 ; 00000036H mov eax, -858993460 ; ccccccccH rep stosd ; int a = 1, b = 2; mov DWORD PTR _a$[ebp], 1 mov DWORD PTR _b$[ebp], 2 ; return add(a,b); ; 使用特定寄存器的方式传递参数,edx和ecx用来传递前两个参数 ; 如果参数超过两个,其余参数就使用堆栈传递 ; 因为堆栈在内存中分配,计算机访问内存速度远慢于访问寄存器的速度 ; 所以使用寄存器传递参数比堆栈传递参数要快 mov edx, DWORD PTR _b$[ebp] mov ecx, DWORD PTR _a$[ebp] call @add@8 ; 因为没有使用入栈的方式传递参数,所以不需要清除堆栈 pop edi pop esi pop ebx add esp, 216 ; 000000d8H cmp ebp, esp call __RTC_CheckEsp mov esp, ebp pop ebp ret 0 _main ENDP _TEXT ENDS ; COMDAT @add@8 _TEXT SEGMENT _b$ = -20 ; size = 4 _a$ = -8 ; size = 4 @add@8 PROC ; COMDAT ; _a$ = ecx ; _b$ = edx ; push ebp mov ebp, esp sub esp, 216 ; 000000d8H push ebx push esi push edi push ecx lea edi, DWORD PTR [ebp-216] mov ecx, 54 ; 00000036H mov eax, -858993460 ; ccccccccH rep stosd pop ecx mov DWORD PTR _b$[ebp], edx mov DWORD PTR _a$[ebp], ecx ; return a + b; mov eax, DWORD PTR _a$[ebp] add eax, DWORD PTR _b$[ebp] pop edi pop esi pop ebx mov esp, ebp pop ebp ret 0 @add@8 ENDP _TEXT ENDS END
微软的
__fastcall与GNU的
__attribute__ ((fastcall))
同前,
__attribute__ ((fastcall))用来兼容微软。
该方法使用ECX和EDX传递前两个参数(从左到右),其余参数在栈上(从右到左依次入栈)。
根据ABI,调用者不需要保护ECX和EDX,因此该规约并不违反ABI。
其他参数传递方式
thiscall调用规约(仅C++)
必须注意,微软的thiscall和g++的thiscall在二进制上是不兼容的。
微软的thiscall采用ECX作为this指针,其余参数全部在栈上(从右到左依次入栈)。
而g++的thiscall就是__cdecl,它把this指针当作第一个参数,连同其他参数一起被放在栈上(从右到左依次入栈)。
显然这两种方法都不违背ABI。
微软
__declspec(naked)与GNU的
__attribute__ ((naked))
__attribute__ ((naked))是g++对微软的兼容。
对于这种类型的函数,对于调用者(C/C++程序),编译器保证其生成代码是符合ABI规定的。
但是被调用函数也就是naked函数本身,编写者必须自己实现对ABI的兼容。
GNU的
__attribute__ ((regparm(n)))
在i386下这里的n取值只能是0、1、2、3。它表示该函数使用几个寄存器来传递参数。
当n=0时,所有参数都在栈上(从右到左依次入栈)。也就是
__cdecl。
当n=1时,第1个参数在EAX,其余参数在栈上(从右到左依次入栈)。
当n=2时,第1个参数在EAX,第2个参数在EDX,其余参数在栈上(从右到左依次入栈)。
当n=3时,第1个参数在EAX,第2个参数在EDX,第3个参数在ECX,其余参数在栈上(从右到左依次入栈)。
由于EAX、EDX、ECX都是不需要被调用者保护的寄存器,所以这里也不违背ABI规定。
Linux内核(i386)的
asmlinkage与
fastcall
asmlinkage被定义为
__attribute__ ((regparm(0)))。
fastcall被定义为
__attribute__ ((regparm(3)))。
Windows API按照平台不同,选择不同的函数调用方式
// 以下是minwindef.h头文件中的部分代码 #if (!defined(_MAC)) && ((_MSC_VER >= 800) || defined(_STDCALL_SUPPORTED)) #define pascal __stdcall #else #define pascal #endif #if defined(DOSWIN32) || defined(_MAC) #define cdecl _cdecl #ifndef CDECL #define CDECL _cdecl #endif #else #define cdecl #ifndef CDECL #define CDECL #endif #endif #ifdef _MAC #define CALLBACK PASCAL #define WINAPI CDECL #define WINAPIV CDECL #define APIENTRY WINAPI #define APIPRIVATE CDECL #ifdef _68K_ #define PASCAL __pascal #else #define PASCAL #endif #elif (_MSC_VER >= 800) || defined(_STDCALL_SUPPORTED) #define CALLBACK __stdcall #define WINAPI __stdcall #define WINAPIV __cdecl #define APIENTRY WINAPI #define APIPRIVATE __stdcall #define PASCAL __stdcall #else #define CALLBACK #define WINAPI #define WINAPIV #define APIENTRY WINAPI #define APIPRIVATE #define PASCAL pascal #endif
__pascal是 Pascal 语言(Delphi)的函数调用方式,也可以在 C/C++ 中使用,参数压栈顺序从左往右。返回时的清栈方式与
__stdcall相同。
参考链接:
维基百科-X86调用约定:https://en.wikipedia.org/wiki/X86_calling_conventions
__stdcall,
__cdecl,
__pascal,
__fastcall的区别:http://c.biancheng.net/cpp/html/2847.html
i386 ABI之寄存器保护规则:http://blog.csdn.net/axx1611/article/details/5138618
StackOverflow-Windows头文件中MAC宏是什么:https://stackoverflow.com/questions/2376478/whats-with-ifdef-mac-in-windows-header-files
附录
1.VC版本清单
_MSC_VER | _MSC_FULL_VER | VC版本 | 备注 |
---|---|---|---|
600 | C/C++Compiler 6.0 | ||
700 | C/C++Compiler 7.0 | ||
800 | VC1.0 | ||
900 | VC2.0 | ||
1000 | VC4.0 | ||
1010 | VC4.1 | ||
1020 | VC4.2 | ||
1100 | VC5.0 | Visual Studio 97 | |
1200 | 12008804 | VC6.0 | Visual Studio 6.0 |
1300 | 13009466 | VC7.0 | Visual Studio.net 2002 |
1310 | 13102292 | VC7.1 | Visual Studio.net 2003 |
1400 | 140050320 | VC8.0 | Visual Studio 2005 |
1400 | 140050727 | VC8.0 | Visual Studio 2005 SP1 |
1500 | 150021022 | VC9.0 | Visual Studio 2008 |
1500 | 150030729 | VC9.0 | Visual Studio 2008 Update1 |
1600 | 160030319 | VC10.0 | Visual Studio 2010 |
1600 | 160040219 | VC10.0 | Visual Studio 2010 Update1 |
1700 | 170050727 | VC11.0 | Visual Studio 2012 |
1700 | 170051106 | VC11.0 | Visual Studio 2012 Update1 |
1700 | 170060315 | VC11.0 | Visual Studio 2012 Update2 |
1700 | 170060610 | VC11.0 | Visual Studio 2012 Update3 |
1700 | 170061030 | VC11.0 | Visual Studio 2012 Update4 |
1800 | 180021005 | VC12.0 | Visual Studio 2013 RTM / Update1 |
1800 | 180030501 | VC12.0 | Visual Studio 2013 Update2 |
1800 | 180030723 | VC12.0 | Visual Studio 2013 Update3 |
1800 | 180031101 | VC12.0 | Visual Studio 2013 Update4 |
1800 | 180040629 | VC12.0 | Visual Studio 2013 Update5 |
1900 | 190023026 | VC14.0 | Visual Studio 2015 |
1900 | 190023506 | VC14.0 | Visual Studio 2015 Update1 |
1900 | 190023918 | VC14.0 | Visual Studio 2015 Update2 |
1900 | 190024210 | VC14.0 | Visual Studio 2015 Update3 |
相关文章推荐
- 从汇编角度看Linux C函数的调用约定和参数传递的细节
- 从汇编角度理解C++虚函数调用机制
- 从汇编的角度分析函数调用过程(2)
- C语言函数调用约定
- C语言函数调用约定
- C语言函数调用约定
- 从汇编看c语言函数调用
- ARM汇编程序---通过ARM汇编调用C语言函数实现累加
- C语言函数和汇编函数相互调用
- 从汇编看c语言函数调用
- C语言函数调用约定
- [32位汇编系列]003 汇编中__stdcall 调用约定以及参数传递
- Win32汇编中的函数调用约定
- 利用汇编查看C++函数调用
- 32位汇编第四讲,干货分享,汇编注入的实现,以及快速定位调用API的数量(OD查看)
- 从汇编角度来理解linux下多层函数调用堆栈运行状态
- cdecl和stdcall调用约定的汇编代码对比
- C语言函数调用约定
- 从汇编看c语言函数调用
- 反汇编时的函数识别及各函数调用约定的汇编代码分析