程序的机器级表示--过程
过程的概念
定义:用一组指定的参数和一个可选的返回值实现了某种功能。
原则:作为抽象机制:
- 隐藏某个行为的具体实现
- 同时提供清晰简介的调用接口定义
形式:函数、方法、子例程、处理函数等
特性:
- 传递控制
- 传递数据
- 分配和释放内存(栈结构的后进先出内存管理原则)。
传递控制
运行时栈
过程调用:
P过程调用Q过程的时候,P过程将暂时被挂起。
Q运行的时候,需要为局部变量分配空间,或者设置到另一个过程的调用。
Q返回的时候,分配的所有局部空间都被释放。
栈数据:
栈和程序寄存器存放着传递控制和数据、分配内存所需要的信息。
x86栈向低地址的方向增长,栈指针%rsp指向栈顶元素。
pushq和popq指令分别对应数据存入栈与数据从栈中取出。
通过栈指针减少一定量来在栈上分配空间,增加值来回收空间。、
栈帧:
寄存器存不下数据的时候会将数据存在栈上,这部分叫做过程的栈帧。
正在执行的过程的帧总在栈顶。
过程P调用Q的时候会将返回地址压入栈中,返回地址是P栈帧的一部分。
栈帧可以用来存放寄存器的值,分配局部变量空间,为调用的过程设置参数。
栈帧大多数是定长的,在过程开始的时候就已经确定,也有部分变长的。
寄存器最多传递6个整数值(指针与整数),多出的时候放在自己的栈帧中。
为了提高空间与效率,x86只分配自己所需要的栈帧。
参数小于6个时候就只需通过寄存器传递即可,所以实际上许多函数并不需要栈帧(不调用其他函数且参数小于6个)。
转移控制
P转移到Q只要将程序计数器(PC)设置为Q的代码的起始位置。从Q放回的时候,只要从记录好的P继续执行的代码位置继续执行。x86通过指令call Q来记录,指令会将地址A压入栈中,并将PC设置为Q的起始地址。被压入的地址称为返回地址,是紧跟call指令后面的指令地址。对应指令ret会从栈中弹出地址A,并把PC设置为A。
调用可以是直接的,也可以是间接的。直接调用是一个符号,间接调用是*后面跟一个操作数。
举个例子:
Beginning of function multstore
400540
400540:53 push %rbx
400541:48 89 d4 mov %rdx,%rbx
...
Return from funtion multstore
40054d:c3 retq
Call to multstore from main
400563:e8 d8 ff ff ff callq 400540
400568:48 8b 54 24 08 mov 0x8(%rsp),%rdx
函数调用期间的栈与PC变化如下
传递数据
调用过程的时候除了传递参数,也会传递数据。返回的时候也可能带一个返回值。
P调用Q的时候必须首先把参数复制到适当的寄存器中。Q返回P时,P可以访问寄存器%rax中的返回值。
寄存器最多存6个参数,且有特殊顺序,寄存器的名字取决与数据类型大小,例如第一个参数是32位的时候,可以用%edi来访问。
传参超过6个时,栈要分配容纳参数7n的空间,超过的部分用栈传递,参数16复制到寄存器上,参数7~n放栈上。
所有数据大小都向8对齐。
参数到位后,用call调用Q过程,Q通过寄存器访问参数,必要是通过栈访问参数。
举个例子:
void proc(long a1, long *a1p, int a2, int *a2p,short a3,short *a3p, char a4, char *a4p){
*a1p += a1;
*a2p += a2;
*a3p += a3;
*a4p += a4;
}
//汇编如下
void proc(a1, a1p, a2, a2p, a3, a3p, a4, a4p)
//参数传递如下
a1 in %rdi (64 bit)
a1p in %rsi (64 bit)
a2 in %edx (32 bit)
a2p in %rcx (64 bit)
a3 in %r8w (16 bit)
a3P in %r9 (64 bit)
a4 in %rsp+8 ( 8 bit)
a4p in %rsp+16 (64 bit)
proc:
movq 16(%rsp), %rax Fetch a4p (64bit)
addq %rdi, (%rsi) *a1p += a1 (64bit)
addl %edx, (%rcx) *a2p += a2 (32bit)
addw %r8w, (%r9) *a3p += a3 (16bit)
movl 8(%rsp), %edx Fetch a4 ( 8bit)
addb %rdl, (%rax) *a4p += a4 ( 8bit)
ret
留意数据大小与对应的指令!
分配和释放内存
栈上的局部存储
局部数据有些时候需要存放在内存中,常见情况如:
- 寄存器房放不下所有的本地数据。
- 对变量使用&运算符。
- 某些局部变量是数组或者结构。
举个例子:
long swap_add(long *xp, long *yp){
long x = *xp;
long y = *yp;
*xp = y;
*yp = x;
return x + y;
}
long caller(){
long arg1 = 534;
long arg2 = 1057;
long sum = swap_add(&arg1, &arg2);
long diff = arg1 - arg2;
return sum * diff;
}
//汇编代码如下:
long caller()
caller:
subq $16, %rsp Allocate 16 bytes for stack frame
movq $534, (%rsp) Store 534 in arg1
movq $1057, 8(%rsp) Store 1057 in arg1
leaq 8(%rsp), %rsi Compute &arg2 as second argument
movq %rsp, %rdi Compute &arg1 as second argument
call swap_add Call swap_add(&arg1, &arg2)
movq (%rsp), %rdx Get arg1
subq 8(%rsp), %rdx Compute diff = arg1 - arg2
imulq %rdx, %rax Compute sum * diff
addq $16, %rsp Deallocate stack frame
ret
寄存器中的局部存储
寄存器是唯一被所有过程共享的资源。我们必须确保被调用者不会覆盖调用要使用的寄存器值!
要保证寄存器值不变:
- 根本不去改变它;
- 把原始数据压入栈中,改变寄存器的值,返回前再从栈中弹出旧值。被压入寄存器的值会被标记为“被保存的寄存器”的一部分。
所有寄存器,除了栈寄存器%rsp,都被分成调用者保存寄存器,即任何过程都能修改他们,所以保存好数据是调用者的责任(P过程)。
递归调用一个函数本身与调用其他函数是相同的,每次函数都有他自己的私有的状态信息(1.保存的返回位置;2.被调用者保存寄存器的值;3.局部变量),栈分配和释放的规则很自然的与函数调用的上述特性顺序匹配。
举个例子:
long P(long x, long y){
long u = Q(y);
long v = Q(x);
return u + v;
}
long P(long x, long y)
x in %rdi, y in %rsi
P:
pushq %rbp Save &rbp
pushq %rbx Save &rbx
subq $8,%rsp Align stack frame
movq %rdi, %rbp Save x
movq %rsi, %rdi Move y to first argument
call Q Call Q(y)
movq %rax, %rbx Save result
movq %rbp, %rdi Move x to first argument
call Q Call Q(x)
addq %rbx, %rax add Q(y) to Q(x)
addq $8, %rsp Deallocate last part of stack
popq %rbx Rstore &rbx
popq %rbp Rstore &rbp
ret