这是对应的C代码
int caller() {
int temp1 = 125;
int temp2 = 80;
int sum = add(temp1, temp2);
return sum;
}
int add(int x, int y) {
return x + y;
}
这是对应的汇编代码:
caller:
push ebp
mov ebp, esp
sub esp, 24
mov [ebp-12], 125
mov [ebp-8], 80
mov eax, [ebp-8]
mov [esp+4], eax
mov eax, [ebp-12]
mov esp, eax
call add
mov [ebp-4], eax
mov eax,[ebp-4]
leave
add:
push ebp
mov ebp, esp
mov eax, [ebp+12]
mov edx, [ebp+8]
add eax, edx
leave
ret
下面开始解释:
第一句:push ebp。push指令是压栈的意思。esp和ebp分别是指向栈顶和栈底的寄存器。push压栈过程:
1.先++,即esp往下走一个单位,起到了扩容的作用。2.把元素放入栈中。即把ebp寄存器内部的值压栈。
这一步的作用是保存上一个函数栈帧的基地址,在该函数(caller)结束之后,可以通过这一个基地址找到上一个函数的函数栈帧基地址。
第一句代码对应图示:
初始状态:
push后的状态.esp++(++指的是加一个单位),ebp的地址入栈
第二句:mov ebp esp。mov是移动的指令。这里写的是intel x86的汇编指令,因此这句话的意思是把esp寄存器内的地址复制一份到ebp寄存器内。也就是说:此时ebp指向esp所指向的地址。也就是栈顶。
第二句代码的图示如下:
第三句:sub esp, 24.意思是把esp减去24个字节。由于用户栈是向下生长的。越下面地址越小,因此减去24就是向下移动。这里假设一个格子是4字节,因此也就是esp向下移动6个格子。
第四,五句:mov [ebp-12], 125。ebp向下三个单位被写入一个立即数125.这个125对应的c语言代码是那个temp1,在c语言代码里,越早定义的局部变量越靠下。mov [ebp-8], 80表示ebp向下2个单位被写入一个立即数80.也就是c语言代码里的temp2
第四,五句对应图示如下:
至此,c语言代码我们现在执行到了这一条语句int sum = add(temp1, temp2);这一条语句涉及到的知识点有函数栈帧的传参的切换。首先要进行的是函数栈帧的传参。要把temp1和temp2两个参数传给add这个函数栈帧。
第6,7,8,9句代码:
mov eax, [ebp-8]
mov [esp+4], eax
mov eax, [ebp-12]
mov esp, eax
首先我们要明确的是:函数传参时,参数应该存在函数栈帧的靠下的部分。
由于汇编当中不允许将内存里的东西mov到内存里。所有我们要借助一个寄存器eax来进行写入。因此这四句话的意思就是,我先把[ebp-8]里面的内容mov到eax里面,然后再将eax里面的内容mov到[esp+4]里面。后面两句同理。
这四句代码的图示如下:
第十句代码:call add。add是一个标识符。这句话就表示我现在要调用add函数了。当使用了call指令的时候,会将当前指令的地址入栈。(也就是PC程序计数器记录的指令地址,在intel x86里面PC程序计数器叫做IP寄存器。)入栈之后,就跳转到了add的函数栈帧里面了
第十句代码对应的图如下:
第11,12句代码:和之前同理,作用就是为当前函数开辟空间。(开辟函数栈帧)。这两句代码是每一个函数都需要的,因为它可以为函数栈帧分配空间。这两句话也可以合起来被称为enter指令。
第13,14,15句代码:
mov eax, [ebp+12]
mov edx, [ebp+8]
add eax, edx
就是把两个参数temp1和temp2拿到寄存器里面相加。相加后放入到eax里面保存。
这里要说一下add函数栈帧是如何找到caller函数栈帧传入的temp1和temp2两个参数的。
第16句代码:leave。leave和enter一样也是一个缩写的指令。leave完整写出来应该是
mov ebp, esq 将esp指向ebp,其实就是收回函数栈帧的空间
pop ebp 将弹出的元素放到ebp里面。这里弹出的元素就是上一个函数栈帧的基地址。
leave把add的最上面那个栈内元素,也就是上一个函数栈帧的ebp地址获取后弹出了,现在ebp回到了上一个函数栈帧的ebp位置,也就是caller的ebp位置。
第17句代码 ret。
ret会把栈顶的IP寄存器所保存的指令地址获取并弹出。这样我们就可以找到我们原来执行到哪一条指令了。
我们将会回到call add这条代码处继续往下执行。
注意区分PC寄存器里的指令地址和ebp的基地址的区别。
基地址是函数栈帧栈底的地址,PC寄存器记录的是我们执行到了哪一条指令。
图示:我们可以发现栈顶(最底下)的ip寄存器已经被pop掉了。
第18, 19, 20句代码
mov [ebp-4], eax
mov eax,[ebp-4]
leave
刚刚add函数帮我计算的结果被保存在了eax寄存器里面。因此我们可以通过访问eax寄存器来得到这个结果,并把它放到sum这个局部变量里。至此,程序结束