第 3 章 CPU、内存和端口⚓︎
约 3135 个字 46 行代码 预计阅读时间 16 分钟
-
CPU
- 算术逻辑单元 (ALU):算术、逻辑和移位运算
- 控制单元 (CU):取指令、解释指令、执行指令
- 寄存器 (register):可以理解为 CPU 下的全局变量,CPU 中只有它是可编程控制的
注:这里只是简单提一下 CPU 的组成,具体原理可参见《计算机组成》笔记(目前还没学到 CPU 部分)
-
内存:存储指令和变量,是程序的运行空间
- 端口:CPU 与 I/O 设备之间的接口
内存⚓︎
物理地址与逻辑地址⚓︎
DOS 系统运行在 CPU 的实模式 (real mode) 下,可访问的地址范围为 \([00000h, 0FFFFFh]\),即最多只能访问 1MB 内存空间。
用单个数值表示的地址称为物理地址(physical address)。由于 8086 芯片的每个寄存器只有 16 位宽度,寄存器无法直接存储物理地址,所以用存储在段地址寄存器和偏移地址寄存器的逻辑地址(logical address)(即段地址 : 偏移地址)来间接访问物理地址。
令物理地址为 \(\text{phy\_addr}\),逻辑地址为 \(\text{seg\_addr:off\_addr}\),物理地址与逻辑地址的转换关系式为:
一个物理地址可以表示成多个逻辑地址,例如:12398h = 1234:0058 = 1235:0048 = 1236:0038 = 1230:0098。
- 段(segment):一块内存,满足:
- 长度为 \(10000h\) 字节,即 64KB
- 20 位首地址的低 4 位必须为 0,换句话说,段首地址的十六进制形式下的偏移地址的个位必须为 0
- 偏移地址(offset address):段内某个变量或标号与行首之间的距离
- 偏移地址 = 物理地址 - 段首地址(注意不是段地址)
- 用
offset 变量名或标号名
表示变量或标号的偏移地址 - 可以用常数表示
- 段地址(segment address):20 位段首地址的高 16 位
- 用
seg 变量名或标号名
或段名
表示变量或标号的段地址 - 不能用常数表示,只能用段寄存器表示
- 段地址的 1 相当于偏移地址的 10h
- 用
如何在已知某个变量的偏移地址off_addr
和段地址seg_addr
访问该变量:
- 将
seg_addr
赋值给某个段寄存器(比如ds
) - 再用
ds:[off_addr]
访问该变量
例子
data segment
s db "ABC"
s db "Hello$world!", 0Dh, 0Ah, 0
data ends
code segment
assume cs:code, ds:data
main:
mov ax, data
mov ds, ax
mov bx, 0
next:
mov dl, s[bx]
cmp dl, 0
je exit
mov ah, 2
int 21h
add bx, 1
jmp next
exit:
mov ah 4Ch
int 21h
code ends
end main
使用 Turbo Debugger 调试该程序:
观察发现:
data
段的首字节为a[0]
,数组s
的偏移地址offset s=3
(底部用白色高亮表示s
的首字节)code
段的首字节为main
标号对应指令的机器码的首字节,标号next
的偏移地址offset next=8
(用蓝色高亮表示next
对应指令的位置,可以看到左侧cs:0008
表示这条命令的逻辑地址)
直接寻址和间接寻址⚓︎
假设变量名或数组名为
var
- 直接寻址
- 一般形式为:
段寄存器:var[常数]
或段寄存器:[var+常数]
- 一般形式为:
- 间接寻址
- 一般形式为:
var[寄存器1+寄存器2+常数]
或[var+寄存器1+寄存器2+常数]
,其中寄存器 1 和寄存器 2 至少存在 1 个,常数是整数,可正可负也可以是 0- 寄存器只能在
bx
、bp
、si
、di
四种寄存器内选 - 如果出现两个寄存器相加的情况,其中一个寄存器必须从
bx
、bp
中选,另一个必须从si
、di
中选
- 寄存器只能在
- 80386 的一般形式为:
段寄存器:[寄存器1+寄存器2*N+常数]
,其中N
是集合 \(\{1, 2, 4, 8\}\) 内的一个元素,寄存器 1 与寄存器 2 只能在eax
、ebx
、ecx
、edx
、esp
、ebp
、esi
、edi
八种寄存器内选
- 一般形式为:
小端规则⚓︎
小端规则(little-endian):当 CPU 写入或读取宽度大于 8 位的数据时,会按照“低位在先高位在后”的顺序存储或获取数据。换句话说,存储在寄存器内的数据的位顺序和我们看到的位顺序是相反的。
例子
假如内存中有以下数据:
实际的内存空间为:
可以看到,白色高亮部分表示的是a
, b
, c
的值,它们的值分 别为 3412
, 7856
和 78563412
缺省段址和段覆盖⚓︎
缺省段址的规则:
- 直接寻址:令段址为
ds
- 间接寻址:
- 偏移地址含寄存器
bp
:令段址为ss
- 偏移地址不含寄存器
bp
:令段址为ds
- 偏移地址含寄存器
段覆盖:在操作数前添加一个段前缀(比如cs:
、ds:
等)来强制改变操作数的段址
注:这两个概念并不是什么特殊情况,寻址的时候一定会涉及两者中的一种。
内存空间划分和显卡地址映射⚓︎
16 位 CP U 只能 访问 0000:000 0 到 F000:FFF F 之 间的 1M 内存空间,其划分如下:
地址范围 | 用途 | 说明 |
---|---|---|
[0000:0000, 9000:FFFF] | 操作系统和用户程序 | 占38 4K 的内存 |
[A000:0000, A000:FFFF] | 映射显卡内存 | 图形模式 |
[B000:0000, B000:FFFF] | 映射显卡内存 | - |
[B800:0000, B800:FFFF] | 映射显卡内存 | 文本模式 |
[C000:0000, F000:FFFF] | 映射 ROM | 占64 0K 的内存 |
其中显卡地址映射分为文本模式(text mode) 和图形模式(graphics mode),具体来说:
文本模式下的显卡地址映射⚓︎
\(80 \times 25\)( 每行 80 个字符, 每列 25 个字符)文本模式的屏幕坐标系统如图所示:
-
每两个内存单元(2 字节)决定屏幕上的一个字符
- 前一个内存单元(1 字节)表示字 符的ASC II 码值
-
后一个内存单元(1 字节)表示字符的颜色,每个位的意义如下所示:
- 高 4 位表示背景色
- 低 4 位表示前景色
- 屏幕 坐标(x, y) 对应的显卡偏移地址
text_mode_offset
的计算公式为:
-
虽然这是一个二维平面,但是在内存中所有的数据都是连续的(图 中的79 x 2和80 x2 表示的是十进制的偏移地址,为了方便就这样写了)
例子
设ds=0B800h
结果是:
图形模式下的显卡地址映射⚓︎
\(320 \times 200\) 图形模式的屏幕坐标系统如图所示:
- 1 个内存单元(1 字节)决定屏幕上的一个点,其值代表该点的颜色(256 种)
- 屏幕 坐标(x, y) 对应的显卡偏移地址
graphics_mode_offset
的计算公式为:
宽度修饰⚓︎
宽度修饰词用于限定变量(不能修饰常数)的宽度,有以下几种:
byte ptr
:变量宽 度 为 8 位word ptr
:变量宽 度为 16 位dword ptr
:变量宽 度为 32 位
不必使用宽度修饰的情况:
- 指令中的变量有变量名时(比如
mov s[1], 0
) - 指令中的另一个操作数有明确宽度时(比如
mov ds:[bx], ax
)
对应的,当某个指令中的变量无变量名,且无法根据另一个操作数推断变量宽度时,必须对该变量加上宽度修饰。
寄存器⚓︎
- 8086 一 共有 14 个寄存器( 均为 16 位宽度
) ,分别为:ax
、bx
、cx
、dx
、sp
、bp
、si
、di
、cs
、ds
、es
、ss
、ip
、fl
- 80386 除了段寄存器 仍为 16 位,其余寄存器均扩 展至 32 位 ,这 14 个寄存器分别为:
eax
、ebx
、ecx
、edx
、esp
、ebp
、esi
、edi
、cs
、ds
、es
、ss
、eip
、efl
按照寄存器的用途可分为:
- 通用寄存器
- 段地址寄存器
- 偏移地址寄存器
- 标志寄存器
通用寄存器⚓︎
- 作用:算术、逻辑、移位运算
-
寄存器:
ax
、bx
、cx
、 dx
(80386 中寄存器名称前多个e
)- 其中
ax
的 低 8 位 和 高 8 位可以分别用寄存器al
和ah
表示。下图展示了eax
、ax
、ah
、al
的关系:
bx
、cx
、dx
与ax
同理。
- 其中
段地址寄存器⚓︎
- 作用:表示段地址
- 寄存器:
cs
:代码段寄存器- 不能用
mov
指令赋值,只能用以下指令间接改变其值jmp far ptr
jmp dword ptr
call far ptr
call dword ptr
retf
int
iret
- 不能用
ds
:数据段寄存器es
:附加段寄存器ss
:堆栈段寄存器- 后三者可以用
mov
指令赋值,但源操作数不能是常数,只能是寄存器或变量- 寄存器只能在
ax
、bx
、cx
、dx
、sp
、bp
、si
、di
中选 - 变量必须是
word ptr
宽度的
- 寄存器只能在
- 后三者可以用
偏移地址寄存器⚓︎
- 作用:表示偏移地址
- 寄存器:
ip
- 与
cs
搭配使用,cs:ip
指向当前将要执行的指令 - 该寄存器的名称不能在任何指令中出现
- 与
sp
- 与
ss
搭配使用,ss:sp
指向堆栈顶端 - 不能置于
[]
内用于间接寻址
- 与
bp
、si
、di
以及bx
(通用寄存器)- 能放在
[]
内用于间接寻址 - 还可以参与算术、逻辑、移位运算
- 能放在
寄存器的初始化
DOS 把可执行程序加载到内存后,即将控制权交给可执行程序前,会对以下寄存器初始化:
cs
:代码段的首地址ip
:首条指令的偏移地址ss
:堆栈段的段地址sp
:堆栈段的长度ds
:PSP 段地址es
:PSP 段地址
其中 PSP(程序段前缀,program segment prefix)是一 个由D OS 分配给当前可执行程序,位于首段之前,长度为100h
字节的内存块,它存储了与当前可执行程序的进程相关的一些信息,比如程序的命令行参数等
标志寄存器⚓︎
- 作用:存储标志位
- 寄存器:
fl
,它里面的位 分 为 3 类- 状态标志:反映当前指令的执行情况,包括:
cf
、zf
、sf
、of
、pf
、af
- 控制标志:控制 CPU,包括:
df
、if
、tf
- 保留位(下 图用 X 表示
) :除 了 第 1 位为 1,其余保留位恒为 0
- 状态标志:反映当前指令的执行情况,包括:
- 各类标志如下:
- ( 第 0 位)进位标志(carry flag)
cf
:- 两数相加产生进位,
cf = 1
- 两数相减产生借位,
cf = 1
- 两数相乘的乘积宽度超过被乘数宽度,
cf = 1
- 移位指令最后移除 的 那 1 位保存在
cf
中 - 相关指令:
jc
、jnc
、clc
、stc
、cmc
、adc
等
- 两数相加产生进位,
- ( 第 6 位)零标志(zero flag)
zf
:- 运算结 果 为 0 时,
zf = 1
;运算结果 不 为 0 时,zf = 0
- 相关指令:
jz
、jnz
、je
、jne
- 运算结 果 为 0 时,
- ( 第 7 位)符号标志(sign flag)
sf
:- 表示运算结果的最高位,当运算结果为正时
sf = 0
,为负时sf = 1
- 相关指令:
js
、jns
- 表示运算结果的最高位,当运算结果为正时
- (第 11 位)溢出标志(overflow flag)
of
:- 用于检测溢出情况,包括:
- 两个正数相加变负数时,
of = 1
- 两个负数相加变正数时,
of = 1
- 两数相乘的乘积宽度超过被乘数宽度时,
of = 1
- 当仅 移 动 1 位且移位前的最 高位HETImathSTART7HETImathE ND 移位后的最高位时,
of = 1
- 两个正数相加变负数时,
- 相关指令:
jo
、jno
- 用于检测溢出情况,包括:
- ( 第 2 位)奇偶校验标志(parity flag)
pf
:- 当运算结 果 低 8 位中
1
的个数为偶数时,pf = 1
,否则pf = 0
(偶校验) - 相关指令:
jp
、jnp
、jpe
、jpo
- 当运算结 果 低 8 位中
- ( 第 4 位)辅助进位标志(auxiliary flag)
af
:- 若执行加法 时 第 3 位 向 第 4 位产生进位时,
af = 1
- 若执行减法 时 第 3 位 向 第 4 位产生借位时,
af = 1
- 无相关指令,但 它跟B CD 码调整指令有关
- 若执行加法 时 第 3 位 向 第 4 位产生进位时,
- (第 10 位)方向标志(direction flag)
df
:- 作用:控制字符串操作指令的运行方向
df = 0
:字符串操作指令按正向(从低到高)运行df = 1
:字符串操作指令按反向(从高到低)运行
- 相关指令
: cld
(使df = 0
) 、 std
(使df = 1
)
- 作用:控制字符串操作指令的运行方向
- ( 第 9 位)中断标志(interrupt flag)
if
:- 作用:控制硬件中断
if = 0
:禁止硬件中断if = 1
:允许硬件中断
- 相关指令
: cli
(使if = 0
) 、 sti
(使if = 1
)
- 作用:控制硬件中断
-
( 第 8 位)陷阱标志(trap flag)
tf
:- 作用: 设置C PU 的运行模式,与调试相关
tf = 0
:常规模式,连续执行指令tf = 1
:单步模式,每执行一条指令后都会跟随执行int 01h
中断指令,用于调试
- 相关指令:
pushf
、popf
- 作用: 设置C PU 的运行模式,与调试相关
- ( 第 0 位)进位标志(carry flag)
端口⚓︎
- CPU 不能直接 控制I /O 设备,它通过以下方式 访问I /O 设备:
- 读:CP U从I /O 设备相关端口读取信号, 获取I /O 设备的反馈信息
- 写:CP U向I /O 设备相关端口发送信号,将控制信号输 出到I /O 设备
- 端口地址 仅有 16 位偏移地址,无段地址,取值范围为 [0000h, 0FFFFh]
- 读写端口地址的指令是
in
和out
评论区