其实这章2天也就看完了,但碰上清明节放假,我拖延了很久的时间才来记录笔记。
程序员学习汇编代码的需求随着时间的推移也发生了变化,开始时只要求程序员能直接用汇编语言编写程序,现在则是要求他们能阅读和理解编译器产生的代码。
###程序编码
机器级编程使用了两种重要的抽象:
- 机器级程序的格式和行为,定义为指令集体系结构,它定义了处理器状态、指令的格式、以及每条指令对状态的影响。
- 机器级程序使用的存储器是虚拟地址,操作系统负责管理虚拟地址空间,将虚拟地址翻译成实际处理器存储器中的物理地址。
在命令行上生成 C 语言源程序的汇编代码:
unix> gcc -O1 -S code.c
O1
表示一级优化。
关于汇编代码格式:
- ATT 格式,这是 GCC 的默认格式。
- Intel 格式,学校使用的是这种格式。
###数据格式
Intel 用术语「字」来表示 16 位数据类型。因此,32 位数为「双字」。
下图为 C 语言数据类型在 IA32 中的大小:
C 声明 | Intel 数据类型 | 汇编代码后缀 | 大小(字节) |
---|---|---|---|
char | 字节 | b | 1 |
short | 字 | w | 2 |
int | 双字 | l | 4 |
long int | 双字 | b | 4 |
char * | 双字 | l | 4 |
float | 单精度 | s | 4 |
double | 双精度 | l | 8 |
###访问信息
一个 IA32 中央处理器包含一组8个32位值的寄存器:%eax, %ecx, %edx, %ebx, %esi, %edi, %esp(栈指针), %ebp(帧指针)
操作数的指示符有多种类型:
- 立即数,也就是常数值
- 寄存器,表示某个寄存器的内容
- 存储器引用,它会根据计算出来的地址访问某个存储器位置
- Imm(Eb, Ei, s),这表示的是最常用的形式。四个组成部分分别为:一个立即数偏移 Imm, 一个基址寄存器 Eb, 一个变址寄存器 Ei 和一个比例因子 s,这里 s 必须是 1、2、4或者8。然后有效地址被计算为:Imm + R[Eb] + R[Ei] * s
数据传送指令有:
- MOV S, D
- MOVS S, D
- MOVZ S, D
- pushl S
- popl D
IA32有一条限制,传送指令的两个操作数不能都指向存储器位置。
MOVS 和 MOVZ 指令类都是讲一个较小的源数据复制到一个较大的数据位置,高位用符号位扩展 (MOVS) 或者零扩展(MOVZ)。
在 IA32 中,栈顶元素的指针是所有栈中元素地址中最低的,而 栈指针 %esp 保存着栈顶元素的地址。
###算术与逻辑操作
指令 | 效果 | 描述 |
---|---|---|
leal S, D | D <- &S | 加载有效地址 |
INC D | D <- D + 1 | 加 1 |
DEC D | D <- D - 1 | 减 1 |
NEG D | D <- -D | 取负 |
NOT D | D <- ~D | 取补 |
ADD S, D | D <- D + S | 加 |
SUB S, D | D <- D - S | 减 |
IMUL S, D | D <- D * S | 乘 |
XOR S, D | D <- D ^ S | 异或 |
OR S, D | D <- D | S | 或 |
AND S, D | D <- D & S | 与 |
SAL S, D | D <- D << k | 左移 |
SAR S, D | D <- D >> a k | 算术右移 |
SHR S, D | D <- D >> l k | 逻辑右移 |
###控制
除了整数寄存器,CPU 还维护着一组单个位的条件码寄存器,它们描述了最近的算术或逻辑操作的属性。我们可以检测这些寄存器来执行条件分支指令。它们分别是:
- CF:进位标志。最近的操作使最高位产生了进位,可以用来检查无符号操作数的溢出。
- ZF:零标志。最近的操作得出的结果为 0.
- SF:符号标志。最近的操作得到的结果为负数。
- OF:溢出标志。最近的操作导致一个补码溢出。
条件分支:
-
原始 C 代码:
int absdiff(int x, int y){ if (x < y) return y - x; else return x - y; }
-
与汇编代码等价的 goto 版本
int gotodiff(int x, int ){ int result; if (x >= y) goto x_ge_y; result = y - x; goto done; x_ge_y: result = x - y; done: return result; }
循环:
-
do-while 循环
-
通用形式:
do body-statement while (test-expr);
-
翻译后的形式:
loop: body-statement t = test-expr; if (t) goto loop;
-
-
while 循环
-
通用形式:
while (test-expr) body-statement
-
翻译后的形式:
t = test-expr if (!t) goto done; loop: body-statement t = test-expr; if (t) goto loop; done:
-
-
for 循环
-
通用形式:
for (init-expr; test-expr; update-expr) body-statement
-
翻译后的形式:
init-expr; t = test-expr; if (!t) goto done; loop: body-statement update-expr; t = test-expr; if (t) goto loop; done:
-
switch 语句
GCC 根据开关情况的数量和开关情况值的稀少程度来翻译开关语句。当开关数量比较多,并且值的范围跨度比较小时,就会使用跳转表
###过程
一个过程调用包括将数据(以过程参数和返回值的形式)和控制从代码的一部分传递到另一部分。
栈帧结构:
栈帧的最顶端以两个指针界定,寄存器 %ebp 为帧指针,而寄存器 %esp 为栈指针。
假设过程 P 调用过程 Q,则 Q 的参数放在 P 的栈帧中。另外,当 P 调用 Q 时, P 中的返回地址被压入栈中,形成 p 的栈帧的末尾。返回地址就是当程序从 Q 返回时应该继续执行的地方。
###数组分配和访问
IA32 的存储器引用指令可以用来简化数组访问。例如,假设 E 是一个 int 型的数组,并且我们想计算 E[i],在此, E 的地址存放再 %edx 中,而 i 存放再 %ecx 中,那么,指令为:
movl (%edx, %ecx,4), %eax
这样会地址会执行 Xe + 4i,允许的缩放因子1、2、4、8覆盖了所有基本简单数据类型的大小。