任务和特权级保护(三)——《x86汇编语言:从实模式到保护模式》读书笔记34
5.2.7 在GDT中创建LDT描述符
处理器要求在GDT中安装每个LDT的描述符。当要使用这些LDT时,可以用它们的选择子来访问GDT,找到LDT描述符并把它加载到LDTR寄存器。
675 ;在GDT中登记LDT描述符
676 mov eax,[es:esi+0x0c] ;LDT的起始线性地址
677 movzx ebx,word [es:esi+0x0a] ;LDT段界限
678 mov ecx,0x00408200 ;LDT描述符,特权级0
679 call sys_routine_seg_sel:make_seg_descriptor
680 call sys_routine_seg_sel:set_up_gdt_descriptor
681 mov [es:esi+0x10],cx ;登记LDT选择子到TCB中
LDT描述符的格式如下图:
LDT本身也是一种特殊的段,最大尺寸为64KB;
G:粒度位,用来表示LDT的界限值是以字节为单位还是以4KB为单位;
D:固定为0;
L:固定为0;
AVL和P:含义与存储器的段描述符相同;
S:固定为0;表示系统的段描述符或者门描述符,以区别于存储器的段描述符(S=1);
TYPE:固定为0010b,表示这是一个LDT描述符;
第678行:我认为是作者的笔误,应该是
678 mov ecx,0x00008200
第681行:将返回的选择子(RPL=0)登记到TCB中;
5.2.8 为用户程序创建TSS
关于TSS的知识,可以参考我的博文TSS详解 ——《x86汇编语言:从实模式到保护模式》读书笔记33
684 mov ecx,104 ;tss的基本尺寸
685 mov [es:esi+0x12],cx
686 dec word [es:esi+0x12] ;登记TSS界限值到TCB
687 call sys_routine_seg_sel:allocate_memory
688 mov [es:esi+0x14],ecx ;登记TSS基地址到TCB
以上代码做的工作有:
1. 申请TSS所需的空间(104字节);
2. 登记TSS的界限值(104-1=103)和基地址到TCB;
接下来,填写TSS表格的内容。
1. Previous Task Link = 0;
2. 填写ESPx和SSx(x=0,1,2)(从TCB中拷贝);
3. 填写LDT选择子(从TCB中拷贝);
4. 填写I/O位图偏移(从TCB中拷贝,等于TSS的界限值103);
5. T=0;
5.2.9 在GDT中创建TSS描述符
和LDT一样,处理器要求必须在GDT中安装TSS的描述符。
719 ;在GDT中登记TSS描述符
720 mov eax,[es:esi+0x14] ;TSS的起始线性地址
721 movzx ebx,word [es:esi+0x12] ;段长度(界限)
722 mov ecx,0x00408900 ;TSS描述符,特权级0
723 call sys_routine_seg_sel:make_seg_descriptor
724 call sys_routine_seg_sel:set_up_gdt_descriptor
725 mov [es:esi+0x18],cx ;登记TSS选择子到TCB
第722行:我觉得是作者的笔误,应该是0x00008900. G=0,P=1,DPL=0,B=0;
第725行:登记TSS的选择子(RPL=0)到TCB;
以上5.2节的内容,算是把过程load_relocate_program
说完了,接下来说如何转到户程序执行。
6. 如何转到用户程序执行
任务寄存器TR
总是指向当前任务的TSS,而LDTR
寄存器也总是指向当前任务的LDT。TSS是任务的主要标志,因此要使TR
寄存器指向当前任务;而使用LDTR
的原因是可以在任务执行期间加速对段的访问。
在多任务环境中,从旧任务切换的新任务的时候,TR
和LDTR
寄存器的值都会更新,以指向新任务。但是,目前我们只有一个任务,而且是特权级为3的任务,我们遇到的问题可以表述为:如何从任务的全局空间(处于特权级0)转移到它自己的局部空间(处于特权级3)?
答案是分为两步:
1. 确立身份,使TR
和LDTR
寄存器指向这个任务;
2. 假装从调用门返回;
6.1. TR
和LDTR
寄存器
TR
和LDTR
寄存器都包括16位的选择器部分和描述符高速缓存器部分(如下图所示)。选择器部分的内容是TSS和LDT描述符的选择子。
6.2. ltr
和lldt
指令
加载任务寄存器TR
的指令是ltr
,其格式为
ltr r/m16
- 这条指令的操作数是通用寄存器或者16位的内存单元,里面的内容是16位的TSS选择子。
- 执行这条指令后,处理器用选择子访问GDT,找到TSS描述符,将基地址、段界限、段属性加载到描述符高速缓存器中,同时将该描述符中的B位置1,但并不执行任务切换。
- 该指令属于特权指令,只能在0特权级下执行。
- 该指令不影响EFLAGS的任何标志位。
加载局部描述符表寄存器LDTR
的指令是lldt
,其格式为
lldt r/m16
原理与上述类似,只不过操作数是16位的LDT选择子。
848 mov eax,mem_0_4_gb_seg_sel
849 mov ds,eax
850
851 ltr [ecx+0x18] ;加载任务状态段
852 lldt [ecx+0x10] ;加载LDT
注意,851~852中的ECX的值是TCB的基地址。以上的代码执行完,用户任务的身份算是确立了。
6.3. 假装从调用门返回
854 mov eax,[ecx+0x44]
855 mov ds,eax ;切换到用户程序头部段
以上两行,向DS中加载用户程序头部段的选择子(RPL=3,TI=1)。
857 ;以下假装是从调用门返回。摹仿处理器压入返回参数
858 push dword [0x08] ;调用前的堆栈段选择子
859 push dword 0 ;调用前的esp
860
861 push dword [0x14] ;调用前的代码段选择子
862 push dword [0x10] ;调用前的eip
863
864 retf
这段代码要结合用户程序头部的格式和调用门的返回过程来分析。
用户程序头部的格式如下图(为了方便阅读,我总是乐此不疲地贴图)。
注意,被内核重定位后的选择子,其RPL的值都等于3.
关于调用门的返回过程,强烈建议复习一下我的博文调用门详解
这篇博文中讲解的从调用门的返回过程是:
- 检测被调用者栈中CS寄存器的RPL字段值,以确定在返回时特权级是否发生改变。
- 弹出并使用被调用过程栈上的值加载EIP和CS寄存器。在此过程中会对代码段描述符和代码段选择子的RPL进行特权级与类型检查。
- 如果远返回指令是带参数的,则将参数和ESP寄存器的当前值相加,以跳过被调用者栈中的参数部分,最后的结果是ESP寄存器指向调用者SS和ESP的压栈值。注意,
retf
指令的参数必须等于调用门中所有参数的总字节数之和。- 如果返回时需要改变特权级,则从栈中将ESP和SS弹出,并把值代入寄存器ESP和SS,切换到调用者的栈。
- 如果远返回指令是带参数的,则将参数和ESP寄存器的当前值相加,以跳过调用者栈中的参数部分,最后的结果是调用者的栈恢复平衡。
- 如果返回时需要改变特权级,则检查DS,ES,FS和GS的内容,如果段选择子指向数据段或者非一致代码段且段描述符的DPL在数值上小于返回后的新CPL,那么就把数值0传送到该段寄存器。
858~862执行完后,栈的布局如下(这其实是内核的栈,并不是用户的0特权级栈):
我们对着上面的返回步骤,一步一步来看。
1. 因为用户程序的CS寄存器中的RPL=3,所以在返回的时候特权级要发生改变。
2. 弹出用户程序的EIP和CS(绿色部分)加载EIP和CS寄存器。
3. 第864行的retf
指令不带参数,所以这步跳过。
4. 从栈中将用户程序的ESP和SS(蓝色部分)弹出,并把值代入寄存器ESP和SS,切换到调用者的栈(实际是用户程序的3特权级栈)。
5. 第864行的retf
指令不带参数,所以这步跳过。
6. 因为DS中的内容是用户程序头部段的选择子,其DPL=3,所以不会把数值0传到DS;至于ES、FS和GS,它们一般会指向内核数据段,其DPL=0,所以这些寄存器很可能被数值0加载,所以用户程序中应该对它们先初始化再引用。
囿于篇幅,就写到这里吧。下次我们正式进入用户程序的执行…