
8086实模式相关机制解析
8086实模式下用户程序结构,段定义与加载协议 一、 分段与对齐 在 Intel 8086 处理器的实模式下,内存访问遵循“段地址:偏移地址”的原则 。一个规范的汇编程序应当通过分段来组织代码、数据和栈空间 。 1. NASM 中的段定义 NASM 编译器
8086实模式下用户程序结构,段定义与加载协议
一、 分段与对齐
在 Intel 8086 处理器的实模式下,内存访问遵循“段地址:偏移地址”的原则 。一个规范的汇编程序应当通过分段来组织代码、数据和栈空间 。
1. NASM 中的段定义
NASM 编译器利用 SECTION 或 SEGMENT 指令来定义段 。
-
段名称:如
header、code、data等,主要用于程序内部引用。 -
段的边界:一旦定义了一个段,其后的内容均归属于该段,直到出现下一个段定义 。
2. 内存对齐
Intel 处理器要求段在内存中的起始物理地址必须至少是 16 字节对齐的 。
-
在源码中,通过
align=16子句强制对齐 。 -
若前一个段的长度不满足对齐要求,编译器会在段间填充
0x00,以确保后续段的起始汇编地址符合规范 。
3. vstart
vstart 是控制标号地址计算的关键。
-
无 vstart:段内标号的汇编地址将从整个程序的开头(文件偏移 0)开始计算 。
-
使用 vstart=0:标号的汇编地址将相对于当前段的开头从 0 开始计算 。这对于多段程序的重定位至关重要。
二、 用户程序头部
加载器之所以能正确读取并运行一个未知的用户程序,全赖于程序开头的用户程序头部(Header) 。 根据协议,头部必须包含以下关键信息:
-
程序总长度:一个 32 位(双字)的数据,指明整个程序占用的字节数,用于指导加载器读取扇区的数量 。
-
应用程序入口点:包括 16 位的偏移地址和 32 位的段地址,告知加载器第一条指令的具体位置 。
-
段重定位表:由于程序加载的物理内存地址是不确定的,头部需记录所有需要重定位的段的起始汇编地址 。加载器在加载后,会根据实际地址回填这些表项 。
三、 加载器的工作流程与内存布局
加载器在执行加载任务前,需要规划好内存的范围 。
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 字节之间 。
-
处理器动作:
-
将指令指针寄存器
IP的当前值(已指向下一条指令)压入栈中 。 -
将
IP的当前值与指令中的 16 位相对偏移量相加,结果存入IP。
-
代码示例:
代码段
call near read_hard_disk_0 ; 使用 near 关键字显式调用
call read_hard_disk_0 ; 默认省略关键字时,编译器视为 16 位相对近调用
二、 16 位间接绝对近调用
这种调用同样限制在当前代码段内,但目标地址通过寄存器或内存单元间接提供 。
-
指令机制:指令操作数是被调用过程的真实偏移地址(绝对地址) 。
-
地址来源:地址可由 16 位通用寄存器或内存单元给出 。
-
处理器动作:
-
将指令指针寄存器
IP的当前值压入栈中 。 -
从指定的寄存器或内存单元中取得 16 位地址,并用其取代
IP原有的内容 。
-
代码示例:
代码段
call cx ; 目标地址由寄存器 CX 提供
call [0x3000] ; 从数据段偏移地址 0x3000 处取得目标地址
call [bx + si + 0x02] ; 通过内存寻址方式间接取得地址
三、 16 位直接绝对远调用
此方式属于段间调用,用于调用位于不同代码段内的过程 。
-
指令机制:操作码为
0x9A,指令中直接包含了 16 位偏移地址和 16 位段地址 。 -
机器码布局:按规定,偏移地址在前(低地址),段地址在后(高地址) 。
-
处理器动作:
-
将当前代码段寄存器
CS的内容压入栈中 。 -
将当前指令指针寄存器
IP的内容压入栈中 。 -
用指令中给出的段地址更新
CS,用偏移地址更新IP。
-
代码示例:
代码段
call 0x2000:0x0030 ; 直接指定段地址 0x2000 和偏移地址 0x0030
四、 16 位间接绝对远调用
此方式也是段间调用,但目标的段地址和偏移地址存储在内存中 。
-
语法要求:必须显式使用关键字
far。 -
内存布局:处理器从内存中连续取得两个字。低地址字作为偏移地址,高地址字作为段地址 。
-
处理器动作:依次将
CS和IP的当前内容压栈,随后从内存中加载新的CS和IP值 。
代码示例:
代码段
call far [0x2000] ; 从 DS:0x2000 处取得两个字作为目标地址
call far [proc_1] ; 通过标号指定的内存位置取得地址
过程返回与栈状态总结
| 调用类型 | 对应返回指令 | 处理器动作 |
|---|---|---|
| 近调用 (Near) | ret | 从栈中弹出一个字到 IP |
| 远调用 (Far) | retf | 从栈中弹出两个字,分别存入 IP 和 CS |
栈指针 (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,则标志位 OF 和 CF 置 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 汇编语言从实模式到保护模式部分内容的梳理,总结