Loading... # 保护模式进阶 ## 大内存读写 ### GDT段 ```asm ;GDT [SECTION .gdt] ; 段基址, 段界限 , 属性 LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符 LABEL_DESC_NORMAL: Descriptor 0, 0ffffh, DA_DRW ; Normal 描述符 LABEL_DESC_CODE32: Descriptor 0, SegCode32Len-1, DA_C+DA_32; 非一致代码段, 32 LABEL_DESC_CODE16: Descriptor 0, 0ffffh, DA_C ; 非一致代码段, 16 LABEL_DESC_DATA: Descriptor 0, DataLen-1, DA_DRW ; Data LABEL_DESC_STACK: Descriptor 0, TopOfStack, DA_DRWA+DA_32; Stack, 32 位 LABEL_DESC_TEST: Descriptor 0500000h, 0ffffh, DA_DRW LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW ; 显存首地址 ; GDT 结束 GdtLen equ $ - LABEL_GDT ; GDT长度 GdtPtr dw GdtLen - 1 ; GDT界限 dd 0 ; GDT基地址 ; GDT 选择子 SelectorNormal equ LABEL_DESC_NORMAL - LABEL_GDT SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT SelectorCode16 equ LABEL_DESC_CODE16 - LABEL_GDT SelectorData equ LABEL_DESC_DATA - LABEL_GDT SelectorStack equ LABEL_DESC_STACK - LABEL_GDT SelectorTest equ LABEL_DESC_TEST - LABEL_GDT SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT ; END of [SECTION .gdt] ``` 与[认识保护模式](http://y0k1n0.online/index.php/archives/16/)当中的代码类似,这里也是定义了GDT的描述符、DT表的属性、选择子等内容 其中`LABEL_DESC_TEST`的段基址被设定为了`0500000h`远远超过实模式的寻址上限 `LABEL_DESC_VIDEO`指向了显存的首地址,用于将特定字符显示在屏幕上 ### 数据段 ```asm ;数据段 [SECTION .data1] ALIGN 32 [BITS 32] LABEL_DATA: SPValueInRealMode dw 0 ; 字符串 PMMessage: db "In Protect Mode now. ^-^", 0 ; 在保护模式中显示 OffsetPMMessage equ PMMessage - $$ StrTest: db "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0 OffsetStrTest equ StrTest - $$ DataLen equ $ - LABEL_DATA ; END of [SECTION .data1] ``` `OffsetPMMessage`定义为`PMMessage`相对于`$$`(即`LABEL_DATA`)的偏移地址 `OffsetStrTest`定义为`StrTest`相对于`$$`(即`LABEL_DATA`)的偏移地址 `DataLen`定义了数据段的长度 ### 堆栈段 ```asm ; 全局堆栈段 [SECTION .gs] ALIGN 32 [BITS 32] LABEL_STACK: times 512 db 0 TopOfStack equ $ - LABEL_STACK - 1 ; END of [SECTION .gs] ``` 这段代码定义了一个512个字节的堆栈段`LABEL_STACK`和一个栈顶指针`TopOfStack` ### 实模式->保护模式代码段 ```asm LABEL_BEGIN: mov ax, cs mov ds, ax mov es, ax mov ss, ax mov sp, 0100h mov [LABEL_GO_BACK_TO_REAL+3], ax mov [SPValueInRealMode], sp ; 初始化 16 位代码段描述符 mov ax, cs movzx eax, ax shl eax, 4 add eax, LABEL_SEG_CODE16 mov word [LABEL_DESC_CODE16 + 2], ax shr eax, 16 mov byte [LABEL_DESC_CODE16 + 4], al mov byte [LABEL_DESC_CODE16 + 7], ah ; 初始化 32 位代码段描述符 xor eax, eax mov ax, cs shl eax, 4 add eax, LABEL_SEG_CODE32 mov word [LABEL_DESC_CODE32 + 2], ax shr eax, 16 mov byte [LABEL_DESC_CODE32 + 4], al mov byte [LABEL_DESC_CODE32 + 7], ah ; 初始化数据段描述符 xor eax, eax mov ax, ds shl eax, 4 add eax, LABEL_DATA mov word [LABEL_DESC_DATA + 2], ax shr eax, 16 mov byte [LABEL_DESC_DATA + 4], al mov byte [LABEL_DESC_DATA + 7], ah ; 初始化堆栈段描述符 xor eax, eax mov ax, ds shl eax, 4 add eax, LABEL_STACK mov word [LABEL_DESC_STACK + 2], ax shr eax, 16 mov byte [LABEL_DESC_STACK + 4], al mov byte [LABEL_DESC_STACK + 7], ah ``` 实模式代码段首先将各个段段首的真实地址写入描述符的段基址当中 注意这两行代码: ```asm mov [LABEL_GO_BACK_TO_REAL+3], ax mov [SPValueInRealMode], sp ``` 这两行代码将实模式下的cs赋值给LABEL_GO_BACK_TO_REAL标签往后数第三、四个字节,将实模式下的sp赋值给SPValueInRealMode 其中`LABEL_GO_BACK_TO_REAL+3`实际上是`jmp 0:LABEL_REAL_ENTRY`中的`0`,观察下图`jmp`指令的内存占用不难发现,`byte4`和`byte5`指向了`jmp`指令的段基址,因此这里是修改`jmp`指令的段基址到实模式下的`cs` ![image-20240911210235987](https://y0k1n0-1323330522.cos.ap-beijing.myqcloud.com/image-20240911210235987.png) ```asm ; 为加载 GDTR 作准备 xor eax, eax mov ax, ds shl eax, 4 add eax, LABEL_GDT ; eax <- gdt 基地址 mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址 ; 加载 GDTR lgdt [GdtPtr] ; 关中断 cli ; 打开地址线A20 in al, 92h or al, 00000010b out 92h, al ; 准备切换到保护模式 mov eax, cr0 or eax, 1 mov cr0, eax ; 真正进入保护模式 jmp dword SelectorCode32:0 ; 执行这一句会把 SelectorCode32 装入 cs, 并跳转到 Code32Selector:0 处 ``` 然后加载GDTR,在关中断后打开地址线A20切换到保护模式 ### 保护模式代码段 ```asm [SECTION .s32]; 32 位代码段. 由实模式跳入. [BITS 32] LABEL_SEG_CODE32: mov ax, SelectorData mov ds, ax ; 数据段选择子 mov ax, SelectorTest mov es, ax ; 测试段选择子 mov ax, SelectorVideo mov gs, ax ; 视频段选择子 mov ax, SelectorStack mov ss, ax ; 堆栈段选择子 mov esp, TopOfStack ``` 保护模式开始先将各个数据段赋给对应的寄存器 ```asm ; 下面显示一个字符串,数据段的基址就是LABLE_DATA的物理地址 mov ah, 0Ch ; 0000: 黑底 1100: 红字 xor esi, esi xor edi, edi mov esi, OffsetPMMessage ; 源数据偏移(相对于LABEL_DESC_DATA的偏移量) mov edi, (80 * 10 + 0) * 2 ; 目的数据偏移。屏幕每行有 80 个字符,每个字符占用两个字节(一个用于字符,一个用于属性) cld .1: ;刷新标志寄存器 lodsb ; 从 esi 指向的内存地址加载一个字节到 al 寄存器,并递增 esi test al, al ; 测试 al 寄存器的值是否为零 jz .2 ; 为0则是终止符,表示显示完毕 mov [gs:edi], ax ; add edi, 2 jmp .1 .2: ; 显示完毕 ``` 然后将`ah`置为`0ch`,设置屏幕属性为黑底红字,将`esi`和`edi`清零[,然后将`OffsetPMMessage`(即`PMMessage`相对于`LABEL_DATA`的偏移地址)送到`esi`,设定目标数据的偏移地址并刷新标志寄存器 然后进入循环,从`esi`指向的内存地址加载一个字节到`al`寄存器,并递增`esi`,然后判断`al`寄存器是否为0(`'\0'`),如果不是终止字符,则向段基址为`gs`(即`SelectorVideo`),偏移地址为`edi`的显存地址中写入数据`ax`(`ah`为显示属性,`al`为显示字符) ```asm .2: ; 显示完毕 call DispReturn call TestRead call TestWrite call TestRead ; 到此停止 jmp SelectorCode16:0 ``` 显示完毕后依次调用 - `DispReturn`:模拟回车显示 - `TestRead` :从内存中读内容到显存 - `TestWrite`:向内存中写内容 从先后两次调用`TestRead`得到的不同的显示结果可以判断是否成功进行了大内存读写 #### TestRead ```asm TestRead: xor esi, esi mov ecx, 8 .loop: mov al, [es:esi] call DispAL inc esi loop .loop call DispReturn ret ``` 函数首先设定`ecx`为8,表示读取8个字节的数据 随后进入循环,从`es:esi`(即`SelectorTest`:`esi`)中读取一个字节的内容到al,并调用`DispAL`函数将它以16进制的方式打印出来 循环结束后模拟打印一个回车并结束函数 #### TestWrite ```asm TestWrite: push esi push edi xor esi, esi xor edi, edi mov esi, OffsetStrTest ; 源数据偏移 cld .1: lodsb test al, al jz .2 mov [es:edi], al inc edi jmp .1 .2: pop edi pop esi ret ``` 函数将`esi`赋值为`OffsetStrTest`(即`StrTest`的偏移地址) 然后进入循环,将整个字符串写入`es`(即`SelectorTest`)中 ### 保护模式->实模式 ```asm [SECTION .s16code] ALIGN 32 [BITS 16] LABEL_SEG_CODE16: ; 跳回实模式: mov ax, SelectorNormal mov ds, ax mov es, ax mov fs, ax mov gs, ax mov ss, ax mov eax, cr0 and al, 11111110b mov cr0, eax LABEL_GO_BACK_TO_REAL: jmp 0:LABEL_REAL_ENTRY ; 段地址会在程序开始处被设置成正确的值 Code16Len equ $ - LABEL_SEG_CODE16 ``` 程序首先将实模式的选择子赋给`ax`,并初始化其他寄存器,然后置`cr0`寄存器`PE`标志位为0,设定程序处于实模式 `jmp 0:LABEL_REAL_ENTRY`指令已经在前面修改为`jmp 实模式下cs对应的地址:LABEL_REAL_ENTRY`,因此程序再次跳转到实模式(即`[SECTION .s16]`代码段)下`LABEL_REAL_ENTRY`标签处 ```asm LABEL_REAL_ENTRY: ; 从保护模式跳回到实模式就到了这里 mov ax, cs mov ds, ax mov es, ax mov ss, ax mov sp, [SPValueInRealMode] in al, 92h ; `. and al, 11111101b ; | 关闭 A20 地址线 out 92h, al ; / sti ; 开中断 mov ax, 4c00h ; `. int 21h ; / 回到 DOS ; END of [SECTION .s16] ``` 最后关闭`A20`地址线并开中断,正式回到实模式 ## LDT(Local Descriptor Table) 本节内容仅仅介绍`pmtest3.asm`相对于`pmtest2.asm`做的改变,并省略初始化描述符代码 ### GDT中添加LDT描述符 ```asm [SECTION .gdt] ; GDT ;...... ; 段基址, 段界限 , 属性 LABEL_DESC_LDT: Descriptor 0, LDTLen - 1, DA_LDT ; LDT ; GDT 结束 GdtLen equ $ - LABEL_GDT ; GDT长度 GdtPtr dw GdtLen - 1 ; GDT界限 dd 0 ; GDT基地址 ; GDT 选择子 ;...... SelectorLDT equ LABEL_DESC_LDT - LABEL_GDT ;...... ; END of [SECTION .gdt] ``` 这部分完成了在GDT中定义LDT描述符的过程,其中`LABEL_DESC_LDT`的段基址被填充为标签`LABEL_LDT`的真实地址 问:为什么要在GDT中定义LDT? 答:全局描述符表(GDT)是用于存储段描述符的表,而局部描述符表(LDT)是特定于某个进程的描述符表。LDT允许进程拥有自己的段描述符,这有助于实现内存保护和隔离。 ### LDT段 ```asm ; LDT [SECTION .ldt] ALIGN 32 LABEL_LDT: ; 段基址, 段界限 , 属性 LABEL_LDT_DESC_CODEA: Descriptor 0, CodeALen - 1, DA_C + DA_32 ; Code, 32 位 LDTLen equ $ - LABEL_LDT ; LDT 选择子 SelectorLDTCodeA equ LABEL_LDT_DESC_CODEA - LABEL_LDT + SA_TIL ; END of [SECTION .ldt] ``` `LABEL_LDT_DESC_CODEA`的基址被填充为标签`LABEL_CODE_A`的真实地址 注意`SelectorLDTCodeA`(即选择子)的定义中多了一项`SA_TIL`,其定义为`SA_TIL EQU 4`,即将选择子的`TIL`标志位设置为1 `TIL`标志位用于区分`GDT`选择子和`LDT`选择子,如果`TIL`为1,则系统会从当前`LDT`中寻找相应的描述符 ### LDT代码段 ```asm ; CodeA (LDT, 32 位代码段) [SECTION .la] ALIGN 32 [BITS 32] LABEL_CODE_A: mov ax, SelectorVideo mov gs, ax ; 视频段选择子(目的) mov edi, (80 * 12 + 0) * 2 ; 屏幕第 10 行, 第 0 列。 mov ah, 0Ch ; 0000: 黑底 1100: 红字 mov al, 'L' mov [gs:edi], ax ; 准备经由16位代码段跳回实模式 jmp SelectorCode16:0 CodeALen equ $ - LABEL_CODE_A ; END of [SECTION .la] ``` `LDT`代码段的实现与32位代码段大致相同,就是在屏幕上以黑底红字打印字母L并模拟回车显示,并在代码段的末尾调转到`SelectorCode16`选择子指向的代码段`LABEL_SEG_CODE16` ### LDT和GDT区别 1. **作用范围**: - **GDT(Global Descriptor Table)**:全局描述符表是系统级别的数据结构,它为整个操作系统定义段描述符。所有的进程和线程都共享同一个GDT。 - **LDT(Local Descriptor Table)**:局部描述符表是特定于进程的数据结构,每个进程可以有自己的LDT,用于定义该进程特有的段描述符。 2. **权限和隔离**: - **GDT**:由于GDT是全局的,它通常包含操作系统核心代码和数据的段描述符,这些描述符通常具有较高的权限级别。 - **LDT**:LDT允许进程拥有自己的段描述符,这有助于实现进程间的内存隔离。每个进程的LDT可以有不同的权限设置,从而提供更细粒度的访问控制。 3. **内容**: - **GDT**:GDT通常包含代码段、数据段、任务状态段(TSS)、门描述符等。 - **LDT**:LDT通常包含该进程特定的代码段、数据段、资源段等。 4. **使用方式**: - **GDT**:操作系统在启动时初始化GDT,并在进程切换时使用GDT中的描述符来加载新的段寄存器。 - **LDT**:进程在创建时可以创建自己的LDT,并通过特定的系统调用(如`set_thread_area`或`modify_ldt`)来加载和切换LDT。 5. **性能影响**: - **GDT**:由于所有进程共享GDT,频繁的GDT更新可能会影响系统性能。 - **LDT**:每个进程有自己的LDT,因此LDT的更新不会影响到其他进程,这可以在一定程度上减少系统开销。 6. **安全性**: - **GDT**:由于GDT是全局的,对GDT的不当修改可能会影响整个系统的稳定性和安全性。 - **LDT**:LDT提供了额外的隔离层,即使一个进程的LDT被破坏,也不会影响到其他进程。 7. **操作系统支持**: - **GDT**:几乎所有的x86操作系统都支持GDT。 - **LDT**:现代操作系统对LDT的支持有所减少,因为现代操作系统更多地依赖于扁平的内存模型和页式内存管理,而不是传统的段式内存管理。 ## 特权级 在IA32的分段机制下,操作系统总共有**4个特权级**,从高到低分别是0、1、2、3。**数字越小表示的特权级越大**。 <img src="https://y0k1n0-1323330522.cos.ap-beijing.myqcloud.com/image-20240912185040718.png" alt="image-20240912185040718" style="zoom:50%;" style=""> #### CPL `CPL`是**当前执行的程序或任务的特权级**。 - 通常情况下:`CPL`等于代码所在的段的特权级。**当程序转移到不同特权级的代码段时,处理器将改变`CPL`**。 - 遇到一致代码段:一致代码段可以被**相同或者更低特权级**的代码访问,当处理器访问一个与CPL特权级不同的一致代码段时,**`CPL`不会被改变**。 #### DPL `DPL`表示**段或者门的特权级** - 数据段:`DPL`规定了可以访问此段的**最低特权级** - 非一致代码段(无调用门):`DPL`规定**访问此段的特权级** - 调用门:`DPL`规定了当前执行的程序或任务可以访问此调用门的**最低**特权级 - 一致代码段和通过调用门访问的非一致代码段:`DPL`规定了访问此段的**最高特权级** - `TSS`:`DPL`规定了可以访问此`TSS`的**最低特权级** #### RPL 对于**非一致代码段**,处理器通过**检查`RPL`和`CPL`来确认一个访问请求是否合法** - `RPL`>`CPL`:比较目标段的`DPL`和当前`RPL` - `RPL`<`CPL`:比较目标段的`DPL`和当前`CPL` ## 特权级转移 #### `jmp`和`call`实现转移 - 规则: - 非一致代码段:`CPL`必须等于目标段的`DPL`,同时要求`RPL`小于等于`DPL` - 一致代码段:则要求`CPL`大于或者等于目标段的`DPL`,`RPL`不做检查,转移后`CPL`不会发生变化 - 缺点: - 对于非一致代码段,只能在相同特权级代码段之间转移 - 遇到一致代码段也最多能从低到高 #### 调用门 ##### 调用门结构 <img src="https://y0k1n0-1323330522.cos.ap-beijing.myqcloud.com/image-20240912191132232.png" alt="image-20240912191132232" style="zoom:67%;" style=""> ##### 调用门作用 笔者认为,调用门实际上充当一个中间人作用,使得**低特权级的代码段可以访问高特权级代码** 假设现在需要由`A代码段`使用`call`指令经`调用门G`访问`B代码段`,设如下几个标记: `CPL` `RPL_A` `DPL_B` `DPL_G` - B代码段为一致代码段: - `A代码段`访问`调用门G`: - 对于`调用门G`,`DPL_G`规定了访问此调用门的**最低特权级** - `CPL`$\le$`DPL_G`且`RPL_A`$\le$`DPL_G` - `调用门G`访问`B代码段`: - 对于一致代码段,`DPL_B`规定了访问此段的**最高特权级** - `DPL_B`$\le$`CPL` - B代码段为非一致代码段: - `A代码段`访问`调用门G`: - 对于`调用门G`,`DPL_G`规定了访问此调用门的**最低特权级** - `CPL`$\le$`DPL_G`且`RPL_A`$\le$`DPL_G` - `调用门G`访问`B代码段`: - **对于一致代码段使用`call`指令时,`DPL_B`规定了访问此段的最高特权级** - `DPL_B`$\le$`CPL`(如果使用`jmp`指令则`DPL_B`$=$`CPL`) 如图所示 <img src="https://y0k1n0-1323330522.cos.ap-beijing.myqcloud.com/image-20240912194306782.png" alt="image-20240912194306782" style="zoom:80%;" style=""> ### 有特权级变化时堆栈的变换 #### TSS **由于x86架构下,各个特权级之间不共享堆栈,因此程序共需要四个堆栈**.这些信息被存储在`TSS`数据结构中: <img src="https://y0k1n0-1323330522.cos.ap-beijing.myqcloud.com/image-20240912200103957.png" alt="image-20240912200103957" style="zoom:80%;" style=""> 可以看到,`TSS`中一共存储了3套`ss`和`esp`,分别对应**0特权级**到**2特权级**(由低特权级到高特权级切换时新堆栈才会从TSS中取得,所以TSS中没有位于最外层的堆栈信息) #### `call`过程堆栈变化 1. 根据目标代码段的`DPL`(新的`CPL`)从`TSS`中选择应该切换至哪个`ss`和`esp` 2. 从TSS中读取新的`ss`和`esp`。在这过程中如果发现`ss`、`esp`或者`TSS`界限错误都会导致异常 3. 对`ss`描述符进行检验,如果发生错误,同样产生异常 4. 暂时性地保存当前`ss`和`esp`的值 5. 加载新的`ss`和`esp`(用新的`ss`和`esp`去替换旧的) 6. 将刚刚保存起来的`ss`和`esp`的值压入新栈 7. 从调用者堆栈(原堆栈)中将参数复制到被调用者堆栈(新堆栈)中,复制参数的数目由调用门中`Param Count`一项来决定。如果Param `Count`是零的话,将不会复制参数 8. 将当前的`cs`和`eip`压栈 9. 加载调用门中指定的新的`cs`和`eip`,开始执行被调用者过程。 <img src="https://y0k1n0-1323330522.cos.ap-beijing.myqcloud.com/image-20240912203234672.png" alt="image-20240912203234672" style="zoom:80%;" style=""> #### `ret`过程堆栈变化 1. 检查保存的`cs`中的`RPL`以判断返回时是否要变换特权级 2. 弹出并加载被调用者堆栈上的`cs`和`eip`(指向`call`语句的下一条命令),并会进行代码段描述符和选择子类型和特权级检验 3. 如果`ret`指令含有参数,则增加`esp`的值以跳过参数,然后`esp`将指向被保存过的调用者`ss`和`esp`。 4. 弹出并加载`ss`和`esp`,切换到调用者堆栈,被调用者的`ss`和`esp`被丢弃。在这里将会进行`ss`描述符、`esp`以及`ss`段描述符的检验 5. 如果`ret`指令含有参数,增加`esp`的值以跳过参数(此时已经在调用者堆栈中) 6. 检查`ds`、`es`、`fs`、`gs`的值,如果其中哪一个寄存器指向的段的`DPL`小于`CPL`(此规则不适用于一致代码段),那么一个空描述符会被加载到该寄存器。 <img src="https://y0k1n0-1323330522.cos.ap-beijing.myqcloud.com/image-20240912203247364.png" alt="image-20240912203247364" style="zoom:80%;" style=""> 最后修改:2024 年 09 月 12 日 © 允许规范转载 赞 如果觉得我的文章对你有用,请随意赞赏