目录
一.栈
二.栈帧
三.函数调用约定
1._cdecl (C调用约定)
2._stdcall(微软制定的标准调用约定)
3.x86 fastcall约定
4.x64 fastcall约定
5.linux下的函数调用
内容后续补充...
这两篇文章挺不错的:
第一篇介绍了栈的运行原理以及push和pop指令的执行过程
汇编语言——寄存器(内存访问 ss栈段寄存器)
第二篇介绍了程序内存布局:
第5篇:C/C++ 内存布局与程序栈
通过程序内存布局可以得知,栈是从高地址向低地址增长的
简述一下push和pop指令的作用,以32位为例:
push eax: //push指令实际上是先开辟空间(也就是调整栈指针)然后再将值压入栈中
esp=esp-4 //
mov [esp],eax
pop eax : //pop指令先将栈顶的值取出来,然后调整栈指针
mov eax,[esp] //
esp=esp+4
后续补充...
特点:
(1)调用方按照从右到左的顺序将函数参数压入栈中
(2)被调函数完成操作后,由调用方负责清除栈中参数
(3)无论有多少个参数,最左边的(第一个)参数总能轻易找到(在栈顶)
所以适用于参数数量可变的函数(如printf)
例子:
假设有一函数: void fun(int x, int y, int z);
当调用这个函数并且传参如下时:
fun(1,2,3);
主调函数中对应的汇编指令为:
push 3 //esp=esp-4
push 2 //esp=esp-4
push 1 //esp=esp-4
call fun //调用函数后,清除参数进行栈平衡,所以esp=esp+12
add esp,12
被调函数对应汇编指令:
push ebp
mov ebp,esp
......
mov esp,ebp
pop ebp
ret
还有另一种形式(GNU编译器,gcc/g++利用这种技巧):
sub esp,12 //主调函数预先分配栈空间
mov [esp+8],3 //依次将参数入栈
mov [esp+4],2 //注意此时是根据与栈指针的偏移值入栈,并没有移动栈指针
mov [esp],1
call fun
add esp,12 //清除参数,栈平衡
无论是哪一种方式,栈顶的值都是最左边的(第一个)参数\
注意这里的标准是微软为自己制定的调用约定所起的名称,并非是真的"标准"
特点:
(1)与_cdecl一样,都是按照从右到左的顺序将参数入栈
(2)区别:函数执行结束时,由被调函数删除栈中的参数
(3)当函数参数个数固定不变时可以使用这种调用方式
仍采用_cdecl中介绍的例子:
主调函数对应汇编指令:
push 3 //esp=esp-4
push 2 //esp=esp-4
push 1 //esp=esp-4
call fun
被调函数对应汇编指令:
push ebp //保存原始帧指针
mov ebp,esp //开辟新的栈帧
...... //具体操作
mov esp,ebp //恢复栈指针
pop ebp //恢复帧指针
ret 12 //即esp=esp+12,再ret 先调整栈指针,再取栈顶返回地址返回
//主函数中没有了add esp,12 这条指令,而是在fun函数结尾使用 ret 12(3*4=12)指令
//ret 后跟的值与参数的大小之和一致
这是stdcall约定的一个变体,规定函数第1,2个参数分别由ecx,edx传参,其余参数传参方式和stdcall相同,并且也是由被调函数清除栈中参数
例子:
主调函数对应汇编指令:
push 3 //此时只有一个参数是压入栈的
mov edx,2
mov ecx,1
call fun
被调函数对应汇编指令和_stdcall中的相同:
push ebp
mov ebp,esp
......
mov esp,ebp
pop ebp
ret 4 //只有一个参数是压入栈的,所以清除参数只需要清除一个int类型的空间
也就是说,只是传参时使用了ecx,edx这两个寄存器,其他参数依次入栈
与x32 fastcall类似,前4参数则先放入ecx、edx、r8、r9寄存器,剩余参数从右到左依次入栈,这里就不再赘述
以上主要是windows下的函数调用约定,简单介绍一下linux下的函数调用约定
(1)x32 使用栈传递参数,参数从右到左入栈
(2)x64 _fastcall方式: 函数参数的 前 6 个参数是从左至右依次存放于 RDI,RSI,RDX,RCX,R8,R9 寄存器里面,剩下的参数通过栈传递,从右至左顺序入栈