标签:
之前做了那么多铺垫,我们终于可以看看第14章的代码了。
对于引导代码和用户程序,依然采用第13章的;对于内核程序(c14_core.asm),编译的时候有几行报错了,只要加上dword
即可解决。
在第13章,为了能使用内核提供的例程,用户程序是用call far
指令直接转移到内核例程(非一致代码段)。因为CPL=目标代码段描述符的DPL=RPL=0
,符合下面表格的条件,所以转移是没有问题的。
但是在本章,用户程序工作在3特权级,而非0特权级,所以是无法直接转移的。不过也不用悲观,我们还是有办法的,可以通过调用门来转移。
关于调用门的知识,可以参考我的博文:调用门详解
调用门的格式如下图:
811 ;以下开始安装为整个系统服务的调用门。特权级之间的控制转移必须使用门
812 mov edi,salt ;C-SALT表的起始位置
813 mov ecx,salt_items ;C-SALT表的条目数量
814 .b3:
815 push ecx
816 mov eax,[edi+256] ;该条目入口点的32位偏移地址
817 mov bx,[edi+260] ;该条目入口点的段选择子
818 mov cx,1_11_0_1100_000_00000B ;特权级3的调用门(3以上的特权级才
819 ;允许访问),0个参数(因为用寄存器
820 ;传递参数,而没有用栈)
821 call sys_routine_seg_sel:make_gate_descriptor
822 call sys_routine_seg_sel:set_up_gdt_descriptor
823 mov [edi+260],cx ;将返回的门描述符选择子回填
824 add edi,salt_item_len ;指向下一个C-SALT条目
825 pop ecx
826 loop .b3
先复习一下过程make_gate_descriptor
.
331 make_gate_descriptor: ;构造门的描述符(调用门等)
332 ;输入:EAX=门代码在段内偏移地址
333 ; BX=门代码所在段的选择子
334 ; CX=段类型及属性等(各属
335 ; 性位都在原始位置)
336 ;返回:EDX:EAX=完整的描述符
816~821:调用过程make_gate_descriptor
构造调用门(请参考调用门的格式),P=1,DPL=3,参数个数=0;
822:调用过程set_up_gdt_descriptor
把构造好的调用门安装到GDT中,返回对应的选择子(TI=0,RPL=0);
264 set_up_gdt_descriptor: ;在GDT内安装一个新的描述符
265 ;输入:EDX:EAX=描述符
266 ;输出:CX=描述符的选择子
823:将返回的调用门选择子回填,覆盖原先的段选择子。如下图(下图是内核符号表中一个表项的示意图)所示:
828 ;对门进行测试
829 mov ebx,message_2
830 call far [salt_1+256] ;通过门显示信息(偏移量将被忽略)
表面上,这是一个间接绝对远调用,通过指令中的内存地址,可以间接取得32位偏移量和16位的代码段选择子;但是,处理器在执行这条指令的时候,会用选择子访问GDT,结果发现是一个调用门,所以忽略32位的偏移量(上图中的绿色部分)。
调用门安装完成后,GDT的示意图如下:
不仅间接绝对远调用是这样,直接绝对远调用也是这样,如果选择子指向的是调用门,偏移量也会被忽略。例如
call 0x0040:0x00001234
结合上图,因为0x40处是调用门,所以偏移0x00001234被忽略。
加载程序并创建一个任务,需要用到很多数据,比如程序大小、加载位置等等。内核应当为每一个任务创建一个内存区域,来记录任务的信息和状态,这个内存区域就称为任务控制块(Task Control Block,TCB)。
需要说明的是:TCB不是处理器的要求,而是我们为了自己方便而发明的。
关于TCB的结构,如原书图14-12(P264)。为了读者方便,我在这里把图再绘制一遍。
请注意,这个格式是作者发明的,并不是说TCB就必须是这种格式。
为了能够追踪所有的任务,可以把每个TCB串起来,形成一个链表。
在代码的核心数据段中,声明了标号tcb_chain
,初始化了一个双字,值为0.
413 ;任务控制块链
414 tcb_chain dd 0
其实,这相当于一个指针,用来指向第一个任务的TCB。当它为0时,表示没有任务。所有任务都按照被创建的先后顺序链接在一起形成一个无头单向非循环链表。
835 ;创建任务控制块。这不是处理器的要求,而是我们自己为了方便而设立的
836 mov ecx,0x46
837 call sys_routine_seg_sel:allocate_memory
838 call append_to_tcb_link ;将任务控制块追加到TCB链表
以上三行用于分配TCB的空间(0x46字节),然后把这个TCB挂到链表上(尾插法)。
840 push dword 50 ;用户程序位于逻辑50扇区
841 push ecx ;压入任务控制块起始线性地址
842
843 call load_relocate_program
以上三行用于加载和重定位用户程序。
464 load_relocate_program: ;加载并重定位用户程序
465 ;输入: PUSH 逻辑扇区号
466 ; PUSH 任务控制块基地址
467 ;输出:无
468 pushad
469
470 push ds
471 push es
472
473 mov ebp,esp ;为访问通过堆栈传递的参数做准备
这是过程load_relocate_program
开头的几行,执行完第473行后,栈的状态如下图所示:
这里主要是复习如何用栈传递参数。需要说明的是:
1. 用EBP寄存器来寻址的时候,默认使用段寄存器SS;
2. 在32位模式下,栈操作的默认操作数大小是双字;
3. 处理器执行压栈指令的时候,如果发现操作数是段寄存器,则将段寄存器的16位值扩展为32位(高16位全0),然后执行压栈操作;出栈指令执行相反的操作,将32位的值截断,仅保留低16位,并传送到相应的段寄存器;
4. 由于load_relocate_program
是通过32位近调用进入的(第843行),所以只压入EIP的内容,没有压入CS;
475 mov ecx,mem_0_4_gb_seg_sel
476 mov es,ecx
477
478 mov esi,[ebp+11*4] ;从堆栈中取得TCB的基地址
以上三行执行完后,ES指向0-4GB数据段;ESI指向TCB的基地址。
GDT一般用于存放全局空间的段描述符。对于任务私有的段描述符,也可以放在GDT中,但是最好放在自己私有的LDT中。
480 ;以下申请创建LDT所需要的内存
481 mov ecx,160 ;允许安装20个LDT描述符
482 call sys_routine_seg_sel:allocate_memory
483 mov [es:esi+0x0c],ecx ;登记LDT基地址到TCB中
484 mov word [es:esi+0x0a],0xffff ;登记LDT初始的界限到TCB中
read_hard_disk_0
加载用户程序;用户程序的头部的格式和第13章完全相同。
这个过程和第13章的基本相同。注意,用户符号表中的调用门选择子,其RPL=3;
通过调用门的控制转移,有可能会改变CPL。如果通过调用门把控制转移到了更高特权级的非一致代码段中,那么CPL就会被设置为目标代码段的DPL值,并且会引起堆栈切换。
为此,必须为每个任务定义额外的栈。对于我们的用户任务,需要为它创建特权级0、1、2的栈。而且,这些栈应当在LDT中有对应的段描述符。
这些栈是内核为用户程序动态创建的,而且需要登记在TSS中,以便处理器固件能够自动访问到它们。不过目前我们还没有创建TSS,所以,有必要先将这些栈的信息登记在TCB中暂时保存(如下图)。
创建x(x=0,1,2)特权级的栈的步骤如下:
1. 申请内存,为栈分配空间;
2. 在LDT中创建栈段描述符(DPL=x);
3. 在TCB中登记栈的信息,包括栈的大小、基地址、选择子(RPL=x)以及ESPx的初始值(=0);
我觉得栈的大小和基地址的登记是没有必要的,因为TSS中不需要填写这些字段。
囿于篇幅,本文就到这里。劳逸结合,休息一下…
【未完待续】
任务和特权级保护(二)——《x86汇编语言:从实模式到保护模式》读书笔记32
标签:
原文地址:http://blog.csdn.net/longintchar/article/details/51472284