您的位置:首页 > 理论基础

程序的机器级表示(三)

2017-02-20 21:17 302 查看

过程

过程调用就是调用方法,这里主要了解调用过程的时候存储结构是如何变化的。

栈帧结构

IA32程序用程序栈来支持过程调用。机器用栈来传递过程参数、存储返回信息、保存寄存器用于以后恢复,以及本地存储。每个过程会分配一个栈帧。下图描绘了栈帧的通用结构。栈帧的最顶端以两个指针界定,寄存器%ebp为帧指针,寄存器%esp为找指针。当程序执行时,栈指针可以移动,因此大多数信息的访问都是相对于帧指针的。



假设过程P(调用者)调用过程Q(被调用者),则Q的参数放在P的栈帧中。当P调用Q时,P中的返回地址被压人栈中,形成P的栈帧的末尾,返回地址就是调用程序时下一条语句的地址。Q的栈帧从被保存的P帧指针的位置开始,见上图。这样被调用者保存了调用者的起始位置,在返回的时候才可以找到调用者的各种变量,继续执行。

转移控制

下表是支持过程调用和返回的指令:



call指令有一个目标,即指明被调用过程起始的指令地址。call指令的效果是将返回地址人栈,并跳转到被调用过程的起始处。返回地址是在程序中紧跟在call后面的那条指令的地址,这样当被调用过程返回时,执行会从此处继续。ret指令从栈中弹出地址,并跳转到这个位置。
4000



上图展示了函数调用时call和ret指令的执行情况。注意%eip存储的值即为返回地址。

用leave指令可以使栈做好返回的准备。它等价于下面的代码序列:



也可以通过直接使用传送和弹出操作来完成这种准备工作。调用一个过程从开始到结束,我们push和pop的数量应该是相同的。

寄存器使用惯例

程序寄存器组是唯一能被所有过程共享的资源。虽然在给定时刻只能有一个过程是活动的,但是我们必须保证当一个过程(调用者)调用另一个过程(被调用者)时,被调用者不会覆盖某个调用者稍后会使用的寄存器的值。为此,IA32采用了一组统一的寄存器使用惯例,所有的过程都必须遵守,包括程序库中的过程。

根据惯例,寄存器%eax, %edx和%ecx被划分为调用者保存寄存器。当过程P调用Q时,Q可以覆盖这些寄存器,而不会破坏任何P所需要的数据。另一方面,寄存器%ebx, %esi和%edi被划分为被调用者保存寄存器。这意味着Q必须在覆盖这些寄存器的值之前,先把它们保存到栈中,并在返回前恢复它们,因为P(或某个更高层次的过程)可能会在今后的计算中需要这些值。当然,还需要维护寄存器%ebp和%esp。

过程示例



下面这段是caller的汇编代码,说明它如何调用swap_ add:



swap add的汇编代码有三个部分:“建立”部分,初始化栈帧;“主体”部分,执行过程的实际计算;“结束”部分,恢复栈的状态,以及过程返回。

下面是swap_ add的建立代码。在到达代码的这个部分之前,call指令已经将返回地址压入栈中。



函数swap_ add需要用寄存器%ebx作为临时存储。因为这是一个被调用者保存寄存器,它会将旧值压人栈中,这是栈帧建立的一部分。而同样将被使用的%eax因为是调用者保存寄存器,所以不需要存储它的值。

下面是swap add的主体代码:



下面是swap_ add的结束代码:



这段代码恢复寄存器%ebx和%ebp的值,同时也重新设定栈指针使它指向存储的返回值,这样ret指令就可以将控制转移回caller。

下面的caller中的代码紧跟在调用swap add的指令后面:



可以观察到,leave指令的使用在返回前,既重置了栈指针,也重置了帧指针。

为了程序能够正确执行,让所有过程都遵循一组建立和恢复栈的一致惯例是很重要的。

递归过程

递归调用一个函数本身与调用其他函数是一样的。栈规则提供了一种机制,每次函数调用都有它自己私有的状态信息(保存的返回位置、栈指针和被调用者保存寄存器的值)存储。

数组分配和访问

基本原则

对于数据类型T和整型常数N,声明如下:

T A
;


它有两个效果。

在存储器中分配一个L*N字节的连续区域。L是数据类型T的大小(单位为字节),用XA来表示起始位置。

引人标识符A。可以用A作为指向数组开头的指针,这个指针的值就是XA。可以用从0到N-1之间的整数索引来访问数组元素。数组元素i会被存放在地址为XA+L⋅i的地方。

如下声明:

char A [12];
char *B [8];
double C[6];
double *D[5];


这些声明产生的数组带下列参数:



指针运算

单操作数的操作符&和可以产生指针和间接引用指针。对于某个对象表达式Expr, &Expr是给出该对象地址的一个指针。对于一个表示地址的表达式AExpr,*AExpr是给出该地址处的值。因此,表达式Expr与&Expr是等价的。即:

p = &a
*p = a
p —— 所指向的地址
*p —— 所指向地址的值


假设整型数组E的起始地址和整数索引1分别存放在寄存器%edx和%ecx中。下面是一些与E有关的表达式:



其中,leal指令用来产生地址,而movl用来引用存储器(除了第一种和最后一种情况,前者是复制一个地址,而后者是复制索引)。

多维数组

对于一个多维数组,例如:

int A[5][3];


等价于下面的声明:

typedef int row3_t[3];
row3_t A[5];


也可以看成一个5行3列的二维数组。数组元素在存储器中按照“行优先”的顺序排列:



通常来说,对于一个数组声明如下:

T D[R][C]


数组元素D[i][j]的存储器地址为 D[i][j]=XD+L(C⋅i+j),是数据类型T以字节为单位的大小。

异质的数据结构

结构

C语言的struct类似于java的对象。例如:

strnct rec{
int i;
int j;
int a [3];
int *P;
};


这个结构包括4个字段——2个4字节in七、1个由3个4字节int组成的数组和1个4字节的整型指针,总共是24个字节,数组a是嵌人到这个结构中的。



上图中顶部的数字给出的是各个字段相对于结构开始处的字节偏移。要产生一个指向结构内部对象的指针,我们只需将结构的地址加上该字段的偏移量。

数据对齐

许多计算机系统对基本数据类型合法地址做出了一些限制,要求某种类型对象的地址必须是

某个值K(通常是2, 4或8)的倍数。

比如如下结构声明:

strnct rec{
int i;
char c;
int j;
};


假设编译器用最小的9字节分配,画出图来是这样的:



它不满足字段的4字节对齐要求,所以修改后如下:



存储器的越界引用和缓冲区溢出

因为对于数组引用不进行边界检查,且局部变量和状态信息(例如保存的寄存器值和返回地址),都存放在栈中。这两种情况结合到一起就可能导致严重的程序错误,对越界的数组元素的写操作会破坏存储在栈中的状态信息。当程序使用这个被破坏的状态,试图重新加载寄存器或执行ret指令时,就会出现很严重的错误。

一种特别常见的状态破坏称为缓冲区溢出(buffer overflow )。缓冲区溢出的一个致命的使用就是让程序执行它本来不愿意执行的函数,这是一种常见的通过计算机网络攻击系统安全的方法。通过缓冲区溢出,将攻击代码的地址覆盖返回地址,执行ret指令的效果就是跳转到攻击代码。当然,我们也有一些应对策略,不做详细介绍了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息