# 前言
上一篇我们学习了页表基址和 2-9-9-12 分页。本篇是 "保护模式" 系列的最后一篇,将介绍 TLB、中断与异常、控制寄存器等内容。
# TLB(Translation Lookaside Buffer,快表)
# 线性地址解析的开销
当计算机通过一个线性地址访问物理页(如 MOV EAX, [0x12345678] )时,CPU 未必只读了 4 个字节。
10-10-12 分页 :
通过线性地址找到 PDE :4 字节
通过 PDE 找到 PTE :4 字节
在物理页中读出 4 字节
共访问了 12 个字节 (如果跨页可能更多)。
2-9-9-12 分页 :
找到 PDPTE :8 字节
找到 PDE :8 字节
找到 PTE :8 字节
在物理页中读出 4 字节
共访问了 28 个字节 (如果跨页可能更多)。
为了提高效率,CPU 内部有一张表来记录线性地址与物理地址的对应关系,效率和寄存器一样快 ,这就是 TLB 。由于效率极高,TLB 的容量不能太大,少则几十条,多则上百条。
思考 :一个进程有 4GB 的线性地址空间,但 TLB 最多只能记录上百条记录,这张表真的有意义吗?
答案是有意义的。程序具有局部性原理 —— 在一段时间内,程序倾向于访问相同或相邻的内存区域,因此 TLB 的命中率其实非常高。
# TLB 结构
属性位
含义
ATTR
属性。 10-10-12 分页: ATTR = PDE属性 & PTE属性 2-9-9-12 分页: ATTR = PDPTE属性 & PDE属性 & PTE属性
LRU
统计信息。 当 TLB 写满、又有新地址即将写入时,TLB 会根据统计信息判断哪些地址不常用,从而将其移除。
注意 :
不同的 CPU,TLB 大小不同。
只要 Cr3 发生变化,TLB 立即刷新 (一核一套 TLB)。
操作系统高 2G 映射基本不变,如果 Cr3 改了全部刷新会很浪费。因此 PDE 和 PTE 中有个 G 标志位 ,如果 G=1,刷新 TLB 时不会刷新 该页的记录。
# TLB 种类
TLB 在 x86 体系中从 Intel 486 CPU 开始使用,一般设有 4 组 TLB:
缓存一般页表 (4KB 页面)的指令页表缓存 (Instruction-TLB)
缓存一般页表 (4KB 页面)的数据页表缓存 (Data-TLB)
缓存大尺寸页表 (2MB/4MB 页面)的指令页表缓存 (Instruction-TLB)
缓存大尺寸页表 (2MB/4MB 页面)的数据页表缓存 (Data-TLB)
以下实验均采用 10-10-12 分页模式。
# 实验 1:体验 TLB 的存在
一、编译以下代码,运行至 int 0x20 位置。
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 #include <stdio.h> #include <windows.h> DWORD x, y, z; void __declspec(naked) PageOnNull() { __asm { push ebp mov ebp, esp sub esp, 0x100 push ebx push esi push edi } DWORD* pPTE; DWORD* pNullPTE; pNullPTE = (DWORD*)0xC0000000 ; pPTE = (DWORD*)(0xC0000000 + (0x50000000 >> 10 )); *pNullPTE = *pPTE; x = *(DWORD*)0 ; pPTE = (DWORD*)(0xC0000000 + (0x60000000 >> 10 )); *pNullPTE = *pPTE; y = *(DWORD*)0 ; __asm { mov eax, cr3 mov cr3, eax } z = *(DWORD*)0 ; __asm { pop edi pop esi pop ebx mov esp, ebp pop ebp iretd } } int main (int argc, char * argv[]) { DWORD* p5 = (DWORD*)VirtualAlloc((LPVOID)0x50000000 , 4 , MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); DWORD* p6 = (DWORD*)VirtualAlloc((LPVOID)0x60000000 , 4 , MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); if (p5 != (DWORD*)0x50000000 || p6 != (DWORD*)0x60000000 ) { printf ("Error alloc!\n" ); return -1 ; } *p5 = 0x1234 ; *p6 = 0x5678 ; __asm { int 0x20 } printf ("1. 读 0 地址数据:\n" ); printf ("*NULL = 0x%x \n\n" , x); printf ("2. 给 0 地址重新挂上物理页\n\n" ); printf ("3. 重新读取 0 地址数据:\n" ); printf ("*NULL = 0x%x \n\n" , y); printf ("4. 刷新 TLB \n\n" ); printf ("5. 再次读取 0 地址数据:\n" ); printf ("*NULL = 0x%x \n" , z); return 0 ; }
二、在反汇编界面查看 PageOnNull 函数首地址。
此时确定中断门描述符: 0040ee00'0008de30 。
三、使用 WinDbg 在 IDT[0x20] 处写入中断门描述符。
四、继续运行程序,结果如下。
分析 :
x 被赋值后,即使 0 地址被挂上了新的物理页,y 和 x 输出的值相同 。
但 Cr3 刷新后,z 输出了新的值 。
这是因为 0 地址第一次被访问时,线性地址与物理地址的对应关系被写入了 TLB。在对 y 赋值时,TLB 记录没有刷新,访问的还是原来的物理页。Cr3 刷新后,TLB 被清空,z 访问了新的物理页。
# 全局页
当 Cr4 的 PGE 位开启时,操作系统支持全局页,全局页用于操作系统内核和共享资源。
特性:
TLB 保留 :上下文切换时,标记为全局的页不会从 TLB 中清除
共享性 :全局页映射的是内核全局资源,所有进程共享
# 实验 2:理解全局页的特性
一、检查 Cr4 的 PGE 位是否开启。
二、编译并运行以下代码(与实验 1 类似,但在 PTE 上设置了 G 位)。
1 2 3 4 5 6 globalPTE = oldPTE | 0x100 ; *pPTE = globalPTE; *pNullPTE = *pPTE;
三、运行结果。
分析 :
x 和 y 输出值相同(TLB 未刷新)
Cr3 刷新后,z 依然没有输出新值
因为 0 地址所在的页被设置为了全局页 ,Cr3 刷新时 TLB 中关于全局页的记录不会被清除 。
# INVLPG 指令
INVLPG 指令用于使指定线性地址对应的 TLB 条目无效:
注意:
INVLPG 只影响 TLB,不影响数据缓存或指令缓存
只能清除当前处理器 的 TLB 条目,对多核的其他核心无直接影响
# 实验 3:理解 INVLPG 指令
在实验 2 的基础上,在 Cr3 刷新后加入 INVLPG 指令:
1 2 3 4 5 6 __asm { INVLPG dword ptr ds:[0 ] } new_z = *(DWORD*)0 ;
一、在反汇编界面查看函数首地址。
确定中断门描述符: 0040ee00'0008dfe0 。
二、使用 WinDbg 写入中断门描述符。
三、运行结果。
分析 :
x、y 相同(TLB 未刷新)
z 依然和 x 相同(全局页,Cr3 刷新无效)
INVLPG 执行后,new_z 输出了新值
INVLPG 指令可以强制清除指定地址的 TLB 缓存,即使是全局页。
# 中断与异常
# 中断
中断通常由 CPU 外部的 I/O 设备(硬件 )触发,供外部设备通知 CPU"有事情需要处理",因此又叫中断请求(Interrupt Request) 。
中断请求的目的是希望 CPU 暂时停止当前程序,转去执行中断处理例程 。
80x86 有两条中断请求线:
可屏蔽中断线 INTR (Interrupt Request)
非屏蔽中断线 NMI (NonMaskable Interrupt)
如果没有中断机制,当一个程序进入死循环时,其他程序就没有机会执行了。
中断的本质 :改变 CPU 的执行路线。
# 可屏蔽中断
可屏蔽中断由中断控制器 (专门的芯片)管理
通常用 IRQ 后跟数字来标识不同的中断(如时钟中断 = IRQ0 )
时钟中断 :
Windows 时钟中断每隔 10~20ms 向 CPU 发送一个请求
CPU 收到请求时,操作系统接管 CPU,有机会进行线程切换
即便程序进入死循环,操作系统依然有机会切换线程
可屏蔽中断的处理 :
时钟中断的 IRQ 编号为 0,所在位置为 IDT[0x30]
IRQ1~IRQ15 分别对应 IDT [0x31]~IDT [0x3F]
注意:
用 CLI 指令清空 EFLAGS 的 IF 位 → 屏蔽可屏蔽中断
用 STI 指令设置 EFLAGS 的 IF 位 → 开启可屏蔽中断
硬件中断与 IDT 表的对应关系并非固定不变 (参见 APIC )
# 非屏蔽中断
注意:
非屏蔽中断产生时,CPU 执行完当前指令后会立即进入中断处理程序
非屏蔽中断不受 EFLAGS 的 IF 位影响 ,一旦发生,CPU 必须处理
# 异常
异常通常是 CPU 在执行指令时检测到的错误 ,如除零、访问无效页面等。
中断与异常的区别 :
中断 来自外部设备 ,CPU 是被动的
异常 来自 CPU 本身 ,CPU 是主动产生的
INT N 虽然被称为 "软件中断",但本质是异常
EFLAGS 的 IF 位对 INT N 无效
# 常见的异常处理
错误类型
IDT 中断号
含义
页错误
0xE
当线性地址指向的物理页无效时触发
段错误
0xD
段运算发生异常时(如权限检查失败)触发
除零错误
0x0
除数为 0 时触发
双重错误
0x8
处理一个异常时又产生了一个错误时触发
# 缺页异常
缺页异常的产生 :
当 PDE/PTE 的 P=0 时
当 PDE/PTE 的属性为只读但程序试图写入 时
一旦发生缺页异常,CPU 会执行 IDT 表中的 0xE 号 中断处理程序,由操作系统接管。
例 1:物理页紧缺时的处理
若一个物理页有效,PDE/PTE 的 P 位为 1
当其他进程的物理页不够用时,操作系统会将当前物理页的内容保存到页面文件 ,再将物理页挂到别人的 PDE/PTE 中,最后将当前进程的 P 位改为 0
再次访问时,操作系统发现 P=0,进入 0xE 号中断处理程序
操作系统从页面文件中读出数据,重新挂上物理页,将 P 位改回 1
整个过程对用户完全透明
例 2:写入只读页面或访问未分配的页面
当 PDE/PTE 属性为只读,但程序试图写入时,CPU 进入 0xE 号中断处理程序
操作系统检测到用户操作不合理(如写只读页面、访问未分配的页面),返回错误(0xC0000005,内存访问失败 )
# 实验 1:分析 IDT [0x2] 的执行流程
一、查看 IDT [0x2] 的中断描述符。
Type 为 0101 ,属于任务门描述符 ,段选择子为 0x58 。
二、查看 GDT:0x58,找到 TSS 段描述符。
分析可知 TSS 的位置在 0x80558768 。
三、获取 TSS 中保存的 EIP。
四、分析具体代码,位于内核的 KiTrap02 函数。
五、使用 IDA 分析 ntoskrnl.exe 中的 KiTrap02 函数。
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 33 34 35 36 37 38 39 40 void __fastcall KiTrap02 (int a1) { _disable(); TSS = KeGetPcr()->TSS; GDT = KeGetPcr()->GDT; v3 = __readeflags(); __writeeflags(v3 & 0xFFFFBFFF ); CurrentProcessorNumber = KeGetCurrentProcessorNumber(); if (dword_4807DC == CurrentProcessorNumber) { if ((unsigned int )dword_4807E0 >= 8 ) { if (dword_4807E0 == 8 && !KdDebuggerNotPresent && KdDebuggerEnabled) KeEnterKernelDebugger(); while (1 ); } } ++dword_4807E0; HalHandleNMI(0 ); if (KeGetPcr()->TSS->Backlink != 88 ) { __asm { iret } } KiSystemFatalException(); }
# 实验 2:分析 IDT [0x8] 的执行流程
一、查看 IDT [0x8] 的中断描述符。
Type 为 0101 ,属于任务门描述符 ,段选择子为 0x50 。
二、查看 GDT:0x50,找到 TSS 段描述符。
TSS 位置在 0x80558700 。
三、获取 TSS 中保存的 EIP。
四、分析代码,位于内核的 KiTrap08 函数。
五、IDA 分析 KiTrap08 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void __noreturn KiTrap08 () { _disable(); GDT = (unsigned int )KeGetPcr()->GDT; *(_BYTE *)(GDT + 85 ) = 0x89 ; v1 = __readeflags(); __writeeflags(v1 & 0xFFFFBFFF ); TSS = KeGetPcr()->TSS; KeBugCheck2(127 , 8 , TSS, 0 , 0 , 0 ); }
# 控制寄存器
控制寄存器共有五个: Cr0 、 Cr1 、 Cr2 、 Cr3 、 Cr4 。
其中:
Cr1 :保留,未使用
Cr3 :页目录表基址(前面已详述)
# Cr0
Cr0 负责控制处理器的关键操作模式。
属性位
含义
PE
启用保护标志。 PE=1:保护模式;PE=0:实地址模式 仅开启段级保护,不启用分页。启用分页需要 PE 和 PG 同时置位。
PG
分页机制标志(必须先开启 PE)。 PG=0, PE=0:实地址模式 PG=0, PE=1:无分页的保护模式 PG=1, PE=0:不存在 PG=1, PE=1:开启分页的保护模式
WP
写保护标志。 当 CPL < 3 时: WP=0:可以读写 任意用户级物理页(只要线性地址有效) WP=1:可以读取 任意用户级物理页,但对只读 物理页不能写
# Cr2
当 CPU 访问某个无效页面时,会产生缺页异常,此时 CPU 将引起异常的线性地址 存放在 Cr2 中。
具体作用:
当 PDE/PTE 的 P 位为 0 时,产生缺页异常
CPU 将引起缺页异常的线性地址存储到 Cr2 中
操作系统的异常处理程序开始处理
处理结束后,从页面文件中读出数据,挂上有效物理页,P 位改回 1
程序继续执行(Cr2 记录了引发缺页异常的线性地址,操作系统据此重新映射物理页)
若操作系统检测到用户访问的是未分配 的页面,则报告错误
# Cr4
Cr4 主要用于启用或禁用扩展功能。
属性位
含义
PAE
PAE=1:2-9-9-12 分页;PAE=0:10-10-12 分页
PSE
页大小扩展(Page Size Extension)
# PWT 与 PCD
在学习 PDE 与 PTE 属性时,有两个属性位之前做了保留 ——PWT 和 PCD。
# CPU 缓存
CPU 缓存位于 CPU 与物理内存 之间,容量比内存小但读写速度快得多
CPU 缓存大小决定了 CPU 的执行速度(越大越快)
# CPU 缓存与 TLB 的区别
TLB :存储线性地址 与物理地址 的对应关系
CPU 缓存 :存储物理地址 与内容 的对应关系
有了 CPU 缓存,查找某个线性地址对应的物理页时:
先查 TLB → 找到物理地址
再查 CPU 缓存 → 找到内容
更多细节参考 Intel 白皮书第三卷第 11 章 。
# PWT(Page Write Through,页写穿模式)
PWT=1:写 Cache 的同时也要将数据写入内存
PWT=0:只写 Cache,是否映射到内存由 CPU 缓存控制器决定
# PCD(Page Cache Disable,页缓存禁用)
典型应用:对于映射到硬件设备寄存器(MMIO)的物理页,应将 PCD 设为 1,以避免 CPU 缓存导致读写操作无法及时反映到设备上。
# 阶段测试
# 题目一
描述 :给定一个线性地址和长度,读取内容。
1 int ReadMemory (OUT BYTE* buffer, IN DWORD dwAddr, IN DWORD dwLength) ;
要求 :
可以自己指定分页方式
页不存在要提示,不能报错
可以正确读取数据
参考答案 (10-10-12 分页):
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 #include <stdio.h> #include <windows.h> DWORD dwPDI; DWORD dwPTI; DWORD dwPageOffset; bool bRead;void __declspec(naked) ParsePage(){ __asm { pushad pushfd mov eax, [esp+0x24 +4 ] mov ebx, eax shr ebx, 22 mov [dwPDI], ebx mov ebx, eax shr ebx, 12 and ebx, 0x3ff mov [dwPTI], ebx mov ebx, eax and ebx, 0xfff mov [dwPageOffset], ebx popfd popad ret 4 } } void __declspec(naked) CateProc(){ __asm { pushad pushfd mov ecx, [esp+0x24 +0x8 +8 ] xor eax, eax Label_loop: test eax, eax jz Label_CallParsePage mov esi, [esp+0x24 +0x8 +4 ] push eax add eax, esi mov esi, 0x1000 xor edx, edx div esi pop eax cmp edx, 0 jnz Label_ReadMemory Label_CallParsePage: mov esi, [esp+0x24 +0x8 +4 ] add esi, eax push esi call ParsePage mov esi, 0xC0300000 mov edx, dwPDI shl edx, 2 add esi, edx mov ebx, [esi] test ebx, 0x1 jz Label_end_false mov esi, 0xC0000000 shl edx, 10 add esi, edx mov edx, dwPTI shl edx, 2 add esi, edx mov ebx, [esi] test ebx, 0x1 jz Label_end_false Label_ReadMemory: mov esi, [esp+0x24 +0x8 +4 ] mov edi, [esp+0x24 +0x8 +0 ] mov dl, [esi+eax] mov byte ptr ds:[edi+eax], dl add eax, 1 cmp eax, ecx jz Label_end_success jmp Label_loop Label_end_success: mov byte ptr ds:[bRead], 1 Label_end_false: popfd popad retf 0xC } } int ReadMemory (OUT BYTE* buffer, IN DWORD dwAddr, IN DWORD dwLength) { char buff[6 ]; *(DWORD*)&buff[0 ] = 0x12345678 ; *(WORD*)&buff[4 ] = 0x48 ; __asm { mov eax, [dwLength] push eax mov eax, [dwAddr] push eax push buffer call fword ptr[buff] } return 0 ; } int main (int argc, char * argv[]) { BYTE *buffer; DWORD dwAddr; DWORD dwSize; DWORD dwCount; DWORD dwLength; int i; char bContinue; printf ("1. 在 GDT:0x48 构造调用门:\n" ); printf ("eq (gdtr+0x48) %04xec03`0008%04x\n" , (DWORD)CateProc >> 16 , (DWORD)CateProc & 0xffff ); getchar(); printf ("2. 读取内存:\n" ); while (1 ) { bRead = false ; printf ("Address: " ); scanf ("%x" , &dwAddr); printf ("Size: " ); scanf ("%d" , &dwSize); if (dwSize != 1 && dwSize != 2 && dwSize != 4 ) { printf ("Error: the size must be 1 or 2 or 4\n\n" ); continue ; } printf ("Count: " ); scanf ("%d" , &dwCount); dwLength = dwSize * dwCount; buffer = (BYTE*)malloc (dwLength + 1 ); memset (buffer, 0 , dwLength + 1 ); ReadMemory(buffer, dwAddr, dwLength); if (bRead) { printf ("buffer: \n" ); switch (dwSize) { case 1 : for (i = 0 ; i < (int )dwLength; i++) { if (i != 0 && i % 16 == 0 ) printf ("\n" ); printf (" %02X" , buffer[i]); } break ; case 2 : for (i = 0 ; i < (int )dwLength / 2 ; i++) { if (i != 0 && i % 8 == 0 ) printf ("\n" ); printf (" %04X" , ((WORD*)buffer)[i]); } break ; case 4 : for (i = 0 ; i < (int )dwLength / 4 ; i++) { if (i != 0 && i % 4 == 0 ) printf ("\n" ); printf (" %08X" , ((DWORD*)buffer)[i]); } break ; } printf ("\n\n" ); } else { printf ("Error: some pages are invalid\n\n" ); } while (1 ) { fflush(stdin ); printf ("是否继续(Y/N)? " ); scanf ("%c" , &bContinue); if (bContinue == 'Y' || bContinue == 'y' || bContinue == 'N' || bContinue == 'n' ) break ; } if (bContinue != 'Y' && bContinue != 'y' ) break ; } printf ("Done!\n" ); return 0 ; }
# 题目二
描述 :
申请长度为 100 的 DWORD 数组,每项用该项的地址初始化
把这个数组所在的物理页挂到 0x1000 的地址上
通过 0x1000 页的线性地址打印数组的值
要求 :数组所在的物理页是同一个页。
参考答案 (10-10-12 分页):
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 #include <stdio.h> #include <windows.h> DWORD* g_dwArr; DWORD* GetPDE (DWORD dwAddr) { DWORD dwPDI = dwAddr >> 22 ; return (DWORD*)(0xC0300000 + dwPDI * 4 ); } DWORD* GetPTE (DWORD dwAddr) { DWORD dwPDI = dwAddr >> 22 ; DWORD dwPTI = (dwAddr >> 12 ) & 0x3FF ; return (DWORD*)(0xC0000000 + dwPDI * 0x1000 + dwPTI * 4 ); } void __declspec(naked) CateProc(){ __asm { pushad pushfd } *GetPDE(0x1000 ) = *GetPDE((DWORD)g_dwArr); *GetPTE(0x1000 ) = *GetPTE((DWORD)g_dwArr); __asm { popfd popad iretd } } int main (int argc, char * argv[]) { int i; printf ("1. 在 IDT[0x20] 构造中断门:\n" ); printf ("eq (idtr+0x20*8) %04xee00`0008%04x\n" , (DWORD)CateProc >> 16 , (DWORD)CateProc & 0xffff ); getchar(); printf ("2. 读取数组:\n" ); g_dwArr = (DWORD*)VirtualAlloc(0 , 0x1000 , MEM_COMMIT, PAGE_READWRITE); printf ("VirtualAlloc: %08X\n" , g_dwArr); memset (g_dwArr, 0 , 0x1000 ); for (i = 0 ; i < 100 ; i++) { g_dwArr[i] = (DWORD)&g_dwArr[i]; } __asm { int 0x20 } DWORD *p = (DWORD*)0x1000 ; for (i = 0 ; i < 100 ; i++) { printf ("%08X\n" , p[i]); } VirtualFree((DWORD*)g_dwArr, 0 , MEM_RELEASE); getchar(); return 0 ; }
# 附录:RET / RETF / IRET / IRETD 区别
# RET
机器指令: C3
描述:近返回,对应 CALL 指令
本质:从栈顶弹出数据给 EIP
# RETF
机器指令: CB
描述:远返回,对应 CALL FAR 指令
相同权限返回:从栈顶弹出 EIP → CS
不同权限返回:从栈顶弹出 EIP → CS → ESP → SS
# IRET / IRETD
机器指令: CF
描述:中断返回,对应 INT N 指令和任务切换的 CALL FAR 指令
NT=0,同特权级 返回:从栈顶弹出 EIP → CS → EFLAGS
NT=0,不同特权级 返回:从栈顶弹出 EIP → CS → EFLAGS → ESP → SS
NT=1:任务切换返回,使用 TSS 表
Intel 原文:IRET and IRETD are mnemonics for the same opcode. The IRETD mnemonic (interrupt return double) is intended for use when returning from an interrupt when using the 32-bit operand size; however, most assemblers use the IRET mnemonic interchangeably for both operand sizes.
参考资料 :
Intel 白皮书第 2 卷 Vol. 2A 3-525 (IRET/IRETD)
Intel 白皮书第 2 卷 Vol. 2B 4-563 (RET/RETF)
上一篇:保护模式(六)| 页表基址与 2-9-9-12 分页