X86基础
Intel 8086/8088 CPU 基础。
基本结构
8086和8088的体系结构、指令系统、指令编码格式和寻址方式完全相同,软件互相兼容。
- 8086和8088的寄存器、寻址和执行都是16位的。
- 8088的数据总线只有8位,因此在一个总线周期只能访问一个8位字节。
- 8086的数据总线有16位。
8088内部有20根地址线,可访问内存为1MB。
- 可随机访问的内存(RAM,640KB,地址空间范围为00000H ~ 9FFFFH)。
- 只读的BIOS(ROM)
- 设备接口空间(ROM/RAM)
寄存器
14个16位寄存器。
8个通用寄存器
- 数据寄存器:AX, BX, CX, DX
- 指针及变址寄存器:SP, BP, SI, DI
数据寄存器
可将AX, BX, CX, DX拆分为AH, AL, BH, BL, CH, CL, DH, DL8个8位寄存器。
虽然被称为通用寄存器,但有约定的用途。
- AX:累加器(accumulator),用于存放操作数和运算结果(如乘除法指令),在输入输出指令中用于传送数据,系统调用功能号(AH)。
- BX:基址寄存器(base register),间接寻址时存放基址。
- CX:计数寄存器(count register),用于循环计数,移位指令使用CL位计数器。
- DX: 一般用于通用寄存器,也用于存放I/O端口的输入输出地址,在乘除法指令中用于AX的高位拓展(乘积高位、被除数高位、余数)。
指针及变址寄存器
指针P(pointer),变址I(index)。
- SP:堆栈指针(stack pointer),指向堆栈顶部的偏移地址。
- BP:基址指针(base pointer),用于堆栈段的基址。
- SI:源变址寄存器(source index),一般用于存放源数据的偏移地址。
- DI:目的变址寄存器(destination index),一般用于存放目的数据的偏移地址。
4个段寄存器
段(segment)即内存中的一段区域,段寄存器存放段的起始地址/基址。根据8086/8088的地址空间,理论上段基址需要20位,但实际上只取高16位。
- CS:代码段寄存器(code segment),存放代码段的基址。
- DS:数据段寄存器(data segment),存放数据段的基址,若有多个数据段,需要相应修改DS寄存器。
- SS:堆栈段寄存器(stack segment),存放堆栈段的基址。
- ES:附加段寄存器(extra segment),用于存放附加数据段的基址,同时使用多个数据段时,可用ES寄存器。
1个指令指针寄存器
IP:指令指针寄存器(instruction pointer),存放(下一条)指令相对于代码段的偏移地址。不能直接修改。
1个标志寄存器
9个标志位,用于存放运算结果的状态信息。被组合为1个程序状态字寄存器PSW(Program Status Word)。
- CF:进位标志(carry flag),用于存放最高位的进位,位移指令中存放被移出的位。
- PF:奇偶标志(parity flag),奇偶校验。
- AF:辅助进位标志(auxiliary carry flag),低4位产生进位时置1,用于BCD运算。
- ZF:零标志(zero flag),运算结果为0时置1。
- SF:符号标志(sign flag),运算结果为负数时置1。
- TF:陷阱标志(trap flag),当置1时,执行一条指令后进入单步调试模式。
- IF:中断标志(interrupt flag),当置1时,允许CPU响应外部中断。
- DF:方向标志(direction flag),用于控制字符串操作指令的方向,置1时,地址递减。
- OF:溢出标志(overflow flag),运算结果超出寄存器范围时置1。
DF、IF、TF是控制标志,需要专门的指令来设置;其他是状态标志。
内存
内存单元
8086/8088内存基本单位为字节,每个字节有唯一地址。
8086/8088字长为16位,任何两个相邻单元的字节都可以组成一个字。采用小端模式,低地址存放低位字节。
8086/8088也支持双字(32位)操作,且在双字内,也是小端模式。
内存地址
8086/8088的段地址和段内地址(偏移地址)都用16位表示,段地址负责高16位,段内地址(偏移地址)负责低16位,两者有12位重叠。
8086/8088中与内存单元一一对应的地址称为物理地址(physical address),物理地址是8086/8088访问内存的实际地址。段地址:偏移地址的组合称为逻辑地址(logical address),这与操作系统中的逻辑地址概念不同。
物理地址 = 段地址 * 10H + 偏移地址
在实际编程中,把16字节称为1节(paragraph),段地的实际大小往往是按节对齐的。
堆栈
我一向觉得这里“堆栈”的命名有问题,因为“堆”和“栈”是两个不同的数据结构。这里的堆栈单指“栈”。
堆栈段的基址固定,堆栈指针SP指向堆栈顶部的偏移地址,堆栈是向低地址方向增长的。如一开始SS:SP=2400H:100H,运行一段时间后变为SS:SP=2400H:80H。
- 压栈:PUSH、PUSHF、CALL、INT(中断)等。
- 出栈:POP、POPF、RET、IRET(中断返回)等。
压栈和出栈的都以字为单位,即16位,SP的值加减2。
指令和寻址
指令格式
; 1
OP DST, SRC
; 2
OP SRC
; 3
OP
- OP:操作码,指令的功能。
- DST:目的操作数,指令的结果存放的位置。
- SRC:源操作数,指令的操作对象。
与精简指令集(RISC)不同,复杂指令集(CISC)的指令格式中,一条指令的长度不固定。8086/8088的指令长度为1~7字节。
操作码
操作码OP一般占用一个字节,的基本格式为:
op d/s + w
- op:操作码,1字节。
- d: 两个操作数,操作数为目的操作数(1)还是源操作数(0)。
- s: 操作数为立即数时是否有符号扩展,有符号(1)或无符号(0)。
- w: 操作数的字长,字长为16位(1)或8位(0)。
8086/8088规定指令的两个操作数必须有一个是寄存器,另一个可以是寄存器、立即数、内存单元。
有时操作码会超过一个字节。
指令寻址方式
用1个字节表示操作数的寻址方式。基本格式为:
7 6 5 4 3 2 1 0
+---+---+---+---+---+---+---+---+
| mod | reg | r/m |
- mod:另一个操作数的寻址方式:
- 00:常量字节寻址。
- 01:带1个相对量字节寻址。寻址方式后面跟着1字节相对量。
- 10:带2个相对量字节寻址。寻址方式后面跟着2字节相对量。
- 11:寄存器寻址。
- reg:寄存器编码,与w位结合:
- w=0:AL, CL, DL, BL, AH, CH, DH, BH
- w=1:AX, CX, DX, BX, SP, BP, SI, DI
- r/m:另一个操作数的寄存器编码或内存单元编码,与mod位结合:
- mod=11:寄存器寻址,r为寄存器编码。
- mod!=11:
- r=000:BX+SI
- r=001:BX+DI
- r=010:BP+SI
- r=011:BP+DI
- r=100:SI
- r=101:DI
- r=110:D16(mod=00), BP(mod=01/10)
- r=111:BX
段超越
如果不适用隐含的段寄存器,显式指定段寄存器,会导致段超越。需要再置入一个字节的前缀。
7 6 5 4 3 2 1 0
+---+---+---+---+---+---+---+---+
|001| | seg | 110 |
seg:
- 00:ES
- 01:CS
- 10:SS
- 11:DS
寻址方式
各指令所需的操作数来自:
- 寄存器
- 立即数
- 内存单元
立即寻址
立即数直接作为操作数,包含在指令中。需要与另一个操作数的位宽一致。
寄存器寻址
操作数是寄存器。必须遵循指令要求(如MOV的数据通路)。
寄存器寻址的操作数可能隐含,比如STD设置DF=1。
直接寻址
操作数在内存中,直接给出内存单元地址。如:
MOV AX, [1234H] ; 相对于DS段
偏移地址制定了相对于段基址的偏移量,CPU通过地址总线访问内存。
下面的代码也是直接寻址:
x DW 1234H
y DW 5678H
MOV AX, x
MOV AX, x+1
寄存器间接寻址
操作数是内存单元,内存单元地址存放在寄存器中。如:
MOV AX, [BX] ; 相对于DS段
MOV BH, [BP] ; 相对于SS段
MOV CX, [SI] ; 相对于DS段
上述代码隐含了段寄存器,也可以显式指定:
MOV AX, DS:[BX]
MOV BH, SS:[BP]
MOV CX, DS:[SI]
一般不会经常段超越。
下面指令是错误的:
MOV AX, [DX] ; DX不能用于间接寻址
MOV AX, [*L/*H] ; 偏移值为16位,不能用于间接寻址
寄存器相对寻址
操作数是内存单元,内存单元地址是一个寄存器加上一个相对量。如:
MOV AX, [SI+10H] ; 相对于DS段
MOV AX, 10H[SI] ; 等价于上面
有一种比较特殊的情况:
MOV AX, xxx[DI+yyy]
其本质是:
MOV AX, [DI+(xxx+yyy)]
基址变址寻址
操作数是内存单元,内存单元地址是一个基址寄存器加上一个变址寄存器。如:
MOV AX, [BX+SI] ; 相对于DS段
MOV AX, [BX][SI] ; 等价于上面
MOV AX, DS:[BP+DI]
下面的也是合法的:
MOV AX, [BX+SI+10H]
需要注意的是,基址变址寻址的基址寄存器只能是BX或BP,变址寄存器只能是SI或DI。
转移地址
段内/段间 直接/间接 寻址。
段内直接寻址
汇编为相对于当前IP的偏移量,属于相对寻址。
- 短跳转:8位偏移量,SHORT操作符。
- 近跳转:16位偏移量,NEAR PTR操作符。
对于条件转移,只能是8位偏移量,忽略SHORT操作符,如果JMP指令省略了操作符,则使用16位偏移量。
段内间接寻址
跳转的有效地址存放在寄存器或内存单元中。存储单元位于数据段中,可以用任何一个寻址方式。
MOV AX, OFFSET label ; AX, BX, CX, DX, SI, DI,BP均可,一般不用SP
CALL AX
需要注意的是,操作数是内存地址和标签的区别:
CALL label ; 直接寻址
CALL DATA ; 间接寻址
更直观比如:
CALL 1234H ; 直接寻址
CALL [1234H] ; 间接寻址
段间直接寻址
指定段地址和偏移地址。
CALL FAR PTR proc1 ; proc1是由FAR定义的过程
段间间接寻址
间接寻址时地址放入内存单元,且为双字。
JMP DWORD PTR [1234H]
转移后,高位字为CS,低位字为IP。
指令系统
- 数据传送指令
- 算术指令
- 逻辑指令
- 串处理指令
- 控制转移指令
- 处理机控制指令
数据通路
MOV指令的数据通路:
- 通用寄存器 -> 通用寄存器
- 通用寄存器 -> 段寄存器
- 通用寄存器 -> 内存
- 段寄存器 -> 通用寄存器
- 段寄存器 -> 内存
- 内存 -> 通用寄存器
- 内存 -> 段寄存器
- 立即数 -> 通用寄存器
- 立即数 -> 内存
注意内存不能直接到内存,段寄存器不能直接到段寄存器,立即数不能直接到段寄存器。
X86汇编
不区分大小写。
指令
[标号:] 指令助记符 指令操作数 [;注释]
- 标号:相对于当前段的偏移量。
- 指令助记符:指令的名称,如MOV、ADD、JMP等。
- 指令操作数:指令的操作数,可以是寄存器、内存单元、立即数等,也可能没有操作数。
- 注释:分号后的内容,不影响程序执行。
伪指令
[符号名] 伪指令 操作数 [;注释]
- 符号名:无冒号,段名、过程名、变量名、常量名等。
- 伪指令:不是真正的指令,是编译器的指令,用于定义数据、段、过程等。
- 操作数:伪指令的操作数。
指令和伪指令中出现的常数,当以A-F开头时,必须在前面加0,以避免被认为是变量名或常量名。
段定义
段名 SEGMENT [对齐类型][组合类型][类别名]
...
段名 ENDS
- 段名:段的名称。
- 对齐类型:
- BYTE:字节对齐。
- WORD:字对齐。
- PARA:节对齐,16字节。
- PAGE:页对齐,256字节。
- 组合类型:
- NONE:缺省,本段独立。
- PUBLIC:公共段,合并为一个段,基址为其中最早的段基址。
- COMMON:共享段,与其它具有相同段名的段共享段基址,段长度为其中最大的段长度。
- STACK:堆栈段,说明为堆栈段,会将所有同名的具有堆栈属性的段合并为一个堆栈段,并将SS初始化为堆栈段的基址,将SP初始化为堆栈段的长度。
- MEMORY: 内存段,表示应该定位在所有其它连接在一起的段之前。
- AT 表达式:直接指定段的物理地址,节对齐。
- 类别名:段的类别名,用单/双引号括起来,链接程序会将类别名相同的段放在连续的内存区域,但仍是独立的段,优先级低于组合类型。
通过ASSUME语句,将段寄存器与段名关联。
过程定义
过程名 PROC [NEAR/FAR]
...
RET
过程名 ENDP
- 过程名:过程的名称,CALL的操作数。
- NEAR/FAR:过程的类型,缺省为NEAR:
- NEAR:近过程,段内过程,CALL后压栈下一条指令的偏移量。RET仍为RET。
- FAR:远过程,段间过程,CALL后压栈段基址(一般是CS)和下一条指令的偏移量。RET汇编为RETF。
数据定义
常量由EQU伪指令定义。
常量名 EQU 常量表达式
当前位置由$符号表示,可以带入常量表达式。
DUP(operand,…,operand)为重复定义。
?表示不予置值。
用DB、DW、DD定义字节、字、双字。
[符号名] DB/DW/DD 操作数[, 操作数, ...]
数值回送
数值回送操作符有TYPE、LENGTH、SIZE、OFFSET和SEG共5种。
- TYPE:
TYPE 变量名/标号
,返回变量的数据类型的值:- BYTE:1,DB定义的变量。
- WORD:2,DW定义的变量。
- DWORD:4,DD定义的变量。
- NEAR:-1,段内标号或过程。
- FAR:-2,段间标号或过程。
- LENGTH:
LENGTH 变量名
,返回变量的长度。 - SIZE:
SIZE 变量名
,返回变量的字节数,SIZE = LENGTH * TYPE。
实际上,SIZE和LENGTH没什么用,因为DUP伪指令会干扰其计算。实际应用时将两个标号相减即可得到实际的字节长度。
- OFFSET:
OFFSET 变量名/标号
,返回变量的偏移地址。 - SEG:
SEG 变量名/标号
,返回变量的段地址。
属性操作
属性操作符有PTR、段操作符、SHORT、THIS、HIGH、LOW共6种。
- PTR:
类型 PTR 地址表达式
,临时修改地址表达式的类型。 - 段操作符:
CS:/DS:/ES:/SS: 地址表达式
,用于段超越。 - THIS:
THIS 类型名
,为一个地址指定另一个类型。 - SHORT:
SHORT 地址表达式
,将地址表达式转换为短地址。 - HIGH:
HIGH 变量名
,返回变量的高位字节。 - LOW:
LOW 变量名
,返回变量的低位字节。
其它伪指令
- ORG: 为下一条指令或数据指定一个偏移地址。
- END: 程序/模块结束,指定一个标号作为程序的入口。
- MACRO/ENDM: 定义宏,ENDM不需要宏名。
程序的调试与DEBUG
- d [段名:][偏移量] [长度]:显示指定地址的内存内容,默认DS。(dump)
- e [段名:][偏移量] [内容表]:修改指定地址的内存内容。(edit)
- r [寄存器]:显示或修改寄存器内容。(register)
- f:填写内存,批量修改内存内容,默认DS。(fill)
- t: 单步跟踪。(trace)
- p:单步执行,不会进入CALL指令。(proceed)
- g [地址]:执行到指定地址,默认CS。(go)
- u [段名:][偏移量] [长度]:反汇编,默认CS。(unassemble)
- a [段名:][偏移量]:汇编,默认CS。(assemble)
- q:退出DEBUG。
控制转移
无条件转移
即JMP指令。
条件转移
无符号:
- JA:大于。
- JAE:大于等于。有时汇编为JNB。
- JB:小于。
- JBE:小于等于。
- JE:等于。
- JNE:不等于。
有符号:
- JG:大于。
- JL:小于。
其它:
- JC:进位。
- JZ:零。
循环
LOOP:CX-1,不为0则转移。 JCXZ:CX为0则转移。
过程调用
CALL:将下一条指令的偏移地址压栈,转移。 RET/RETF:从栈中弹出地址,转移。
中断
INT n:调用中断向量表中的第n个中断处理程序。
跳表
在C语言中,很多初学者不能理解switch-case中需要break的原因,抑或是认为if-else和switch-case是等价的。实际上,switch-case模仿的是跳表或者跳转表的实现。switch-case判断的变量理论上是在判断一个偏移量,以下是一个简单的跳转表:
TABLE DW CASE1, CASE2, CASE3, CASE4, CASE5
MOV BX, VAR
JMP TABLE[BX]
CASE1:
...
JMP BREAK
CASE2:
...
JMP BREAK
CASE3:
...
JMP BREAK
CASE4:
...
JMP BREAK
CASE5:
...
JMP BREAK
BREAK:
串处理
串处理操作隐含的是相对于DS和ES的SI和DI的间接寻址。它典型的寻址过程是:
DS:[SI] -> ES:[DI]
取串:
- LODSB:把DS:[SI]字节取到AL,SI的值加减1。
- LODSW:把DS:[SI]字取到AX,SI的值加减2。
存串:
- STOSB:把AL存到ES:[DI],DI的值加减1。
- STOSW:把AX存到ES:[DI],DI的值加减2。
串传送:
- MOVSB:把DS:[SI]字节传送到ES:[DI],SI和DI的值加减1。
- MOVSW:把DS:[SI]字传送到ES:[DI],SI和DI的值加减2。 此处是特例,其不满足MOV指令的数据通路,而且两个操作数都是内存单元。
串比较:
- CMPSB:比较DS:[SI]和ES:[DI],SI和DI的值加减1。
- CMPSW:比较DS:[SI]和ES:[DI],SI和DI的值加减2。 一般会和条件转移指令结合。
串扫描:
- SCASB:扫描ES:[DI]的字节,判断是否为AL,DI的值加减1。
- SCASW:扫描ES:[DI]的字,判断是否为AX,DI的值加减2。 扫描方法是用AL/AX减去ES:[DI],会影响标志位。
串重复:
- REP:重复执行串操作,例如
REP MOVSB
,直到CX为0。 - REPE:重复执行串操作,直到CX为0或ZF为0。
- REPNE:重复执行串操作,直到CX为0或ZF为1。
修改方向
CLD:使DF=0,串操作从低地址向高地址。 STD:使DF=1,串操作从高地址向低地址。
注意传操作会改变一些寄存器的值,或者需要手动修改一些寄存器的值,因此调用时注意压栈。
调用和组织
子程序
过程(子程序)通过伪指令定义:
过程名 PROC [NEAR/FAR]
...
过程名 ENDP
NEAR和FAR对应的RET和RETF等相关对应不再赘述。
参数传递
- 寄存器
- 约定的内存变量
- 堆栈
保存环境信息
需要防止子程序破坏调用者的环境信息。一方面减少修改段寄存器等,另一方面用堆栈保存子程序中修改的寄存器的原始值。
保存寄存器的值有两种方式,一种是在子程序中被动保存,另一种是调用者主动保存。在书上一般使用前者,大概是因为可以降低程序的耦合。在C语言中,有__stdcall
和__cdecl
等调用约定,与此相关。
堆栈平衡
堆栈平衡是指子程序在返回时,堆栈的状态与调用时一致。否则无法返回到正确的地址。
堆栈传参
堆栈传参是指将参数压栈,然后子程序从堆栈中取出参数。这种方式的优点是参数个数不受限制,缺点是压栈和出栈的操作有性能开销。
一般的操作是在子程序内用BP寄存器保存刚刚进入子程序时的SP值,然后用BP寄存器相对寻址的方式从内存中取出参数。
返回时需要在RET指令后加上参数的字节数,以便平衡堆栈正确返回。这种情况会使得PUSH和POP的个数不相等。
PNAME PROC NEAR
PUSH BP
MOV BP, SP
... ; 环境保存
MOV AX, [BP+4] ; 第一个参数,4是BP和IP的偏移量,如果是FAR则是6
MOV BX, [BP+6] ; 第二个参数
...
... ; 子程序操作
... ; 环境恢复
POP BP
RET n ; n是参数的字节数
PNAME ENDP
与riscv(规范使用堆栈传递参数)等不同,X86并没有一个严格的参数调用规范,但堆栈传递会比较通用和稳定。
宏
子程序需要引入额外开销,而宏是在编译时展开的,因此效率更高。宏的定义和调用如下:
宏名 MACRO [参数1, 参数2, ...] ; 形参
...
ENDM
宏名 [参数1, 参数2, ...] ; 实参
宏的参数可以是寄存器、内存单元、立即数等,也可以是字符串。宏的展开是简单的文本替换,不利于调试。
结构
结构是一种数据类型,可以包含多个数据成员。结构的定义和使用如下:
; 定义结构
结构名 STRUC
DB/DW/DD 数据成员1, 数据成员2, ...
...
结构名 ENDS
; 使用结构
变量名 结构名 <字段值表>
; 访问结构成员
变量名.数据成员1
结构本身不占用内存空间,只是提供了一种新的数据类型或者说新的数据组织方式。
事实上,结构提供的组织方式并不一定要用于声明的结构变量,通过数组.数据成员
的方式也可以应用结构的组织,结构本质上是内存区域.偏移量
。
中断
BIOS中断
在内存较高地址区域内的ROM种固化了BIOS系统。
中断号小于10H的为硬中断,由IO硬件事件触发,一般不由程序调用(虽然可以调用);大于10H的为软中断,由系统软件和用户程序调用。
中断号 | 功能 | 中断号 | 功能 |
---|---|---|---|
00H | 除法溢出 | 10H | 显示器 |
01H | 单步中断 | 11H | 设备检验 |
02H | 不可屏蔽中断 | 12H | 内存大小检查 |
03H | 断点中断 | 13H | 磁盘 |
04H | 溢出中断 | 14H | 异步通信 |
05H | 打印屏幕中断 | 15H | IO系统扩充 |
08H | 时钟中断 | 16H | 键盘 |
09H | 键盘中断 | 17H | 打印机 |
0BH | UART1中断 | 18H | 驻留BIOS |
0CH | UAR0中断 | 19H | 引导 |
0DH | 硬盘中断 | 1AH | 时钟 |
0EH | 软盘中断 | 1BH | 键盘Break |
0FH | 并行打印机中断 | 1CH | 定时器 |
DOS中断
DOS中断是BIOS中断的扩展,提供了更多的功能。DOS借用了BIOS中软中断的功能带哦用,大部分为用户提供的调用都位于21H中断中。
中断号 | 功能 | 中断号 | 功能 |
---|---|---|---|
20H | 退出程序 | 26H | 绝对磁盘写 |
21H | DOS功能 | 27H | 终止并驻留内存 |
22H | 结束地址 | 28H-3EH | DOS内部使用 |
23H | ctrl-break | 2FH | 补充 |
24H | 严重错误 | 30H-3FH | 保留 |
25H | 绝对磁盘读 |
常用DOS功能中断
使用21H中断,功能号放在AH寄存器中。有些功能需要传递出口参数和入口参数。
AH=
- 01H: 键盘输入字符,返回AL。
- 02H: 显示字符,DL为字符ASCII码。
- 09H: 显示字符串,DS:DX指向字符串。
- 0AH: 输入字符串,DS:DX指向缓冲区。
- 4CH: 退出程序,返回AL为退出码。