第三章 程序的机器级表示
3.1 历史观点
intel处理器系列俗称x86,经历了一个长期的发展过程。
每个后继处理器的设计都是后向兼容的,即较早版本上编辑的代码可以在较新的处理器上运行。
3.2 程序编码
假设一个c程序,有两个文件p1.c和p2.c,在IA32机器上用unix命令行编译代码如下:
unix> gcc -01 -o p p1.c p2.c
命令gcc是指GCC C编译器
-01告诉编译器使用第一级优化(提高优化级别会使最终程序运行的更快,但是编译时间会更长,调试更难)
实际上gcc命令调用了一系列程序,将源代码转化成可执行代码:
首先,c预处理器扩展源代码
然后,编译器产生两个源代码的汇编代码 .s
接下来,汇编器将汇编代码转化为二进制目标代码 .o
最后,链接器将两个目标代码文件与实现库函数的代码合并,产生最终的可执行代码文件p
3.2.1机器级代码
计算机系统使用了多种不同形式的抽象,利用更简单的抽象模型来隐藏实现的细节。
对于机器级编程来说,有两种重要的抽象:
1.机器级程序的格式和行为,定义为指令集体系结构(ISA),它定义了处理器状态、指令的格式,每条指令对状态的影响
大多数ISA逐条执行指令。
2.机器级程序使用的储存器地址是虚拟地址,提供的存储器模型看上去是一个非常大的字节数组。
整个编译过程中编译器完成大部分工作。汇编代码和机器代码的二进制格式相比,有一个主要特点:它用可读性更好的文本格式表示。
IA32机器代码和原始的c代码差别很大,一些通常对c语言程序员隐藏的处理器状态是可见的,一些存储器:
·程序计数器(PC,用%eip表示):指示将要执行的下一条指令在存储器中的地址
·整数寄存器:包含8个命名的位置,分别存储32位的值,可以存储地址(c语言的指针)或整数数据。
·条件码寄存器:保存最近执行的算数或逻辑指令的状态信息,用来实现控制或数据流中的条件变化。
·浮点寄存器:存放浮点数据。
3.2.2代码示例
要查看目标代码文件的内容,最有价值的是反汇编器,在linux中,带-d的命令行标志的程序objdump可以充当这个角色。
机器代码和它的反汇编表示的特性值:
·IA32指令长度从1到15个字节不等
·设计指令格式的方式是,从某个给定位置开始,可以将字节唯一的解码成机器指令
·反汇编器只是基于机器代码文件中的字节序列来确定汇编代码,不需要访问程序的源代码或汇编代码
·反汇编器使用的指令命名规则与GCC生成的汇编代码使用的有些差别
3.3 数据格式
c语言基本数据类型对应的IA32表示:
大多数gcc生成的汇编代码指令都有一个字符后缀,表明操作数的大小,如:
movb 传送字节
movw 传送字
movl 传送双字
3.4 访问信息
一个IA32中央处理单元(CPU)包含一组8个存储32位值的寄存器:
3.4.1操作数指令符
IA32支持多种操作数格式,如下图:
操作数分为三种:
·立即数,即常数值
·寄存器,表示某个寄存器的内容
·存储器,根据有效地址访问某个存储器位置
3.4.2数据传送指令
传送指令分成指令类:一类中的指令执行一样的操作,只不过操作数大小不同。
mov类的指令将源操作数的值复制到目的操作数中
movs和movz指令都是将一个较小的源数据复制到一个较大的数据位置,高位用符号位扩展(movs)或零扩展(movz)
符号位扩展:目的位置的所有高位用源值的最高位数值填充
零扩展:高位用0填充
pushl和popl可以将数据压入程序栈和从程序栈弹出数据。
3.5 算数和逻辑操作
3.5.1加载有效地址
指令 leal S,D,效果 D<-&S
将有效地址写入目的操作数
3.5.2一元操作和二元操作
一元操作:只有一个操作数,既是源又是目的,可以是一个寄存器也可以是一个存储器位置
二元操作:第二个操作数即是源又是目的,两个操作数不能同时是存储器位置
3.5.3移位操作
先给出移位量,再给出移位的数值,可以进行算数和逻辑右移,但只能进行0-31位的移位
3.6 控制
3.6.1条件码
CF 进位标志
ZF 零标志
SF 符号标志
OF 溢出标志
3.6.2访问条件码
条件码通常不会直接读取,常用的使用方法有三种:
1、根据条件码的某个组合,将一个字节设置为0或者1;
2、条件跳转到程序的某个其他的部分;
3、有条件地传送数据。
3.6.3跳转指令及其编码
跳转指令jmp,跳转的目的用一个标号指明,label
跳转是有条件的,根据条件码的某个组合,或跳转或继续执行代码序列的下一条命令
jmp指令:
3.6.4翻译条件分支
将条件表达式和语句从c语言翻译成机器代码,最常用的方式是结合有条件和无条件跳转。
3.6.5循环
1.do-while
先执行才判断
2.while
先判断才执行,即第一次执行就可能终止
3.for
3.6.6条件传送指令
实现条件操作的传统方法是利用控制的条件转移。
数据的条件转移是一种替代的策略,这种方法先计算一个条件操作的两种结果,然后再根据条件是否满足从而选择一个。
3.6.7switch语句
switch语句可以根据一个整数索引值进行多重分支,处理具有多种可能结果的测试时这种语句特别有用,它们不仅提高了c代码的可读性,而且通过使用跳转表这种数据结构使得实现更加高效。
3.7 过程
一个过程调用包括将数据(以过程参数和返回值的形式)和控制从代码的一部分传递到另一部分,另外,还必须在进入时为过程的局部变量分配空间,并在退出时释放这些空间。
3.7.1栈帧结构
机器用栈来传递过程参数、存储返回信息、保存寄存器用于以后恢复,以及本地存储。为单个过程分配的那部分栈称为栈帧。
假设过程p(调用者)调用过程q(被调用者),则q的参数放在p的栈帧中,另外,当p调用q时,p中的返回地址被压入栈中,形成p的栈帧的末尾。返回地址就是当程序从q返回时应该继续执行的地方。q的栈帧从保存的帧指针的值开始,后面是保存的其他寄存器的值。
过程q也用栈来保存其它不能存放在寄存器中的局部变量,这样做的原因如下:
·没有足够多的寄存器存放所有的局部变量
·有些局部变量是数组或结构,因此必须通过数组或结构引用来访问
·要对一个局部变量使用地址操作符&,我们必须能够为它生成一个地址
3.7.2转移控制
下表是支持过程调用和返回的指令:
call指令有一个目标,即指明被调用过程起始的指令地址,调用可以是直接的也可以是间接的,汇编代码中,直接调用的目标是一个符号,简介调用的目标是*后跟一个操作数指示符。
call指令的效果是将返回地址入栈,并跳转到被调用过程的起始处。
ret指令从栈中弹出地址,并跳转到这个位置。
3.7.3寄存器使用惯例
必须保证一个调用者调用被调用者时,被调用者不会覆盖某个调用者稍后会用到的寄存器的值。
实现以上要求的两个方式:
·在调用q之前,将y的值存放在自己的栈帧之中,当q返回时,过程p就可以从栈中取出y的值,即调用者保存y的值。
·将y的值保存在被调用者保存寄存器,调用后返回前恢复该值。