跳转至

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}\),物理地址与逻辑地址的转换关系式为:

\[ \text{phy\_addr} = \text{seg\_addr} \times 10h + \text{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
      • 寄存器只能在bxbpsidi四种寄存器内选
      • 如果出现两个寄存器相加的情况,其中一个寄存器必须从bxbp中选,另一个必须从sidi中选
    • 80386 的一般形式为:段寄存器:[寄存器1+寄存器2*N+常数],其中N是集合 \(\{1, 2, 4, 8\}\) 内的一个元素,寄存器 1 与寄存器 2 只能在eaxebxecxedxespebpesiedi八种寄存器内选

小端规则⚓︎

小端规则(little-endian):当 CPU 写入读取宽度大于 8 位的数据时,会按照“低位在先高位在后”的顺序存储或获取数据。换句话说,存储在寄存器内的数据的位顺序和我们看到的位顺序是相反的。

例子

假如内存中有以下数据:

data segment
    a dw 1234h
    b dw 5678h
    c dd 12345678h
data ends

实际的内存空间为:

可以看到,白色高亮部分表示的是a , b , c的值,它们的值分 别为 3412 , 785678563412

缺省段址和段覆盖⚓︎

缺省段址的规则:

  • 直接寻址:令段址为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 字节)表示字符的颜色,每个位的意义如下所示:

      7: 闪烁
      6: 红
      5: 蓝
      4: 绿
      
      3: 高亮
      2: 红
      1: 绿
      0: 蓝
      
      • 4 位表示背景色
      • 4 位表示前景色
      • 屏幕 坐标(x, y) 对应的显卡偏移地址text_mode_offset的计算公式为:
        text_mode_offset = (y * 80 + x) * 2
        
  • 虽然这是一个二维平面,但是在内存中所有的数据都是连续的(图 中的79 x 2和80 x2 表示的是十进制的偏移地址,为了方便就这样写了)

例子

ds=0B800h

mov byte ptr ds:[0], 'A'
mov byte ptr ds:[1], 74h
mov byte ptr ds:[2], 'B'
mov byte ptr ds:[3], 72h

结果是:

图形模式下的显卡地址映射⚓︎

\(320 \times 200\) 图形模式的屏幕坐标系统如图所示:

  • 1 个内存单元(1 字节)决定屏幕上的一个点,其值代表该点的颜色(256 种)
  • 屏幕 坐标(x, y) 对应的显卡偏移地址graphics_mode_offset的计算公式为:
    graphics_mode_offset = y * 320 + x
    

宽度修饰⚓︎

宽度修饰词用于限定变量(不能修饰常数)的宽度,有以下几种:

  • byte ptr:变量宽 8
  • word ptr:变量宽 度为 16
  • dword ptr:变量宽 度为 32

不必使用宽度修饰的情况:

  • 指令中的变量有变量名时(比如mov s[1], 0
  • 指令中的另一个操作数有明确宽度时(比如mov ds:[bx], ax

对应的,当某个指令中的变量无变量名,且无法根据另一个操作数推断变量宽度时,必须对该变量加上宽度修饰。

寄存器⚓︎

  • 8086 共有 14 个寄存器( 均为 16 位宽度,分别为:axbxcxdxspbpsidicsdsesssipfl
  • 80386 除了段寄存器 仍为 16 位,其余寄存器均扩 展至 32 ,这 14 个寄存器分别为:eaxebxecxedxespebpesiedicsdsessseipefl

按照寄存器的用途可分为:

  • 通用寄存器
  • 段地址寄存器
  • 偏移地址寄存器
  • 标志寄存器

通用寄存器⚓︎

  • 作用:算术、逻辑、移位运算
  • 寄存器:axbxcxdx80386 中寄存器名称前多个e

    • 其中ax 8 8 位可以分别用寄存器alah表示。下图展示了eaxaxahal的关系:

    • bxcxdxax同理。

段地址寄存器⚓︎

  • 作用:表示段地址
  • 寄存器:
    • cs:代码段寄存器
      • 不能用mov指令赋值,只能用以下指令间接改变其值
        • jmp far ptr
        • jmp dword ptr
        • call far ptr
        • call dword ptr
        • retf
        • int
        • iret
    • ds:数据段寄存器
    • es:附加段寄存器
    • ss:堆栈段寄存器
      • 后三者可以用mov指令赋值,但源操作数不能是常数,只能是寄存器或变量
        • 寄存器只能在axbxcxdxspbpsidi中选
        • 变量必须是word ptr宽度的

偏移地址寄存器⚓︎

  • 作用:表示偏移地址
  • 寄存器:
    • ip
      • cs搭配使用,cs:ip指向当前将要执行的指令
      • 该寄存器的名称不能在任何指令中出现
    • sp
      • ss搭配使用,ss:sp指向堆栈顶端
      • 不能置于[]内用于间接寻址
    • bpsidi以及bx(通用寄存器)
      • 能放在[]内用于间接寻址
      • 还可以参与算术、逻辑、移位运算

寄存器的初始化

DOS 把可执行程序加载到内存后,即将控制权交给可执行程序前,会对以下寄存器初始化:

  • cs:代码段的首地址
  • ip:首条指令的偏移地址
  • ss:堆栈段的段地址
  • sp:堆栈段的长度
  • dsPSP 段地址
  • esPSP 段地址

其中 PSP(程序段前缀,program segment prefix)是一 个由D OS 分配给当前可执行程序,位于首段之前,长度为100h字节的内存块,它存储了与当前可执行程序的进程相关的一些信息,比如程序的命令行参数等

标志寄存器⚓︎

  • 作用:存储标志位
  • 寄存器:fl,它里面的位 3
    • 状态标志:反映当前指令的执行情况,包括:cfzfsfofpfaf
    • 控制标志:控制 CPU,包括:dfiftf
    • 保留位(下 图用 X 表示:除 1 位为 1,其余保留位恒为 0
  • 各类标志如下:
    • 0 位)进位标志(carry flag) cf
      • 两数相加产生进位,cf = 1
      • 两数相减产生借位,cf = 1
      • 两数相乘的乘积宽度超过被乘数宽度,cf = 1
      • 移位指令最后移除 1 位保存在cf
      • 相关指令:jcjncclcstccmcadc
    • 6 位)零标志(zero flag) zf
      • 运算结 0 时,zf = 1;运算结果 0 时,zf = 0
      • 相关指令:jzjnzjejne
    • 7 位)符号标志(sign flag) sf
      • 表示运算结果的最高位,当运算结果为正时sf = 0,为负时sf = 1
      • 相关指令:jsjns
    • (第 11 位)溢出标志(overflow flag) of
      • 用于检测溢出情况,包括:
        • 两个正数相加变负数时,of = 1
        • 两个负数相加变正数时,of = 1
        • 两数相乘的乘积宽度超过被乘数宽度时,of = 1
        • 当仅 1 位且移位前的最 高位HETImathSTART7HETImathE ND 移位后的最高位时,of = 1
      • 相关指令:jojno
    • 2 位)奇偶校验标志(parity flag) pf
      • 当运算结 8 位中1的个数为偶数时,pf = 1,否则pf = 0(偶校验)
      • 相关指令:jpjnpjpejpo
    • 4 位)辅助进位标志(auxiliary flag) af
      • 若执行加法 3 4 位产生进位时,af = 1
      • 若执行减法 3 4 位产生借位时,af = 1
      • 无相关指令,但 它跟B CD 码调整指令有关
    • (第 10 位)方向标志(direction flag) df
      • 作用:控制字符串操作指令的运行方向
        • df = 0:字符串操作指令按正向(从低到高)运行
        • df = 1:字符串操作指令按反向(从高到低)运行
      • 相关指令cld(使df = 0std(使df = 1
    • 9 位)中断标志(interrupt flag) if
      • 作用:控制硬件中断
        • if = 0:禁止硬件中断
        • if = 1:允许硬件中断
      • 相关指令cli(使if = 0sti(使if = 1
    • 8 位)陷阱标志(trap flag) tf

      • 作用: 设置C PU 的运行模式,与调试相关
        • tf = 0:常规模式,连续执行指令
        • tf = 1:单步模式,每执行一条指令后都会跟随执行int 01h中断指令,用于调试
      • 相关指令:pushfpopf
      ; 令tf = 1
      push            ; 将fl压入堆栈中
      pop  ax         ; 从堆栈中弹出`fl`的值并保存到ax中
      or   ax, 100h   ; 把ax的第8位置1
      push ax
      popf            ; 从堆栈中弹出ax的值并保存到fl中,此时tf = 1
      
      ; 令tf = 0
      pushf
      pop  ax
      and  ax, 0FEFFh  ; 把ax的第8位清零
      push ax
      popf             ; 从堆栈中弹出ax的值并保存到fl中,此时tf = 0
      

端口⚓︎

  • CPU 不能直接 控制I /O 设备,它通过以下方式 访问I /O 设备:
    • 读:CP U从I /O 设备相关端口读取信号, 获取I /O 设备的反馈信息
    • 写:CP U向I /O 设备相关端口发送信号,将控制信号输 出到I /O 设备
  • 端口地址 仅有 16 位偏移地址,无段地址,取值范围为 [0000h, 0FFFFh]
  • 读写端口地址的指令是inout

评论区

如果大家有什么问题或想法,欢迎在下方留言~