第三章 程序的机器级表示
1 有关汇编
用高级语言编写的程序可以在很多不同的机器上编译和执行,而汇编代码则是与特定机器密切相关的。
对于机器级编程来说,有两种抽象很重要:1 由指令集体系结构或指令集架构来定义机器级程序的格式或行为,它定义了处理器状态,指令的格式,以及每条指令对状态的影响。2 机器级程序使用的内存地址是虚拟地址,提供的内存模型看上去是一个非常大的字节数组。(程序内存用虚拟地址来寻址,操作系统负责管理虚拟地址空间,将虚拟地址翻译成实际处理器内存中的物理地址)
有关具体的代码csapp采用的是x86-64,之前看过的计算机组成——软硬件接口这本书采用的是MIPS,上学期又刚学了80C51的汇编规范,有点弄混,但是大概思想是基本相同的。所以有关语言方面,不在本文赘述,但要注意以下几点:
1 链接器的任务之一是为函数调用找到匹配的函数的可执行代码的位置。
2 寄存器用来存储整数数据和指针。(其实汇编就是对各个寄存器或各个地址进行的操作)(访问寄存器比访问内存快的多)
3 寻址方式
4 数据传送
5 栈的应用
6 算术和逻辑,移位
7 条件码
8 跳转
9 条件分支
10 子程序调用
11 循环
等等等等
2 有关过程
传递控制:在进入过程Q的时候,程序计数器必须被设置为Q的代码的起始地址,然后在返回时,要把程序计数器设置为P中调用Q后面那条指令的大小。
传递数据:P必须能够向Q提供一个或多个参数,Q必须能够向P返回一个值。
分配和释放内存:在开始时,Q可能需要为局部变量分配空间,而在返回时前,又必须释放这些存储空间。
1) 运行时栈
当过程需要的存储空间超过寄存器能够存放的大小时,就会在栈上分配空间。这个部分称为过程的栈帧。(栈用来传递参数,存储返回信息,保存寄存器,以及局部存储)
2) 转移控制
将控制从函数P转移到函数Q只需要简单地把程序计数器(PC)设置为Q的代码的起始位置。不过,当稍后从Q返回的时候,处理器必须记录好它需要继续P的执行的代码位置。(把返回地址压入栈的简单的机制能够让函数在稍后返回到程序中正确的点)
3) 数据传送
大部分过程间的数据传送是通过寄存器实现的。当过程P调用过程Q时,P的代码必须首先把参数复制到适当的寄存器中。
4) 栈上的局部存储
大多数过程都不需要超出寄存器大小的本地存储区域,但是有些情况,局部数据必须存放在内存中,常见情况包括:
·寄存器不足够存放所有的本地数据
·对一个局部变量使用地址运算符‘&’,因此必须能够为它产生一个地址。
·某些局部变量是数组或结构,因此必须能够通过数组或结构引用被访问到。
运行时栈提供了一种简单的在需要时分配,函数完成时释放局部存储的机制。
5)寄存器中的局部存储空间
寄存器是唯一被所有过程共享的资源。
必须确保党一个过程(调用者)调用另一个过程(被调用者)时,被调用者不会覆盖调用者稍后会使用的寄存器值。
6)递归过程
每个过程调用在栈中都有它自己的私有空间,因此多个未完成调用的局部变量不会相互影响。
递归调用一个函数本身与调用其他函数是一样的。
栈规则提供了一种机制,每次函数调用都有它自己私有的状态信息(保存的返回位置和被调用者保存寄存器的值)存储空间。如果需要,它还可以提供局部变量的存储。栈分配和释放的规则很自然地就与函数调用—返回的顺序匹配。
7)数组分配和访问
1)基本原则
T A[N]; 起始位置表示为xA。
这个声明有两个效果,首先,它在内存中分配了一个L*N字节的连续区域。这里L是数据类型T的大小(单位为字节)。其次,它引入了标识符A,可以用A来作为指向数组开头的指针,这个指针的值是xA。可以用0~N-1的整数索引来访问该数组元素。数组元素i会被存放在地址为xA+L*i的地方。
2)指针运算
c语言允许对指针进行运算,而计算出来的值会根据该指针引用的数据类型大小进行伸缩。
3)嵌套的数组,定长的数组,变长的数组
8)异质的数据结构
1 结构
类似于数组的实现,结构的所有组成部分都存放在内存中一段连续的区域,而指向结构的指针就是结构中第一个字节的地址。编译器维护关于每个结构类型的信息,指示每个字段的字节偏移。它以这些偏移作为内存引用指令中的位移,从而产生对结构元素的引用。
例:
struct rec{
int i;
int j;
int a[2];
int *p;
};
这个结构包括4个字段,两个4字节 int,一个由两个类型为int的元素组成的数组和一个8字节整型指针,总共是24个字节。(偏移内容)
2 联合
联合提供了一种方式,能够规避c语言的类型系统,允许以多种类型来引用一个对象。联合声明的语法和结构的语法一样,只不过语义相差比较大。它们使用不同的字段来引用相同的内存块。
一个联合的总的大小等于它最大字段的大小。
3 数据对齐
许多计算机系统对基本数据类型的合法地址做出了一些限制,要求某种类型对象地址必须是某个值K(通常是2.4.8)的倍数。这种对齐限制简化了形成处理器和内存系统之间接口的硬件设计。例如,假设一个处理器总是从内存中取8个字节,则地址必须为8的倍数。如果我们能保证将所有的double类型数据的地址对齐成8个字节,则地址必须为8的倍数。否则对象可能被芬芳在两个8字节内存块中。
对齐原则是任何K字节的基本对象的地址必须是K的倍数。
对于包含结构的代码,编译器可能需要在字段的分配中插入间隙,以保证每个结构元素都满足它的对齐要求。而结构本身对它的起始地址也有一些对齐要求。
9)在机器级程序中将控制与数据结合起来
1 内存越界引用和缓冲区溢出
对越界的数组元素的写操作会破坏存储在栈中的状态信息。当程序使用这个被破坏的状态,试图重新加载寄存器或执行ret指令时,就会出现很严重的错误。比如,让程序执行它本来不愿意执行的函数。
注:蠕虫和病毒的区别。
对抗方法:栈随机化,栈破坏检测,限制可执行的代码区域。
10)浮点代码
(本章内容很多都要结合汇编代码分析,本文只是简单的列了提纲,还是看书比较重要)
[第三章完]