CPU、内存和端口
约 3277 个字 145 行代码 预计阅读时间 18 分钟
数据的表示方式¶
汇编语言使用关键词 db (Define Byte) 来定义字节类型的变量,展开来讲即:
关键词 | 位数 | 符号 | 变量 |
---|---|---|---|
db | 8 | byte | char |
dw | 16 | word | short int |
dd | 32 | double word | long int 或 float |
dq | 64 | quadruple word | __int64 , long long (十进制 %lld , 十六进制 %llx ) 或 double |
dt | 80 | ten bytes | long double (%Lf ) |
1 word 的位数表示一个系统单步能处理的最大位数
那么变量定义的格式如下:
当我们需要多个相同的初始值时,可以使用 DUP 运算符:
内存¶
内存以字节为单位分配地址,DOS系统运行在 CPU 的实模式下,可访问的地址范围为 [00000h,0FFFFFh]
,即最多只能访问 1MB 内存空间
我们将形如 0FFFFFh
的单个数值表示的地址称作物理地址。由于 8086 每个寄存器都均为 16 位宽度,没有一个寄存器能容纳一个 20 位的物理地址,为此 Intel 公司设计 8086 时定义了 4 个段地址寄存器和 4 个偏移地址寄存器,以便用逻辑地址形式即 Seg:Offset
组合来间接访问物理地址。
段地址和偏移地址两个概念都和段(segment)有关:
- 偏移地址指段内某个变量或标号与段首之间的距离
- 段地址是指 20 位段首地址的高 16 位
段 Segment
段是符合以下两个条件的一块内存空间:
- 内存块的长度位 10000h 字节,即 64KB
- 内存块的 20 位首地址的低 4 位必须等于 0
例如,对于段 [12340h,2233Fh]
,其段地址为 1234h
逻辑地址¶
在 8086 中,逻辑地址由 16bit Seg:16bit Offset
构成,可以根据如下公式将其转换成物理地址:
在汇编语言程序中,如果要访问逻辑地址 1234h:5678h
(物理地址 179B8h
) 指向的字节并赋值给寄存器 AL,可以这样写:
- 用
offset 变量名或标号名
引用变量或标号的偏移地址 - 用
seg 变量名或标号名或段名
引用段地址
在引用变量或数组元素时,若用常数表示它们的偏移地址,称这种寻址方式为直接寻址,如 ds:[5678h]
;若它们的偏移地址含有寄存器,称这种寻址方式为间接寻址,如 ds:[bx+2]
在源程序中,设某个变量或数组名字为 var
,则直接寻址的一般形式为:段寄存器:var[常数]
或 段寄存器:[var+常数]
。经过编译后变量名或数组名会替换成它们的偏移地址。例如,假设 offset s=8
,那么 ds:[s+1]
, ds:s[-2]
分别编译成 ds:[9]
和 ds:[6]
8086 规定的间接寻址一般形式为: 段寄存器:[寄存器1+寄存器2+常数]
,其中两个寄存器可以同时存在也可以缺少一个。仅有 BX
,BP
,SI
,DI
这四个寄存器允许出现在方括号中,且 BX
和 BP
不能同时出现;SI
和 DI
不能同时出现。
缺省段址
若逻辑地址中省略段地址,则使用默认缺省值:
- 直接寻址方式缺省段址为 DS
- 间接寻址依据偏移地址是否含有寄存器
BP
区分- 若有,则缺省段址为 SS
- 若无,则缺省段址为 DS
小端规则¶
Intel 用的是小端存储法(little-endian):
内存空间划分和显卡地址映射¶
16 位 CPU 只能访问 0000:0000
至 F000:FFFF
之间的 1M 内存空间,其划分如下:
地址范围 | 用途 |
---|---|
[0000:0000,9000:FFFF] | 操作系统和用户程序 |
[A000:0000,A000:FFFF] | 映射显卡内存 |
[B000:0000,B000:7FFF] | 映射显卡内存 |
[B800:0000,B800:7FFF] | 映射显卡内存 |
[C000:0000,F000:FFFF] | 映射 ROM |
所以操作系统和用户程序总共可用的内存空间为 [0,9FFFF]
共 640KB
寄存器¶
8086 一共有 14 个 16 位寄存器:
- 通用寄存器:包括
AX
,BX
,CX
,DX
,用于算术、逻辑、移位运算 - 段地址寄存器:包括
CS
,DS
,ES
,SS
,用来表示段地址 - 偏移地址寄存器:包括
IP
,SP
,BP
,SI
,DI
,用来表示偏移地址 - 标志寄存器:包括
FL
,用来存储标志位
80386中,除了段寄存器外所有寄存器均扩充到 32 位宽度,前面加上 E
通用寄存器¶
通用寄存器包括 AX
,BX
,CX
,DX
,作用是做算术、逻辑、移位运算。其中 AX
的高八位和低八位成为 AH
,AL
,其它寄存器同理。
在 80386 中,32 位寄存器 EAX
低 16 位就是 AX
,而高 16 位没有名字。
其实命名不是按照字母表前四个字母来的,不过我们写程序的时候随意用也没关系
AX
: Accumulator 累加器BX
: Base 基地址寄存器CX
: Counter 计数器DX
: Data 数据寄存器
段地址寄存器¶
段地址寄存器包括 CS
,DS
,ES
,SS
CS
:代码段寄存器DS
:数据段寄存器ES
:附加段寄存器SS
:堆栈段寄存器
其中 CS
不能用 mov
指令赋值;DS
,ES
,SS
可以用 mov
赋值,但源操作数不能是常数,且如果是寄存器的话仅限 AX
,BX
,CX
,DX
,SP
,BP
,SI
,DI
(除 IP
外),位宽必须是 word ptr
宽度。
DOS 把某个 EXE 载入到内存后,在将控制权交给程序之前,会对以下寄存器赋初值:
CS
= 代码段的段地址IP
= 首条指令的偏移地址SS
= 堆栈段的段地址SP
= 堆栈段的长度DS
= PSP 段址ES
= PSP 段址
CS:IP
即指向当前将要执行的指令;SS:SP
指向堆栈顶端;PSP 是程序段前缀(program segment prefix),它是一个由 DOS 分配给当前 EXE 的,位于首段之前的,长度为 100h 字节的内存块,PSP 中存储了与当前 EXE 进程相关的一些信息。
由于 DS
的初始值并非数据段的段地址,因此在程序刚开始运行时不能用 DS
的当前值作为段地址去引用或访问数据段内任何一个变量,而是应该先把 DS
赋值为 seg data
偏移地址寄存器¶
偏移地址寄存器包括 IP
,SP
,BP
,SI
,DI
IP
需要跟CS
搭配构成CS:IP
指向当前将要执行的指令,所以该寄存器的名字不能在任何指令中引用SP
需要跟SS
搭配构成SS:SP
指向堆栈顶端,该寄存器不能置于 [] 内用于间接寻址- 剩下的三个寄存器可以用来间接寻址,但总共有四个段寄存器,所以 Intel 安排通用寄存器
BX
也可以用来间接寻址BX
,BP
,SI
,DI
四个寄存器除了用来间接寻址外,都可以参与算术、逻辑、移位运算- 寄存器
BP
默认跟栈搭配,因此指令mov si, [bp+6]
等效于mov si, word ptr ss:[bp+6]
寄存器相对寻址时,方括号内必须是BX,BP,SI,DI
,不然会报错
标志寄存器¶
FL
是标志寄存器,它里面有些位用来反应当前指令的执行状态,称为状态标志;有些位用于控制 CPU,称为控制标志;剩余 7 位是保留位,除了第一位恒为 1
外,其它保留位恒为 0
。
- 状态标志共 6 个,包括:
CF
.ZF
,SF
,OF
,PF
,AF
- 控制标志共 3 个,包括:
DF
,IF
,TF
状态标志¶
进位标志 CF
add
,sub
,mul
,imul
以及移位指令都会影响 CF(Carry Flag)- 两数相加产生进位时 CF 置 1
- 两数相减产生借位时 CF 置 1
- 两数相乘乘积超过被乘数宽度时 CF 置 1
- 移位指令最后移出的那一位保存在 CF 中
如,可以利用带进位加法指令 adc
以及逻辑左移指令 shl
实现进制转换:
零标志 ZF
- 算术运算指令、逻辑运算指令、移位运算指令均会影响 ZF(Zero Flag)
- 当运算结果为 0 时 ZF 置 1
- 当运算结果非 0 时 ZF 置 0
- 注意:
jz
(jump if zero) 和je
(jump if equal) 跳转依据均为ZF==1
,故二者等价;同理,jnz
和jne
也等价
符号标志 SF
- 算术运算指令、逻辑运算指令、移位运算指令均会影响 SF(Sign Flag)
- 当运算结果为正时 SF 置 0
- 当运算结果为负时 SF 置 1
- 注意: 相当于运算结果的最高位;符号标志相关的跳转指令可以用来判定正负
js
(jump if sign) 依据为SF==1
jns
(jump if no sign) 依据为SF==0
溢出标志 OF
add
,sub
,mul
,imul
、移位运算指令均会影响 OF(Overflow Flag)- 当两正数相加变负数时 OF 置 1
- 当两负数相加变正数时 OF 置 0
- 当两数相乘乘积宽度超过被乘数宽度时 OF 置 1(此时 CF 也置 1)
- 当仅移动一位,且移位前最高位不等于移位后最高位时 OF 置 1
奇偶校验位 PF
- 当运算结果低 8 位中二进制 1 的个数为偶数时 PF 置 1;否则置 0
辅助进位标志 AF
- 若执行加法指令时第 3 位向第 4 位产生进位则 AF 置 1
- 若执行减法指令时第 3 位向第 4 位产生借位则 AF 置 1
- 注意: AF(Auxiliary Flag) 并没有相关跳转指令,它跟 BCD 码调整指令有关,如
AAA
,AAS
,DAA
,DAS
遇到这种题好好算算二进制减法吧
若以下指令序列的操作数均为符号数,请写出每个指令序列执行后标志位CF、ZF、SF、OF的值,并指出CMP指令的前后操作数的大小关系。
(1) mov al, 99h
cmp al, 34h ; CF=0, ZF=0, SF=0, OF=1, 99h < 34h
(2) mov ah, 81h
cmp ah, 0FFh ; CF=1, ZF=0, SF=1, OF=0, 81h < 0FFh
(3) mov bx, 1234h
cmp bx, 8086h ; CF=1, ZF=0, SF=1, OF=1, 1234h > 8086h
(4) mov cx, 0FFFFh
cmp cx, 0FFFEh ; CF=0, ZF=0, SF=0, OF=0, 0FFFFh > 0FFFEh
(5) mov dx, 3F7Dh
cmp dx, 1000h ; CF=0, ZF=0, SF=0, OF=0, 3F7Dh > 1000h
控制标志¶
方向标志 DF
- DF(Direction Flag)用来控制字符串操作指令如
rep
,movsb
的运行方向- 当
DF=0
时,字符串操作指令按正方向运行(先操作低地址再操作高地址) - 当
DF=1
时,字符串操作指令按反方向运行(先操作高地址再操作低地址)
- 当
- 注意:
cld
(clear direction) 指令设置DF=0
;std
(set direction) 指令设置DF=1
- 通常,若源首地址小于目标首地址,则置
DF
为 1;相应的,若源首地址大于目标首地址,则置DF
为 0
例如,我们希望将位于 1000
处的字符串 ABCDE
复制到地址 1002
处,如果我们设置 DF=0
的话:
中断标志 IF
- IF(Interrupt Flag)用于禁止、允许硬件中断
- 当
IF=0
时,禁止硬件中断 - 当
IF=1
时,允许硬件中断
- 当
- 注意:
cli
(clear interrupt) 指令设置IF=0
;sti
(set interrupt) 指令设置IF=1
- 我们通常使用的
int 21h
属于软件中断,是显式(Explicit)的 - 硬件中断是指由硬件某个事件触发,并由CPU自动插入并调用一个隐式(implicit)的
int n
指令来调用中断服务子函数
int n
函数的指针保存在 0:n*4
处,例如 int 1h
函数的首地址为 1234h:5678h
:
我们可以手动将其地址修改,将 int
中断的指针指向我们自己定义的函数,实现侵入。
陷阱标志 TF
- TF(Trap Flag)位于
FL
寄存器第八位,用于设置 CPU 的运行模式- 当
TF=1
时,CPU 进行单步模式 - 当
TF=0
时,CPU 进行常规模式
- 当
- 注意: 当 CPU 进入单步模式后,每执行一条指令后都会跟随执行一条
int 01h
中断指令
TF
并不能通过单独指令修改,但可以通过如下操作置 1:
端口¶
端口地址独立于内存地址,并且不像内存那样既有段地址又有偏移地址,端口地址仅有 16 位偏移地址,范围是 [0000h,0FFFFh]
。
读写端口地址的指令是 in
,out
,例如 in al, 61h
表示从 61h 号端口读取一个字节的信号并保存到 AL
中。
以下程序利用端口实现输出当前时间信息:
对于从键盘读取输入:
<1> 高 dos中断调用
相当于 al=getchar()
但不能读取方向键
<2> 中 bios中断调用(basic input/output system)
AX = 键盘编码,可以读取方向键、功能键、PgUp等键,但不能读取单独的ctrl键
<3> 低 端口操作
60h
是键盘输入的端口,可以读取各个键的编码
文本模式的显卡地址映射¶
可以得到类似如下图所示的效果: