Skip to content

Part 2. 数据访问

!!! warning "注意"

不知怎么回事,这一页的[heti插件](https://github.com/TonyCrane/mkdocs-heti-plugin)渲染爆炸,排版弄得一团乱,可能给读者带来不良的观看体验,请见谅。

~~(也许之后我会改改这个bug)~~

寄存器

  • 8086一共有14个寄存器(均为16位宽度),分别为:axbxcxdxspbpsidicsdsesssipfl
  • 80386除了段寄存器仍为16位,其余寄存器均扩展至32位,这14个寄存器分别为:eaxebxecxedxespebpesiedicsdsessseipefl
    • 如果在8086汇编程序中使用32位的寄存器,需要在程序开头插入一行.386;并且在数据段开头(即data segment)处补上use16(即改写为data segment use16),以保证偏移地址仍然是16位的

按照寄存器的用途,可将寄存器分为以下几类:

通用寄存器

  • 作用:算术、逻辑、移位运算

  • 寄存器:axbxcxdx(80386中寄存器名称前多个e

    • 其中ax的低8位和高8位可以分别用寄存器alah表示。下图展示了eaxaxahal的关系:
    ![](images/4_dark.png#only-dark) ![](images/4_light.png#only-light)
    • 单独使用al运算时,溢出的高位并不会被保存在ah中,因为此时alah被视为两个独立的寄存器
    • bxcxdxax同理。

段地址寄存器

  • 作用:表示段地址
  • 寄存器:
    • cs:代码段寄存器,存放代码段的段地址

      • 不能用mov指令赋值,只能用jmpcallretfintiret等指令间接改变其值
      • CPU执行指令的流程为:
        1. CPU从cs:ip指向的内存单元读取指令,该指令会进入指令缓冲器
        2. ip = ip + length_of_instruction,即指向下一条指令
        3. 执行指令,跳到步骤a,重复这个过程
      • 8086CPU通电或复位后,cp = ffffh, ip = 0000h
    • ds:数据段寄存器,存放数据段的段地址

    • es:附加段寄存器

    • ss:堆栈段寄存器,存放堆栈段的段地址

    • 后三者可以用mov指令赋值,但源操作数不能是常数,只能是寄存器(而且只能在axbxcxdxspbpsidi中选)或变量(必须是单字宽度(2字节)的)

偏移地址寄存器

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

???+ info "寄存器的初始化"

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

- `cs`:代码段的首地址
- `ip`:首条指令的偏移地址
- `ss`:堆栈段的段地址
- `sp`:堆栈段的长度
- `ds`:[PSP](3.md#运行)段地址
- `es`:PSP段地址

标志寄存器

  • 作用:存储标志位,这些信息通常被称为程序状态字(PSW)。
  • 寄存器:fl(不能直接出现在指令中)。它里面的位分为3类
    • 状态标志:反映当前指令的执行情况,包括:cfzfsfofpfaf
    • 控制标志:控制CPU,包括:dfiftf
    • 保留位(下图用 X 表示):除了第1位为1,其余保留位恒为0
![](images/17_dark.png#only-dark) ![](images/17_light.png#only-light)

!!! info "注"

几乎每个标志位都有对应的指令,其中以`j`开头的指令都是**条件跳转指令**,它们将对应标志位的信息作为跳转的条件。[Part 4](4.md#条件跳转指令)列出了常用的条件跳转指令表格。

各类标志如下(前6种标志称为状态标志,后3种标志称为控制标志):

  • (第0位)进位标志(carry flag) cf:
    • 该标志仅对无符号数有意义
    • 两数相加(不包括inc指令)产生进位,cf = 1
    • 两数相减(不包括dec指令)产生借位,cf = 1
    • 两数相乘的乘积宽度超过被乘数宽度,cf = 1
    • 移位指令最后移出的那1位保存在cf中
    • 相关指令:jcjncclc(令cf = 0)、stc(令cf = 1)、cmc(对cf取反)、adc
  • (第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位且移位前的最高位移位后的最高位时,of = 1
    • 相关指令:jojno
  • (第2位)奇偶校验标志(parity flag) pf:
    • 当运算结果低8位中1的个数为偶数时,pf = 1,否则pf = 0(偶校验)
    • 相关指令:jpjnpjpejpo
  • (第4位)辅助进位标志(auxiliary flag) af:
    • 若执行加法时第3位向第4位产生进位(或者理解为低4位向高4位进位)时,af = 1
    • 若执行减法时第3位向第4位产生借位(或者理解为低4位向高4位借位)时,af = 1
    • BCD码调整指令有关

  • (第10位)方向标志(direction flag) df:

    • 作用:控制字符串操作指令的运行方向,具体而言控制的是si和di的增减
      • df = 0:字符串操作指令按正向(从低到高)运行,即每次操作后si、di递增
      • df = 1:字符串操作指令按反向(从高到低)运行,即每次操作后si、di递减
    • 相关指令:cld(使df = 0)、std(使df = 1)、movsbmovsw
  • (第9位)中断标志(interrupt flag) if:

    • 作用:控制硬件中断
      • if = 0:禁止硬件中断
      • if = 1:允许硬件中断
    • 相关指令:cli(使if = 0)、sti(使if = 1)
  • (第8位)陷阱标志(trap flag) tf:

    • 作用:设置CPU的运行模式,与调试相关
      • tf = 0:常规模式,连续执行指令
      • tf = 1:单步模式,每执行一条指令后都会跟随执行int 01h中断指令,用于调试
    • 相关指令:pushfpopf
    asm
    ; 令 tf = 1
    pushf           ; 将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

内存

小端规则

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

??? example "例子"

假如内存中有以下数据:

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

实际的内存空间为:

<div style="text-align: center">
    <img src="images/3.png" width="50%">
</div>

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

物理地址与逻辑地址

DOS系统运行在CPU的实模式 (real mode) 下,可访问的地址范围为[00000h, 0FFFFFh],即最多只能访问 1MB 内存空间。

  • 物理地址(physical address):用单个数值表示,每个内存单元都有一个唯一的物理地址

    • CPU通过地址总线传给存储器的必须是一个内存的物理地址
  • 逻辑地址(logical address):物理地址的间接表示,形式为:段地址:偏移地址

    • 原因:由于 8086CPU 的每个寄存器只有 16 位宽度,而CPU的地址总线宽度为 20 位,因此无法直接传递物理地址,而是通过逻辑地址来访问内存的
    • 段地址和偏移地址是两个 16 位的地址,它们合在一起形成一个 20 位的物理地址,转换关系式为:
    phy_addr=seg_addr×10h+off_addr
    • 一个物理地址可以表示成多种逻辑地址,换句话说,不同的段地址和偏移地址可以形成同一个物理地址,例如:12398h = 1234:0058 = 1235:0048 = 1236:0038 = 1230:0098。
  • 偏移地址(offset address):段内某个变量或标号与行首之间的距离

    • 偏移地址 = 物理地址 - 段首地址( = 段地址 * 10h)
    • offset 变量名或标号名表示变量或标号的偏移地址
    • 可以用常数表示
  • 段地址(segment address):20位段首地址的高16位

    • seg 变量名或标号名段名(在assume伪指令中与段寄存器有联系)表示变量或标号的段地址
    • 不能用常数表示,只能用段寄存器表示
    • 段地址的1相当于偏移地址的10h
  • (segment):一块内存,包含若干内存单元,实际上内存内部并没有分段,而是由 CPU 划分的

    • 成为段的要求:
      • 20 位地址的低 4 位必须为 0,换句话说,段首地址的十六进制形式下的偏移地址的个位必须为0(16的倍数)
    • 段的容量不大于 10000h 字节(偏移地址的最大变化范围),即64KB
    • 段的种类:
      • 数据段
      • 代码段
      • 堆栈段
      • 附加段

??? info "补充:32位系统的逻辑地址(仅做了解)"

- 偏移地址扩展为32位
- 段地址保持16位,但是段首地址不再是从在16进制下的段地址后面填0得到,而是通过查表(称为全局描述符表,简称gdt表)得到段首地址
    - gdt表其实是一个数组,该数组的首地址存放在gdtr寄存器内,数组中每个元素的宽度均为8字节
    - gdt表首地址 + 段地址得到数组元素地址,然后从该数组元素的第2、3、4、7个字节逆向排列得到段首地址
    - 段首地址 + 偏移地址 = 物理地址
    - 数组元素的其余4个字节看作32位,其中20位用于表示段的长度(单位为字节或页(4KB)),剩余的12位中有一部分位用来表示段的ring级别(0、1、2、3共4级)及权限(读R、写W、执行X)
        - 系统代码是ring0,而用户代码是ring3,只有用户代码的ring级别小于对应段描述的ring级别时才能访问该段

寻址方式

假定:

  • seg_reg:段寄存器
    • 段覆盖:通过在操作数前面加一个段前缀seg_reg:来强制改变操作数的段地址
  • var:变量名 / 数组名
  • idata:常数 / 立即数(整数)
  • reg1reg2:寄存器1、寄存器2

80x86CPU提供以下几种寻址方式:

  • 直接寻址(方括号内只有立即数):一般形式为seg_reg:var[idata]seg_reg:[var+idata]
  • 间接寻址(方括号内还有寄存器):一般形式为var[reg1+reg2+idata][var+reg1+reg2+idata]
    • 其中寄存器1和寄存器2至少存在1个

    • 寄存器只能在bxbpsidi四种寄存器内选

    • 如果在[...]内只使用寄存器bp,且缺省段地址,则默认使用ss中的段地址(堆栈段);其他情况下默认使用ds中的段地址(数据段)

    • 如果出现两个寄存器相加的情况,其中一个寄存器必须从bxbp中选,另一个必须从sidi中选

    • 变体(这里忽略var):

      • 表示一维数组:idata[si], idata[bi]
      • 表示二维数组:[bx][idata], [bx][si], idata[bx][si]
      • 表示结构体:[bx].idata, [bx].idata[si]bx定位整个结构体,idata定位结构体的某个字段,si定位结构体数组字段的某个元素)
    • 80386的一般形式为:seg_reg:[reg1+reg2*N+idata],其中N是集合 {1, 2, 4, 8} 内的一个元素,寄存器1与寄存器2只能在eaxebxecxedxespebpesiedi八种寄存器内选,但可以重名

相关的操作符:

  • offset var/label:获取变量或标号的偏移地址

具体到变量的引用:

  • 单个变量/数组首元素的引用:var[var]
  • i个数组元素的引用:a[i * n][a + i * n],其中a是元素宽度为n字节的数组(与C语言略有不同)
  • 在数据段中
    • varoffset var都可以作为伪指令dw的操作数,表示var的偏移地址(近指针)
    • var还可以作为伪指令dd的操作数,表示该变量的偏移地址的远指针
  • 在代码段中,只能用offset var引用该变量的偏移地址,用seg var或数据段名引用该变量的段地址

位置计数器(location counter):一个用于记录当前段内变量或标号的偏移地址

  • 在段定义开始时,编译器会自动把位置计数器清零
  • 每编译完一条指令或伪指令语句时,编译器会把该语句的宽度(即对应机器码的字节数)加到位置计数器中
  • 一种特殊的操作数$,它表示当前位置计数器的值,可以用它来计算数组的长度

变量定义

  • 一般情况下变量在数据段中被定义,根据变量的定义位置来决定段地址和偏移地址

  • 变量的命名规则:

    • 可用字符有:大小写字母、数字、符号@$?_
    • 不得以数字开头
    • $?不能单独作为名称
    • 名称长度不超过 31 个字符
    • 在缺省情况下,变量名及标号名不区分大小写
    • 相同名称不得重复定义
    • 不能与 80x86 指令、伪指令、汇编指示指令名相同
    • 标号名的命名规则同上
  • 要用到的操作符:

    • db:定义字节大小的变量
    • dw:定义字大小的变量
    • dd:定义双字大小的变量
  • 格式:

    asm
    varname db|dw|dd|dq|dt value
    • varname表示变量名
    • dbdwdddqdt是伪指令,分别表示不同位宽的数据(具体含义见Part 1
    • value表示初始值
  • dup运算符:用于生成重复(duplicate) 的数据,格式为:

    asm
    varname db|dw|dd|dq|dt n dup(x1[, x2, ..., xm])

    其中n表示重复的次数,x1, x2, ..., xm表示重复项,可以有 1 个或多个,且允许嵌套

    ???+ example "例子"

      ```asm
      y db 2 dup('A', 3 dup('B'), 'C')
      ; 等价于
      y db 'A', 'B', 'B', 'B', 'C', 'A', 'B', 'B', 'B', 'C'
      ```
    

数据宽度

以下情况,指令中的数据宽度是确定的:

  • 指令中的变量有变量名时(因为变量定义时已指明宽度),比如mov s[1], 0
  • 指令中的另一个操作数有明确宽度时(寄存器的宽度是已知的),比如mov ds:[bx], ax

若指令中有多个操作数,且操作数的宽度都是未知的,则需要使用宽度修饰词来指明变量 / 内存单元(不能修饰常数)的宽度,有以下几种修饰词:

  • byte ptr:宽度为8位(1字节)
  • word ptr:宽度为16位(1字)
  • dword ptr:宽度为32位(1双字)
  • fword ptr:宽度为48位(一般用于32位系统的远指针)

  • 基础知识

  • 结合段寄存器ss和偏移地址寄存器spss:sp指向栈顶元素

    • 在空栈中,ss:sp指向栈空间最高地址单元的下一个单元
    ![](images/28_dark.png#only-dark) ![](images/28_light.png#only-light)
  • 具体操作op的宽度为字(2字节)或双字(4字节)):

    • 入栈:push opss:sp减小)
    • 出栈:pop opss:sp增大)
    ![](images/29_dark.png#only-dark) ![](images/29_light.png#only-light)
    • 可以结合这两条指令实现mov指令或xchg指令的等价操作
    • 这两个操作本质上只是修改了寄存器sp,因此栈顶变化范围为0-FFFFH,栈的容量最大为64KB
  • 栈顶越界:对空栈执行pop操作,或对满栈执行push操作时发生

    • 8086CPU 不会处理越界问题,因此在编程时需要我们格外注意
  • 堆栈本质上是一块内存空间,但有着特殊的访问方式

  • 应用:暂存数据,比如多次使用loop指令,编写递归函数等,这些情况下可能不得不多次使用同一个寄存器,但又希望保留其原来的值

端口

我们在基础部分提到过CPU不会直接访问外部设备,而是通过接口卡进行间接访问的。而CPU会把外设的寄存器视为端口,对它们进行统一编址,从而建立一个统一的端口地址空间,每个端口在这个地址空间中有一个地址。

在8086PC中,CPU最多可以访问64KB,即65536个不同的端口,对应的端口地址仅有16位偏移地址,无段地址,取值范围为[0000h, 0FFFFh]。

端口访问相关的指令:

  • in reg, port_num:从port_num号端口读取数据到reg
  • out port_num, reg:向port_num号端口写入位于reg内的数据

其中regport_num具体指:

  • 访问8位端口:reg表示寄存器alport_num表示8位立即数
  • 访问16位端口:reg表示寄存器axport_num表示dx

常用的端口号:

  • 70h71h:分别是CMOS RAM的地址端口和数据端口,因此访问CMOS RAM时需要同时处理这两个端口的信号
  • 60h:键盘输入