过程调用的机器级表示
函数调用时如何传递参数,如何把控制转移到被调用的过程,寄存器的使用约定,递归函数的实现。
7.1
7.1.1 过程调用概述
复制 int add(int x, int y)
{
return x+y;
}
int main()
{
int t1 = 125;
int t2 = 80;
int sum = add(t1, t2);
return sum;
}
通过这个程序,关注的问题:
main为调用函数,add为被调用函数,main中有一串指令序列是存放参数的调用add,在add里肯定是有指令序列取出参数的。
CALL指令和RET指令,在硬件层次上做的事情。
**参数通过栈(stack)来传递,**那么栈在哪里呢?
栈是个存储空间,接下来要看看存储空间。
可执行文件在磁盘上,装入RAM的时候是映射到一个存储空间里的,不同的东西反倒不同的段里。
栈在一个比较高的位置,栈由上向下生长。
调用过程:
3(被调用者)保存调用者的现场,并为自己的非静态局部变量 分配空间
12前面两个过程是CALL指令做的事情;3为准备阶段,保存现场,分配自己的空间,生成栈帧,然后就可以做具体的处理了,返回值放到合适的地方,然后5恢复现场,6执行RET指令,就回去了。
现场指的是通用寄存器的内容,因此寄存器是同一套。
关于寄存器的约定,哪些在调用过程用,哪些在被调用过程用。IA32的寄存器使用约定。
调用者在调用前去保存内存的寄存器:EAX,EDX,ECX,被调用者可以直接使用不需要保护内容
被调用者需要去保存的寄存器:EBX,ESI,EDI,子程序要先压栈在使用,在过程调用返回前要弹出
EBP和ESP是帧指针寄存器和栈指针寄存器,用来指向当前栈帧的底部和顶部
为了减少准备阶段和结束阶段的开销,Q里面可以刻意的去选择EAX,EDX,ECX,不够用在去用其他的。
7.1.2 过程调用的参数传递
入口参数的位置。
最右边的参数先进去
复制 movl canshu3, 8(%esp)
movl canshu2, 4(%esp)
movl canshu1, (%esp)
call add
call做了三个事情
M[R[esp]] <- 返回地址(call的下一条指令的地址)
每个过程的第一条指令总是
复制 pushl %ebp ;ebp在main里的值
movl %esp, %ebp ;新的栈底是老的栈顶
所以,参数1的位置就是EBP+8,EBP+12
IA32里,char和short也是分配4个字节,因此在看反汇编程序的时候,要能看出这是在调用参数。
举个例子,交换两个数的程序,按地址传送和按值传送,观察
复制 int add(int x, int y)
{
return x+y;
}
int caller()
{
int t1 = 125;
int t2 = 80;
int sum = add(t1, t2);
return sum;
}
复制 caller:
pushl %ebp
movl %esp, %ebp
subl $24, %esp
movl $125, -12(%ebp)
movl $80, -8(%ebp)
movl -8(%ebp), %eax
movl %eax, 4(%esp)
movl -12(%ebp), %eax
movl %eax, (%esp)
call add
movl %eax, -4(%ebp)
movl -4(%ebp), %eax
leave
ret
add:
pushl %ebp
movl %esp, %ebp
...
经历了准备阶段,分配局部变量,准备入口参数,调用子函数。
调用嵌套也是一样的。
返回值放在 EAX 寄存器里,约定好的。
leave指令是退栈,等价于
复制 movl %ebp, %esp
popl %ebp
把当前的栈切换到调用caller的函数的栈,caller的内容就释放掉了。
总结一下一个C语言过程(函数)的结构
7.1.3 过程调用的参数传递
举例子,在调用子函数int fun(int a1,int a2,int a3)
前,准备了3个参数
复制 movl parameter3, 8(%esp)
movl parameter2, 4(%esp)
movl parameter1, (%esp)
call fun
参数入栈的顺序从右往左,在栈里面就是自上而下。
执行了call之后,需要4个字节存放返回地址,原来esp的值给ebp了,并且ebp压栈,这时候原来的参数地址可以用 (%ebp)+8
来找到。
IA-32 体系里,不管参数是什么类型,都是分配的4个字节。
参数传递举例子
复制 #include <stdio.h>
void swap(int *x, int *y)
{
int t = *x;
*x = *y;
*y = t;
}
void main()
{
int a = 15, b = 22;
printf("a=%d, b=%d\n");
swap(&a, &b);
printf("a=%d, b=%d\n");
}
复制 #include <stdio.h>
void swap(int x, int y)
{
int t = x;
x = y;
y = t;
}
void main()
{
int a = 15, b = 22;
printf("a=%d, b=%d\n");
swap(&a, &b);
printf("a=%d, b=%d\n");
}
编译后反汇编查看程序做了什么事情
复制 main:
...
leal -8(%ebp), %eax
movl %eax, 4(%esp)
leal -4(%ebp), %eax
movl %eax, (%esp)
call swap
...
ret
swap:
//准备阶段
pushl %ebp
movl %esp, %ebp
pushl %ebx /* 被调用者保存 */
//过程体
movl %8(ebp), %edx
movl (%edx), %ecx
movl %12(ebp), %eax
movl (%edx), %ebx
movl %ebx, (%edx)
movl %ecx, (%eax)
//结束段
popl %ebx
popl %ebp
ret
按照地址传递,用的指令是leal
复制 main:
...
movl -8(%ebp), %eax
movl %eax, 4(%esp)
movl -4(%ebp), %eax
movl %eax, (%esp)
call swap
...
ret
swap:
//准备阶段
pushl %ebp
movl %esp, %ebp
//过程体
movl %8(ebp), %edx
movl %12(ebp), %eax
movl %eax, 8(%ebp)
movl %ecx, 12(%ebp)
//结束段
popl %ebp
ret
在看这类调用时,心里有栈,对程序和指令的行为会清晰很多。
这是c语言课里讲的参数传递的问题,现在从底层角度有了更深入的认识。
7.1.4 过程调用举例
复制 void test(int x, int *ptr)
{
if (x>0 && *ptr>0)
*ptr += x;
}
void caller(int a, int y)
{
int x = a > 0 ? a : a + 100;
test(x, &y);
}
caller的过程为P,
7.1.6 递归过程调用
一个最简单的递归调用程序
复制 int nn_sum(int n)
{
int result;
if (n<=0)
result=0;
else
result = n + nn_sum(n-1);
return result;
}
复制 nn_sum:
pushl %ebp
movl %esp, %ebp
pushl %ebx
subl %4, $esp
movl 8(%ebp), %ebx
movl $0, %eax /* result = 0 */
cmpl $0, %ebx /* at&t逆序比较,intel顺序 */
jle .L2
leal -1(%ebx), %eax
movl %eax, (%esp)
call nn_sum
addl %ebx, %eax
.L2
addl $4, %esp
popl %ebx
popl %ebp
ret
由分析可以看出,栈一直向下生长,每递归一次,都有个新栈帧,所以递归次数太多会使得空间开销过大。
7.1.6 过程调用举例
复制 double fun(int i)
{
volatile double d[1] = {3.14};
volatile long int a[2];
a[i] = 1073741824;
return d[0];
}
不同系统执行可能不太一样。编译器对局部变量分配方式可能不同。
复制 fun(0) -> 3.14
fun(1) -> 3.14
fun(2) -> 3.1399998664856
fun(3) -> 2.00000061035156
fun(4) -> 3.14 然后存储保护错
有一些问题需要去考虑
复制 fun:
push %ebp
movl %esp, %ebp
sub $0x10, %esp /* 移动栈指针跨过局部变量 */
fldl 0x8048518 /* 把此地址的数据装载到x87 ST(0)里面去 */
fstpl -0x8(%ebp) /* x87栈顶存到这个位置 */
mov -0x8(%ebp), %eax /* 读入口参数 */
movl $0x40000000, -0x10(%ebp, %eax, 4)
fldl -0x8(%ebp) /* 返回值为浮点,x87栈顶 */
leave
ret
i = 4时,EBP就被改变了,后面的就乱了,会有存储保护错。
7.2 选择和循环
7.2.1 选择
if-else
语句的机器级表示
复制 if (cond_expr)
then_statement
else
else_statement
举例子
复制 int get_cont(int *p1, int *p2)
{
if (p1 > p2)
return *p2;
else
return *p1;
}
复制 get_cont:
movl 8(%ebp), %eax
movl 12(%ebp), %edx
cmpl %edx, %eax
jbe .L1
movl (%edx), %eax
jmp .L2
.L1:
movl (%eax), %eax
.L2:
机器实现基本上和 C 差不多,还是很好理解的。
switch-case
语句也是一种选择分支
复制 int sw_test(int a, int b, int c)
{
int result;
switch (a)
{
case 15:
c = b & 0x0f;
case 10:
result = c + 50;
break;
case 12:
case 17:
result = b + 50;
break;
case 14:
result = b;
break;
default:
result = a;
}
return result;
}
7.2.2 循环