第7章 处理器体系架构详解
本章详细解析x86-64处理器体系架构,重点介绍三种运行模式:实模式(16位)、保护模式(32位)和IA-32e模式(64位)。处理器通过寄存器扩展(从16位到64位)和新增指令集(如CPUID)不断演进,为操作系统提供内存保护、特权级隔离等关键功能。64位模式通过增加寄存器数量、简化段机制等改进大幅提升性能。操作系统利用这些硬件特性实现进程隔离、内存管理等核心功能,处理器架构的进步直接推动了操作
第7章 处理器体系架构详解
处理器是一台计算机的运算和控制核心。操作系统的所有功能——无论是进程调度、内存管理还是设备驱动——最终都要落实到处理器指令的执行上。如果不理解处理器的工作原理,就无法真正理解操作系统为什么要这样设计。
x86处理器家族从1978年的8086一路走到今天的64位处理器,经历了几次重大的架构升级。每一次升级都引入了新的运行模式和硬件机制,操作系统也随之进化。从最早的DOS只能使用1MB内存,到Windows 95开始利用32位保护模式,再到今天的64位操作系统可以寻址海量内存——这些进步的根基都在处理器架构上。
本章将系统性地讲解x86-64处理器的体系架构,包括寄存器、运行模式、地址空间、内存管理机制等核心内容。这些知识是理解操作系统底层运作方式的必备基础。
7.1 核心功能与新增特性
7.1.1 处理器的运行模式
x86-64处理器支持多种运行模式,每种模式对应不同的能力和限制。理解这些模式是理解整个处理器架构的前提。
实模式(Real Mode) 是处理器上电或复位后进入的初始模式。在这种模式下,处理器的行为与1978年的8086几乎完全相同——16位操作数、20位地址总线、最多寻址1MB内存、没有内存保护、没有特权级区分。所有代码都在同一个特权级下运行,任何程序都可以直接访问硬件、修改中断向量表、读写任意内存地址。
实模式看起来功能简陋,但它在系统启动过程中扮演着不可替代的角色。BIOS在实模式下运行,引导扇区代码也在实模式下执行。操作系统的引导程序(Bootloader)需要在实模式下完成很多准备工作——读取磁盘、获取内存信息、设置显示模式——然后才能切换到更高级的模式。即便是最新的处理器,每次开机时依然从实模式启动(除非使用UEFI的64位入口,但那本质上是固件帮你完成了模式切换)。
保护模式(Protected Mode) 是Intel 80386引入的32位运行模式。这是一次历史性的飞跃——处理器从此具备了现代操作系统所需要的关键硬件支撑。保护模式提供了32位地址空间(可寻址4GB内存)、硬件级别的内存保护(通过段机制和页机制)、四个特权级(Ring 0到Ring 3)以及完善的虚拟内存支持。
保护模式的"保护"二字体现在多个方面。段描述符中的DPL(描述符特权级)配合代码段选择子中的CPL(当前特权级)和RPL(请求特权级),构成了一套严密的访问控制体系。页表项中的U/S位区分了用户页和内核页,R/W位区分了只读页和可写页。这些硬件机制让操作系统可以把内核空间和用户空间严格隔离开来——用户程序无法读写内核内存,也无法直接执行特权指令(如修改控制寄存器、操作I/O端口等)。
Windows 3.1是在保护模式下运行的早期商业操作系统(虽然它的保护做得并不彻底)。到了Windows NT和Linux 1.0时代,保护模式的各种硬件特性才被充分利用起来。OS/2也是较早全面利用保护模式的操作系统。
IA-32e模式(也叫长模式,Long Mode) 是AMD在2003年率先推出的64位扩展模式。Intel后来也采纳了这套架构,称之为Intel 64或EM64T。IA-32e模式又分为两个子模式:
64位模式(64-bit Mode):这是真正的64位运行环境。通用寄存器扩展到64位,地址空间扩展到理论上的16EB(实际实现通常支持48位虚拟地址和52位物理地址),新增了R8到R15共8个通用寄存器,寻址方式更加灵活(支持RIP相对寻址)。64位模式下段机制被大幅简化——代码段和数据段的基地址固定为0,界限固定为最大值,段机制在地址计算中基本被架空了,内存保护的重任完全交给了页机制。
兼容模式(Compatibility Mode):这是为了向后兼容32位应用程序而设计的子模式。在兼容模式下运行的程序看到的是一个32位的执行环境,但操作系统内核运行在64位模式下。这就是为什么64位的Windows和Linux可以直接运行绝大部分32位程序——操作系统通过兼容模式为32位程序提供了一个"模拟"的32位环境。Windows中的WoW64(Windows on Windows 64)子系统就是利用兼容模式来运行32位程序的。
还有一个系统管理模式(System Management Mode,SMM),它是一个特殊的模式,用于执行固件级别的代码,通常与操作系统无关——比如处理电源管理事件、硬件错误等。SMM通过SMI(系统管理中断)触发,进入时CPU保存完整的上下文到SMRAM(一块专用的内存区域),执行SMM代码后恢复上下文返回原来的模式。操作系统通常感知不到SMM的存在,但如果SMI处理程序执行时间过长,可能会影响操作系统的实时性能。
模式之间的切换路径是固定的。从实模式到保护模式需要设置好GDT、打开CR0的PE位、执行一个远跳转来刷新代码段。从保护模式到IA-32e模式需要打开PAE分页、设置CR4的PAE位、通过MSR寄存器(EFER.LME)启用长模式、打开分页(CR0.PG),然后执行远跳转加载64位代码段。不能直接从实模式跳到IA-32e模式——必须经过保护模式这个中间站。
7.1.2 通用寄存器体系
寄存器是处理器内部最快速的存储单元。处理器执行指令时,操作数通常需要先从内存加载到寄存器中,运算完成后再从寄存器写回内存。寄存器的数量和宽度直接影响程序的运行效率。
在16位的8086时代,通用寄存器只有8个,每个16位宽:AX、BX、CX、DX、SI、DI、BP、SP。每个寄存器都有其"传统用途"——AX用于累加运算,BX常作为基址寄存器,CX用于循环计数,DX在I/O操作和乘除法中使用,SI和DI分别是源地址和目的地址索引,BP是栈帧指针,SP是栈顶指针。当然这些只是惯例,很多指令并不限制只能用特定的寄存器。
到了32位保护模式,这8个寄存器被扩展到32位,名称前面加了字母E:EAX、EBX、ECX、EDX、ESI、EDI、EBP、ESP。低16位仍然可以用原来的名字访问(AX等),AX的高8位和低8位也可以分别用AH和AL访问。
进入64位模式后,寄存器架构发生了两个重大变化。第一,原有的8个寄存器全部扩展到64位,名称前缀改为R:RAX、RBX、RCX、RDX、RSI、RDI、RBP、RSP。第二,新增了8个通用寄存器:R8到R15,这些也是64位宽的。
x86asm
64位寄存器布局:
|63 31 15 7 0|
| RAX |
| EAX |
| AX |
| AH | AL |
R8到R15的子寄存器命名规则:
R8 -> R8D (低32位) -> R8W (低16位) -> R8B (低8位)
R9 -> R9D -> R9W -> R9B
… 以此类推
从8个寄存器增加到16个看似只是翻了一倍,但对编译器优化的影响非常深远。在32位模式下,8个通用寄存器中ESP通常不能挪作他用(必须指向栈顶),EBP也经常被占用(用作栈帧指针),实际可自由使用的寄存器只有6个。编译器在优化循环和复杂表达式时经常发现寄存器不够用,不得不频繁地将中间结果"溢出"(spill)到栈上的内存中,然后再读回来。每次溢出都是一次内存访问,速度比寄存器操作慢得多。
64位模式下有16个通用寄存器,而且64位的调用约定允许编译器省略帧指针(RBP可以作为通用寄存器使用),加上System V AMD64 ABI规定前6个整数参数通过寄存器传递(RDI、RSI、RDX、RCX、R8、R9)而不是压栈,这些改进让编译器有了更大的优化空间。很多实际的性能测试显示,仅仅将32位程序重新编译为64位版本,性能就能提升5%到15%,寄存器数量的增加是主要原因之一。
Windows的64位调用约定与Linux不同。Windows使用RCX、RDX、R8、R9传递前4个整数参数,而且要求调用者在栈上预留32字节的"影子空间"(Shadow Space),即使参数不超过4个也必须预留。这个设计是为了方便调试——被调用函数可以把寄存器参数保存到影子空间中,使得调试器在检查调用栈时能看到完整的参数值。
除了通用寄存器,64位处理器还有专门的浮点和SIMD寄存器。x87浮点单元有8个80位的浮点寄存器(ST0到ST7),采用栈式结构。SSE引入了16个128位的XMM寄存器(XMM0到XMM15,32位模式下只有XMM0到XMM7)。AVX进一步将这些寄存器扩展到256位的YMM寄存器。AVX-512则扩展到512位的ZMM寄存器,并且将寄存器数量增加到32个(ZMM0到ZMM31)。这些SIMD寄存器在图形处理、科学计算、视频编解码等领域有重要的性能意义。
操作系统在进行进程上下文切换时,必须正确保存和恢复所有这些寄存器的值。这不是一件小事——如果算上通用寄存器、SIMD寄存器、浮点状态、控制寄存器等,一次完整的上下文保存可能涉及几千字节的数据。Linux使用XSAVE/XRSTOR指令来高效地保存和恢复扩展状态(包括SSE、AVX等),并且采用"延迟保存"(lazy save)策略——只有当新调度的进程确实要使用浮点/SIMD指令时,才保存前一个进程的浮点状态并恢复当前进程的。这通过CR0的TS(Task Switched)标志位来实现:上下文切换时设置TS位,当程序执行浮点/SIMD指令时触发#NM(设备不可用)异常,异常处理程序再完成延迟的状态保存和恢复。
7.1.3 CPUID指令的应用
CPUID是x86处理器提供的一条特殊指令,它不执行任何运算,而是返回处理器本身的信息——制造商标识、型号、支持的功能特性等。操作系统在启动时需要通过CPUID来探测处理器的能力,然后根据处理器支持的特性来决定启用哪些功能。
CPUID的使用方式是先在EAX中设置"叶号"(Leaf,也叫功能号),可选地在ECX中设置"子叶号"(Sub-leaf),然后执行CPUID指令,处理器把结果放在EAX、EBX、ECX、EDX四个寄存器中返回。
c
// 执行CPUID指令的内联汇编封装
static inline void cpuid(unsigned int leaf, unsigned int subleaf,
unsigned int *eax, unsigned int *ebx,
unsigned int *ecx, unsigned int *edx)
{
asm volatile (“cpuid”
: “=a”(*eax), “=b”(*ebx), “=c”(*ecx), “=d”(*edx)
: “a”(leaf), “c”(subleaf));
}
最基本的叶号是0,它返回处理器制造商的标识字符串和CPUID支持的最大叶号。
c
void detect_cpu_vendor(void)
{
unsigned int eax, ebx, ecx, edx;
char vendor[13];
cpuid(0, 0, &eax, &ebx, &ecx, &edx);
// 制造商字符串分布在EBX、EDX、ECX中(注意顺序)
*(unsigned int *)(vendor + 0) = ebx;
*(unsigned int *)(vendor + 4) = edx;
*(unsigned int *)(vendor + 8) = ecx;
vendor[12] = '\0';
printk("CPU Vendor: %s\n", vendor);
printk("Max CPUID leaf: %d\n", eax);
}
Intel处理器返回 “GenuineIntel”,AMD处理器返回 “AuthenticAMD”,还有一些虚拟化环境中的虚拟CPU返回特定的字符串——比如KVM返回 “KVMKVMKVM”,VMware返回 “VMwareVMware”,Hyper-V返回 “Microsoft Hv”。操作系统可以利用这个信息来检测自己是否运行在虚拟机中,并据此做出优化。比如Linux内核在检测到运行在KVM中时,会使用半虚拟化(paravirtualization)接口来替代某些低效的硬件模拟操作。
叶号1返回处理器的型号信息和功能特性标志。功能特性标志是一组位(bit),每个位对应一个处理器特性,1表示支持,0表示不支持。
c
void detect_cpu_features(void)
{
unsigned int eax, ebx, ecx, edx;
cpuid(1, 0, &eax, &ebx, &ecx, &edx);
// ECX中的特性标志
int has_sse3 = (ecx >> 0) & 1;
int has_ssse3 = (ecx >> 9) & 1;
int has_sse4_1 = (ecx >> 19) & 1;
int has_sse4_2 = (ecx >> 20) & 1;
int has_avx = (ecx >> 28) & 1;
int has_xsave = (ecx >> 26) & 1;
int has_hypervisor = (ecx >> 31) & 1; // 运行在虚拟机中
// EDX中的特性标志
int has_fpu = (edx >> 0) & 1; // x87浮点单元
int has_pse = (edx >> 3) & 1; // 页面大小扩展(4MB大页)
int has_tsc = (edx >> 4) & 1; // 时间戳计数器
int has_msr = (edx >> 5) & 1; // 模型特定寄存器
int has_pae = (edx >> 6) & 1; // 物理地址扩展
int has_apic = (edx >> 9) & 1; // 本地APIC
int has_mtrr = (edx >> 12) & 1; // 内存类型范围寄存器
int has_pge = (edx >> 13) & 1; // 页面全局使能
int has_pat = (edx >> 16) & 1; // 页面属性表
int has_sse = (edx >> 25) & 1;
int has_sse2 = (edx >> 26) & 1;
printk("CPU Features:\n");
if (has_fpu) printk(" FPU");
if (has_pae) printk(" PAE");
if (has_apic) printk(" APIC");
if (has_sse) printk(" SSE");
if (has_sse2) printk(" SSE2");
if (has_avx) printk(" AVX");
printk("\n");
}
操作系统根据CPUID的结果来决定是否启用某些功能。比如:
PAE(物理地址扩展):如果处理器支持PAE,操作系统可以使用三级页表来访问超过4GB的物理内存。32位Linux和32位Windows Server都利用PAE来支持大于4GB的物理内存。
APIC:如果处理器有本地APIC,操作系统使用APIC来管理中断,而不是传统的8259A PIC。多处理器系统必须使用APIC。
SSE/AVX:操作系统需要启用对应的硬件支持(设置CR4和XCR0中的相关位),否则用户程序执行SSE/AVX指令会触发异常。
XSAVE:如果处理器支持XSAVE指令,操作系统在上下文切换时使用XSAVE/XRSTOR来保存和恢复扩展状态,否则使用老的FXSAVE/FXRSTOR。
扩展叶号0x80000001用于查询AMD特有的扩展特性和Intel的扩展特性。其中一个重要的位是EDX的第29位——NX位(No-Execute,AMD叫NX,Intel叫XD)。如果处理器支持NX位,页表项中就可以设置NX标志来禁止某些内存页上的代码执行。这是一个重要的安全特性——数据段(如堆和栈)通常不应该被当作代码执行,如果恶意代码设法往栈上写入了一段机器码然后跳转过去执行(这是缓冲区溢出攻击的经典手法),NX位可以阻止这种执行。Windows的DEP(Data Execution Prevention,数据执行保护)和Linux的NX保护就是利用这个硬件特性实现的。
扩展叶号0x80000002到0x80000004返回处理器的品牌字符串,最长48个字符,比如 “Intel® Core™ i7-10700K CPU @ 3.80GHz” 或 “AMD Ryzen 9 5950X 16-Core Processor”。操作系统可以把这个字符串显示在系统信息中。Linux中 cat /proc/cpuinfo 显示的 “model name” 就来自这里。
7.1.4 标志寄存器EFLAGS/RFLAGS
标志寄存器是处理器中一个特殊的寄存器,它的每一位(或几位)都代表一个独立的标志,记录着处理器的状态信息或控制处理器的某些行为。在64位模式下,标志寄存器是64位的RFLAGS,但高32位目前保留未用,实际有效的仍然是低32位(即EFLAGS)。
标志寄存器中的标志可以分为三类:
状态标志(Status Flags) 反映上一条算术或逻辑运算指令的结果。
CF(Carry Flag,位0):进位标志。当无符号运算产生进位或借位时置1。比如两个8位无符号数255+1=256,结果溢出了8位的范围,CF就会被设置。
PF(Parity Flag,位2):奇偶标志。当结果的低8位中1的个数为偶数时置1。这个标志主要用于串行通信中的奇偶校验,在现代编程中很少直接使用。
AF(Auxiliary Carry Flag,位4):辅助进位标志。当运算的第3位向第4位产生进位时置1。这个标志主要用于BCD(二进制编码的十进制)运算。
ZF(Zero Flag,位6):零标志。当运算结果为0时置1。这是最常用的标志之一——条件分支指令(如JE、JNE)通常就是检查ZF的值。
SF(Sign Flag,位7):符号标志。等于运算结果的最高位。对于有符号数,SF为1表示结果为负数。
OF(Overflow Flag,位11):溢出标志。当有符号运算的结果超出了表示范围时置1。比如两个正数相加得到了负数,就说明发生了有符号溢出。
状态标志是条件分支指令的判断依据。编译器把高级语言中的if语句编译成比较指令(CMP)加条件跳转指令(Jcc),CMP指令执行减法并设置标志位但不保存结果,Jcc指令根据标志位的组合决定是否跳转。比如:
c
if (a == b) // CMP a, b; JE target (检查ZF=1)
if (a < b) // CMP a, b; JL target (有符号: SF!=OF)
if (a > b) // CMP a, b; JG target (有符号: ZF=0 且 SF=OF)
if (a <= b) // CMP a, b; JBE target (无符号: CF=1 或 ZF=1)
控制标志(Control Flag) 控制处理器的某些行为。
DF(Direction Flag,位10):方向标志。控制字符串操作指令(如MOVS、CMPS、SCAS等)的方向。DF=0时地址递增,DF=1时地址递减。C语言的memcpy和strcmp底层可能使用字符串指令,DF的状态影响它们的工作方向。x86-64的ABI规定在函数调用时DF必须为0,所以如果你在内联汇编中设置了DF,必须在函数返回前清除它。
系统标志(System Flags) 与操作系统和处理器的工作模式相关,通常只有内核态代码才能修改。
IF(Interrupt Flag,位9):中断使能标志。IF=1时允许外部硬件中断,IF=0时禁止。CLI指令清除IF(禁止中断),STI指令设置IF(允许中断)。操作系统在执行关键代码段(如修改中断描述符表、操作调度队列等)时需要临时禁止中断,防止中断处理程序在数据结构处于不一致状态时访问它们。但禁止中断的时间不能太长,否则会导致中断丢失和系统响应延迟。Linux内核使用自旋锁时通常会同时禁止本地中断(spin_lock_irqsave),就是因为如果不禁止中断,中断处理程序可能会尝试获取同一个自旋锁,导致死锁。
TF(Trap Flag,位8):陷阱标志。TF=1时,处理器在每执行一条指令后产生#DB(调试异常)。这就是调试器的单步执行功能的硬件基础。当你在GDB中按 s(step)或 n(next)时,调试器设置TF=1然后让目标程序继续运行,程序执行一条指令后触发调试异常,调试器重新获得控制权。
IOPL(I/O Privilege Level,位12-13):I/O特权级。这两位定义了可以执行I/O指令(IN、OUT、CLI、STI等)的最低特权级。IOPL通常被操作系统设置为0,意味着只有Ring 0(内核态)的代码可以直接操作I/O端口。用户态程序如果要进行I/O操作,必须通过系统调用请求内核代劝。不过Linux提供了ioperm()和iopl()系统调用,允许特权用户程序直接访问指定的I/O端口(通过修改TSS中的I/O Permission Bitmap),这在某些特殊场景下(如用户态设备驱动)是有用的。
NT(Nested Task,位14):嵌套任务标志。在32位保护模式下用于硬件任务切换机制。64位模式下硬件任务切换已经被淘汰,这个标志基本不再使用。
AC(Alignment Check,位18):对齐检查标志。配合CR0中的AM位,当两者都被设置且处于Ring 3时,对未对齐的内存访问产生#AC异常。在x86上,未对齐的内存访问通常不会产生异常(只是性能下降),但在启用对齐检查后就会。某些安全工具利用对齐检查来检测内存访问异常。
ID(Identification,位21):如果这个标志可以被修改(先设置再清除能成功),说明处理器支持CPUID指令。在非常早期的处理器上可能不支持CPUID,可以用这个方法来探测。
7.1.5 控制寄存器组
控制寄存器(CR0到CR4,以及CR8)控制着处理器的运行模式和各种硬件特性的开关。只有在最高特权级(Ring 0)下才能读写控制寄存器。
CR0 是最重要的控制寄存器,它控制着处理器的工作模式和基本特性。
PE(Protection Enable,位0):保护模式使能。设置这个位使处理器从实模式切换到保护模式。清除这个位则返回实模式。这是模式切换中最关键的一步。
MP(Monitor Coprocessor,位1):监控协处理器。与TS位配合使用。
EM(Emulation,位2):仿真。设置后执行浮点指令会触发#NM异常,操作系统可以用软件来模拟浮点运算。在早期没有浮点硬件的计算机上,这个机制允许系统通过软件仿真来支持浮点运算。
TS(Task Switched,位3):任务已切换。每次硬件任务切换或手动设置后,执行浮点/SIMD指令会触发#NM异常。操作系统利用这个机制实现浮点状态的延迟保存。
ET(Extension Type,位4):扩展类型。在386时代用于区分287和387协处理器,现在已经没有意义。
NE(Numeric Error,位5):数值错误。设置后浮点错误通过#MF异常报告而不是通过外部中断。
WP(Write Protect,位16):写保护。设置后,Ring 0的代码也不能写入只读页面。这个特性对于操作系统非常重要——COW(写时复制)机制依赖它。当fork()创建子进程时,内核将父子进程的页面都标记为只读。如果没有WP位,内核在代替用户进程写入这些页面时不会触发页错误,COW就无法正常工作。Linux和Windows都会设置WP位。
AM(Alignment Mask,位18):对齐掩码。配合EFLAGS的AC位启用对齐检查。
NW(Not Write-through,位29)和CD(Cache Disable,位30):控制处理器缓存的行为。NW控制写穿透,CD禁用缓存。操作系统在某些特殊情况下(如访问MMIO设备寄存器)需要禁用缓存。
PG(Paging,位31):分页使能。设置这个位启用分页机制。分页是虚拟内存的基础。PE和PG的组合决定了处理器的地址转换模式——PE=0是实模式(无分页),PE=1且PG=0是保护模式但不分页(直接使用线性地址作为物理地址),PE=1且PG=1是带分页的保护模式(线性地址通过页表转换为物理地址)。
CR2 存储最近一次页错误(#PF)的线性地址。当发生缺页异常时,操作系统的缺页处理程序读取CR2来获知是哪个地址引起了异常。
CR3 存储页目录(或PML4表)的物理地址。操作系统通过修改CR3来切换地址空间——每个进程有自己的页表结构,进程切换时加载新进程的CR3就完成了地址空间的切换。CR3还控制着页面级别的缓存行为(PCD和PWT位),但在启用了PAT(页面属性表)后,这些位的作用被PAT取代了。
CR3的更新会使整个TLB失效。这是上下文切换开销的主要来源之一——TLB失效后,后续的内存访问都需要遍历页表来解析地址,直到TLB被重新填充。为了减少TLB失效的影响,Linux在较新的版本中引入了PCID(Process Context Identifier)支持——CR3的低12位可以存储一个PCID,TLB条目带有PCID标签,切换CR3时不必刷新所有TLB条目,只有PCID不匹配的条目才会被视为无效。这对频繁进行上下文切换的工作负载有明显的性能提升。Intel在2017年修复Meltdown漏洞时引入的KPTI(Kernel Page Table Isolation)补丁严重依赖PCID来减轻性能损失——KPTI导致每次系统调用和中断都需要切换页表,如果没有PCID,TLB会被频繁刷新,性能损失可能高达30%以上。
CR4 控制各种扩展特性的开关。
VME(位0):虚拟8086模式扩展。
PVI(位1):保护模式虚拟中断。
TSD(位2):时间戳禁止。设置后RDTSC指令只能在Ring 0执行。
DE(位3):调试扩展。
PSE(位4):页面大小扩展。启用后32位模式可以使用4MB大页。
PAE(位5):物理地址扩展。启用三级页表和64位页表项,支持超过4GB的物理内存寻址。进入IA-32e模式前必须先启用PAE。
MCE(位6):机器检查异常使能。
PGE(位7):页面全局使能。允许页表项中设置Global标志,标记为Global的页面在CR3更新时不会从TLB中被刷新。内核页面通常被标记为Global,因为所有进程共享相同的内核地址映射,切换进程时没有必要刷新内核页面的TLB条目。
PCE(位8):性能计数器使能。允许Ring 3执行RDPMC指令。
OSFXSR(位9):操作系统对FXSAVE/FXRSTOR的支持。设置后允许使用SSE指令。
OSXMMEXCPT(位10):操作系统对SIMD浮点异常的支持。
UMIP(位11):用户模式指令预防。设置后Ring 3不能执行SGDT、SIDT、SLDT、SMSW、STR等指令。这是一个安全特性——这些指令可以泄露内核数据结构的地址信息,ASLR(地址空间布局随机化)的效果会被削弱。
SMEP(位20):管理模式执行保护。设置后Ring 0的代码不能执行用户页面(U/S=1)上的代码。这防止了一类攻击——攻击者在用户空间放置恶意代码,然后通过内核漏洞跳转到用户空间执行。Linux从3.0版本开始支持SMEP。
SMAP(位21):管理模式访问保护。设置后Ring 0的代码不能读写用户页面。这比SMEP更严格——即使是数据访问也被禁止。内核需要访问用户空间数据时,必须使用特定的函数(如Linux的copy_from_user/copy_to_user),这些函数内部临时清除EFLAGS中的AC位来允许访问。
CR8(64位模式专用):任务优先级寄存器(TPR),控制外部中断的优先级阈值。只有优先级高于CR8设定值的中断才会被接受。这在使用APIC时取代了对8259A中断屏蔽寄存器的操作。
7.1.6 MSR寄存器组详解
MSR(Model Specific Registers,模型特定寄存器)是一组通过专用指令RDMSR和WRMSR访问的寄存器。之所以叫"模型特定",是因为不同型号的处理器可能有不同的MSR集合。但随着时间推移,很多MSR已经变成了事实上的标准——Intel和AMD的处理器都支持。
RDMSR和WRMSR是特权指令(只能在Ring 0执行)。RDMSR将ECX中指定的MSR的值读入EDX:EAX(高32位在EDX,低32位在EAX),WRMSR将EDX:EAX的值写入ECX指定的MSR。
c
static inline unsigned long read_msr(unsigned int msr)
{
unsigned int low, high;
asm volatile (“rdmsr” : “=a”(low), “=d”(high) : “c”(msr));
return ((unsigned long)high << 32) | low;
}
static inline void write_msr(unsigned int msr, unsigned long value)
{
unsigned int low = value & 0xFFFFFFFF;
unsigned int high = value >> 32;
asm volatile (“wrmsr” : : “c”(msr), “a”(low), “d”(high));
}
以下是几个对操作系统开发最重要的MSR:
IA32_EFER(MSR地址0xC0000080):Extended Feature Enable Register,扩展功能使能寄存器。这是进入IA-32e模式的关键MSR。
SCE(位0):SYSCALL/SYSRET使能。设置后可以使用SYSCALL和SYSRET指令来进行系统调用。
LME(位8):长模式使能。设置这个位(配合CR0.PG=1和CR4.PAE=1)激活IA-32e长模式。
LMA(位10):长模式活跃(只读)。当长模式实际生效时,处理器设置这个位。
NXE(位11):NX位使能。设置后页表项中的第63位成为NX(No-Execute)位,可以禁止某些页面上的代码执行。
IA32_STAR(MSR地址0xC0000081)、IA32_LSTAR(0xC0000082)、IA32_FMASK(0xC0000084):这三个MSR用于配置SYSCALL/SYSRET快速系统调用机制。
SYSCALL是AMD率先引入的系统调用指令,比INT 0x80(Linux传统的系统调用方式)和SYSENTER(Intel的方案)更高效。SYSCALL不通过IDT查找处理程序,也不压栈保存上下文,而是直接将RIP保存到RCX,将RFLAGS保存到R11,然后从LSTAR寄存器加载新的RIP(系统调用入口地址),从STAR寄存器加载新的CS和SS。FMASK寄存器指定在进入系统调用时需要清除的RFLAGS位(通常清除IF和TF)。
c
void setup_syscall(void)
{
// STAR: [63:48]=用户代码段选择子基址, [47:32]=内核代码段选择子
// 内核CS=0x08, 内核SS=0x10 (STAR[47:32]=0x0008会自动加偏移得到)
// 用户CS会从STAR[63:48]+16得到, 用户SS从STAR[63:48]+8得到
unsigned long star = ((unsigned long)0x0023 << 48) | ((unsigned long)0x0008 << 32);
write_msr(0xC0000081, star);
// LSTAR: SYSCALL入口地址
write_msr(0xC0000082, (unsigned long)syscall_entry);
// FMASK: 进入SYSCALL时清除的RFLAGS位
write_msr(0xC0000084, 0x200); // 清除IF位
// 启用SYSCALL/SYSRET
unsigned long efer = read_msr(0xC0000080);
efer |= 1; // SCE位
write_msr(0xC0000080, efer);
}
Linux内核在64位模式下使用SYSCALL作为系统调用的主要入口方式。glibc中的系统调用包装函数会执行SYSCALL指令,系统调用号放在RAX中,参数依次放在RDI、RSI、RDX、R10、R8、R9中。内核的系统调用入口函数(entry_SYSCALL_64)保存用户态的上下文,根据系统调用号在系统调用表中查找对应的处理函数并调用,完成后通过SYSRET返回用户态。SYSRET从RCX恢复RIP,从R11恢复RFLAGS,切换回用户态CS和SS。
IA32_FS_BASE(0xC0000100) 和 IA32_GS_BASE(0xC0000101):在64位模式下,FS和GS段寄存器的基地址不再从GDT/LDT中的描述符获取,而是直接从这两个MSR读取。这为线程本地存储(Thread Local Storage,TLS)提供了硬件支持。
Linux在64位模式下使用FS段寄存器来实现用户态的线程本地存储——每个线程有自己的FS基地址,指向该线程的TLS区域。glibc中的 __thread 变量和pthread_getspecific()函数都依赖FS段寄存器。GS段寄存器在内核态使用,指向每CPU数据(per-CPU data),让内核可以快速访问当前CPU私有的数据结构而不需要加锁。
Windows使用GS段寄存器来访问TEB(Thread Environment Block),GS:[0x30]指向TEB的指针。内核态使用GS段访问KPCR(Kernel Processor Control Region),这是Windows内核的每CPU数据结构。
IA32_KERNEL_GS_BASE(0xC0000102):配合SWAPGS指令使用。SWAPGS交换GS_BASE和KERNEL_GS_BASE的值。在系统调用和中断入口处,SWAPGS从用户态的GS基地址切换到内核态的GS基地址;在返回用户态前,再次SWAPGS切换回来。
IA32_APIC_BASE(0x1B):控制本地APIC的基地址和使能状态。
IA32_PAT(0x277):页面属性表,定义了8种内存类型,页表项可以通过PAT、PCD、PWT三个位的组合来选择其中一种。常见的内存类型包括Write-Back(WB,正常缓存)、Write-Through(WT,写穿透)、Uncacheable(UC,不缓存,用于MMIO)、Write-Combining(WC,写合并,用于显存)等。
7.2 地址空间体系
7.2.1 虚拟地址的本质
虚拟地址(Virtual Address),也叫线性地址(Linear Address),是程序在运行时使用的地址。当你在C语言中对一个指针取值(dereference),指针中存储的就是虚拟地址。当编译器生成代码时,所有的跳转目标、全局变量地址、函数地址,都是虚拟地址。
虚拟地址最大的好处是"解耦"——程序不需要知道自己被加载到物理内存的哪个位置,只需要使用虚拟地址,由处理器的内存管理单元(MMU)和操作系统维护的页表来完成虚拟地址到物理地址的翻译。这带来了几个关键优势:
第一,进程隔离。每个进程拥有独立的虚拟地址空间,进程A的虚拟地址0x400000和进程B的虚拟地址0x400000对应不同的物理内存。进程之间互相看不到对方的内存内容,一个进程的bug(比如野指针)不会破坏其他进程的数据。
第二,内存超配。操作系统可以分配给进程比实际物理内存更多的虚拟内存。不是所有分配的虚拟内存都需要立即对应物理内存——只有真正被访问到的虚拟页面才需要物理页面。操作系统还可以把暂时不用的物理页面"交换"到磁盘上(swap),腾出物理内存给更紧急的需求。
第三,灵活的内存布局。链接器可以把所有程序都链接到相同的虚拟地址(比如Linux的可执行文件默认从0x400000开始),不用担心地址冲突。共享库可以被映射到不同进程的不同虚拟地址上,但底层共享同一份物理内存。
在64位模式下,虚拟地址理论上是64位的,可以寻址16EB的空间。但目前的处理器实际上只使用了48位(Intel后来扩展到57位,叫5-level paging)。48位虚拟地址可以寻址256TB的空间,对目前的应用来说完全够用了。
48位虚拟地址有一个特殊的规则——“规范地址”(Canonical Address)。虚拟地址的第47位必须被"符号扩展"到第48到63位。也就是说,如果第47位是0,则第48到63位也必须全是0;如果第47位是1,则第48到63位也必须全是1。这导致虚拟地址空间被分成两半:
低半部分: 0x0000000000000000 到 0x00007FFFFFFFFFFF (128TB)
(用户空间)
中间空洞: 0x0000800000000000 到 0xFFFF7FFFFFFFFFFF
(非规范地址,不可使用)
高半部分: 0xFFFF800000000000 到 0xFFFFFFFFFFFFFFFF (128TB)
(内核空间)
大多数64位操作系统把低半部分分配给用户空间,高半部分分配给内核空间。Linux的虚拟地址空间布局在文档Documentation/x86/x86_64/mm.txt中有详细描述——用户空间占据低128TB,内核空间的高128TB又被细分为多个区域:直接映射区(将所有物理内存线性映射到虚拟地址空间中)、vmalloc区、vmemmap区等。
Windows 64位的默认虚拟地址空间布局是:用户空间从0到0x7FFFFFFFFFFF(128TB),内核空间从0xFFFF800000000000开始。Windows还允许应用程序通过IMAGE_FILE_LARGE_ADDRESS_AWARE标志来声明自己支持大地址空间。
7.2.2 物理地址的含义
物理地址是内存总线上的真实地址,直接对应物理内存芯片中的存储单元。当CPU的MMU完成虚拟地址到物理地址的翻译后,物理地址被送到内存控制器,内存控制器根据物理地址访问对应的DRAM芯片。
在现代系统中,物理地址空间不仅仅用于DRAM。很多设备将自己的寄存器或缓冲区映射到物理地址空间中,这叫做MMIO(Memory-Mapped I/O,内存映射I/O)。比如PCI Express设备的配置空间和BAR(Base Address Register)空间、APIC寄存器、显存等,都占据了物理地址空间中的特定区域。
64位处理器的物理地址空间通常是52位宽(4PB),但实际实现可能只有40位(1TB)或46位(64TB),取决于具体的处理器型号。CPUID叶号0x80000008的EAX寄存器返回处理器支持的物理地址宽度和虚拟地址宽度。
物理内存的分布不是连续的——低端640KB是传统的可用内存,640KB到1MB是"内存空洞"(legacy hole),1MB以上是高端内存,在4GB附近可能有MMIO区域占据的空洞。如果物理内存大于4GB,超出的部分位于4GB以上的物理地址空间中。操作系统需要通过BIOS提供的E820内存映射来了解物理内存的实际布局。
7.3 实模式运行环境
7.3.1 实模式工作原理总述
实模式是x86处理器最古老的运行模式。它直接继承了8086处理器的架构——16位寄存器、20位地址总线、最多1MB的寻址能力。虽然现代处理器的能力已经远远超越了8086,但在实模式下,处理器会自我限制,只展现出8086级别的能力。
在实模式下,处理器没有任何内存保护机制。所有代码都运行在同一个特权级,任何指令都可以被任何程序执行——包括CLI(禁止中断)、IN/OUT(I/O操作)、修改段寄存器等在保护模式下属于特权操作的指令。任何程序都可以读写任何内存地址,包括中断向量表(位于物理地址0x00000到0x003FF)和BIOS数据区(位于物理地址0x00400到0x004FF)。
这种完全不设防的设计在8086的年代并不是问题——那时候计算机只运行一个程序,不需要隔离。但到了多任务操作系统时代,缺乏硬件保护就成了致命缺陷。DOS时代的程序经常因为野指针破坏了中断向量表或BIOS数据区而导致系统崩溃,只能重启。早期的Windows(1.0到3.0)也运行在实模式下,稳定性很差。
尽管如此,实模式在系统启动阶段仍然是不可替代的。BIOS中断服务(INT 10h显示、INT 13h磁盘、INT 15h内存检测等)只能在实模式下调用。操作系统的Bootloader必须在实模式下完成必要的硬件信息收集工作,然后再切换到保护模式或IA-32e模式。
7.3.2 实模式下的段寻址机制
实模式的内存寻址采用"段:偏移"的方式。一个逻辑地址由16位的段基址和16位的偏移量组成,物理地址的计算公式是:
物理地址 = 段基址 × 16 + 偏移量
段基址存储在段寄存器(CS、DS、ES、SS、FS、GS)中,偏移量来自指令的寻址模式(寄存器间接、基址+变址等)。由于段基址左移4位(乘以16)后最大为0xFFFF0,加上偏移量最大0xFFFF,所以理论上能够访问的最高物理地址是0xFFFF0 + 0xFFFF = 0x10FFEF,略超过1MB。这多出来的将近64KB的地址空间叫做HMA(High Memory Area),在真正的8086上因为只有20根地址线,地址会回绕到0,但在286及以后的处理器上可以通过A20地址线来访问这个区域。DOS的HIMEM.SYS驱动就是用来管理HMA的。
段寻址机制有一个容易让人困惑的特性——同一个物理地址可以用多种不同的段:偏移组合来表示。比如物理地址0x007C00(BIOS加载引导扇区的位置)可以表示为0x0000:0x7C00,也可以表示为0x07C0:0x0000。这种多对一的映射在编程时需要特别注意。
在实模式下编写程序时,如果数据量超过64KB(一个段的最大大小),就需要手动切换段寄存器来访问不同的段。这在当年是非常繁琐的——C编译器为此引入了"内存模型"的概念:small模型所有指针都是16位的near指针,large模型所有指针都是32位的far指针(包含段和偏移),huge模型支持超过64KB的数组。Turbo C和Microsoft C编译器的使用者对这些内存模型一定不陌生。
7.3.3 实模式的中断向量表结构
实模式下的中断和异常通过IVT(Interrupt Vector Table,中断向量表)来分发。IVT位于物理内存的最低端,从地址0x00000开始,共256个向量条目,每个条目4字节(2字节段值 + 2字节偏移),总共1KB。
apache
地址 内容
0x0000:0x0000 向量0的偏移量(2字节)
0x0000:0x0002 向量0的段基址(2字节)
0x0000:0x0004 向量1的偏移量(2字节)
0x0000:0x0006 向量1的段基址(2字节)
…
0x0000:0x03FC 向量255的偏移量(2字节)
0x0000:0x03FE 向量255的段基址(2字节)
当中断或异常发生时,处理器:
将RFLAGS(实际上是FLAGS,因为是16位模式)压栈
清除IF和TF标志
将CS和IP压栈
从IVT中读取对应向量的段:偏移,加载到CS:IP
开始执行中断处理程序
中断处理程序执行完毕后,通过IRET指令返回——IRET从栈上弹出IP、CS和FLAGS,恢复到中断前的执行状态。
IVT中最重要的一些向量包括:
向量0x00:除法错误
向量0x08:IRQ 0(定时器,BIOS默认映射)
向量0x09:IRQ 1(键盘)
向量0x10:BIOS视频服务
向量0x13:BIOS磁盘服务
向量0x15:BIOS扩展内存服务
向量0x16:BIOS键盘服务
向量0x19:BIOS引导加载
向量0x1A:BIOS时间服务
在实模式下修改IVT是非常简单的——直接向对应的内存地址写入新的段:偏移值就行了。DOS时代的很多程序通过修改IVT来hook系统中断,实现TSR(Terminate and Stay Resident,驻留程序)功能。这也是早期计算机病毒的常用手法——通过修改INT 13h或INT 21h的向量来截获磁盘读写和文件操作。
7.4 保护模式运行环境
7.4.1 保护模式工作原理总述
保护模式是Intel 80386引入的里程碑式的运行模式。从操作系统的角度来看,保护模式提供了三项革命性的硬件支持:分段保护机制、分页虚拟内存机制和特权级保护机制。
特权级机制 将CPU的执行环境分为4个特权级(Ring 0到Ring 3)。Ring 0权限最高(内核态),Ring 3权限最低(用户态)。Ring 1和Ring 2是中间级别,最初设计是给设备驱动和系统服务使用的,但实际上几乎所有主流操作系统都只使用Ring 0(内核)和Ring 3(用户程序),跳过了Ring 1和Ring 2。OS/2是少数使用Ring 2的操作系统之一。
特权级检查发生在多个场合:
代码段间的跳转:不能直接从Ring 3跳转到Ring 0的代码段,必须通过调用门(Call Gate)、中断门或陷阱门。
数据段的访问:当前特权级(CPL)必须数值上小于等于(即权限更高或相同)数据段的DPL,否则产生#GP异常。
特权指令的执行:HLT、LGDT、LIDT、MOV CRn、WRMSR等指令只能在Ring 0执行。
I/O操作:IN、OUT、CLI、STI等指令受IOPL控制。
这种硬件级别的保护确保了操作系统内核的安全——用户程序无法绕过操作系统直接操作硬件,无法修改其他进程的内存,也无法访问内核数据。任何违规操作都会被处理器检测到并产生异常,操作系统可以据此采取行动(终止违规进程、记录日志等)。
进入保护模式的步骤:
准备GDT(全局描述符表),定义至少一个代码段和一个数据段
用LGDT指令加载GDT
设置CR0的PE位
执行一个远跳转(Far Jump)来加载新的CS段选择子
远跳转是必须的——设置PE位后,处理器虽然进入了保护模式,但CS寄存器中仍然是实模式的段值。远跳转加载新的CS(保护模式的段选择子),处理器才能正确地按照保护模式的规则取指令。如果不做远跳转,处理器会试图用实模式的段值来访问GDT,导致不可预测的行为。
nasm
; 进入保护模式
cli ; 禁止中断
lgdt [gdt_descriptor] ; 加载GDT
mov eax, cr0
or eax, 1 ; 设置PE位
mov cr0, eax
jmp 0x08:protected_mode_entry ; 远跳转,0x08是GDT中代码段的选择子
[bits 32]
protected_mode_entry:
mov ax, 0x10 ; 0x10是数据段的选择子
mov ds, ax
mov es, ax
mov ss, ax
mov fs, ax
mov gs, ax
mov esp, 0x90000 ; 设置栈指针
; 现在处于32位保护模式
7.4.2 保护模式下的段管理体系
保护模式的段机制比实模式复杂得多。在实模式下,段寄存器直接包含段的基地址(左移4位)。在保护模式下,段寄存器中存放的是段选择子(Segment Selector),它是GDT或LDT中段描述符的索引,段的基地址、大小和属性都在段描述符中定义。
段选择子 是16位的值,格式如下:
位 15-3: 描述符表索引(GDT或LDT中的第几个描述符)
位 2: TI(Table Indicator):0=GDT, 1=LDT
位 1-0: RPL(Requested Privilege Level):请求特权级
比如选择子0x08的二进制是00000000 00001000,索引=1,TI=0(GDT),RPL=0。这指向GDT中的第1个描述符(第0个是空描述符)。
段描述符 是8字节的数据结构,定义了一个段的所有属性。32位保护模式下段描述符的格式比较复杂,各字段分散在8个字节的不同位置(这是为了与286的描述符格式兼容):
apache
字节 0-1: 段限长 (位0-15)
字节 2-4: 段基地址 (位0-23)
字节 5: 访问权限字节
位 7: P (Present) - 段是否在内存中
位 6-5: DPL - 段的特权级
位 4: S - 系统段(0)还是代码/数据段(1)
位 3-0: 类型
字节 6:
位 7: G (Granularity) - 粒度,0=字节,1=4KB页
位 6: D/B - 默认操作数大小,0=16位,1=32位
位 5: L - 64位代码段标志(IA-32e模式)
位 4: AVL - 供系统软件使用
位 3-0: 段限长 (位16-19)
字节 7: 段基地址 (位24-31)
类型字段的含义取决于S位的值。当S=1(代码/数据段)时:
对于数据段:
位0: A (Accessed) - 段是否被访问过
位1: W (Writable) - 是否可写
位2: E (Expand-Down) - 向下扩展(用于栈段)
位3: 0(表示数据段)
对于代码段:
位0: A (Accessed) - 段是否被访问过
位1: R (Readable) - 代码段是否可读
位2: C (Conforming) - 一致代码段
位3: 1(表示代码段)
当S=0时,描述符定义的是系统段——TSS描述符、LDT描述符、调用门、中断门、陷阱门等。
GDT(全局描述符表) 是保护模式下最核心的数据结构之一。它是一个描述符数组,存放在内存中的任意位置,通过LGDT指令告知处理器GDT的位置和大小。GDT的第0个条目必须是空描述符(全零),如果段选择子指向第0个条目,处理器会产生#GP异常(除了加载到DS/ES/FS/GS时不会立即异常,但尝试通过它访问内存时会异常)。
一个典型的GDT设置:
apache
索引0: 空描述符
索引1: 内核代码段 (基址=0, 限长=4GB, DPL=0, 可执行/可读)
索引2: 内核数据段 (基址=0, 限长=4GB, DPL=0, 可读写)
索引3: 用户代码段 (基址=0, 限长=4GB, DPL=3, 可执行/可读)
索引4: 用户数据段 (基址=0, 限长=4GB, DPL=3, 可读写)
索引5: TSS描述符
注意内核段和用户段的基地址和限长都是一样的(覆盖整个4GB地址空间),区别只在DPL。这就是所谓的"平坦模型"(Flat Model)——段机制虽然存在,但不用它来做内存隔离(内存隔离交给分页机制),段机制只用来区分代码段和数据段以及实现特权级保护。Linux、Windows、macOS等现代操作系统在32位模式下都使用平坦模型。
LDT(局部描述符表) 是另一种描述符表,可以为每个进程定义私有的段。Linux早期版本曾经使用LDT来实现进程隔离,但后来改用分页机制后,LDT基本不再使用了。不过Linux仍然保留了对LDT的支持——某些使用16位或段式内存模型的旧程序(如Wine运行的Windows程序)可能需要LDT。
TSS(任务状态段) 在保护模式下有两个重要用途。第一,存储不同特权级的栈指针——当从Ring 3切换到Ring 0时(比如系统调用或中断),处理器需要切换到内核栈,Ring 0的栈指针(SS0和ESP0)就存储在TSS中。第二,存储I/O Permission Bitmap,控制Ring 3程序能够访问哪些I/O端口。
在32位保护模式下,TSS还可以用于硬件任务切换——处理器可以自动保存和恢复TSS中的所有寄存器来实现任务切换。但这个功能由于性能不佳(比软件任务切换慢),几乎没有操作系统使用。Linux唯一使用TSS的地方就是存储Ring 0的栈指针和I/O Bitmap。
7.4.3 保护模式下的中断与异常处理体系
保护模式使用IDT(Interrupt Descriptor Table,中断描述符表)来替代实模式的IVT。IDT的功能与IVT类似——根据中断/异常的向量号找到处理程序入口——但IDT提供了更丰富的控制能力。
IDT中可以包含三种类型的门描述符:
中断门(Interrupt Gate):当CPU通过中断门进入处理程序时,自动清除EFLAGS中的IF标志(禁止外部中断)。中断处理完毕后,IRET指令恢复原来的EFLAGS,IF也随之恢复。
陷阱门(Trap Gate):与中断门的区别仅在于进入处理程序时不清除IF标志。外部中断仍然可以打断陷阱门处理程序的执行。陷阱门通常用于调试异常(#DB)和断点异常(#BP),因为调试器需要在异常处理程序中正常响应其他中断。
任务门(Task Gate):通过TSS进行硬件任务切换。进入时自动切换到任务门指定的TSS所描述的任务环境。主要用于处理Double Fault等严重异常——当处理一个异常时当前栈可能已经不可用了,通过任务门切换到一个已知良好的栈可以避免Triple Fault。但在64位模式下,任务门被IST机制取代。
32位保护模式下IDT描述符的格式(8字节):
apache
字节 0-1: 处理程序偏移量 (位0-15)
字节 2-3: 段选择子
字节 4: 保留(必须为0)
字节 5: 类型和属性
位 7: P (Present)
位 6-5: DPL
位 4: 0 (固定)
位 3-0: 类型 (0x5=任务门, 0xE=中断门, 0xF=陷阱门)
字节 6-7: 处理程序偏移量 (位16-31)
当异常发生时,处理器的行为取决于异常发生时的特权级和目标代码段的特权级:
如果特权级没有变化(比如内核态代码本身触发了异常),处理器在当前栈上按以下顺序压入:
subunit
[高地址]
EFLAGS
CS
EIP
Error Code (如果有的话)
[低地址, ESP指向这里]
如果特权级发生了变化(比如从Ring 3到Ring 0),处理器还会额外保存用户态的栈指针,并切换到Ring 0的栈:
subunit
[高地址]
SS (用户态的)
ESP (用户态的)
EFLAGS
CS
EIP
Error Code (如果有的话)
[低地址, ESP指向这里]
Ring 0的栈指针从当前TSS的SS0和ESP0字段获取。这就是为什么每个进程/线程的TSS(或者更准确地说,TSS中的ESP0)需要在上下文切换时被更新——新进程需要有自己的内核栈。
并非所有异常都有错误码。有错误码的异常包括:#DF(8)、#TS(10)、#NP(11)、#SS(12)、#GP(13)、#PF(14)、#AC(17)。错误码的格式因异常类型而异——#PF的错误码包含了导致页错误的原因(是读还是写、是用户态还是内核态、页面是否存在等),#GP的错误码通常是导致故障的段选择子。
7.4.4 保护模式下的页面管理体系
分页机制是虚拟内存的硬件基础。在32位保护模式下,不启用PAE时使用两级页表结构:
虚拟地址(32位)的分解:
[位 31-22] 页目录索引(10位,1024个条目)
[位 21-12] 页表索引(10位,1024个条目)
[位 11-0] 页内偏移(12位,4KB页面)
页目录(Page Directory) 占一个4KB页面,包含1024个4字节的条目(PDE,Page Directory Entry)。每个PDE指向一个页表,或者直接映射一个4MB大页(当PDE的PS位被设置时)。
页表(Page Table) 也占一个4KB页面,包含1024个4字节的条目(PTE,Page Table Entry)。每个PTE映射一个4KB的物理页面。
页目录条目(PDE)和页表条目(PTE)的格式:
yaml
位 31-12: 物理页帧号(页面对齐的物理地址的高20位)
位 11-9: AVL(供操作系统使用,处理器不关心)
位 8: G(全局页面,PTE有效,PDE中为PS时表示大页)
位 7: PAT / PS(PTE中是PAT位,PDE中是Page Size位)
位 6: D(Dirty,脏页标志,只对PTE有效)
位 5: A(Accessed,已访问标志)
位 4: PCD(Page Cache Disable)
位 3: PWT(Page Write-Through)
位 2: U/S(User/Supervisor,0=内核页,1=用户页)
位 1: R/W(Read/Write,0=只读,1=可读写)
位 0: P(Present,0=页面不在内存中,1=在内存中)
几个关键位的含义值得展开说明:
P(Present)位 是分页机制中最重要的位。当P=0时,处理器在访问这个虚拟地址时会触发#PF(缺页异常)。操作系统利用这个机制实现了多种强大的功能:
按需分配(Demand Paging):分配虚拟内存时只修改内核数据结构,不分配物理页面也不设置页表映射(或者设置P=0)。当程序首次访问这个虚拟地址时触发缺页异常,异常处理程序分配物理页面、建立映射、设置P=1,然后返回让程序重试。这样做的好处是只有真正被使用的虚拟内存才会消耗物理内存。
交换(Swapping):当物理内存不足时,操作系统选择一些暂时不活跃的页面,将其内容写入磁盘上的交换分区(或交换文件),然后清除P位释放物理页面。当程序再次访问这些被交换出去的页面时,触发缺页异常,操作系统从磁盘读回页面内容,重新分配物理页面并建立映射。Windows的"虚拟内存"设置(pagefile.sys)就是配置交换文件的。Linux的swap分区或swap文件也是这个功能。当P=0时,页表条目的其他位可以被操作系统自由使用——通常用来存储页面在磁盘上的位置信息。
COW(写时复制):fork()创建子进程时,父子进程共享相同的物理页面,但页表条目被设置为只读(R/W=0)。当任何一方尝试写入时,触发缺页异常(写保护异常),操作系统分配一个新的物理页面,复制内容,然后让写入方使用新页面。这避免了fork()时复制整个地址空间的开销。
D(Dirty)位 由处理器在页面被写入时自动设置。操作系统利用这个位来判断一个页面是否被修改过——如果要将一个"干净的"页面交换到磁盘上,不需要实际写入(因为磁盘上已经有相同的内容),只有"脏"的页面才需要写回。
A(Accessed)位 由处理器在页面被访问(读或写)时自动设置。操作系统的页面置换算法利用这个位来近似判断页面的活跃程度。经典的"时钟算法"(Clock Algorithm)就是定期扫描所有页面的A位——A=1的页面被认为是活跃的(清除A位后继续),A=0的页面被认为是不活跃的,可以被选为置换目标。Linux使用了更复杂的LRU(Least Recently Used)链表来管理页面的活跃程度,但底层仍然依赖A位。
U/S位 区分用户页面和内核页面。当CPL=3(用户态)时,只能访问U/S=1的页面。如果用户态代码尝试访问U/S=0的页面,触发#PF异常。这是内核空间和用户空间隔离的硬件基础。
启用PAE后 的页表结构变为三级:
虚拟地址(32位)的分解(PAE模式):
[位 31-30] PDPT索引(2位,4个条目)
[位 29-21] 页目录索引(9位,512个条目)
[位 20-12] 页表索引(9位,512个条目)
[位 11-0] 页内偏移(12位,4KB页面)
PAE模式下页表条目扩展到8字节(64位),物理地址字段从20位增加到最多40位(1TB),这使得32位操作系统也可以使用超过4GB的物理内存。但注意,单个进程的虚拟地址空间仍然是4GB,因为虚拟地址仍然是32位的。超过4GB的物理内存通过内核的高端内存映射机制来访问。32位Linux的ZONE_HIGHMEM和Windows的PAE模式就是利用这个特性。
7.4.5 保护模式的地址翻译全过程
一个完整的地址翻译过程包含两个阶段:分段阶段和分页阶段。
第一阶段:逻辑地址到线性地址(分段)
逻辑地址由段选择子和偏移量组成。处理器根据段选择子在GDT(或LDT)中查找段描述符,获取段的基地址,然后将基地址加上偏移量,得到线性地址。
线性地址 = 段基地址 + 偏移量
处理器在这个阶段还会检查:
段选择子是否有效(索引不超过GDT/LDT的限长)
段描述符是否存在(P位)
访问权限是否允许(DPL检查)
偏移量是否在段限长范围内
对于代码段,是否试图写入(代码段不可写)
任何一项检查失败都会产生异常(#GP或#NP)。
在平坦模型下,所有段的基地址都是0,所以线性地址等于偏移量。分段阶段的地址计算实际上是"透明的"——只有特权级检查仍然有效。
第二阶段:线性地址到物理地址(分页)
如果分页被启用(CR0.PG=1),线性地址需要通过页表转换为物理地址。
以不启用PAE的两级页表为例:
处理器从CR3获取页目录的物理地址
用线性地址的高10位(位31-22)作为索引,从页目录中读取PDE
检查PDE的P位:如果P=0,触发#PF
如果PDE的PS=1(4MB大页),直接用PDE中的物理帧号加上线性地址的低22位得到物理地址
如果PDE的PS=0,用PDE中的物理帧号作为页表的基地址
用线性地址的中间10位(位21-12)作为索引,从页表中读取PTE
检查PTE的P位:如果P=0,触发#PF
用PTE中的物理帧号加上线性地址的低12位(页内偏移)得到物理地址
这个过程涉及两次内存访问(读PDE和读PTE),加上最终的数据访问,一共三次内存访问。每次内存访问大约需要几十到几百个CPU周期(取决于是否命中缓存),这个开销是不可忽视的。
TLB(Translation Lookaside Buffer) 是处理器内部的一个高速缓存,专门用来缓存最近使用的虚拟地址到物理地址的映射。当处理器需要翻译一个虚拟地址时,先在TLB中查找——如果命中(TLB hit),直接获得物理地址,不需要遍历页表;如果未命中(TLB miss),才进行页表遍历,完成后将结果存入TLB供后续使用。
TLB的命中率通常非常高(超过99%),因为程序的内存访问具有很强的局部性——短时间内访问的虚拟地址集中在少数几个页面中。但TLB的容量有限(通常几十到几千个条目),而且修改页表后需要使相关的TLB条目失效(通过INVLPG指令使单个条目失效,或者通过重新加载CR3使所有条目失效)。
7.5 IA-32e长模式运行环境
7.5.1 IA-32e模式工作原理总述
IA-32e模式(也叫长模式,Long Mode)是x86架构的64位扩展。它由AMD在K8系列处理器(Athlon 64)中首先实现,后来Intel也加入了支持(称为Intel 64或EM64T)。IA-32e模式是现代64位操作系统运行的基础。
IA-32e模式包含两个子模式:
64位模式 是完全的64位执行环境。在这个模式下:
通用寄存器扩展到64位,新增R8到R15
默认地址大小为64位,默认操作数大小为32位(可通过REX前缀选择64位操作数)
支持48位(或57位)虚拟地址和最多52位物理地址
分段机制被大幅简化
强制使用分页
引入了新的SYSCALL/SYSRET快速系统调用机制
引入了RIP相对寻址模式
兼容模式 允许在64位操作系统中运行32位(和16位)代码。兼容模式下代码段的L位(Long mode)为0、D位为1,处理器的行为与32位保护模式几乎完全相同——32位地址、32位操作数、可使用的寄存器与32位模式一样。但操作系统内核仍然运行在64位模式下,中断和异常的处理也按照64位模式的规则进行。
从32位保护模式切换到IA-32e模式的完整步骤:
apache
- 禁止中断 (CLI)
- 启用PAE (设置CR4.PAE=1)
- 建立IA-32e模式的页表结构(四级页表)
- 将PML4表的物理地址加载到CR3
- 通过WRMSR设置IA32_EFER.LME=1 (启用长模式)
- 设置CR0.PG=1 (启用分页)
—— 此时处理器进入IA-32e模式的兼容子模式 - 执行远跳转,加载L=1的64位代码段选择子
—— 此时正式进入64位模式
步骤6和7之间有一个微妙的中间状态。设置CR0.PG后,长模式被激活(IA32_EFER.LMA自动变为1),但由于当前CS中加载的仍然是32位代码段描述符(L=0),处理器实际上处于兼容模式。只有执行远跳转加载了64位代码段描述符(L=1、D=0)后,处理器才真正进入64位模式。
一个在实际开发中很容易犯的错误是:在设置LME之前忘记启用PAE。长模式强制要求PAE分页,如果CR4.PAE=0时就设置CR0.PG,处理器不会进入长模式而是进入普通的32位分页保护模式。更糟糕的是,如果IA32_EFER.LME已经被设置但PAE没有启用,设置CR0.PG时处理器会产生#GP异常。
64位Windows的启动过程中,bootmgr和winload.efi(或winload.exe)负责完成从实模式(或UEFI环境)到64位模式的切换,然后加载ntoskrnl.exe。64位Linux的GRUB引导程序也有类似的模式切换代码。
7.5.2 IA-32e模式下的段管理体系
在IA-32e的64位模式下,分段机制被极大地简化了。这是x86架构发展中一个重要的设计决策——段机制曾经是x86内存管理的核心,但在实践中被证明过于复杂且不必要(分页机制已经能够提供所需的内存保护和隔离),所以在64位模式下被大幅裁减。
具体来说,64位模式下的段处理规则如下:
CS(代码段) 仍然有效,但方式与32位不同。CS描述符中的L位(位53)和D位(位54)决定了代码段的模式:L=1且D=0是64位模式,L=0且D=1是兼容模式(32位),L=0且D=0是兼容模式(16位)。CS描述符中的DPL仍然用于特权级检查。但CS的基地址被固定为0,不可修改。段限长也被忽略——64位代码可以访问整个虚拟地址空间。
DS、ES、SS 的基地址被强制为0,段限长检查也被禁用。这三个段寄存器在64位模式下对地址计算没有任何影响。但SS的DPL仍然用于确定当前特权级(CPL = SS.DPL),而且在进行栈切换时SS仍然需要被正确加载。
FS和GS 是例外——它们的基地址不固定为0,可以通过MSR寄存器(IA32_FS_BASE和IA32_GS_BASE)来设置。这使得FS和GS可以用于线程本地存储等需要基地址偏移的场景。处理器在计算FS/GS段的有效地址时,会将MSR中的基地址加上偏移量。
FS段有效地址 = IA32_FS_BASE + 偏移量
GS段有效地址 = IA32_GS_BASE + 偏移量
Linux在用户态使用FS段来访问线程本地存储(Thread Local Storage)。C语言中声明为 __thread 的变量实际上存储在FS段指向的区域中。glibc在创建线程时通过 arch_prctl(ARCH_SET_FS, addr) 系统调用来设置每个线程的FS基地址。
GS段在Linux内核中用于访问每CPU变量(per-CPU variables)。每个CPU核心有自己的GS基地址,指向该CPU私有的数据区域。这样内核可以通过GS段快速访问当前CPU的数据而不需要先查询CPU编号再做数组索引。在系统调用和中断入口处,SWAPGS指令交换GS_BASE和KERNEL_GS_BASE,实现用户态GS和内核态GS的快速切换。
Windows的使用方式略有不同——Windows在用户态使用GS段(而不是FS)来访问TEB(线程环境块),内核态使用GS段访问KPCR(内核处理器控制区域)。
尽管段机制在64位模式下被简化了,GDT仍然是必须的。处理器在进行特权级切换时需要从GDT中读取段描述符来验证特权级。TSS描述符也在GDT中,64位模式的TSS描述符扩展到了16字节(因为TSS的基地址可能超过4GB,需要更多的地址位)。
7.5.3 IA-32e模式下的中断与异常处理体系
64位模式下的IDT描述符扩展到了16字节,以容纳64位的处理程序入口地址:
apache
字节 0-1: 处理程序偏移量 (位0-15)
字节 2-3: 段选择子
字节 4: IST (低3位)
字节 5: 类型和属性
字节 6-7: 处理程序偏移量 (位16-31)
字节 8-11: 处理程序偏移量 (位32-63)
字节 12-15: 保留 (必须为0)
64位模式下不再支持任务门——只有中断门和陷阱门。任务门的功能被IST(Interrupt Stack Table)机制取代。
IST机制 是64位模式的一项重要改进。在32位模式下,如果异常发生时栈已经损坏(比如栈溢出导致ESP指向了无效地址),处理器尝试切换到Ring 0的栈时可能也会失败,导致Double Fault甚至Triple Fault。IST机制允许某些中断/异常使用完全独立的栈——TSS中定义了7个IST栈指针(IST1到IST7),IDT描述符中的IST字段指定使用哪个IST栈。当IST字段非零时,处理器无条件切换到对应的IST栈,不管当前栈的状态如何。
Linux为以下异常配置了IST栈:
Double Fault (#DF) 使用专用的IST栈,确保即使内核栈损坏也能正确处理双重故障
NMI 使用专用的IST栈,因为NMI可能在任何时候发生,包括在切换栈的过程中
Machine Check (#MC) 使用专用的IST栈
Debug (#DB) 使用专用的IST栈
64位模式下中断/异常发生时处理器的栈操作也有变化。无论特权级是否变化,处理器都会按以下顺序压栈:
subunit
[高地址]
SS (8字节, 64位模式下始终压入)
RSP (8字节)
RFLAGS (8字节)
CS (8字节)
RIP (8字节)
Error Code (8字节, 如果有的话)
[低地址]
注意在64位模式下,即使特权级没有变化,SS和RSP也会被压栈。这与32位模式不同(32位模式下只有特权级变化时才压入SS和ESP),统一的栈帧格式简化了中断处理程序的编写。
7.5.4 IA-32e模式下的页面管理体系
IA-32e模式使用四级页表结构(4-Level Paging),这是目前最常见的64位分页模式。Intel后来还引入了五级页表(5-Level Paging),将虚拟地址扩展到57位,但本节主要讨论四级页表。
四级页表的结构:
json
虚拟地址(48位有效)的分解:
[位 47-39] PML4索引(9位,512个条目)
[位 38-30] PDPT索引(9位,512个条目)
[位 29-21] PD索引(9位,512个条目)
[位 20-12] PT索引(9位,512个条目)
[位 11-0] 页内偏移(12位,4KB页面)
四级页表名称从高到低分别是:
PML4(Page Map Level 4):第四级页表,有512个条目。CR3指向PML4表。每个PML4条目覆盖512GB的虚拟地址空间。
PDPT(Page Directory Pointer Table):第三级页表,每个PML4条目指向一个PDPT。每个PDPT条目覆盖1GB的虚拟地址空间。如果PDPT条目设置了PS位,可以直接映射1GB大页。
PD(Page Directory):第二级页表,每个PDPT条目指向一个PD。每个PD条目覆盖2MB的虚拟地址空间。如果PD条目设置了PS位,可以直接映射2MB大页。
PT(Page Table):第一级页表,每个PD条目指向一个PT。每个PT条目映射一个4KB的物理页面。
每个页表占4KB(512个8字节条目),页表条目的格式(以PTE为例):
yaml
位 63: NX (No-Execute,需要IA32_EFER.NXE=1才有效)
位 62-52: 保留/供软件使用
位 51-12: 物理页帧号(M-1:12,其中M是物理地址宽度)
位 11-9: AVL (供操作系统使用)
位 8: G (Global)
位 7: PAT
位 6: D (Dirty)
位 5: A (Accessed)
位 4: PCD
位 3: PWT
位 2: U/S
位 1: R/W
位 0: P (Present)
NX位(位63)是64位分页相对于32位分页的一个重要新增特性。NX=1表示这个页面上的代码不允许被执行。操作系统可以将数据页(堆、栈、数据段)标记为NX,防止恶意代码注入攻击。在32位非PAE模式下,页表条目只有32位,没有空间放NX位;PAE模式将条目扩展到64位后有了NX位,但需要处理器支持(通过CPUID扩展叶0x80000001的EDX位29检查)。
关于大页面(Huge Pages)的支持:
1GB大页:在PDPT级别设置PS=1,一个PDPT条目直接映射1GB的连续物理内存。好处是减少了TLB压力——一个TLB条目就覆盖了1GB的地址空间,而用4KB小页需要262144个TLB条目。适合大型数据库和内存密集型应用。
2MB大页:在PD级别设置PS=1,一个PD条目直接映射2MB的连续物理内存。这是最常用的大页大小。
Linux通过Huge Pages特性支持大页面。有两种使用方式:hugetlbfs文件系统(需要预先分配大页面)和Transparent Huge Pages(THP,操作系统自动将合适的4KB页面合并为2MB大页)。Oracle数据库、Java虚拟机、Redis等内存密集型应用都建议启用大页面以提升性能。
Windows通过Large Pages功能支持2MB大页面(需要"锁定内存页"权限)。SQL Server会自动使用大页面来存储缓冲池。
四级页表在内存效率方面有一个重要优势——“按需分配”。一个进程可能有很大的虚拟地址空间,但实际使用的内存区域很少。四级页表不需要为所有可能的虚拟地址预留页表空间——只有实际使用到的虚拟地址区域才需要分配对应的页表。如果PML4中某个条目的P=0(没有映射),那么该条目覆盖的整个512GB虚拟地址空间都不需要任何下层页表。
举个例子:一个进程只使用了两段虚拟地址空间——低端的代码和数据(几MB)和高端的栈(几MB)。在四级页表中,只需要1个PML4表、2个PDPT、2个PD、若干个PT,总共占用的内存不超过几十KB。如果使用一级的线性页表来映射整个48位虚拟地址空间,需要256TB / 4KB = 64G个条目,光页表就需要512GB的空间——这显然是不可行的。层次化的页表结构用稀疏映射来避免了这个问题。
7.5.5 IA-32e模式下的完整地址翻译流程
在IA-32e的64位模式下,地址翻译的完整过程如下:
第一步:分段(极度简化)
程序产生的逻辑地址中,段基地址对于CS、DS、ES、SS来说固定为0(FS和GS除外),所以线性地址等于偏移量。这一步在64位模式下几乎是透明的。
对于FS和GS段的访问:
线性地址 = FS_BASE(或GS_BASE)+ 偏移量
第二步:规范地址检查
处理器检查线性地址是否是规范的——位47必须被符号扩展到位48-63。如果线性地址不是规范的,产生#GP异常(对于数据访问)或#SS异常(对于栈操作)。
第三步:四级页表遍历
apache
-
从CR3获取PML4的物理基地址
PML4基地址 = CR3[51:12] << 12 -
计算PML4条目的物理地址
PML4E_addr = PML4基地址 + 线性地址[47:39] * 8 -
读取PML4条目
如果PML4E.P = 0,触发#PF
设置PML4E.A = 1(标记已访问) -
计算PDPT条目的物理地址
PDPTE_addr = PML4E[51:12] << 12 + 线性地址[38:30] * 8 -
读取PDPT条目
如果PDPTE.P = 0,触发#PF
设置PDPTE.A = 1
如果PDPTE.PS = 1(1GB大页):
物理地址 = PDPTE[51:30] << 30 + 线性地址[29:0]
翻译完成 -
计算PD条目的物理地址
PDE_addr = PDPTE[51:12] << 12 + 线性地址[29:21] * 8 -
读取PD条目
如果PDE.P = 0,触发#PF
设置PDE.A = 1
如果PDE.PS = 1(2MB大页):
物理地址 = PDE[51:21] << 21 + 线性地址[20:0]
翻译完成 -
计算PT条目的物理地址
PTE_addr = PDE[51:12] << 12 + 线性地址[20:12] * 8 -
读取PT条目
如果PTE.P = 0,触发#PF
设置PTE.A = 1
如果是写操作,设置PTE.D = 1
物理地址 = PTE[51:12] << 12 + 线性地址[11:0]
在整个遍历过程中,处理器还会进行以下访问权限检查:
U/S检查:如果CPL=3(用户态),所有经过的页表条目(PML4E、PDPTE、PDE、PTE)的U/S位都必须为1。任何一级U/S=0都会触发#PF。
R/W检查:如果是写操作且CPL=3,所有经过的页表条目的R/W位都必须为1(或者CR0.WP=0时内核态写操作忽略R/W位,但CR0.WP=1时即使内核态也不能写只读页面)。
NX检查:如果是取指令操作,所有经过的页表条目的NX位都必须为0。任何一级NX=1都会阻止代码执行。
SMEP检查:如果CR4.SMEP=1且CPL=0,不能在U/S=1的页面上执行代码。
SMAP检查:如果CR4.SMAP=1且CPL=0且EFLAGS.AC=0,不能读写U/S=1的页面。
当触发#PF时,处理器会:
将导致页错误的线性地址存入CR2
在栈上压入错误码,错误码的格式如下:
位0(P):0=页面不存在,1=页面存在但访问违规
位1(W/R):0=读操作触发,1=写操作触发
位2(U/S):0=内核态触发,1=用户态触发
位3(RSVD):1=页表条目中的保留位被设置
位4(I/D):1=取指令触发(需要SMEP或NX支持)
跳转到IDT中向量14的处理程序
操作系统的缺页处理程序根据CR2(故障地址)和错误码来判断应该如何处理——是分配新页面(按需分配)、从磁盘读回交换页面、执行COW复制,还是向进程发送段错误信号。
完整的四级页表遍历需要4次内存访问(读PML4E、PDPTE、PDE、PTE),加上最终的数据访问,总共5次。如果没有TLB缓存,每次内存访问的延迟是几十到上百纳秒,这意味着一次普通的内存访问可能需要500纳秒而不是100纳秒——5倍的开销。这就是TLB如此关键的原因。
现代处理器的TLB通常是多级的:
L1 ITLB(指令TLB):几十个条目,1个时钟周期命中延迟
L1 DTLB(数据TLB):几十个条目,1个时钟周期命中延迟
L2 STLB(共享TLB,统一的二级TLB):几百到几千个条目,几个时钟周期命中延迟
Intel的Skylake架构的STLB有1536个条目(4KB页面)和16个条目(2MB大页面)。使用大页面可以显著减少TLB Miss——一个2MB大页面的TLB条目等效于512个4KB小页面的TLB条目。
处理器还使用一种叫做"页表遍历缓存"(Page Walk Cache或Paging Structure Cache)的机制来加速TLB Miss时的页表遍历。这个缓存保存了PML4E、PDPTE和PDE等中间级别的页表条目,这样在TLB Miss时不需要从PML4开始完整地遍历四级页表,可以从中间某一级开始。
回顾本章的全部内容,我们从处理器的运行模式讲起,依次分析了寄存器体系、CPUID探测、标志寄存器、控制寄存器和MSR寄存器的功能与用途,然后深入探讨了实模式、保护模式和IA-32e模式这三种运行模式下的段管理、中断处理和分页机制。
这些知识构成了操作系统开发的硬件基础。操作系统的很多设计决策——为什么要做模式切换、为什么进程有独立的地址空间、为什么内核态和用户态要隔离、为什么系统调用比普通函数调用慢——都可以从处理器架构中找到答案。处理器提供了机制(mechanism),操作系统制定了策略(policy),二者的配合才构成了我们每天使用的计算机系统。
理解处理器架构还有一个实际的好处——当你遇到操作系统级别的问题(蓝屏、kernel panic、段错误、性能异常等)时,对硬件机制的了解能帮助你更快地定位问题根源。很多看似诡异的bug,追根溯源往往是某个控制寄存器的位没有正确设置、页表映射有误、或者特权级检查未通过。这些问题如果不了解处理器架构,就只能靠猜测和试错来解决。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)