第三章 程序的机器级表示
使用现代化的优化编译器,产生的代码至少与一个熟练的汇编语言程序员手工编写的代码一样有效。
逆向工程(reverse engineering)
3.1 历史观点
3.2 程序编码
3.2.1 机器级代码
汇编程序员和C程序员看到的机器差别很大:
程序计数器(%eip)
整数寄存器文件包含8个被命名的位置,分别存储32位的值。
条件码寄存器
浮点寄存器文件包含8个位置,用来存放浮点数据。
程序存储器(program memory)包含程序的目标代码,操作系统需要的一些信息,用来管理过程调用和返回的运行时栈,以及用户分配的存储器块。
一条机器指令只执行非常基本的操作。
3.2.2 代码示例
3.2.3 关于格式的注解
3.3 数据格式
3.4 访问信息
3.4.1 操作数指示符
操作数有三种:
立即数(immediate)
寄存器(register)
存储器引用,根据计算出来的地址(有效地址),访问某个存储器位置。
3.4.2 数据传送指令
3.4.2 数据传送指令
最频繁使用的指令是执行数据传送的指令。
3.4.3 数据传送示例
过程体(body)
3.5 算术和逻辑操作
3.5.1 加载有效地址
3.5.2 一元和二元操作
3.5.3 移位操作
3.5.4 讨论
3.5.5 特殊的算术操作
3.6 控制
目前为止,考虑的都是访问数据和操作数据的方法。程序执行的另一个很重要的部分就是控制被执行操作的顺序。
默认是顺序控制流。
3.6.1 条件码
CF:进位标志。
ZF:零标志。
SF:符号标志。
OF:溢出标志。
3.6.2 访问条件码
3.6.3 跳转指令和它们的编码
跳转(jump)指令会导致执行切换到程序中一个全新的位置。
3.6.4 翻译条件分支
有条件跳转 无条件跳转
3.6.5 循环
3.6.6 switch语句
跳转表
3.7 过程
过程调用包括将数据(以过程参数和返回值的形式)和控制从代码的一部分传递到另一部分。
数据传递、局部变量的分配和释放是通过操纵程序栈实现的。
3.7.1 栈帧结构
栈帧(stack frame)
寄存器%ebp 作为帧指针,寄存器%esp作为栈指针。
栈指针可以移动,大多数信息的访问都是相对于帧指针的。
参数,返回地址在调用者P的栈帧中。返回地址是当程序Q(被调用者)返回时应该继续执行的地方。
过程Q也用栈来保存其他不能存放在寄存器的局部变量,是因为:
寄存器不够存放所有的局部变量。
有些局部变量是数组或结构,必须通过数组或结构引用来访问。
要对一个局部变量使用地址操作符&,因此我们必须能够为它们产生一个地址。
3.7.2 转移控制
支持过程调用和返回的指令:
call指令目标,指明被调用过程起始的指令地址。
call指令的效果是将返回地址入栈,并跳转到被调用过程的起始处。
3.7.3 寄存器使用惯例
%eax、%edx、%ecx被划分为调用者保存(caller save)寄存器。当过程P调用Q时,Q可以覆盖这些寄存器,而不会破坏任何P需要的数据。
%ebx、%esi、%edi被划分为被调用者保存(callee save)寄存器。Q在覆盖它们前,将这些寄存器的值保存到栈中,并在返回前恢复它们。
%ebp 帧指针
%esp 栈指针
3.7.4 过程示例
3.7.5 递归过程
Fibonacci
3.8 数组分配和访问
3.8.1 基本原则
3.8.2 指针运算
3.8.3 数组与循环
3.8.4 嵌套数组
3.8.5 固定大小的数组
3.8.6 动态分配的数组
3.9 异类的数据结构
结构 struct
联合 union
3.9.1 结构
结构 所有的组成部分都存放在存储器中连续的区域内,而指向结构的指针是结构第一个字节的地址。
3.9.2 联合
联合提供了一种方式,能够规避C的类型系统,允许以多种类型来引用一个对象。
union U3 {
char c;
int i[2];
double v;
}
3.10 对齐(alignment)
3.11 综合:理解指针
3.12 现实生活:使用GBD调试器
3.13 存储器的越界引用和缓冲区溢出
3.14 浮点代码
3.14.1 浮点寄存器
8个,当做浅栈(shallow stack)来对待的。
3.14.2 栈的表达式求值
3.14.3 浮点数据的传送和转换操作
3.14.4 浮点算术指令
3.14.5 在过程中使用浮点
3.14.6 测试和比较浮点值
3.15 在C程序中嵌入汇编代码
3.15.1 基本的内嵌汇编
3.15.2 asm的扩展格式