目录
前言
本文将带领读者走进80x86汇编的世界,后续章节也会从基础的指令集到复杂的程序设计,从简单的数据处理到系统级的编程技巧,逐步展开一幅汇编语言的宏伟画卷。
x86 指令集最初由 Intel 公司开发,是一种 32 位的复杂指令集架构,自 80386 处理器起一直沿用至今。Intel 官方将这种指令集称为“IA-32”。
X86_64 则是一种 64 位指令集,它与 x86 的主要区别在于处理数据位宽的不同。x86_64 指令集允许 CPU 一次处理 64 位数据,相较于 32 位的 x86 指令集,它提供了更高的处理效率。
这种 32 位与 64 位的区分也对应于我们常说的 32 位和 64 位操作系统,它们分别支持相应位宽的指令集和数据处理能力。
在 x86 系列中,字母“X”代表一个广泛的指令集型号范围。为了统一称呼,x86 现在特指 32 位指令集。
x86 家族的指令集发展历程如下:
- 8086 和 8088 处理器使用 16 位寄存器。
- 80186 和 80286 处理器是过渡产品。
- 80386、80486 以及后续的处理器型号均采用 32 位寄存器。
一、汇编相关概念
1.1 数据表示与类型
下面的数据表示也会在汇编中使用
BCD码(Binary-Coded Decimal)是一种将十进制数字转换为二进制表示的方法。在这种编码方式中,每一位十进制数字(0到9)都用4位二进制数来表示。BCD码有多种类型,其中最常见的是8421BCD编码。8421BCD编码直接对应于0到9的二进制表示,其中“8421”代表4位二进制数的权重(从最高位到最低位依次为8、4、2、1),因此这种编码方式也被称为“自然BCD码”。
1.2 汇编语言的构成
1. 汇编指令:这些是机器码的助记符,与特定的机器码相对应。
2. 伪指令和其他符号:这些由编译器执行和识别。
1.3 存储器及指令、数据
CPU的构成包括运算器、控制器、寄存器等组件,这些组件通过内部总线相互连接。
内部总线负责在CPU内部各组件之间传输信息;而外部总线则负责CPU与主板上的其他组件之间的通信。
在CPU内部:
运算器负责执行信息处理任务;
寄存器用于存储信息;
控制器负责协调和控制各个组件的工作;
内部总线则在各个组件之间传输数据。
CPU需要指令和数据来进行操作,这些指令和数据都存储在存储器中。无论是存储在内存还是磁盘中的信息,都是二进制形式。指令和数据的区分由我们通过总线来设定。各类存储器在物理上是独立的,都和CPU的总线相连,CPU对他们进行读或写的时候都通过控制线发出的内存读写命令,不同的计算机系统的内存地址空间分配情况是不同的。
各类存储器
1.4 存储单元
存储器被分为多个存储单元,每个单元从0开始按顺序编号,常用的存储单位包括B(字节)、KB(千字节)、MB(兆字节)、GB(吉字节)和TB(太字节)。
1.5 CPU对存储器的读写操作
CPU在进行数据的读写操作时,需要与外部器件进行以下三种信息的交互:
1. 存储单元的地址(地址信息)。
2. 器件的选择以及读或写的命令(控制信息)。
3. 读或写的数据(数据信息)。
总线是连接CPU与其他芯片的导线,从逻辑上分为地址总线、数据总线和控制总线。
地址总线
CPU使用地址总线来指定存储单元,地址总线的宽度决定了CPU的寻址能力(例如,32位CPU的寻址能力为2^32,即4GB)。
数据总线
CPU通过数据总线与内存等器件进行数据传输,数据总线的宽度决定了CPU与外界的数据传输速度。
控制总线
控制总线包含多种控制信号,CPU通过控制总线对外部器件进行控制。控制总线的宽度决定了CPU对外部器件的控制能力。
1.6 CPU读写内存单元的过程
1. CPU通过地址线发送地址信息。
2. CPU通过控制线发送内存读命令,选择存储器芯片,并通知其进行数据的读或写操作。
3. 存储器通过数据线将指定地址单元中的数据发送给CPU,或者CPU通过数据线将数据写入指定的内存单元。
1.7 intel CPU发展
Intel 8086: 该处理器由总线接口单元(BIU)和执行单元(EU)组成。总线接口单元BIU包含一组段寄存器、一个指令指针、一个6字节的指令队列、地址生成器和总线控制器。执行单元EU则包含一个算术逻辑单元(ALU)、一组16位的通用寄存器和标志寄存器,负责执行指令并进行算术逻辑运算。
Intel 8088: 为了适应当时主流的8位处理器市场,Intel公司推出了8086的8位版本——8088处理器。8088拥有与8086相同的架构,包括EU和BIU等部件以及16位寄存器,但区别在于8088只有8根外部数据线,因此每次只能按字节存取内存单元。
Intel 80186: Intel公司在1981年推出了80186处理器,它在8086的基础上增加了片选逻辑部件、两个独立的高速直接存储器访问通道、三个可编程时钟、一个可编程中断控制器和一个时钟发生器等。80186的指令集涵盖了从早期的8080开始的所有指令,并新增了十余条新指令。从汇编语言程序设计的角度来看,80186仅比8086多了几条指令。
Intel 80286: 1982年2月,Intel公司推出了一款超级16位微处理器——80286。它在速度和性能上相较于8086/8088和80186有显著提升,拥有24根地址线,最大物理寻址空间达到16MB,并增加了实模式和保护模式。在实模式下,80286相当于一个快速的80186,寻址空间限制为1MB;只有在保护模式下,80286才能充分发挥其全部功能,实现对16MB物理地址空间的寻址。
1.8 8086 内部结构
二、寄存器
2.1 寄存器概览
下面是x86处理器的基本寄存器
寄存器之间的关系如下:
2.2 32位寄存器
EAX:扩展累加寄存器,在乘法和除法操作中被主动使用,也可用于其他目的。
ECX:循环计数器,通常用于循环操作的计数,但在其他情况下也可以用作其他用途。
EDX:数据寄存器,经常用来存储64位数据的较高32位,同样可以用于其他目的。
EBX:基址寄存器,常用作存储内存地址,也可以用于其他用途。
ESP:堆栈指针,指向堆栈顶部(即当前栈帧的栈顶),在绝大多数情况下不能用作其他用途。
EBP:基址指针,指向当前栈帧的栈底,通常情况下不能用作其他用途。
ESI:扩展源索引寄存器,由高速内存数据传输指令使用,可以用于其他目的。
EDI:扩展目的索引寄存器,由高速内存数据传输指令使用,可以用于其他目的。
EIP:指令指针,指向即将执行的下一条指令的地址,几乎不能用于其他用途。
2.3 16位寄存器
2.4 通用寄存器
8086处理器的16位通用寄存器包括:
AX、BX、CX、DX、SI、DI、BP 和 SP。
这些寄存器中的前四个(AX、BX、CX、DX)可以进一步细分为高8位和低8位两个独立的寄存器。
8086处理器的8位通用寄存器则是:
AH、BH、CH、DH 以及 AL、BL、CL、DL。
对这些8位寄存器的操作不会影响到与之对应的另一个8位寄存器的数据。
2.4.1 数据寄存器
数据寄存器用于存储计算结果和操作数,同时也可以用于存储地址。每个寄存器都有其特定的用途:
**AX**(累加器):使用频率最高,用于执行算术和逻辑运算,以及与外部设备进行信息传输。
**BX**(基址寄存器):常用作存储内存地址。
**CX**(计数器):在循环和字符串操作等指令中作为隐含的计数器。
**DX**(数据寄存器):常用于存储双字长数据的高16位,或者用于存储外部设备的端口地址。
2.4.2 变址寄存器
变址寄存器在存储器寻址过程中提供地址信息。具体来说:
**SI**(源变址寄存器):在字符串操作指令中,SI 用于指向源数据的位置。
**DI**(目的变址寄存器):在字符串操作指令中,DI 用于指向目标数据的位置。
在字符串操作类指令中,SI 和 DI 扮演着关键角色,它们分别用于指定数据源和数据目标的内存地址。
2.4.3 指针寄存器
指针寄存器用于定位内存堆栈中的数据。具体来说:
**SP**(堆栈指针寄存器):指示堆栈顶部的偏移地址,SP 具有专用目的,不能用于其他用途。
**BP**(基址指针寄存器):表示数据在堆栈段中的基地址。
SP 和 BP 寄存器通常与 SS(堆栈段)寄存器联合使用,以确定堆栈段中的具体存储单元地址。
2.4.4 栈
栈(Stack)是主存中的一个特殊区域,虽然它不属于寄存器,但它遵循先进后出(FILO,First In Last Out)或后进先出(LIFO,Last In First Out)的原则进行存取操作,这与随机存取操作方式不同。
堆栈通常由处理器自动管理。在8086处理器中,堆栈的位置由堆段寄存器(SS)和堆指针寄存器(SP)共同指示,确保数据的正确存取和管理。
2.4.5 指令指针寄存器
指令指针寄存器(IP)指示代码段中指令的偏移地址。它与代码段寄存器(CS)联合使用,共同确定下一条指令的物理地址。计算机通过CS:IP寄存器来控制指令序列的执行流程。IP寄存器是一个专用寄存器,其主要功能是指示即将执行的指令的位置。
2.4.6 标志寄存器
标志(Flag)用于反映指令执行的结果或控制指令的执行形式。在8086处理器中,各种标志组合成一个16位的标志寄存器,称为FLAGS。这个寄存器包含了多个标志位,每个标志位都有其特定的用途,用于指示运算结果或影响后续指令的执行。
三、标志位
状态标志用于记录程序运行结果的状态信息,许多指令的执行都会相应地设置这些标志。在8086处理器中,状态标志包括进位标志(CF)、零标志(ZF)、符号标志(SF)、奇偶标志(PF)、溢出标志(OF)和辅助进位标志(AF)。
控制标志则可以由程序根据需要通过指令来设置,用于控制处理器执行指令的方式。在8086处理器中,控制标志包括方向标志(DF)、中断允许标志(IF)和陷阱标志(TF)。这些标志影响处理器的行为,例如数据操作的方向、是否允许中断以及是否进入单步调试模式。
3.1 进位标志 CF
进位标志(CF,Carry Flag)用于指示运算结果的最高有效位是否产生了进位(在加法中)或借位(在减法中)。当发生进位或借位时,进位标志被置为1,即CF=1;否则,CF=0。
例如:
- 在3A + 7C = B6的运算中,没有发生进位,因此CF=0。
- 在AA + 7C = (1)26的运算中,由于最高有效位产生了进位,因此CF=1。
3.2 零标志ZF
零标志(ZF,Zero Flag)用于指示运算结果是否为零。如果运算结果为0,则ZF=1;否则,ZF=0。需要注意的是,ZF为1表示运算结果是0。
例如:
- 在3A + 7C = B6的运算中,结果不是零,因此ZF=0。
- 在84 + 7C = (1)00的运算中,结果是零,因此ZF=1。
3.3 符号标志
符号标志(SF,Sign Flag)用于指示运算结果的最高位是否为1。对于有符号数据,最高有效位表示数据的符号,因此最高有效位的状态就是符号标志的状态。如果运算结果的最高位为1,则SF=1;否则,SF=0。
例如:
- 在3A + 7C = B6的运算中,结果的最高位D7=1,因此SF=1。
- 在84 + 7C = (1)00的运算中,结果的最高位D7=0,因此SF=0。
3.4 奇偶标志
奇偶标志(PF,Parity Flag)用于指示运算结果最低8位中“1”的个数是否为零或偶数。如果最低8位中“1”的个数为零或偶数,则PF=1;否则,PF=0。需要注意的是,PF标志仅反映最低8位中“1”的个数是偶数还是奇数,即使是进行16位字操作也是如此。
例如:
- 在3A + 7C = B6 = 10110110B的运算中,结果的最低8位中有5个“1”,是奇数,因此PF=0。
3.5 溢出标志
溢出标志(OF,Overflow Flag)用于指示算术运算的结果是否产生了溢出。如果运算结果超出了16位有符号数的表示范围(即+32767到-32768),则OF=1;否则,OF=0。溢出意味着有符号数的运算结果不正确。
溢出和进位的区别:溢出标志OF和进位标志CF是两个不同的标志。进位标志表示无符号数运算结果是否超出范围,运算结果仍然正确;而溢出标志表示有符号数运算结果是否超出范围,运算结果已经不正确。
例如:
- 在3A + 7C = B6的运算中,产生了溢出,因此OF=1。
- 在AA + 7C = (1)26的运算中,没有产生溢出,因此OF=0。
3.6 辅助进位标志
辅助进位标志(AF,Auxiliary Carry Flag)用于指示运算时D位(低半字节,即最低4位)是否有进位或借位。如果在低半字节有进位或借位,则AF=1;否则,AF=0。这个标志主要由处理器内部使用,用于十进制算术运算调整指令中,用户一般不必关心。
例如:
- 在3AH + 7CH = B6H的运算中,D位有进位,因此AF=1。
3.7 方向标志
方向标志(DF,Direction Flag)用于控制串操作指令中存储器地址的变化方向。如果设置DF=0,存储器地址将自动增加;如果设置DF=1,存储器地址将自动减少。
例如:
- CLD指令用于复位方向标志,执行后DF=0,这将导致存储器地址自动增加。
- STD指令用于置位方向标志,执行后DF=1,这将导致存储器地址自动减少。
3.8 中断允许标志
中断允许标志(IF,Interrupt-enable Flag)用于控制外部可屏蔽中断是否可以被处理器响应。如果设置IF=1,则允许中断;如果设置IF=0,则禁止中断。
例如:
- CLI指令用于复位中断标志,执行后IF=0,这将禁止外部可屏蔽中断。
- STI指令用于置位中断标志,执行后IF=1,这将允许外部可屏蔽中断。
3.9 陷阱标志
陷阱标志(TF,Trap Flag)用于控制处理器是否进入单步操作方式。如果设置TF=0,处理器将正常工作;如果设置TF=1,处理器将单步执行指令。单步执行指令意味着处理器在每条指令执行结束后会产生一个编号为1的内部中断,这种内部中断称为单步中断。因此,TF也被称为单步标志。利用单步中断可以对程序进行逐条指令的调试,这种逐条指令调试程序的方法就是单步调试。
四、分段管理
8086 CPU 拥有 20 条地址线,因此其最大可寻址空间为 (2^{20} = 1MB),物理地址范围从 00000H 到 FFFFFH。为了简化内存管理,8086 CPU 将这 1MB 的内存空间划分为多个逻辑段(Segment),每个段的最大限制为 64KB(即 (2^{16}) 字节)。段地址的低 4 位固定为 0000B,这意味着段地址总是 16 的倍数。
由于这种分段机制,一个存储单元除了具有一个唯一的物理地址外,还可以通过不同的段地址和偏移地址组合来访问,从而具有多个逻辑地址。例如,一个物理地址可以通过不同的段地址和偏移地址对来表示,只要它们的组合能正确映射到该物理地址。这种设计使得内存管理更加灵活,但也增加了地址转换的复杂性。
4.1 物理地址和逻辑地址
物理地址和逻辑地址是内存管理中的两个重要概念。每个物理存储单元都有一个唯一的20位编号,这就是物理地址,其范围从00000H到FFFFFH。
在用户编程时,采用的是逻辑地址,其形式为“段基地址:段内偏移地址”。为了将逻辑地址转换为物理地址,需要将逻辑地址中的段地址左移4位(即乘以16),然后加上偏移地址,这样就可以得到20位的物理地址。
一个物理地址可以对应多个逻辑地址。例如,逻辑地址1460:100和1380:F00都对应同一个物理地址14700。这是因为:
1) 对于逻辑地址1460:100,段地址1460H左移4位得到14600H,加上偏移地址100H,得到物理地址14700H。
2)对于逻辑地址1380:F00,段地址1380H左移4位得到13800H,加上偏移地址F00H,同样得到物理地址14700H。
这种逻辑地址到物理地址的转换机制使得同一个物理地址可以通过不同的逻辑地址来访问,从而提供了内存管理的灵活性。
4.2 段寄存器和逻辑段
8086 CPU 拥有四个16位的段寄存器,它们分别是:
**CS(代码段寄存器)**:指明代码段的起始地址。代码段用于存放程序的指令序列。处理器利用CS:IP(指令指针寄存器)来取得下一条要执行的指令。
**SS(堆栈段寄存器)**:指明堆栈段的起始地址。堆栈段确定了堆栈所在的主存区域。处理器利用SS:SP(堆栈指针寄存器)来操作堆栈顶的数据。
**DS(数据段寄存器)**:指明数据段的起始地址。数据段用于存放运行程序所用的数据。处理器利用DS:EA(有效地址)来存取数据段中的数据。
**ES(附加段寄存器)**:指明附加段的起始地址。附加段是一个附加的数据段,也用于数据的保存。处理器利用ES:EA来存取附加段中的数据。在串操作指令中,附加段被用作目的操作数的存放区域。
每个段寄存器都用来确定一个逻辑段的起始地址,每种逻辑段都有其特定的用途。这种分段机制使得8086 CPU能够有效地管理内存,同时也为程序的组织和数据的管理提供了灵活性。
4.3 段超越前缀指令
段超越前缀指令允许程序员在访问内存时改变默认的段寄存器。在没有指明的情况下,一般的数据访问默认在DS段进行,而使用BP寄存器访问主存时,则在SS段。然而,默认情况是可以改变的,这时就需要使用段超越前缀指令。8086指令系统中提供了四个段超越前缀指令:
- **CS:** 代码段超越,用于访问代码段中的数据。
- **SS:** 堆栈段超越,用于访问堆栈段中的数据。
- **DS:** 数据段超越,用于访问数据段中的数据。
- **ES:** 附加段超越,用于访问附加段中的数据。
以下是一些指令实例:
- 没有段超越的指令实例:
MOV AX, [2000H] ; AX <- DS:[2000H]
这条指令从默认的DS数据段中取出地址为2000H的数据,并将其存入AX寄存器。
- 采用段超越前缀的指令实例:
MOV AX, ES:[2000H] ; AX <- ES:[2000H]
这条指令从指定的ES附加段中取出地址为2000H的数据,并将其存入AX寄存器。
通过使用段超越前缀指令,程序员可以在需要时改变数据访问的默认段,从而实现更灵活的内存访问。
4.4 段寄存器的使用规定
4.5 存储器的分段
存储器的分段是8086 CPU内存管理的一个重要特性。8086对逻辑段有一些特定的要求和限制:
- **段地址低4位均为0**:这意味着段地址总是16的倍数。
- **每段最大不超过64KB**:每个逻辑段的最大大小限制为64KB(即 (2^{16}) 字节)。
然而,8086对逻辑段并不要求:
- **必须是64KB**:一个段可以小于64KB。
- **各段之间完全分开**:逻辑段之间可以重叠。
关于1MB空间的段数:
- **1MB空间最多能分成多少个段**:由于每隔16个存储单元就可以开始一个段,所以1MB最多可以有 ( frac{2^{20}}{2^4} = 2^{16} = 64K ) 个段。
- **1MB空间最少能分成多少个段**:如果每隔64KB(即 (2^{16}) 字节)开始一个段,那么1MB最少可以有 ( frac{2^{20}}{2^{16}} = 2^4 = 16 ) 个段。
这种分段机制提供了灵活性,使得程序可以根据需要组织和管理内存。
五、本节总结
8086 CPU 拥有丰富的寄存器资源,包括:
**8个8位通用寄存器**:这些寄存器可以用于存储数据和地址,包括AH、AL、BH、BL、CH、CL、DH、DL。
**8个16位通用寄存器**:这些寄存器同样可以用于存储数据和地址,包括AX、BX、CX、DX、SI、DI、BP、SP。
此外,8086 CPU 还包括:
**6个状态标志**:这些标志用于反映指令执行的结果,包括CF(进位标志)、PF(奇偶标志)、AF(辅助进位标志)、ZF(零标志)、SF(符号标志)、OF(溢出标志)。
**3个控制标志**:这些标志用于控制处理器的行为,包括DF(方向标志)、IF(中断允许标志)、TF(陷阱标志)。
8086 CPU 采用分段管理方式来管理1MB的存储空间,为此提供了:
**4个段寄存器**:这些寄存器分别对应4种逻辑段,包括CS(代码段寄存器)、DS(数据段寄存器)、SS(堆栈段寄存器)、ES(附加段寄存器)。
为了在需要时改变默认的段寄存器,8086 CPU 提供了:
**4个段超越前缀指令**:这些指令用于明确指定数据所在的逻辑段,包括CS:、DS:、SS:、ES:。
通过这些寄存器和指令,8086 CPU 能够有效地管理和访问内存,同时也为程序的组织和数据的管理提供了灵活性。