Skip to content

Lecture 2. 汇编语言 Part1:寄存器与基础汇编结构知识 ​

寄存器 ​

寄存器是处理器当中的存储位置,访问速度非常快

8086 微处理器当中有四个 通用寄存器(general purpose register):AX, BX, CX, DX,在初学阶段,可以把这四个玩意儿当作变量使用。每个寄存器都是 16 位 的,可以存储 的无符号数或者 的有符号数。

每个 _X 通用寄存器可以分为 _H 和 _L 两个八位寄存器,分别代表高位和低位。这两个八位寄存器可以存储无符号数的范围降低到 。于是,在汇编代码当中,可以使用的寄存器就有了 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 系列共计八个新寄存器。

6432168备注
%rax%eax%ax%alaccumulator(常用于运算与 IO);返回值;可接收 int 21h
%rbx%ebx%bx%blbase(常用于地址索引);被调用者保存
%rdx%ecx%cx%clcount(计数,常用于保存计算值);第 4 个参数,可配合 loop
%rdx%edx%dx%dldata(数据传递);第 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,用于比较大小)、奇偶性之类的。

指令代码作用
incinc AA = A + 1
decdec AA = A - 1
addadd A, BA = A + B
subsub A, BA = A - B
cmpcmp A, B将 A 减去 B,不修改 A 的值,但反映结果给 flag

flag 寄存器 ​

这个 flag 寄存器是 16 位的,每一位都代表一个 flag,结构如图:

flag register

从高位到低位看,有 7 个位未定义(U),此外还有 9 个 flag,含义分别是:

位名称缩写解释分类
11Ooverflow针对 有符号数 的运算,表示是否有溢出状态
10Ddirection若 D=1 表示字符串从高位地址向低位地址访问;D=0 相反控制
9Iinterrupt若 I=1 则处理器会识别外部中断,否则忽略所有的外部中断控制
8Ttrap用于调试,若 T=1,则每执行一步指令都进行一次中断控制
7Ssign用于判断正负,若运算后 MSB 是 1,则意味着是负数,于是 S=1状态
6Zzero若上一次的运算结果是 0 则 Z=1状态
4Aauxiliary辅助进位标志,与 BCD 码有关,表示低四位到高四位有无进位状态
2Pparity运算结果当中有偶数个 1 就 P=1,有奇数个 1 就 P=0状态
0Ccarry加法运算若产生了进位(或对于减法,借位),则此位设为 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,这个地址就是当前执行的指令的地址:

instruction pointer

栈 ​

指令可以指定,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。
  • 高级语言,代码的功能性更强大,但是需要借助编译器或解释器转化成机器语言

汇编语言程序结构分析 ​

asm
    .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 格式的代码,可以使用命令:

shell
gcc -S -masm=intel src.c

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-BreakAL=输入字符
09显示字符串DS:DX=串地址 ‘$’结束字符串