从汇编角度查看C语言函数调用约定【非常有用】

转自:https://blog.csdn.net/Holmofy/article/details/76094986

为了防止出现不必要的代码影响汇编语言的查看,所以程序中不使用任何库函数,以保持汇编代码的简洁。

这里所使用的汇编是VC的MASM。

默认函数调用方式__cdecl
int add(int a, int b) {
    return a + b;
}

int main() {
    int a = 1, b = 2;
    return add(a,b);
}
1
2
3
4
5
6
7
8
对应汇编代码:

; 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
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);
}
1
2
3
4
5
6
7
8
对应汇编代码:

; 省略前面的若干伪指令
...

;   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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
微软的__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);
}
1
2
3
4
5
6
7
8
对应汇编代码:

; 省略前面的若干伪指令
...

;   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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
微软的__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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
__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
--------------------- 
作者:Holmofy 
来源:CSDN 
原文:https://blog.csdn.net/Holmofy/article/details/76094986 
版权声明:本文为博主原创文章,转载请附上博文链接!

猜你喜欢

转载自blog.csdn.net/hemeinvyiqiluoben/article/details/84963631
今日推荐