1.函数调用者必须在ccallee返回之后清理堆栈。
2.每个函数拥有固定数量的参数,这意味着被调用函数可以在一个地方对参数进行清理,即在被调用函数内部进行堆栈参数的清理,而不是分散在每一次调用该函数的代码中。
3.以下是调用过程:
1)将参数从右到左压入堆栈:
参数从右到左依次压入堆栈,每次压入一个。调用者(caller)必须明确有多少Byte的参数,以便函数返回后清理掉。
2)调用函数:
处理器将下一条指令的EIP(即函数返回值)内容压入堆栈,同时EIP设置成被调函数的地址。这步完成之后,控制权交给Callee。到此时为止,%EBP不发生任何变化。
3)保存和更新%EBP:
现在,我们进入了新的函数(callee),需要一个局部栈帧, 使用%EBP指向新的栈帧, 老的%EBP值(属于caller的栈帧)保存在堆栈上, %EBP指向新的栈顶。 push ebp
mov ebp, esp // ebp <- esp
然后可以通过%EBP访问函数参数,如8(%ebp),12(%ebp). 注意0(%ebp)是caller的%EBP值,4(%ebp)是函数返回值.
4)分配局部变量:
函数可以使用堆栈空间来存放局部变量,直接减去%ESP值就相当于分配了堆栈空间. 分配是按照4Bytes对齐. 此时,局部变量在%ebp和%esp之间. 虽然通过%ebp和%esp都可以访问局部变量,但是约定(convension)使用%ebp寄存器来访问,所以 -4(%ebp)代表第一个局部变量.
5)保存要使用的寄存器:
如果这个函数要使用一些寄存器, 需要先保存这些寄存器的值,这些值将被保存在堆栈上,compiler需要记录下保存的顺序,以便之后恢复.
6)执行函数的功能:
此时,栈帧已经设置好,所有的参数和局部变量都通过%ebp的偏移来访问.
16(%ebp) :第三个参数
12(%ebp) :第二个参数
8(%ebp) :第一个参数
4(%ebp) :函数返回值
0(%ebp) :老的%EBP(caller的%EBP)
-4(%ebp) :第一个局部变量
-8(%ebp) :第二个局部变量
-12(%ebp):第三个局部变量
函数中可以自由使用任何已经保存过的寄存器, 但是堆栈指针(%esp)不能改变.
7)释放局部空间:
第四步中函数通过减去%esp来分配局部的临时空间,这里是一个相反的过程,通常通过给%esp加上减去的值来实现,一系列POP指令也能达到相同的效果.
8)恢复保存的寄存器:
第五部中保存的寄存器值,在这里按照相反顺序恢复.
9)恢复老的%ebp:
恢复第三步中保存的%ebp, 当前的栈帧也就被丢弃掉.
10)函数返回:
这是callee的最后一步, RET指令从堆栈中弹出%EIP并跳转到那里.控制权重新交回给caller. Ret指令只修改%esp和%eip.
11)清理压入的参数:
在__cdecl约定中,caller负责清理堆栈上的参数,和第七步中类似,可以通过POP指令,也可以通过直接加%esp附:X86采用eax作为返回值。
4.X86主要是采用堆栈传递参数,除非指定以寄存器传递(通过"regparm (NUMBER)"注:NUMBER<=3指定)。
5.如果指定寄存器传递参数,则eax为第一个参数,edx为第二个参数, ecx为第三个参数。