CPU 的工作模式以及寄存器
CPU 进行计算工作,需要存放操作数以及运算结果的空间,这个空间称为寄存器。x86 架构处理器有 8 个通用寄存器:
| 64 位寄存器(长模式) | 32 位寄存器(保护模式) | 16 位寄存器(实模式) | 8 位寄存器 | 描述 |
|---|---|---|---|---|
| RAX | EAX | AX | AH, AL | 累加器,常用于算术和逻辑运算,会被乘除法指令自动使用 |
| RBX | EBX | BX | BH, BL | 基址寄存器,常用于内存寻址 |
| RCX | ECX | CX | CH, CL | 计数寄存器,常用于循环计数,会被 LOOP 系列指令自动使用 |
| RDX | EDX | DX | DH, DL | 数据寄存器,常用于 I/O 操作 |
| RSI | ESI | SI | 源索引寄存器,常用于字符串/数组操作 | |
| RDI | EDI | DI | 目标索引寄存器,常用于字符串/数组操作 | |
| RBP | EBP | BP | 基指针寄存器,常用于引用栈帧 | |
| RSP | ESP | SP | 堆栈指针寄存器,指向栈顶 |
从上表可以看出,在 x86 架构中,寄存器的位数扩展遵循向下兼容原则,通过寄存器分段命名实现数据操作的灵活性:
- 16 位处理器(如 8086),主要工作模式为实模式
- 仅支持 16 位及以下寄存器(AX、BX 等),且无 32 位概念
- 典型结构:AX = AH(高 8 位) + AL(低 8 位)
- 32 位处理器(如 80386),支持实模式,但主要工作在保护模式
- 扩展出 32 位寄存器(EAX、EBX 等),但仍兼容 16 位模式
- 层级关系:EAX(32 位)→ AX(低 16 位)→ AH/AL(8 位高低字节)
- (注意:AX 是 EAX 的低 16 位)
- 64 位处理器(x86-64/AMD64),支持以往的模式,但主要工作在长模式
- 新增 64 位寄存器(RAX、RBX 等),同时完全兼容 32 位以下操作
- 层级关系:RAX(64 位)→ EAX(低 32 位)→ AX(低 16 位)→ AH/AL(8 位高低字节)
- 新增 R8-R15 通用寄存器,同样支持 64/32/16/8 位操作
除了上述通用寄存器外,CPU 中还有专门用于访问内存的寄存器,称为段寄存器:
| 段寄存器 | 8086 中的用途(实模式) | x86 中的用途(保护模式) | x86-64 中的用途(长模式) |
|---|---|---|---|
| CS | 代码段寄存器,存储当前执行代码的段地址。 | 代码段寄存器,存储代码段选择子,用于访问代码段。 | 代码段寄存器,主要用于系统调用和返回前指令地址。 |
| DS | 数据段寄存器,存储数据的段地址。 | 数据段寄存器,存储数据段选择子,用于访问数据段。 | 数据段寄存器,主要用于访问全局变量和静态数据。 |
| SS | 堆栈段寄存器,存储栈的段地址。 | 堆栈段寄存器,存储堆栈段选择子,用于访问栈。 | 堆栈段寄存器,主要用于访问栈。 |
| ES | 附加段寄存器,用于字符串操作等额外的内存段地址存储。 | 附加段寄存器,存储附加段选择子,用于访问附加段。 | 附加段寄存器,主要用于 BIOS 调用和某些操作系统服务。 |
| FS | 无(8086 中不存在) | FS 寄存器,存储局部段选择子,用于访问局部变量和权限数据。 | FS 寄存器,存储局部段选择子,用于访问线程特定数据(例如 TLS)。 |
| GS | 无(8086 中不存在) | GS 寄存器,存储全局段选择子,用于访问全局变量和权限数据。 | GS 寄存器,存储全局段选择子,用于访问线程特定数据(例如 TLS)。 |
段寄存器的使用与内存寻址模型相关,下面根据 CPU 的工作模式分别介绍:
实模式
x86 架构在 16 位处理器(如 8086)时期,只支持单任务处理,运行在实模式下。实模式采用分段的内存模型,使用下面的方式获取物理地址:
物理地址 = 段寄存器 × 16 + 偏移量
即使寄存器只有 16 位,但通过这种形式得以扩展到 20 位的物理地址,所以有 1MB 的内存空间可供程序使用。
访存示例代码如下:
; 假设我们有一个数据段,其中包含了一个字(word)的数据; 数据段定义如下:data_segment segment data_word dw 1234h ; 定义一个字(word)的数据data_segment ends
; 堆栈段定义如下:stack_segment segment stack dw 256 dup(?) ; 分配256字的空间作为堆栈stack_segment ends
; 代码段开始code_segment segment; 告诉汇编器,引用某个段的数据时,使用哪个段,比如通过 data_word 标签引用数据时就会自动加上 dsassume cs:code_segment, ds:data_segment, ss:stack_segment, es:data_segmentstart: ; 初始化段寄存器 mov ax, data_segment mov ds, ax ; 将数据段地址加载到DS寄存器 mov es, ax ; 将数据段地址也加载到ES寄存器,用于额外的段操作
; 访问DS段中的数据 mov bx, 0 ; BX寄存器作为偏移量 mov ax, [bx] ; 将DS:BX指向的数据移动到AX寄存器 ; 此时,AX寄存器中存储的就是data_word的值,即1234h
; 访问ES段中的数据 mov bx, 0 ; 重置BX寄存器 mov ax, es:[bx] ; 将ES:BX指向的数据移动到AX寄存器 ; 此时,AX寄存器中存储的同样是data_word的值,即1234h
mov bx, 0 mov ax, data_word ; 因为设置了 assume ds:data_segment,所以汇编器会自动为其设置段和偏移
; 使用SS寄存器进行堆栈操作 mov ax, stack_segment mov ss, ax ; 将堆栈段地址加载到SS寄存器 mov sp, 256 ; 设置堆栈指针SP为256,即堆栈顶部
push ax ; 将AX寄存器的值压入堆栈 pop ax ; 从堆栈中弹出值到AX寄存器
; 结束程序 mov ax, 4C00h int 21h ; 调用DOS中断来结束程序code_segment ends; 因为设置了 assume cs:code_segment,所以汇编器才知道 start 需要设置为 cs 段end start保护模式
在 32 位处理器时期,资源得到扩展,而之前的实模式因不存在内存保护、无法建立隔离的内存空间等问题,无法支持多任务处理。因此引入了保护模式。
在保护模式下,段寄存器中存储的不再是直接的段基址,而是选择子(selector),通过选择子访问全局描述符表(GDT)或局部描述符表(LDT)中的段描述符,从而获取段的基址、界限和访问权限等信息。这样可以实现不同段之间的隔离和保护,防止程序随意访问其他段的内存,提高系统的安全性。
-
段选择子(Segment Selector):段选择子是一个 16 位的值,用于选择特定的段描述符。它存储在段寄存器(如 CS、DS、SS、ES、FS、GS)中,用于定位内存段。段选择子在保护模式下代替了实模式中段寄存器直接存储段基地址的功能。
15 3 2 1 0 bit位+-----------------------------------+---+--+--+| 描述符索引(Index) |TI | RPL |+-----------------------------------+---+--+--+- TI:指明是从 GDT(0)还是 LDT(1)中获取段描述符
- RPL:请求的特权级,用于权限检查。对应段描述符中的 DPL
- 通常情况下,CS 和 SS 中 RPL 就组成了 CPL(当前权限级别),所以常常是 RPL = CPL。CPL 表示发起访问者要以什么权限去访问目标段,当 CPL 大于目标段 DPL 时,CPU 禁止访问;只有 CPL 小于等于目标段 DPL 时才能访问
-
段描述符(Segment Descriptor):段描述符是一个 8 字节(64 位)的结构体,存储段的基地址、段限长(大小)以及访问权限等信息。
63 56 55 54 53 52 48 47 45 44 40 15 0+-----------------+---+----+---+---+------------------+---+-----+---+------+----------------- --+-----------------+| 基址高8位[24:31] | G | DB | - | A | 限长高4位[16:19] | P | DPL | S | TYPE | 基址低24位[0:23] | 限长低16位[0:15] |+-----------------+---+----+---+---+------------------+---+-----+---+------+--------------------+------------------- 基址(BASE):32 位基地址,定义段的起始线性地址
- 限长(LIMIT):20 位限长,表示段的长度,单位以「G」字段的属性为准
- G(Granularity):粒度,1 表示 4KB 粒度,0 表示字节粒度
- DB(Default Operation Size):默认操作大小,1 表示 32 位,0 表示 16 位
- A(Available):系统软件保留位,可自由使用
- P(Present):段有效标志,1 表示段存在。不存在则会触发中断
- DPL(Descriptor Privilege Level):段特权级,值为 0(最高)到 3(最低)
- S(Descriptor Type):段描述符类型,0 表示系统段,1 表示代码或数据段
- TYPE:段类型
bit 3 (T) = 代码段(1)或数据段(0)bit 2 (C) = 是否可执行bit 1 (R) = 是否可读bit 0 (A) = 是否已访问(由CPU自动设置)
-
全局描述符表(GDT):全局的段描述符表,存储系统中所有全局段描述符信息,存储在内存中,由操作系统内核进行初始化。其位置和大小由 GDTR 寄存器指定。GDTR 的格式为:
47 15 0+-------------------+------------------+| 32位基地址 | 16位界限 |+-------------------+------------------+- 系统只有一个 GDT- 第一个描述符(索引 0)必须为空- 包含系统中所有段的描述符- 通过 LGDT 指令加载 -
局部描述符表(LDT):进程私有的段描述符表,其位置和大小由 LDTR 寄存器指定。每个任务可以有一个 LDT,LDT 自身的描述符必须存在于 GDT 中,通过 LLDT 指令加载。
-
中断门描述符:保护模式下的中断也需要进行权限检查,所以每个中断使用中断门描述符来表示;
63 47 39 36 31 15 0+--------------------+--------------------------+----+-------+----------------+--------------------+| 目标代码段偏移31:16 | 1 | DPL | 0 | TYPE | 000 | 保留 | 目标代码段选择子 | 目标代码段偏移 0:15 |+--------------------+--------------------------+------------+----------------+--------------------+- DPL(Descriptor Privilege Level):段特权级,值为 0(最高)到 3(最低)
- TYPE:段类型
bit 3 = 0表示16位,1表示32位bit 2 = 1bit 1 = 1bit 0 = 0表示中断门,1表示陷阱门
-
任务状态段(TSS):TSS 是保护模式下进行任务切换的关键数据结构,存储了任务的处理器状态信息。其位置和大小由 TR 寄存器指定。
31 0+-------------------+| CR3 | 0x00+-------------------+| EIP | 0x04+-------------------+| EFLAGS | 0x08+-------------------+| EAX | 0x0C+-------------------+| ECX | 0x10+-------------------+| EDX | 0x14+-------------------+| EBX | 0x18+-------------------+| ESP | 0x1C+-------------------+| EBP | 0x20+-------------------+| ESI | 0x24+-------------------+| EDI | 0x28+-------------------+| ES | 0x2C+-------------------+| CS | 0x30+-------------------+| SS | 0x34+-------------------+| DS | 0x38+-------------------+| FS | 0x3C+-------------------+| GS | 0x40+-------------------+| LDT | 0x44+-------------------+| T | 0x48+-------------------+| I/O位图基址 | 0x4C+-------------------+| I/O位图 |+-------------------+ -
分页机制:虽然段机制在保护模式下提供了基本的内存管理功能,但为了进一步支持虚拟内存和更细粒度的内存管理,保护模式后续主要使用新引入的分页机制(依赖 MMU 单元)来管理内存。
保护模式下的中断
保护模式下产生中断后,CPU 首先会检查中断号是否大于最后一个中断门描述符。x86 CPU 最大支持 256 个中断源(即中断号:0~255),然后检查描述符类型(是否是中断门或陷阱门)、是否为系统描述符、是否存在于内存中。接着,检查中断门描述符中的段选择子指向的段描述符。最后做权限检查:如果 CPL 小于等于中断门的 DPL,并且 CPL 大于等于中断门中段选择子所指向段描述符的 DPL,就使用段描述符的 DPL。
进一步地,若 CPL 等于中断门中段选择子指向段描述符的 DPL,则为同级权限,不进行栈切换;否则进行栈切换。如果进行栈切换,还需要从 TSS 中加载具体权限的 SS、ESP,当然也要对 SS 中段选择子指向的段描述符进行检查。
做完这一系列检查之后,CPU 才会加载中断门描述符中目标代码段选择子到 CS 寄存器,把目标代码段偏移加载到 EIP 寄存器中执行。
保护模式的启动和访存流程
GDT 加载:
用户程序访存:
保护模式下用户空间程序的访存
; 用户空间内存访问示例 (Linux, x86_64); 编译: nasm -f elf64 -o memory_access.o memory_access.asm; 链接: ld -o memory_access memory_access.o
section .data ; 已初始化数据段 message1 db "Hello from data segment!", 0 number1 dq 12345678h array1 dd 1, 2, 3, 4, 5
section .bss ; 未初始化数据段 buffer resb 64 ; 64字节的缓冲区 array2 resd 10 ; 10个双字的数组 number2 resq 1 ; 一个八字节数值
section .text global _start
; 字符串打印函数print_string: ; 保存使用到的寄存器 push rax push rdi push rsi push rdx
; 计算字符串长度 mov rdi, rsi ; rsi中存放要打印的字符串地址 xor rcx, rcx ; 清零计数器.count_loop: cmp byte [rdi], 0 je .count_done inc rcx inc rdi jmp .count_loop.count_done:
; 调用系统调用write(1, string, length) mov rax, 1 ; write系统调用号 mov rdi, 1 ; 文件描述符 (stdout) mov rdx, rcx ; 字符串长度 syscall
; 恢复寄存器 pop rdx pop rsi pop rdi pop rax ret
_start: ; 1. 演示数据段访问 ; 直接访问静态数据 mov rsi, message1 call print_string
; 读取数值数据 mov rax, [number1] ; 可以对rax进行运算操作
; 访问数组元素 mov eax, [array1 + 4] ; 访问array1[1] mov eax, [array1 + 8] ; 访问array1[2]
; 2. 演示BSS段访问 ; 初始化buffer mov rdi, buffer mov rcx, 64 mov al, 'A'.fill_loop: mov [rdi], al inc rdi dec rcx jnz .fill_loop
; 初始化array2 mov rdi, array2 mov rcx, 10 mov eax, 1.init_array: mov [rdi], eax add rdi, 4 inc eax dec rcx jnz .init_array
; 3. 演示栈操作 ; 保存一些值到栈上 push qword [number1] push 0x1234567890ABCDEF
; 分配栈空间用于局部变量 sub rsp, 32 ; 分配32字节的栈空间 mov byte [rsp], 'H' mov byte [rsp+1], 'i' mov byte [rsp+2], 0
; 打印栈上的字符串 mov rsi, rsp call print_string
; 恢复栈 add rsp, 32 pop rax pop rax
; 4. 演示系统调用获取内存 ; 使用mmap系统调用分配内存 mov rax, 9 ; mmap系统调用号 mov rdi, 0 ; 让内核选择地址 mov rsi, 4096 ; 分配4KB mov rdx, 0x3 ; PROT_READ | PROT_WRITE mov r10, 0x22 ; MAP_PRIVATE | MAP_ANONYMOUS mov r8, -1 ; fd (-1 for anonymous mapping) mov r9, 0 ; offset syscall
; 检查是否分配成功 cmp rax, -1 je exit
; 使用分配的内存 mov rdi, rax ; 保存分配的地址 mov byte [rdi], 'M' mov byte [rdi+1], 'M' mov byte [rdi+2], 'A' mov byte [rdi+3], 'P' mov byte [rdi+4], 0
; 打印mmap区域的内容 mov rsi, rdi call print_string
; 使用munmap释放内存 mov rax, 11 ; munmap系统调用号 ; rdi中已经是内存地址 mov rsi, 4096 ; 大小 syscall
exit: ; 退出程序 mov rax, 60 ; exit系统调用号 xor rdi, rdi ; 返回值0 syscall保护模式下的平坦模型
因为分段模型存在诸多缺陷,并且随着分页机制逐渐完善,操作系统逐步淡化了分段机制。由于硬件约束的原因,x86 CPU 并不能直接使用分页模型,而是要在分段模型的前提下,根据需要决定是否开启分页。所以操作系统内核一般会将所有的段基址设置为 0,界限设置为最大值存放到 GDT 的固定位置,具体参考源码:torvalds/linux
长模式
长模式又名 AMD64,因为这个标准是 AMD 公司最早定义的。它使 CPU 在现有基础上具备了 64 位的处理能力,既能完成 64 位的数据运算,也能寻址 64 位的地址空间。长模式依旧继承了保护模式的诸多特性,只是段描述符中的基址和限长变成了无效项,CPU 不再对段基址和段长度进行检查,只对 DPL 进行相关检查,这个检查流程与保护模式下一样。
长模式下的中断
在 32 位保护模式下,中断门描述符支持的目标代码段偏移只支持 32 位,所以在长模式下,中断门描述符从 8 字节扩展到了 16 字节,其中高 8 字节中的低 32 位作为目标代码段偏移的扩展,其他位保持不变。
其余工作流程与保护模式相同。
注:除了这些通用寄存器外,还有一些我们会间接使用到的标志寄存器 FLAGS、指令指针寄存器 IP 以及单指令多数据、浮点运算等功能专用的寄存器,以及控制寄存器 CR0/CR2/CR3/CR4 等,这里不做具体介绍。
参考
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






