Lecture 2. 汇编语言 Part1:寄存器与基础汇编结构知识
寄存器
寄存器是处理器当中的存储位置,访问速度非常快
8086 微处理器当中有四个 通用寄存器(general purpose register):AX
, BX
, CX
, DX
,在初学阶段,可以把这四个玩意儿当作变量使用。每个寄存器都是 16 位 的,可以存储 $[0, 65535]$ 的无符号数或者 $[-32768, 32767]$ 的有符号数。
每个 _X
通用寄存器可以分为 _H
和 _L
两个八位寄存器,分别代表高位和低位。这两个八位寄存器可以存储无符号数的范围降低到 $[0, 255]$。于是,在汇编代码当中,可以使用的寄存器就有了 12 个:4 * (_X
, _H
, _L
)。
寄存器是不能乱用的。CSAPP 3.4 节指出,8086 的 8 个 16 位寄存器都有特殊用途,这是由于指令集历史演化造成的。8086 当中的 %al
拥有类似累加器的作用,最常使用。
8086 只有八个 16 位的寄存器,即 %ax
到 %sp
;扩展到 IA32 之后,8086 的寄存器变成了 32 位,标号从 %eax
到 %esp
;再扩展到 x86-64 之后,原先八个扩展到 64 位,标号 %rax
到 %rsp
,还新增了 %r8
到 %r15
系列共计八个新寄存器。
64 | 32 | 16 | 8 | 备注 |
---|---|---|---|---|
%rax |
%eax |
%ax |
%al |
accumulator(常用于运算与 IO);返回值;可接收 int 21h |
%rbx |
%ebx |
%bx |
%bl |
base(常用于地址索引);被调用者保存 |
%rdx |
%ecx |
%cx |
%cl |
count(计数,常用于保存计算值);第 4 个参数,可配合 loop |
%rdx |
%edx |
%dx |
%dl |
data(数据传递);第 3 个参数,可传给 int 21h |
%rsi |
%esi |
%si |
%sil |
源变址寄存器;第 2 个参数 |
%rdi |
%edi |
%di |
%dil |
目的变址寄存器;第 1 个参数 |
%rbp |
%ebp |
%bp |
%bpl |
基址指针寄存器;被调用者保存 |
%rsp |
%esp |
%sp |
%spl |
指向目前的栈位置;栈指针 |
%r8 |
%r8d |
%r8w |
%r8b |
第 5 个参数 |
%r9 |
%r9d |
%r9w |
%r9b |
第 6 个参数 |
%r10 |
%r10d |
%rw |
%r10b |
调用者保存 |
%r11 |
%r11d |
%r11w |
%r11b |
调用者保存 |
%r12 |
%r12d |
%r12w |
%r12b |
被调用者保存 |
%r13 |
%r13d |
%r13w |
%r13b |
被调用者保存 |
%r14 |
%r14d |
%r14w |
%r14b |
被调用者保存 |
%r15 |
%r15d |
%r15w |
%r15b |
被调用者保存 |
8086 汇编算术运算
CPU 每执行一条指令,都会在一个 特殊寄存器(flag)上记录指令的运行结果。这个 flag 可以用来检查是否为零(zero,表示相等)、正负性(carry,用于比较大小)、奇偶性之类的。
指令 | 代码 | 作用 |
---|---|---|
inc |
inc A |
A = A + 1 |
dec |
dec A |
A = A - 1 |
add |
add A, B |
A = A + B |
sub |
sub A, B |
A = A - B |
cmp |
cmp A, B |
将 A 减去 B,不修改 A 的值,但反映结果给 flag |
flag 寄存器
这个 flag 寄存器是 16 位的,每一位都代表一个 flag,结构如图:
从高位到低位看,有 7 个位未定义(U),此外还有 9 个 flag,含义分别是:
位 | 名称 | 缩写 | 解释 | 分类 |
---|---|---|---|---|
11 | O | overflow | 针对 有符号数 的运算,表示是否有溢出 | 状态 |
10 | D | direction | 若 D=1 表示字符串从高位地址向低位地址访问;D=0 相反 | 控制 |
9 | I | interrupt | 若 I=1 则处理器会识别外部中断,否则忽略所有的外部中断 | 控制 |
8 | T | trap | 用于调试,若 T=1,则每执行一步指令都进行一次中断 | 控制 |
7 | S | sign | 用于判断正负,若运算后 MSB 是 1,则意味着是负数,于是 S=1 | 状态 |
6 | Z | zero | 若上一次的运算结果是 0 则 Z=1 | 状态 |
4 | A | auxiliary | 辅助进位标志,与 BCD 码有关,表示低四位到高四位有无进位 | 状态 |
2 | P | parity | 运算结果当中有偶数个 1 就 P=1,有奇数个 1 就 P=0 | 状态 |
0 | C | carry | 加法运算若产生了进位(或对于减法,借位),则此位设为 1 | 状态 |
课件上说,其中,OF,SF,ZF,PF,CF 既是 conditional(status) flags 也是 control flags。跟网上查到的很不一样!
指令指针 instruction pointer
CS 寄存器(代码段)与 IP 寄存器(指令指针)是 8086 当中最关键的两个寄存器,因为它们指示了处理器要读取指令的地址。
代码段的大小限制是 1MB。CS 寄存器给出代码段地址,IP 寄存器给出偏移距离(offset distance)。
总线接口单元(BIU)会把 CS 和 IP 给结合在一起,以此来计算出正确的地址,从而 fetch 到该地址的指令。
如:CS 寄存器的值为 0x348A,IP 寄存器的值为 0x4214,于是指令地址可以写作:CS:IP 即 348A:4214,计算 0x348A0 + 0x4214 = 0x38AB4,这个地址就是当前执行的指令的地址:
栈
指令可以指定,ALU 的运算结果,是输出到寄存器当中,还是某个内存地址上,还是存到栈里。栈拥有一个指针 SP,永远指向栈顶。
段寄存器 segment register
8086 处理器的 地址总线宽度是 20 bit,因此内存地址空间大小就是 2^20 字节,即 1MB。
8086 处理器的 寄存器共有 14 个,都是 16 位的。
那么 8086 如何根据 16 位的寄存器运算找到 20 位的地址呢?8086 处理器在形成物理地址时,先将段寄存器的内容左移 4 位,形成 20 位的段地址,然后再同 16 位的偏移地址相加,得到 20 位的物理地址。
于是,16 位的段寄存器,相当于存储了 20 位地址的高 16 位,可以把 1MB 的空间不重叠的划分成 65536(64K)个小段,每一个段都是 2^4 = 16 个 字节,可以用偏移地址 0x0000~0x000f 计算得到。
为了让一个段的空间尽可能地大,每个段的基址,都是 0xX0000,也就是表示成十六进制,后四位都是零。然后,偏移地址取值 0x0000~0xFFFF,共 65536 个,这意味着,每个段的大小最多只能是 64K。
总结的来说: 8086 的 1MB 地址空间最多可以分为 64K 个段,每个段均为 16 个字节;最少可分为 16 个段,每个段均为 64KB。
段寄存器的种类
有四种:
- CS,code segment,代码段寄存器
- SS,stack segment,栈段寄存器
- ES,extra segment,额外段寄存器,如果同一时间需要使用到两个段,那么 ES 就指向第二个
- DS,data segment,数据段寄存器
栈段寄存器
栈用于在程序执行期间存储地址和数据。
SI(source index,源索引)寄存器,DI(destination index,目标索引)寄存器,以及 BP(base pointer,基指针)寄存器,在本质上来说,都是通用(general purpose)寄存器,但实际上通常用于存储段寄存器的临时数据。
课件中的栈,是「递减堆栈」,因为执行压栈操作的时候,SP 减小;相反,执行出栈操作的时候,SP 增大。
SS:SP 就像 CS:IP 一样,SS 左移 4 位再加上 SP,表示栈顶的物理地址。
编程语言
- 机器语言,一系列的二进制数字,描述指令。要读懂这种代码需要手算编译,很费时间。
- 汇编语言,把每一个二进制指令替换成了一一对应的英文助记符(mnemonic),于是跟容易读懂和编写。而且汇编代码跑得快,编译好的程序通常非常小,比如 hello world 程序只需要二三十个字节。但是对于复杂的问题,汇编还是不太方便。不同处理器上的汇编指令助记符号不同。比如赋值运算在其他处理器上的指令是 load 的缩写
ld
,而在 8086 上是 move 的缩写mov
。 - 高级语言,代码的功能性更强大,但是需要借助编译器或解释器转化成机器语言
汇编语言程序结构分析
.MODEL small ; 表示程序的模式是 small,具体 small 的含义以后再说
.STACK ; 默认栈是 1024 字节
.DATA ; 数据段
.CODE ; 代码段
.STARTUP ; 表示程序开始运行的地方
nop ; 执行 nop 指令
.EXIT ; 表示程序终止运行
END ; 整个文件的结束标志
操作符与操作数(operator & operand)
8086 微处理器是冯诺依曼结构的,意味着,指令和数据,存储在一块内存区域内。
指令可以拆分成两部分,即操作符与操作数。操作符表示这一条指令做什么,操作数是这个操作需要用到的数据。
CSAPP:ATT 与 Intel 汇编代码格式
CSAPP 书中使用 ATT 格式的汇编代码,GCC 等工具默认生成的汇编代码也是 ATT 格式的。对于 GCC 来说,如果想要生成 Intel 格式的代码,可以使用命令:
Intel 格式与 ATT 格式有些许不同:
- Intel 格式省略了指令表示大小的后缀,比如
pushq
moveq
的后缀 q 被省略了 - Intel 格式省略了寄存器之前的 % 符号
- 描述内存位置,ATT 格式使用
(%rbx)
,而 Intel 格式是QWORD PTR [rbx]
- 对于有多个 operand 的指令,操作数的顺序与 ATT 格式相反
分段架构
8080 处理器使用 分段架构(segmented architecture),分成了 Code 段、Data 段、Stack 段。存储可执行代码的内存在 Code 段当中,存储变量数组字符串等数据的内存在 Data 段当中,栈在 Stack 段里面。
中断
中断(interrupt) 提供了硬件接口。
所有 PC 都是有中断机制的。这是 DOS 系统的基础。
中断类似于函数调用,利用名叫 向量表(vector table) 的数据结构查询将要调用的函数的地址。8086 汇编指令 int
不是 integer 的缩写,而是 interrupt 的缩写。int X
即调用 X 对应的函数(功能)。例如:int 0x21
的作用是查找向量表当中第 0x21 个地址是哪个函数,然后调用它。不过,在汇编当中,十六进制要用 XXh 表示 0xXX,即应当写作 int 21h
或 int 33
而不是 int 0x21
。
int 21h 常用功能表
AH | 功能 | 调用参数 | 返回参数 |
---|---|---|---|
01 | 键盘输入并回显 | AL=输入字符 | |
02 | 显示输出 | DL=输出字符 | |
07 | 键盘输入(无回显) | AL=输入字符 | |
08 | 键盘输入(无回显)检测Ctrl-Break | AL=输入字符 | |
09 | 显示字符串DS:DX=串地址 ‘$’结束字符串 |