CSAPP:第三章程序的机器级表示3
程序的机器级表示3
关键点:过程、调试、指针
过程1.运行时栈2.转移控制3.数据传递4.栈上的局部存储5.寄存器中的局部存储空间理解指针使用GDB调试器
过程
1.运行时栈
x86-64的栈向低地址方向增长,而栈指针%rsp指向低地址方向增长,而栈指针%rsp指向栈顶元素。可以用pishq和popq指令将数据存入栈中或是从栈中取出。将栈指针减小一个适当的量可以为没有指定初始值的数据在栈上分配空间。类似地,可以通过增加栈指针来释放空间。
当x86-64过程需要的存储空间超出寄存器能够存放的大小时,就会在栈上分配空间。这个部分称为过程的栈帧(statck fram)。如图给出了运行时栈的通用结构,包括把它划分为栈帧,当前正在执行的过程帧总是在栈顶。当过程P调用过程Q时,会把返回地址压入栈中,指明当Q返回时要从P程序的哪个位置继续执行。我们把这个返回地址当做P的栈帧的一部分,因为它存放的是与P相关的状态。Q的代码会扩展到当前栈的边界,分配它的栈帧所需要的空间(可以保存寄存器的值、分配局部变量空间,为它调用的过程设置参数)
2.转移控制
将控制函数从P转移到函数Q,只需要简单地把程序计数器(PC)设置为Q的代码的起始位置。当从Q返回的时候,处理器必须记录好它需要继续P的执行的代码位置。在x86-64机器中,这个信息是通过call指令调用过程记录。
- call Label: 过程调用
- call *operand :过程调用(间接调用)
3.数据传递
在x86-64中,可以通过寄存器最多传递6个整型参数。寄存器的使用有特殊的顺序,寄存器使用的名字取决于要传递的数据类型大小。
如果一个函数有大于6个整型参数,超出6个部分就要通过栈来传递。假设过程P调用过程Q,有n个整型参数,且
1void proc(long a1,long *a1p,
2 int a2,int *a2p,
3 short a3,short *a3p,
4 char a4,char *a4p)
5{
6 *a1p += a1;
7 *a2p += a2;
8 *a3p += a3;
9 *a4p += a4;
10}
11//反汇编
12/**
13*a1 in %rdi (64bits)
14*a1p in %rsi (64bits)
15*a2 in %edx (32bits)
16*a2p in %rcx (64bits)
17*a3 in %r8w (16bits)
18*a3p in %r9 (64bits)
19*a4 at %rsp + 8 (8bits)
20*a4p at %rsp + 16 (64bits)
21*/
220000000000000000 <proc>:
23 0: 48 8b 44 24 10 mov(q) 0x10(%rsp),%rax;//%rax = ((%rsp)+16) 即取出参数ap4的值存入寄存器(%rax = a4p)
24 5: 48 01 3e add(q) %rdi,(%rsi);//*a1p+=a1;
25 8: 01 11 add(l) %edx,(%rcx);//*a2p+=a2;
26 a: 66 45 01 01 add(w) %r8w,(%r9); //*a3p+=a3;
27 e: 8b 54 24 08 mov(l) 0x8(%rsp),%edx;//%edx = ((%rsp)+8) 将参数a4的的值存入%edx寄存器中
28 12: 00 10 add(b) %dl,(%rax);%dl取%edx低8位,*a4p+=a4;
29 14: c3 retq
30
310000000000000015 <main>:
32 15: b8 00 00 00 00 mov $0x0,%eax
33 1a: c3 retq
4.栈上的局部存储
大都数过程示例都不需要超出寄存器大小的本地存储区域,不过有些时候,局部数据必须存放在内存中,常见的情况有:
- 寄存器不足够存放所有本地数据。
- 对一个局部变量使用地址运算符&,因此必须能够为它产生一个地址。
- 某些局部变量是数组或者结构,因此必须能够通过数组或者结构引用被访问到。
一般来说,过程通过减小栈指针在栈上分配空间。分配的结果作为栈帧的一部分,标号为“局部变量”。
1long swap_add(long *xp,long *yp)
2{
3 long x = *xp;
4 long y = *yp;
5 *xp = y;
6 *yp = x;
7 return x + y;
8}
9long caller()
10{
11 long arg1 = 534;
12 long arg2 = 1057;
13 long sum = swap_add(&arg1,&arg2);
14 long diff = arg1 - arg2;
15 return sum * diff;
16}
17//反汇编
18caller:
19 subq $16, %rsp ; //%rsp-16 分配栈空间(2*8)*8bits
20 movq $534,(%rsp); //arg1 = 534
21 movq $1057,8(%rsp);//arg2 = 1057 %rsp-8
22 leaq 8(%rsp),%rsi; //加载%rsp+8地址到arg2
23 moveq %rsp, %rdi; //加载%rsp地址到arg1
24 call swap_add; //call swap_add(&arg1,&arg2)
25 movq (%rsp), %rdx; //获取arg1的值存入%rdx
26 subq 8(%rsp), %rdx;//diff = arg1 - arg2
27 imulq %rdx, %rax; //%rax = %rax * diff
28 addq $16,%rsp; //释放栈空间
29 ret //return %rax
5.寄存器中的局部存储空间
寄存器组是唯一被所有过程共享的资源。虽然在给定的时刻只有一个过程是活动的,我们仍然必须确保当一个过程(调用者)调用另一个过程(被调用者)时,被调用者不会覆盖调用者稍后会使用的寄存器值。为此,x86-64采用了一组统一的寄存器使用惯例,所有的过程(包括程序库)都必须遵循。
根据惯例,寄存器%rbx、%rbp和%r12~%r15被划为被调用者保存的寄存器。当过程P调用Q时,Q必须保存这些寄存器的值,保证它们的值在Q返回到P时与Q被调用时是一样的。
理解指针
- 每个指针都对应一个类型。
- 每个指针都有一个值。这个值是某个指定对象的地址。
- 指针用'&'运算符创建
- *操作符用于间接引用指针。
- 数组与指针紧密联系。
- 将指针从一种类型强制转成另外一种类型,只改变它的类型,而不改变它的值。
- 指针也可以指向函数。
使用GDB调试器
linux下通过命令启动GDB。gdb xxx。下图为GDB的一些常用命令