上节课讲了递归、堆栈,及每一个函数的栈帧在堆栈中是怎么排列的,但并未深入到机器级的层次,对 CPU 来说,它到底是怎么维持这个堆栈的,以及上节课提到的 EPB 和 ESP 到底是如何处理的。
预备知识
EBP:永远指向当前栈帧的开始处。
ESP:永远指向栈的栈顶。
部分汇编指令
正题:如何使用汇编语言执行一个简单的加法运算。
程序用 C 语言表示:
int demo(){
int x = 10;
int y = 20;
int sum = add(&x, &y);
printf("the sum is %d\n", sum);
return sum;
}
int add(int *xp, int *yp){
int x = *xp;
int y = *yp;
return x+y;
}
用汇编语言表示:
demo:
1 pushl %ebp
2 movl %esp %ebp
3 subl %24 esp
4 movl $10 -4(%ebp)
5 movl $20 -8(%ebp)
6 leal -8(%ebp) %eax
7 movl %eax 4(%esp)
8 leal -4(%ebp) %eax
9 movl %eax esp
10 call add
11 打印结果(略)
add:
1 pushl %ebp
2 movl %esp %ebp
3 pushl %ebx
4 movl 8(%ebp) %edx
5 movl 12(%ebp) %ecx
6 movl (%edx) %ebx
7 movl (%ecx) %eax
8 add %ebx %eax
9 popl %ebx
10 popl %ebp
11 ret
人肉 CPU
假设当前已有两个函数帧,ebp 当前指向的地址是1000,esp 当前指向的地址是804。
1. 把 ebp 的值压栈。
因为 esp 永远指向栈顶,所以当前 esp 的值为800。
2. 把 esp 中的值放到 ebp 中。
此时 ebp 和 esp 中的值都是800。
3. 把 esp 中的值减去24。
esp 中的值为776。
4. 把10这个值放到 ebp 指向地址减去4个字节的地方(796)。
5. 把20这个值放到 ebp 指向地址减去8个字节的地方(792)。
6. 把 ebp 减去8得到的地址,放到 eax 寄存器中(eax 的值是792)。
7. 把 eax 的值(792)放到 esp 的地址加4的地方去(780)。
8. (与前面两步类似)把 ebp 减去4得到的地址,放到 eax 寄存器中(eax 的值是796)。
9. 把 eax 的值(796)放到 esp 指向的地址处(776)。
10. 调用 add 函数。
1. (函数的前两步基本一致)把 ebp 的值压栈(800)。
2. 把 esp 中的值,放到 ebp 中去(此时同为768)。
3. 把 ebx 的值压栈(额外操作,因为接下来需要使用 ebx ,防止之前保存的值被冲掉,所以需要先保存到内存中,Intel 做出如下规定:对于 ax 、cx、ex,调用者需要保存,对于 bx,被调用者保存——有 push 操作,必定对应着后面的 pop 操作,第3行对应第9行)。
4. 把 ebp 地址加8处的值,放到 edx 中(edx 的值是796)。
5. 把 ebp 地址加12处的值,放到 ecx 中(edx 的值是792)。
6. 把 edx 里的值作为地址寻值,把找到的数放到 ebx 中(ebx 的值是10)。
7. 把 ecx 里的值作为地址寻值,把找到的数放到 eax 中(eax 的值是20)。
8. 把 ebx 和 eax 中的值加起来放到 eax 中(eax 的值是30)。
9. 弹出栈顶的值放到 ebx 中。
10. 弹出栈顶的值放到 ebp 中(此时 ebp 指向800)。
11. return,执行地址为100处的指令。
之后执行 demo 方法中的第11行。