目录
前言寄存器分配寄存器使用约定栈帧构造堆栈操作
函数调用约定
常见调用约定
cdecl调用约定stdcall调用约定(微软命名)fastcall调用约定thiscall调用约定naked call调用约定pascal调用约定
调用约定影响x86函数传送参数方法
整型和指针参数传送浮点参数传送构造体和结合体参数传送
x86函数返回值传送方法总结
前言
C语言程序执行实质上的函数的连续调用。
运行程序时,系统通过程序入口调用main函数,在main函数中又不时调用其它函数。
程序的每个进程都包括一个调用栈构造(Call Stack)。
调用栈的作用:
传送函数参数保管返回地址临时保管寄存器原有值(保管现场)
寄存器分配
寄存器指CPU中可以停止高速运算的缓冲区。用于寄存程序执行中用到的数据和指令。
Intel 32位构造寄存器(IA32)包含8个通用寄存器,每个寄存器4个字节(32位)。
通用寄存器依照AT&T语法,寄存器名以**%e**开头。
若依照Intel语法,寄存器名直接按e开头。
通用寄存器包括:EAX、EBX、ECX、EDX、ESI、EDI、ESP、EBP
数据寄存器:EAX、EBX、ECX、EDX
变址寄存器:ESI、EDI
指针寄存器:ESP、EBP
X86架构中,EIP寄存器指向下一条待执行的命令地址。
ESP是栈指针寄存器,指向当前栈帧的栈顶。
EBP是栈帧基址寄存器,指向当前栈帧的基地址。
不同架构的cpu寄存器名前缀不同。
例如:x86架构的寄存器用字母e作为前缀(extended),标明寄存器大小是32位。
x86_64架构用字母r作为前缀,标明寄存器大小是64位。
ABI协议规定了寄存器、堆栈的使用规则以及参数传送规则。用于约束硬件与系统之间的通信协议。编译器必需依照ABI给出的寄存器功能定义,将C程序转为汇编程序。
寄存器使用约定
寄存器是唯一能被被所有函数共享的资源。因而,在函数中调用其它函数时,需要考虑到数据的保管与覆盖问题(即防止被调函数直接修改寄存器导致主调函数的数据被覆盖)。
IA32采用了统一的寄存器使用约定,所有函数必需遵守。
EAX、ECX、EDX为主调函数保管寄存器,即在调用被调函数之前,主调函数假设希望保管这三个寄存器的数据,需要将数据保管到堆栈中,然后调用被调函数。EBX、ESI、EDI是被调函数保管寄存器,被调函数假设向使用这三个寄存器,需要先将其中的数据保管到堆栈中,然后操作寄存器,最后将堆栈中的数据复原。EBP和ESP指向当前的栈,每个函数对应一个栈帧。被调函在返回前,需将主调函数的栈帧复原。即恢复到调用前的状态。
栈帧构造
注意,程序的栈从高地址向低地址增长!
函数调用由堆栈停止处置,每个函数都单独在堆栈中占用一块连续的区域。这块区域叫做每个函数的栈帧。栈帧是堆栈的逻辑片段。
栈帧中保管 传入的参数 部分变量 和 用于返回上一栈帧的信息。
栈帧的边境由EBP和ESP决定。EBP指向栈帧的底部(高地址),ESP指向栈顶地址(低地址)。ESP可以看作是EBP的偏移量,始终指向栈帧的顶部。
EBP为帧基指针,ESP为栈顶指针。
函数调用栈演示如下:
参数2 | 参数1 | 主调函数返回地址(EIP) | 主调函数栈帧基址(EBP) | 被调函数保管寄存器(可选) | 部分变量1 | 部分变量2 | 函数被调用时,压栈的顺序:
参数2 -> 参数1 -> 主调函数返回地址 -> 主调函数栈帧基址 -> 被调函数保管寄存器(可选) -> 部分变量 -> 部分变量2
注意,参数是从右向左依次入栈。
参数压栈完成后,紧接着被压入的是EIP指针所指向的地址,也就是主调函数下一个要执行的命令的地址。(用于被调函数执行完后继续执行程序)
然后,将主调函数EBP栈帧基地址压入栈帧,用于复原现场。并把ESP赋值给EBP,使EBP成为被调函数的栈帧基地址。
继续,改变SP的值,给被调函数部分变量预留空间。
这时候,EBP指向被调函数的栈底,向上是主调函数返回地址,向下是部分变量。该地址还保管主调函数的栈帧基址。
函数调用完毕后,EBP赋值给ESP,使ESP指向被调函数栈底,释放被调函数部分变量。再将主调函数栈帧基地址弹出给EBP,并弹出返回地址到EIP。
堆栈操作
函数调用流程
函数调用时的详细操作:
主调函数依照约定,将参数压入栈中。(x86将参数压入栈帧,x86_64具有16个通用寄存器,前六个参数通常由寄存器保管,其余参数压入栈中。)主调函数将控制权转给被调函数,返回地址(EIP)保管在栈中(在call指令中执行)。被调函数设置栈帧基址,即用ESP给EBP赋值。若有必要,保管被调函数希望坚持的寄存器的数据。被调函数修改栈顶指针,为部分变量预留空间。并向低地址方向开端寄存部分变量和临时变量。被调函数执行任务,若被调函数返回值,一般寄存在EAX中。栈顶指针指向EBP,释放部分变量空间。恢复4中保管的主调函数寄存器中的数据。并恢复3中的栈帧基址。被调函数控制权交还给主调函数(ret指令),也可能肃清参数。主调函数得到控制器,可能将栈上的参数肃清。
函数调用常用命令
压栈(push):栈顶指针减小4个字节,以字节为单位将数据压入栈中。(缺乏补0)
出栈(pop):栈顶指针数据被取回,ESP增大4个字节。
调用(call):将EIP(call的下一条指令地址)压入栈帧,然后EIP指向被调函数代码开端处。
分开(leave):恢复主调函数栈帧,等价于 mov ebp esp 、pop ebp
返回(ret):与call对应,从栈顶弹出返回地址给EIP。继续执行程序。
C调用约定典型的函数序和函数跋如下:
| 指令序列 | 含义 | 函数序(prologue) | push %ebp | 将主调函数栈基指针ebp压栈,即保管旧栈帧基址以便函数返回时恢复旧栈帧。 | mov %esp %ebp | 将主调函数栈顶指针赋值给ebp,此时,ebp执行被调函数栈帧底部。 | | sub %esp | 将栈顶指针下移,为部分变量开拓空间,n通常为16的倍数,以便于字节对齐停止编译优化。 | | push | 可选,如有必要,被调函数保管某些寄存器的值(ebx,edi,esi) | | 函数跋(epilogue) | pop® | 可选,如有必要,被调函数恢复某些寄存器的值(ebx,edi,esi) | mov %ebp %esp* | 恢复主调函数栈顶指针esp,将其指向被调函数栈底。部分变量空间被释放,但数据未肃清。 | | pop %ebp | 恢复主调函数栈帧基地址,此时,esp指向返回地址寄存处。 | | ret | 从栈中弹出返回地址到eip,继续执行主调函数。再由主调函数恢复栈。 | | *:这两条指令序列也可以由leave实现,详细方式由编译器决定。 | | | C语言函数调用的两种压栈方式:
压栈方式一 | 压栈方式二 | push 4push 3push 2push 1call CdeclDemoadd $16, %ebp | sub $16, %espmov $4, 12(%esp)mov $3, 8(%esp)mov $2, 4(%esp)mov $1, (%esp)call CdeclDemo | 两种压栈方式区别:
方式一是传统方式,一个参数一个参数的压栈,然后调用,最后释放栈。
方式二是预先开拓空间,然后将参数复制到空间,最后没有回收空间。
函数调用约定
创建栈帧最重要的步骤是参数的传送。函数选择特定调用约定,以特定方式停止参数传送。调用约定还规定在函数调用完毕后,由主调函数还是被调函数对栈停止清理。
函数调用约定包括以下方面:
常见调用约定
cdecl调用约定
别名 C调用约定,C/C++编译器默认调用约定。
所有非C++成员函数,和未使用stdcall、fastcall声明的函数默认都是cdecl调用。
参数依照从右向左的顺序入栈,主调函数负责清空栈,返回值保管在EAX中。
cdecl调用支持可变参数函数,对于C函数,名字修饰是在函数名前加 _ 。
对于C++,除非使用**extern"C"**修饰,否则有不同的名字修饰方法。
stdcall调用约定(微软命名)
Pascal程序缺省调用方式,WinAPI也多采用该调用约定。
参数从右向左入栈,被调函数负责清空栈,返回值保管在EAX。
stdcall仅适用于参数个数固定的函数,因为被调函数无法晓得栈上参数个数。
C函数中,stdcall的名字修饰是在名字前加_,在名字后加@和参数大小。
fastcall调用约定
stdcall的变形,通常使用ECX、EDX寄存器传送前两个DWORD(四字节双字)类型或更少的字节的函数参数,其余从右向左入栈。
被调函数负责清空栈中参数。返回值保管在EAX中。
函数名两边使用@修饰,并在后面用十进制表示参数列表大小(字节)。
thiscall调用约定
C++类的非静态成员函数必需接收一个主调对象的指针(this指针),并频繁的使用该指针。编译器默认使用thiscall调用约定进步伐用效率。
参数依照从右向左的顺序入栈。
若参数数目固定,this指针通过ECX传送,被调函数负责清理堆栈。
若参数数目不固定,this指针在所有参数入栈后再入栈,主调函数清理堆栈。
thiscall不是C++关键字,不能用于修饰函数,只能由编译器使用。
naked call调用约定
naked call调用,编译器不产生保管和恢复寄存器的代码。也不能使用return语句。
只能使用内嵌的汇编返回结果。用于某些特殊场所,如非C/C++上下文中的函数,程序员需自行编写初始化和清栈的内嵌汇编指令。
pascal调用约定
Pascal语言调用约定,参数从右向左入栈。只支持固定数量参数。
被调函数清理堆栈,函数名称无修饰且全部大写。
上述约定的特点:
调用方式 | stdcall(Win32) | cdecl | fastcall | thiscall(C++) | naked call | 参数压栈顺序 | 从右至左 | 从右至左 | 自定义,Arg1在ecx,Arg2在edx | 从右至左,this指针在ecx | 自定义 | 参数位置 | 栈 | 栈 | 栈 + 寄存器 | 栈,寄存器ecx | 自定义 | 负责清栈函数 | 被调函数 | 主调函数 | 被调函数 | 被调函数 | 自定义 | 支持可变参数 | 否 | 是 | 否 | 否 | 自定义 | 函数名字格式 | _name@number | _name | @name@number | | 自定义 | 参数表开端特征 | “@@YG” | “@@YA” | “@@YI” | | 自定义 | 注:C++因支撑函数重载、命名空间和成员函数等语法特征,采用更为复杂的名字修饰战略。C++函数修饰名以"?“开端,后面紧跟函数名、参数表开端标识和依照类型代号拼出的返回值参数表。例如,函数int Function(char *var1,unsigned long)对应的stdcall修饰名为”?Function@@YGHPADK@Z"。 | | | | | | Windows下可直接在函数声明前添加关键字__stdcall、__cdecl或__fastcall等标识确定函数的调用方式,如int __stdcall func()。
Linux下可借用函数attribute 机制,如int attribute((stdcall)) func()。
被调函数CalleeFunc分别声明为cdecl、stdcall和fastcall约定时,汇编代码比较:
| cdecl | stdcall | fastcall | 主调函数职责 | sub $0xc, %espmov $0x33, 0x8(%esp)mov $0x22, 0x4(%esp)mov $0x11,(%esp)call 8048354 | sub $0xc, %espmov $0x33, 0x8(%esp)mov $0x22, 0x4(%esp)mov $0x11,(%esp)call 8048354 sub $0xc, %esp | sub $0x4,%esp movl $0x33,(%esp) mov $0x22,%edx mov $0x11,%ecx call 8048354 sub $0x4,%esp | 被调函数职责 | push %ebpmov %ebp %espmov 0xc(%ebp), %eaxadd 0x8(%ebp), %eaxadd 0x10(%ebp), %eaxpop %ebpret | push %ebpmov %ebp %espmov 0xc(%ebp), %eaxadd 0x8(%ebp), %eaxadd 0x10(%ebp), %eaxpop %ebpret $0xc 执行ret指令并清理参数占用的堆栈(栈顶指针上移参数个数*4=12个字节,以释放压栈的参数) | push %ebp mov %esp,%ebp sub $0x8,%esp mov %ecx,0xfffffffc(%ebp) mov %edx,0xfffffff8(%ebp) mov 0xfffffff8(%ebp),%eax add 0xfffffffc(%ebp),%eax add 0x8(%ebp),%eax leave ret $0x4 //ret <压栈参数字节数>。若参数不超越两个,则ret指令不带立即数,因为无参数被压栈 |
调用约定影响
不同编译器产生栈帧的方式不尽相同,主调函数不一定能完成清理堆栈的工作,而被调函数一定可以。
同时,为了保证不同平台堆栈正常,一般使用stdcall调用。(通常用于A语言调用B语言函数)
此外,主调函数和被调函数采用相同调用约定,但分别使用C和C++时,会呈现链接错误。
这是因为:两种语言函数名称修饰符不一样。处置方法是使用**extern “C”**修饰被调函数。
同时应该考虑,被调函数也有可能是C++编译的。通常这样声明头文件:
#ifdef _cplusplus
extern "C" {
#endif
type Func(type para);
#ifdef _cplusplus
}
#endif
x86函数传送参数方法
x86处置器的ABI规范中规定,所有参数从右向左压入栈中。
整型和指针参数传送
整型参数与指针参数传送方式相同,在32位的x86处置器上整型与指针大小相同(四个字节)。
下表给出这两品种型在栈帧中位置关系:
调用语句 | 参数 | 栈帧地址 | tail(1, 2, 3, (void *)0); | 1 | 8(%ebp) | 2 | 12(%ebp) | | 3 | 16(%ebp) | | (void *)0 | 20(%ebp) | |
浮点参数传送
浮点参数的传送与整型类似,区别在于参数大小。
x86处置器中浮点类型占8个字节,因而在栈中也需要占8个字节。
下表给出浮点参数在栈中位置关系:
调用语句 | 参数 | 栈帧地址 | tail(1.414, 2, 3.998e10); | word 0: 1.414 | 8(%ebp) | word 1: 1.414 | 12(%ebp) | | 2 | 16(%ebp) | | word 0: 3.998e10 | 20(%ebp) | | word 1: 3.998e10 | 24(%ebp) | |
构造体和结合体参数传送
构造体和结合体的传送与整型、浮点型类似,只是占用大小不同。
x86处置器栈宽是4字节,故构造体在栈上大小是4的倍数。
编译器会对构造体停止适当的填充使得构造体4字节对齐。
对于其它处置器,参数传送并不全部通过栈停止。构造体可能通过指针传送。
x86函数返回值传送方法
函数返回值可通过寄存器传送:
若返回值不超越4字节(int、指针),通常保管在EAX中。若返回值大于4字节但不超越8字节(long long),通常保管在EAX+EDX,EDX保管高4字节,EAX保管低4字节。若返回值为浮点类型(float double),则通过专用的协处置器浮点数寄存器栈的栈顶返回。若返回值为构造体或结合体,主调函数额外传送一个参数,该参数是一个保管返回值的空间地址。
注意:函数如何保管构造体或结合体返回值取决于详细实现。
总结
以上为个人经历,希望能给大家一个参考,也希望大家多多支持网站。 |