实模式(Real Mode)

本文主要介绍实模式(Real Mode)的基本概念、主要特性,以及其在x86架构启动流程中的作用。通过理解实模式,可以帮助大家更好地理解现代操作系统为何要经历“实模式→保护模式→长模式”等阶段,以及16位、20位寻址等历史遗留问题的成因。

实模式(Real Mode)

什么是实模式?

实模式是 Intel 8086 处理器的原生工作模式,也是所有 x86 CPU 上电后的默认状态。它被称为“实模式”(Real Mode)是因为程序可以直接访问“真实”的物理内存地址,没有任何保护机制。

实模式的特点:

特性 说明 限制
位宽 16位寄存器和指令 单次只能处理16位数据
寻址能力 20位地址总线 最多访问1MB内存(2^20)
内存保护 无 ❌ 程序可以覆盖任意内存
多任务 不支持 ❌ 只能运行一个程序
特权级 无 ❌ 所有代码Ring 0权限
虚拟内存 不支持 ❌ 只能使用物理地址

段地址机制

实模式使用“段:偏移”的方式来寻址:

1
2
3
4
5
6
7
8
9
10
11
12
13
+------------------------------------------+
| 物理地址计算公式 |
| |
| 物理地址 = 段地址 × 16 + 偏移地址 |
| = 段地址 << 4 + 偏移地址 |
| |
| 示例: |
| 段地址 = 0x1000 |
| 偏移地址 = 0x0050 |
| 物理地址 = 0x1000 × 16 + 0x0050 |
| = 0x10000 + 0x0050 |
| = 0x10050 |
+------------------------------------------+

为什么要乘以16(左移4位)?

这是因为段寄存器只有16位,但地址总线有20位:

1
2
3
4
段寄存器(16位)  :0001 0000 0000 0000
左移4位 (x16) :0001 0000 0000 0000 0000 (20位)
偏移地址(16位) :0000 0000 0101 0000
相加得到物理地址:0001 0000 0000 0101 0000 = 0x10050

四大段寄存器

实模式下有四个主要的段寄存器:

寄存器 全称 默认用途 可以改变吗
CS Code Segment 代码段,存储程序指令 只能通过JMP/CALL改变
DS Data Segment 数据段,存储普通数据 可以自由修改 ✅
SS Stack Segment 栈段,存储栈数据 可以修改但需小心 ⚠️
ES Extra Segment 附加段,额外数据段 可以自由修改 ✅

寄存器组合规则:

默认的段:偏移组合:

1
2
3
4
5
6
+--------------------------------------+
| 指令读取: CS:IP |
| 栈操作: SS:SP / SS:BP |
| 数据访问: DS:SI / DS:DI / DS:BX |
| 字符串操作: DS:SI -> ES:DI |
+--------------------------------------+

寄存器说明:

  • IP (Instruction Pointer):指令指针寄存器,与 CS 配合,指向下一条要执行的指令的地址。CPU 会自动更新该寄存器。
  • SP (Stack Pointer):栈指针寄存器,与 SS 配合,指向当前栈顶。pushpop 操作会自动更新 SP
  • BP (Base Pointer):基址指针寄存器,与 SS 配合,通常用于在栈帧中定位函数参数和局部变量,作为访问栈的基准地址。
  • SI (Source Index):源变址寄存器,通常与 DS 配合,在字符串操作中指向源数据地址。
  • DI (Destination Index):目的变址寄存器,通常与 ES 配合,在字符串操作中指向目标数据地址。
  • BX (Base Register):基址寄存器,可以与 DS 配合,是通用寄存器中唯一可以作为内存寻址基址的。

内存布局(实模式下的1MB空间)

地址范围 大小 用途
0x00000-0x003FF 1KB 中断向量表 (IVT)
0x00400-0x004FF 256B BIOS数据区
0x00500-0x07BFF ~30KB 可用内存
0x07C00-0x07DFF 512B 引导扇区 (MBR) ⭐
0x07E00-0x7FFFF ~480KB 可用内存
0x80000-0x9FFFF 128KB 扩展BIOS数据区
0xA0000-0xBFFFF 128KB 显存 (VGA)
0xC0000-0xC7FFF 32KB 显卡BIOS
0xC8000-0xEFFFF 160KB 其他ROM/映射区
0xF0000-0xFFFFF 64KB 系统BIOS ROM

关键地址说明:

  1. 0x7C00 (MBR加载地址)
    • BIOS将引导扇区加载到这里
    • 引导程序从这里开始执行
    • 可用空间:512字节
  2. 0xB8000 (VGA文本显存)
    • 25行 × 80列 = 2000个字符
    • 每个字符占2字节(字符+属性)
    • 实际访问:0xB800:0x0000
  3. 0x00000 (中断向量表)
    • 256个中断向量 × 4字节 = 1024字节
    • 格式: 偏移16位

地址计算实践

让我们通过几个实际例子来理解地址计算:

例1:访问MBR

1
2
3
4
5
6
7
8
9
10
11
; 方法1: 段地址=0, 偏移=0x7C00
mov ax, 0x0000
mov ds, ax
mov si, 0x7C00
mov al, [ds:si] ; 读取 0x0000:0x7C00 = 物理地址 0x7C00

; 方法2: 段地址=0x07C0, 偏移=0
mov ax, 0x07C0
mov ds, ax
mov si, 0x0000
mov al, [ds:si] ; 读取 0x07C0:0x0000 = 0x7C00

例2:访问VGA显存写"A"

1
2
3
4
5
6
mov ax, 0xB800    ; VGA文本模式段地址
mov es, ax
mov di, 0 ; 第一个字符位置
mov byte [es:di], 'A' ; 写入字符
mov byte [es:di+1], 0x07 ; 写入属性(白色)
; 物理地址 = 0xB800 x 16 + 0 = 0xB8000

例3:同一物理地址的不同表示

物理地址 0x12345 可以表示为:

段地址 偏移 计算
0x1234 0x0005 0x12340 + 0x0005
0x1230 0x0045 0x12300 + 0x0045
0x1000 0x2345 0x10000 + 0x2345
0x0000 无法表示 偏移超过16位

这种"一个物理地址,多种表示"的特性叫做地址重叠

实模式的限制

1. 1MB内存限制

20位地址线只能访问 2^20 = 1,048,576 字节 = 1MB

最大地址: 段地址: 0xFFFF 偏移: 0xFFFF 物理地址 = 0xFFFF × 16 + 0xFFFF = 0x10FFEF ≈ 1.06MB

但是!实际只有20根地址线,所以: 超过 0xFFFFF 的地址会"回卷"到 0x00000 这就是著名的"A20地址线"问题

2. 无内存保护

任何程序都可以:

  • 修改其他程序的代码和数据
  • 覆盖BIOS数据区
  • 改写中断向量表
  • 直接访问硬件端口

后果:一个程序的错误会导致整个系统崩溃

3. 单任务环境

由于没有内存保护和特权级:

  • 不能同时运行多个程序
  • 不能实现真正的多任务
  • 不能隔离用户程序和内核

从实模式到保护模式

为了克服这些限制,Intel 在 80286 引入了保护模式 (Protected Mode):

对比项 实模式 保护模式
位宽 16位 32位(64位扩展)
寻址 1MB 4GB (32位) / 无限 (64位)
内存保护 ✅ 段级+页级
多任务 ✅ 硬件支持
特权级 ✅ Ring 0-3
虚拟内存 ✅ 分页机制

切换到保护模式是不可逆的(在不重启的情况下)!

这就是为什么引导程序需要非常小心地准备好一切,再进行切换。

💡 深入理解:特权级 (Privilege Levels / Protection Rings)

保护模式最重要的特性之一就是特权级机制,它是操作系统安全性的基础。

什么是特权级?

x86 CPU定义了4个特权级别,通常称为 Ring 0-3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
+-----------------------------------------------------------------+ <--- 最低特权
| Ring 3 (用户模式) | 应用程序运行在这里
| +-----------------------------------------------------------+ |
| | Ring 2 (很少使用) | |
| | +-----------------------------------------------------+ | |
| | | Ring 1 (很少使用) | | |
| | | +-----------------------------------------------+ | | |
| | | | Ring 0 (内核模式) | | | | <--- 最高特权
| | | | | | | | 操作系统内核运行在这里
| | | | 操作系统内核 | | | |
| | | +-----------------------------------------------+ | | |
| | | 设备驱动 | | |
| | +-----------------------------------------------------+ | |
| | 系统服务 | |
| +-----------------------------------------------------------+ |
| 用户应用程序 |
+-----------------------------------------------------------------+

实际使用:

大多数操作系统只使用 Ring 0 和 Ring 3:

Ring 名称 用途 权限
Ring 0 内核模式 (Kernel Mode) 操作系统内核、关键驱动 完全访问硬件、修改所有内存、执行特权指令
Ring 1 设备驱动 (很少用) 设备驱动程序 有限的硬件访问
Ring 2 设备驱动 (很少用) 系统服务 更多的权限
Ring 3 用户模式 (User Mode) 应用程序 受限访问、不能执行特权指令、不能直接访问硬件

特权级如何体现?

CPU通过以下位置记录当前特权级:

  1. CS寄存器的低2位 (CPL - Current Privilege Level)

    1
    2
    3
    4
    5
    6
     15              3 2  1 0
    +-----------------+--+---+
    | 选择子索引 |TI|RPL| <- CS 寄存器
    +-----------------+--+---+
    |
    +-- 这2位就是当前特权级 (CPL)
    这2位就是当前特权级(CPL)

  2. 段描述符中的DPL (Descriptor Privilege Level) 每个段描述符都有DPL字段,表示访问该段需要的最低特权级

  3. 选择子中的RPL (Requested Privilege Level) 程序请求访问某个段时指定的特权级

特权级检查规则:

访问数据段: CPL ≤ DPL (数字越小特权越高) RPL ≤ DPL

调用代码段:更复杂,涉及到Call Gate(后续章节)

示例: - Ring 0 代码 (CPL=0) 可以访问任何 DPL 段 - Ring 3 代码 (CPL=3) 只能访问 DPL=3 的段 - 如果 Ring 3 代码试图访问 Ring 0 的段,CPU会触发保护异常 (#GP)

特权指令:

只有在 Ring 0 才能执行的指令(特权指令):

指令 用途 为什么要保护
cli / sti 关闭/开启中断 防止用户程序禁止中断导致系统无响应
lgdt / lidt 加载GDT/IDT 防止用户程序篡改系统表
lldt 加载LDT 防止破坏内存保护
ltr 加载任务寄存器 防止破坏任务切换机制
mov cr0, eax 修改控制寄存器 防止用户程序切换CPU模式
in / out 端口I/O 防止用户程序直接控制硬件
hlt 停止CPU 防止用户程序挂起系统

如果在 Ring 3 执行这些指令,CPU会触发 #GP (General Protection Fault) 异常。

实例:用户程序试图关闭中断

1
2
3
4
; 用户程序 (Ring 3)
cli ; ❌ 触发 #GP 异常
; CPU自动跳转到 #GP 异常处理程序
; 操作系统会终止该程序 (Segmentation Fault)

系统调用:如何从 Ring 3 请求 Ring 0 服务?

用户程序不能直接执行特权操作,但可以通过系统调用请求内核帮忙:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
+--------------------------------+
| 用户程序 (Ring 3) |
| |
| int 0x80 / sysenter / syscall|
+--------------+-----------------+
|
| (陷入内核)
v
+--------------+-----------------+
| 内核空间 |
| |
| 系统调用入口 (Ring 0) |
| | |
| +-------v-------+ |
| | 检查权限 | |
| +-------+-------+ |
| | |
| +-------v-------+ |
| | 执行特权操作 | |
| +-------+-------+ |
| | |
| +-------v-------+ |
| | 返回结果 | |
| +---------------+ |
|
| (返回用户空间)
v
+--------------+-----------------+
| 用户程序 (Ring 3) |
| |
| iret / sysexit / sysret |
+--------------------------------+

Linux 系统调用示例:

1
2
3
4
5
6
7
8
; Linux x86-32 系统调用示例
mov eax, 4 ; sys_write 系统调用号
mov ebx, 1 ; 文件描述符: stdout
mov ecx, msg ; 字符串地址
mov edx, len ; 字符串长度
int 0x80 ; 触发系统调用(切换到Ring 0)
; 内核执行write操作
; 返回到Ring 3

特权级转换时的栈切换:

从 Ring 3 切换到 Ring 0 时,CPU会自动切换栈:

1
2
3
4
5
6
7
8
9
10
11
12
+---------------------------+       +---------------------------------+
| Ring 3 栈 (用户栈) | | Ring 0 栈 (内核栈) |
| | | |
| [ 局部变量 ] | | [ SS (R3) ] <-------------------+
| [ ... ] | | [ ESP (R3) ] | CPU 自动保存
| | | [ EFLAGS ] | 用户栈信息
| | | [ CS (R3) ] |
+---------------------------+ | [ EIP (R3) ] <------------------+
| | [ 参数 ] |
| 系统调用 int 0x80 | [ ... ] |
+--------------------->| | <--- ESP (内核栈)
+---------------------------------+

每个任务都有专门的内核栈(TSS定义)。

为什么需要特权级?

  1. 安全性:防止用户程序破坏系统

    1
    2
    3
    4
    5
    6
    7
    if (用户程序能直接访问硬件) {
    恶意程序可以:
    - 读取其他程序的内存(窃取密码)
    - 格式化硬盘
    - 禁用安全机制
    - 接管整个系统
    }
  2. 稳定性:隔离错误

    
    
    graph LR
        A["用户程序崩溃"] --> B{"只影响自己"} --> C["操作系统继续运行"]
        D["没有特权级"] --> E{"一个程序错误"} --> F["整个系统崩溃"]
    
    
  3. 多任务:资源管理

    操作系统 (Ring 0):
    • 决定哪个程序运行
    • 分配内存和CPU时间
    • 防止程序互相干扰