8086实模式相关机制解析

8086实模式下用户程序结构,段定义与加载协议 一、 分段与对齐 在 Intel 8086 处理器的实模式下,内存访问遵循“段地址:偏移地址”的原则 。一个规范的汇编程序应当通过分段来组织代码、数据和栈空间 。 1. NASM 中的段定义 NASM 编译器

8086实模式下用户程序结构,段定义与加载协议

一、 分段与对齐

在 Intel 8086 处理器的实模式下,内存访问遵循“段地址:偏移地址”的原则 。一个规范的汇编程序应当通过分段来组织代码、数据和栈空间 。

1. NASM 中的段定义

NASM 编译器利用 SECTIONSEGMENT 指令来定义段 。

  • 段名称:如 headercodedata 等,主要用于程序内部引用。

  • 段的边界:一旦定义了一个段,其后的内容均归属于该段,直到出现下一个段定义 。

2. 内存对齐

Intel 处理器要求段在内存中的起始物理地址必须至少是 16 字节对齐的 。

  • 在源码中,通过 align=16 子句强制对齐 。

  • 若前一个段的长度不满足对齐要求,编译器会在段间填充 0x00,以确保后续段的起始汇编地址符合规范 。

3. vstart

vstart 是控制标号地址计算的关键。

  • 无 vstart:段内标号的汇编地址将从整个程序的开头(文件偏移 0)开始计算 。

  • 使用 vstart=0:标号的汇编地址将相对于当前段的开头从 0 开始计算 。这对于多段程序的重定位至关重要。


二、 用户程序头部

加载器之所以能正确读取并运行一个未知的用户程序,全赖于程序开头的用户程序头部(Header) 。 根据协议,头部必须包含以下关键信息:

  1. 程序总长度:一个 32 位(双字)的数据,指明整个程序占用的字节数,用于指导加载器读取扇区的数量 。

  2. 应用程序入口点:包括 16 位的偏移地址和 32 位的段地址,告知加载器第一条指令的具体位置 。

  3. 段重定位表:由于程序加载的物理内存地址是不确定的,头部需记录所有需要重定位的段的起始汇编地址 。加载器在加载后,会根据实际地址回填这些表项 。


三、 加载器的工作流程与内存布局

加载器在执行加载任务前,需要规划好内存的范围 。

1. 内存空间划分

在 1MB 的实模式寻址空间内,可用区域主要集中在 0x10000 - 0x9FFFF 之间(约 500多 KB) 。

  • 0x00000 - 0x0FFFF:加载器及其栈的预留空间 。

  • 0xA0000 - 0xFFFFF:ROM BIOS 和外围设备(如显存)的映射区 。

2. 地址转换逻辑

加载器通常设定一个起始物理地址(如 phy_base = 0x10000)来承载用户程序 。为了将 20 位的物理地址转换为 16 位的段地址,加载器会进行除以 16(右移 4 位)的操作:

例如,0x10000 转换后的段地址即为 0x1000 。

3. 编程规范:常量的应用

在加载器源码中,推荐使用 equ 伪指令定义常量,如扇区起始号 app_lba_start equ 100 。这类常量不占用实际内存和汇编地址,仅在编译阶段替换数值,避免 Magic Number 出现 。


8086 处理器过程调用

在 8086 汇编语言中,过程调用是通过 call 指令实现的。根据调用目标位置和寻址方式的不同,可以将其归纳为四种主要类型 。以下是基于书本的梳理:


一、 16 位相对近调用

此调用方式用于同一代码段内部的过程调用 。

  • 指令机制:操作码为 0xE8,后跟 16 位操作数 。

  • 寻址原理:该操作数是一个相对量,由目标过程的汇编地址减去 call 指令下一条指令的汇编地址计算得出 。

  • 跳转范围:由于操作数是有符号的 16 位整数,跳转范围被限制在当前指令后的 -32768 至 32767 字节之间 。

  • 处理器动作

    1. 将指令指针寄存器 IP 的当前值(已指向下一条指令)压入栈中 。

    2. IP 的当前值与指令中的 16 位相对偏移量相加,结果存入 IP

代码示例

代码段

call near read_hard_disk_0  ; 使用 near 关键字显式调用 
call read_hard_disk_0       ; 默认省略关键字时,编译器视为 16 位相对近调用 

二、 16 位间接绝对近调用

这种调用同样限制在当前代码段内,但目标地址通过寄存器或内存单元间接提供 。

  • 指令机制:指令操作数是被调用过程的真实偏移地址(绝对地址) 。

  • 地址来源:地址可由 16 位通用寄存器或内存单元给出 。

  • 处理器动作

    1. 将指令指针寄存器 IP 的当前值压入栈中 。

    2. 从指定的寄存器或内存单元中取得 16 位地址,并用其取代 IP 原有的内容 。

代码示例

代码段

call cx                     ; 目标地址由寄存器 CX 提供 
call [0x3000]               ; 从数据段偏移地址 0x3000 处取得目标地址 
call [bx + si + 0x02]       ; 通过内存寻址方式间接取得地址 

三、 16 位直接绝对远调用

此方式属于段间调用,用于调用位于不同代码段内的过程 。

  • 指令机制:操作码为 0x9A,指令中直接包含了 16 位偏移地址和 16 位段地址 。

  • 机器码布局:按规定,偏移地址在前(低地址),段地址在后(高地址) 。

  • 处理器动作

    1. 将当前代码段寄存器 CS 的内容压入栈中 。

    2. 将当前指令指针寄存器 IP 的内容压入栈中 。

    3. 用指令中给出的段地址更新 CS,用偏移地址更新 IP

代码示例

代码段

call 0x2000:0x0030          ; 直接指定段地址 0x2000 和偏移地址 0x0030 

四、 16 位间接绝对远调用

此方式也是段间调用,但目标的段地址和偏移地址存储在内存中 。

  • 语法要求:必须显式使用关键字 far

  • 内存布局:处理器从内存中连续取得两个字。低地址字作为偏移地址,高地址字作为段地址 。

  • 处理器动作:依次将 CSIP 的当前内容压栈,随后从内存中加载新的 CSIP 值 。

代码示例

代码段

call far [0x2000]           ; 从 DS:0x2000 处取得两个字作为目标地址 
call far [proc_1]           ; 通过标号指定的内存位置取得地址 

过程返回与栈状态总结

调用类型对应返回指令处理器动作
近调用 (Near)ret从栈中弹出一个字到 IP
远调用 (Far)retf从栈中弹出两个字,分别存入 IPCS

栈指针 (SP) 变化说明:在执行 call 之前,程序通常会使用 push 保存寄存器环境;在 ret 之前,必须使用 pop 指令以反序恢复寄存器,确保 SP 回到调用发生时的位置,从而正确获取压栈的返回地址 。


算法和硬件控制


一、 算术与位移指令的应用

1. 32 位加法与乘法逻辑

在 16 位 8086 处理器上,处理 32 位地址或数据需要分步完成。

  • 带进位加法 (adc):该指令将目的操作数、源操作数与标志寄存器中的进位位(CF)相加 。在计算 32 位物理地址时,先使用 add 处理低 16 位,随后使用 adc 处理高 16 位,以确保低位的进位被正确计入高位结果 。

  • 无符号乘法 (mul)

    • 8 位乘法:目的操作数与 AL 相乘,结果存入 AX

    • 16 位乘法:目的操作数与 AX 相乘,结果存入 DX:AX,其中高 16 位在 DX,低 16 位在 AX

    • 若乘法结果的高一半(如 32 位结果中的 DX)不为全 0,则标志位 OFCF 置 1,否则清零 。

2. 逻辑移位与循环移位

位移指令在物理地址转换为逻辑段地址(右移 4 位)的过程中起关键作用。

  • 逻辑右移 (shr):操作数连续向右移动,空出位补 0,挤出的位送入 CF

  • 循环右移 (ror):每右移一次,移出的位既送入 CF,也送入左边空出的位置 。

代码示例:物理地址转换为段地址

代码段

; 假设 DX:AX 存储 32 位物理地址
; 计算逻辑:将 32 位数右移 4 位得到 16 位段地址
shr ax, 4           ; AX 右移 4 位,丢弃低 4 位 
ror dx, 4           ; DX 循环右移 4 位 
and dx, 0xf000      ; 保留 DX 移位过来的高 4 位,其余清零 
or ax, dx           ; 合并 DX 的高位到 AX 的高位 

二、 无条件转移指令

8086 提供了多种 jmp 指令格式,用于处理不同范围和类型的程序跳转 。

转移类型操作码/关键字范围与说明
相对短转移0xEB / short段内转移,位移量为 1 字节有符号数(-128 至 127 字节) 。
16 位相对近转移0xE9 / near段内转移,位移量为 2 字节有符号数(-32768 至 32767 字节) 。
16 位间接绝对近转移near目标偏移地址存于寄存器(如 bx)或内存单元中 。
16 位直接绝对远转移指令直接给出段地址和偏移地址(如 jmp 0x0000:0x7c00) 。
16 位间接绝对远转移far从内存中取得 4 字节数据,前 2 字节入 IP,后 2 字节入 CS

代码示例:加载器跳转至用户程序

代码段

; 从用户程序头部偏移 0x04 处取出 4 字节地址实现远转移 
jmp far [0x04]      ; 跳转至重定位后的用户程序入口点 

三、 程序重定位与内存管理

1. 段重定位

加载器读取用户程序头部的段重定位表,将各段的汇编地址转换为实际的逻辑段地址并写回原处 。

  • 重定位表项通常为双字(32 位汇编地址),加载器通过调用 calc_segment_base 过程将其计算为 16 位段地址 。

2. 内存保留与初始化

  • resb/resw/resd:用于在编译时保留未初始化的内存空间 。

  • vstart 子句:用于定义段内汇编地址的起始基准(如 vstart=0 表示标号地址从该段起始处开始计算) 。


四、 硬件控制

在文本模式下,显卡通过 I/O 端口提供对光标位置的控制。

1. 端口访问

  • 索引端口 (0x3d4):通过写入索引值选择目标寄存器 。

  • 数据端口 (0x3d5):读写被选中的寄存器数据 。

    • 索引 0x0e:光标位置高 8 位 。

    • 索引 0x0f:光标位置低 8 位 。

2. 光标计算逻辑

标准屏幕为 80 列 times 25 行。光标位置 P 的线性地址范围为 0 - 1999 。

  • 回车 (0x0d):将光标移动至当前行行首 。

  • 换行 (0x0a):将光标移动至下一行 。

代码示例:读取光标高 8 位

代码段

mov dx, 0x3d4       ; 索引端口
mov al, 0x0e        ; 准备访问高 8 位寄存器 
out dx, al
mov dx, 0x3d5       ; 数据端口
in al, dx           ; 读取高 8 位数据到 AL 

五、 过程调用与嵌套

汇编程序支持过程嵌套调用。每次 call 指令执行时,处理器将返回地址压入栈中,ret 指令则从栈中恢复地址 。

  • 逻辑判断:常用 or 指令判断字符串是否结束(0 终止符) 。

代码示例:字符串显示逻辑

代码段

.put_char_loop:
    mov cl, [ds:bx]     ; 从内存取得字符 
    or cl, cl           ; 判断字符是否为 0 
    jz .done            ; 若为 0 则结束跳转 
    call put_char       ; 调用单字符显示过程 
    inc bx              ; 指向下一字符 
    jmp .put_char_loop
.done:
    ret

结语

本博客是对 x86 汇编语言从实模式到保护模式部分内容的梳理,总结