从实模式到保护模式笔记11任务切换
从实模式到保护模式笔记11任务切换
0x00 代码
本章代码基于上一章代码,因此不再全部展现,只列出各段中不同(如果没有不同,对应段将不展示)
;====================================================
SECTION sys_routine vstart=0
terminate_current_task:;终止当前任务的运行
;此例程由当前任务调用,算当前任务的一部分(全局空间)
pushfd;以32位模式压当前EFLAGES入栈(不论是16位还是32位模式下)
;该指令由编译器提供
mov edx,[esp]
add esp,4;这三条指令 等价于pushfd pop edx
;这么写只是为了展示32位中可以用[esp]
mov eax,core_data_seg_sel
mov ds,eax
test dx,0100_0000_0000_0000B ;看NT位是否为1
jnz .b1;为1则是嵌套任务,返回前一级任务(会自动从TSS里找到前一级任
;务TSS的选择子)
mov ebx,core_msg1;当前任务不嵌套,没有前一级,用jmp返回切换到
;任务管理器任务
call sys_routine_seg_sel:put_string
jmp far [prgman_tss];任务管理程序任务
.b1:
mov ebx,core_msg0
call sys_routine_seg_sel:put_string
iretd
...
sys_routine_end:
;===================================================
SECTION core_data vstart=0
...
;程序管理器的任务信息
prgman_tss dd 0 ;程序管理器的TSS基地址
dw 0 ;程序管理器的TSS描述符选择子
prgman_msg1 db 0x0d,0x0a
db '[PROGRAM MANAGER]: Hello! I am Program Manager,'
db 'run at CPL=0.Now,create user task and switch '
db 'to it by the CALL instruction...',0x0d,0x0a,0
prgman_msg2 db 0x0d,0x0a
db '[PROGRAM MANAGER]: I am glad to regain control.'
db 'Now,create another user task and switch to '
db 'it by the JMP instruction...',0x0d,0x0a,0
prgman_msg3 db 0x0d,0x0a
db '[PROGRAM MANAGER]: I am gain control again,'
db 'HALT...',0
core_msg0 db 0x0d,0x0a
db '[SYSTEM CORE]: Uh...This task initiated with '
db 'CALL instruction or an exeception/ interrupt,'
db 'should use IRETD instruction to switch back...'
db 0x0d,0x0a,0
core_msg1 db 0x0d,0x0a
db '[SYSTEM CORE]: Uh...This task initiated with '
db 'JMP instruction, should switch to Program '
db 'Manager directly by the JMP instruction...'
db 0x0d,0x0a,0
...
core_data_end:
;====================================================
SECTION core_code vstart=0
...
load_relocate_program:;加载并重定位用户程序
...
;创建用户程序的TSS
mov ecx,104 ;tss的基本尺寸
mov [es:esi+0x12],cx
dec word [es:esi+0x12] ;登记TSS界限值到TCB
call sys_routine_seg_sel:allocate_memory
mov [es:esi+0x14],ecx ;登记TSS基地址到TCB
;登记基本的TSS表格内容
mov word [es:ecx+0],0 ;反向链=0
mov edx,[es:esi+0x24] ;登记0特权级堆栈初始ESP
mov [es:ecx+4],edx ;到TSS中
mov dx,[es:esi+0x22] ;登记0特权级堆栈段选择子
mov [es:ecx+8],dx ;到TSS中
mov edx,[es:esi+0x32] ;登记1特权级堆栈初始ESP
mov [es:ecx+12],edx ;到TSS中
mov dx,[es:esi+0x30] ;登记1特权级堆栈段选择子
mov [es:ecx+16],dx ;到TSS中
mov edx,[es:esi+0x40] ;登记2特权级堆栈初始ESP
mov [es:ecx+20],edx ;到TSS中
mov dx,[es:esi+0x3e] ;登记2特权级堆栈段选择子
mov [es:ecx+24],dx ;到TSS中
mov dx,[es:esi+0x10] ;登记任务的LDT选择子
mov [es:ecx+96],dx ;到TSS中
mov dx,[es:esi+0x12] ;登记任务的I/O位图偏移
mov [es:ecx+102],dx ;到TSS中
mov word [es:ecx+100],0 ;T=0
mov dword [es:ecx+28],0 ;登记CR3(PDBR)
;访问用户程序头部,获取数据填充TSS
mov ebx,[ebp+11*4] ;从堆栈中取得TCB的基地址
mov edi,[es:ebx+0x06] ;用户程序加载的基地址
mov edx,[es:edi+0x10] ;登记程序入口点(EIP)
mov [es:ecx+32],edx ;到TSS
mov dx,[es:edi+0x14] ;登记程序代码段(CS)选择子
mov [es:ecx+76],dx ;到TSS中
mov dx,[es:edi+0x08] ;登记程序堆栈段(SS)选择子
mov [es:ecx+80],dx ;到TSS中
mov dx,[es:edi+0x04] ;登记程序数据段(DS)选择子
mov word [es:ecx+84],dx ;到TSS中。注意,它指向程序头部段
mov word [es:ecx+72],0 ;TSS中的ES=0
mov word [es:ecx+88],0 ;TSS中的FS=0
mov word [es:ecx+92],0 ;TSS中的GS=0
pushfd
pop edx
mov dword [es:ecx+36],edx ;EFLAGS
;在GDT中登记TSS描述符
mov eax,[es:esi+0x14] ;TSS的起始线性地址
movzx ebx,word [es:esi+0x12] ;段长度(界限)
mov ecx,0x00408900 ;TSS描述符,特权级0
call sys_routine_seg_sel:make_seg_descriptor
call sys_routine_seg_sel:set_up_gdt_descriptor
mov [es:esi+0x18],cx ;登记TSS选择 子到TCB
...
start:
...
;为程序管理器的TSS分配内存空间(程序管理器即指当前的内核任务代码)
mov ecx,104
call sys_routine_seg_sel:allocate_memory
mov [prgman_tss+0x00],cx;保存程序管理器的TSS基地址
;设置prgmanTSS的必要项目
mov word [es:ecx+96],0;没有lDT
mov word [es:ecx+102],103;没有I/O位图(也就是IO操作只看CPL和IOPL的关系)
mov word [es:ecx+0],0;反向链(上一级任务TSS选择子)为0
mov dword [es:ecx+28],0;登记CR3
mov word [es:ecx+100],0;T=0,不开启调试
;不需要 0、1、2特权级堆栈设置
;创建TSS描述符 并安装到GDT
mov eax,ecx;tss起始线性地址
mov ebx,103;界限
mov ecx,0x00408900;TSS描述符 特权级0
call sys_routine_seg_sel:make_seg_descriptor
call sys_routine_seg_sek:set_up_gdt_descriptor
mov [prgman_tss+0x04],cx;保存prgman的tss选择子
;将TR寄存器指向当前任务的TSS,也就是prgman
ltr cx
;从此认为prgman在执行中
mov ebx,prgman_msg1
call sys_routine_seg_sel:put_string
mov ecx,0x46
call sys_routine_seg_sel:allocate_memory
call append_to_tcb_link;新建一个空的的TCB并加到tcb链中(现在看来tcb
;链并没有卵用,估计是为以后抢占式按时切换做准备)
;返回的ecx是tcb线性基地址,在load_relocate_program中使用
push dword 50
push ecx;tcb基地址
call load_relocate_program;加载任务,把任务第一次运行时的TSS设置好了
;方便切换
call far [es:ecx+0x14];切换任务 call发起的切换,要看特权级cpl和dpl
;不改变旧任务busy位(仍为1)(描述符中) 不改变其nt位 新任务busy置1 nt置1(
;表示有上一级任务/有嵌套) 会把TSS里的 0偏移指向旧任务的TSS选择子
;重新加载并切换任务
mov ebx,prgman_msg2
call sys_routine_seg_sel:put_string
mov ecx,0x46
call sys_routine_seg_sel:allocate_memory
call append_to_tcb_link;又新建一个空的tcb
push dowrd 50
push ecx
call load_relocate_program;重新加载的 是同一个程序的一个新的任务
jmp far [es:ecx+0x14];执行任务切换
mov ebx,program_msg3
call sys_routine_seg_sel:put_string
hlt
core_code_end:
上述代码编译通过,运行正确
0x01 代码讲解
任务切换概述
多任务系统,是同时能够执行两个及以上任务的系统。即当前一个任务没有执行完,下一个任务也可以开始执行。什么时候切换到下一个任务,以及切换到哪一个任务,是由操作系统负责的,处理器只负责具体的切换过程,包括保护前一个任务的现场。
切换任务的方式有两种,一种是协同式,从一个任务切换到另一个任务,需要当前任务主动地请求暂时放弃执行权,或者在通过调用门请求操作系统服务时,由操作系统趁机将控制权转移到另一个任务。这种方式依赖于每个任务的“自律”性,当一个任务失控时,其他任务可能得不到执行的机会。
另一种是抢占式,利用定时器中断,在中断服务中实施任务切换。硬件中断信号会定时出现,不管处理器在做什么,中断都会适时地发生,任务切换顺利进行。每个任务能够获得平等的执行,一个任务的失控也不会影响其他任务。
上一章是特权级间的控制转移,它是任务内从全局空间切换到局部空间,并不算是任务间转移。如下图,所有任务共享全局空间,这是内核或操作系统提供的,包含系统服务程序(不只例程)和数据;局部空间时一个任务区别于其他任务的私有代码和数据:
同一个任务内,全局空间和局部空间具有不同的特权级别(通常),使用门,可以从3特权级的局部空间转移到0特权级的全局空间,以使用内核或者操作系统提供的服务。
系统任务prgman
而任务切换是以任务位单位,离开一个任务,转到另一个任务中执行。任务转移较为复杂,因为任务执行时,处理器的各个部分都和该任务相关:段寄存器指向该任务所使用的内存段;通用寄存器保存着该任务的中间结果。离开当前任务,转移到另一个任务开始执行,要保存旧任务的各种状态,并回复新任务的运行环境。
上一章我们是通过从任务的全局空间模拟调用门的方式切换岛任务的局部空间执行。显得很别扭,实际上我们可以进入保护模式后,创建和执行系统的0特权级任务,然后从该任务切换到其他任务,而不用管他们是哪个特权级别。
所以我们本章的内核代码段入口函数,先显示处理器品牌信息,以及安装每个任务的调用门。接下来就是创建0特权级的内核任务,并将当前正在执行的内核代码划归该任务。这个内核任务的作用是创建其他任务,管理它们,所以称为任务管理器。
TSS是一个任务存在的标志,没有它就无法执行任务切换,因为任务切换时需要保存旧任务的各种状态数据和恢复新任务的数据。所以我们先要申请创建TSS的内存。内核为了追踪prgman(任务管理)的TSS,需要在内核数据段保存prgman_tss的基地址和选择子。前32位基地址后16位选择子。
接着是对TSS进行填充,也就是设置任务下一次被执行时,恢复到的状态,由于prgman任务当前已经在执行,因此有些位置不用填写(处理器在从prgman切换出去时会自动填写),而有些位置必须要写,值得注意的是,由于prgman任务特权级为0,因此并没有额外栈需要填写。
mov word [es:ecx+96],0 ;没有LDT。处理器允许没有LDT的任务。
mov word [es:ecx+102],103 ;没有I/O位图。0特权级事实上不需要。
mov word [es:ecx+0],0 ;反向链=0
mov dword [es:ecx+28],0 ;登记CR3(PDBR)
mov word [es:ecx+100],0 ;T=0
接着就是创建TSS描述符和安装描述符到GDT
mov eax,ecx ;TSS的起始线性地址
mov ebx,103 ;段长度(界限)
mov ecx,0x00408900 ;TSS描述符,特权级0
;0000_0000_0100_0000_1000_1001_0000_0000
;G位0 表示字节粒度 D位1 32位 L位0 非64位 AVL不使用
;P位1 表示段存在内存中 DPL 00 特权级0 S位0 表示系统段(非代码 数据 栈)
;TYPE 10B1 表示TSS段描述符 B位默认为0
call sys_routine_seg_sel:make_seg_descriptor
call sys_routine_seg_sel:set_up_gdt_descriptor
mov [prgman_tss+0x04],cx ;保存程序管理器的TSS描述符选择子
最后将TR指向当前任务prgman TSS选择子,表明当前任务是prgman,该命令会用该选择子访问GDT,找到对应TSS描述符,将其TYPE中的B位置1,表明prgman正在执行,同时还要讲该描述符传送到TR高速缓存部分。
至此prgman任务创建并实际执行且表明在执行了。
任务切换的方法
处理器并没有提供额外的指令用于任务切换,用的是熟悉的老指令和老手段,但是扩展他们的功能,使之也能够实施任务切换操作。
有中断和call/jmp两种方式:
方式一是借助中断,这是抢占式多任务的基础。只要中断没有被屏蔽,就能随时发生,定时中断更是能够以准确的时间间隔发生,他可以强制实施任务切换。
实模式下,内存最低地址的1KB是中断向量表,保存着256个中断向量(中断例程对应的偏移地址和段地址)。当中断发生时,中断号*4,作为表内索引号访问中断向量表,去除地址,转移执行。
而保护模式下,不再使用中断向量表,取代的是中断描述符表(IDT)类似于GDT LDT,保存门描述符(中断门 陷阱门和任务门)。中断发生时,处理器用中断号 * 8(每个描述符占8字节)来访问IDT,取出门描述符,拿到线性地址,转移到对应位置执行。
一般中断处理例程在IDT里储存的是中断门和陷阱门,这样只是从任务的局部空间转移到更高特权级的全局空间执行一些系统级的管理工作(中断处理),本质是任务内的控制转移。
而如果在IDT里的是任务门,就会进行任务切换,中断当前任务的执行,保护现场,转移到另一个任务去执行。以下是任务门的描述符:
可以看到任务门描述符中很多位置未使用,主要是TSS描述符选择子和属性,P位在任务门描述符中表述该任务门是否有效,为0则无效,DPL是特权级,但是注意,如果是中断而发起,则无视任务门的DPL,但是任务门也有非中断调用,比如call/jmp far 任务门选择子,此时DPL起作用。
当中断发生时,处理器访问IDT,如果中断号对应的描述符是任务门,就取出任务门描述符,再取出TSS选择子,再用TSS选择子访问GDT,取出TSS描述符。先把当前任务状态保存到旧TSS中(TR指向的TSS),然后从新TSS中恢复各寄存器状态(包括LDTR),最后TR指向新TSS描述符。并将该TSS描述符的TYPE里的B位置1,表示任务忙,执行中。
然而,这种用中断执行的任务转移,也要使用iret(Interupt Return),而iret如何知道是常规中断返回还是任务切换中断返回呢?(这两种中断返回处理器要执行的操作不同)。
32位EFLAGS中有NT位(位14 第15位),意为Nested Task Flag,嵌套任务标志。如果是任务切换中断,那么切换到新任务后会把NT位置1(也会把新任务TSS描述符B位置1),因此返回时看NT位,为1即是任务切换中断的返回,为0则是常规中断返回。
任务中断返回会将NT重新置0,并且把TSS描述符B位置“0”,表示非忙,保存任务状态后,切换到被中断的任务。
方式二是CALL/JMP直接发起任务切换,操作数是任务的TSS描述符选择子或者任务门选择子:
call 0x0010:0x00000000
jmp 0x0010:0x00000000
当使用这种方式切换任务时,首先用指令中给出的TSS选择子访问GDT中的TSS描述符,如果权限通过(TSS本身也是一个数据段,访问TSS也需要比较CPL、RPL和其描述符的DPL,如果TSS描述符的DPL为0,那么就只有特权级0才能切换到该TSS对应的任务)。
上面的例子中,偏移量0x00000000将会被忽略,因为TSS选择子对应TSS里包含了所有寄存器的状态,所以EIP也会自行设置。
call和jmp发起任务切换的不同之处是,call指令发起的任务切换类似于中断发起的,都是嵌套的,也就是旧任务的TSS描述符的B位保持1不变,旧任务的EFLAGES(TSS中的)NT位不变;新任务的TSS的上一级域指向旧的TSS选择子,EFLAGS中NT位置1,表示是嵌套的,描述符B位置1。
而jmp则是非嵌套的,是完全的切换,旧任务的TSS描述符B位置0,不再忙,旧任务中的EFLAGS中的NT位置0,嵌套失效;新任务的B位置1, EFLAGS中NT为0, TSS中上一级域为0。
通过任务嵌套,call发起的任务切换,可以通过iret指令返回前一个任务,返回后旧任务(返回前的任务)的TSS描述符B位 以及EFLAGS中的NT位都置0。
任务是不可重入的,本质是一个TSS描述符B位为1的任务不能作为切换的目标。分为两种情况:
1不能从自己切换到自己
2如果一个任务作为自己的上一级,(也就是本任务嵌套于该任务)那么可以用iret返回到该任务,但是不能再用call或者中断切换任务到该任务,不然就是循环嵌套,A的上一级是B B的上一级是A。(对应到进程,就是子进程不能是父进程的进程)。
用CALL/JMP/IRET指令发起任务切换的实例
前面的代码中已经完成了prgman的任务的创建和载入了。现在开始加载用户程序并切换到任务程序:
首先加载用户程序,先分配一个TCB(task control block),然后将它挂到TCB链上,然后压如TCB基地址和逻辑扇区号,作为参数调用load_relocate_program。
load_relocate_program的工作过程和上一章差不多,只是对TSS的填写比较完整了(因为我们这次要以切换的方式开始其代码执行,因此要填写TSS使其切换后能正确的到代码的入口处)。当我们从prgram切换到用户程序,会从用户程序的TSS读取个寄存器的状态来执行用户程序,因此都要填写好,而其中CS、EIP决定了切换后从哪儿运行。
需要注意的是TSS中也要填EFLAGS 我们用的是pushfd pop edx 这样的方式从prgman任务中导出一份副本填写的EFLAGS。一般IOPL字段为0,所以切换到用户程序任务后CPL<IOPL,因此用户程序创建的任务均无法访问IO。
同样的在load_relocate_program中填写完tss后就创建并安装tss描述符到gdt中。并把选择子填到TCB中。
最后通过call far tss选择子的形式切换任务。处理器用选择子访问GDT,发现对应是一个TSS描述符,然后把当前寄存器值都保存在TR指示的TSS描述符的TSS中,然后从新TSS中读取各寄存器的值,最后将TR指向新TSS的描述符选择子,并加载高速缓存部分,处理器开始新任务的执行。因为我们已经在load_relocate_program中设置了各个段寄存器,所以我们在程序里,不用再初始化各个段寄存器了。
prgman是计算机启动的第一个任务,任务切换前,其TSS描述符B位1,EFLAGS中NT位0,因为本次切换call发起的,因此切换后,旧任务的描述符的B位仍是1,EFLAGS的NT位仍是0,而新任务的TSS描述符B位为1,EFLAGS中NT位为1,TSS中的上一级域保存旧任务(prgman)的tss选择子。
用户程序的任务通过调用门调用TerminateProgram例程,该例程根据当前任务(即是通过调用门,仍没有切换任务)的EFLAGS中的NT位来判断,当前任务是由CALL还是JMP切换来的,如果是CALL切换来的,那么返回切换时,就可以直接iret了,如果是JMP,那就直接切换到prgman。
注意我们用的是iretd指令,该指令是编译器提供的,该指令会不论在16位模式下还是32位模式下都使用32位的iret,即iret的操作数(如果有的话)会是32位的。
[bits 16]
iret ;CF
iretd ;66 CF
[bits 32]
iret ;CF
iretd ;CF
接下来的代码中我们又新创建了一个任务,并转到该任务执行(prgman任务来实现这些):
新任务与上次相比,仍然是从硬盘的50号逻辑扇区开始加载的,是同一个程序的不同任务。这次是用JMP指令发起的任务切换,新任务不会嵌套于旧任务中,任务切换后,prgman(旧任务)的TSS描述符的B位会被清零,EFLAGS的NT位不变;新任务TSS描述符的B位置1,EFLAGS的NT位不变(通常是0);新任务的TSS描述符上一级域不变(通常为0)
(个人认为call 发起的任务切换类似于进程树,所有父节点B位都为1,只能从根节点往父节点置0,而jmp则是独立的,新任务相对于旧任务独立,因此切换后旧任务B位清零,而新任务置1)。
这即是本章所有代码的流程。
处理器在任务切换时的操作
处理器使用4中方法进行控制转移:
①使用jmp/call TSS描述符选择子
②使用jmp/call 任务门描述符选择子
③中断 任务门描述符选择子
④iret返回
其具体过程如下:
①从jmp/call 指令的操作数、任务门或者当前任务的TSS任务链(上一级区)取得新任务的TSS描述符选择子。
②检查是否允许从当前任务(旧任务)切换到新任务。数据访问的特权级检查规则适用于jmp/call指令,旧任务的CPL和新任务的TSS描述符选择子RPL必须数值上小于或者等于目标TSS或任务门的DPL。异常、中断和iret指令引起的任务切换忽略目标任务门或者TSS描述符的DPL。而int n指令引起的中断,则要检查DPL。
③检查新任务的TSS描述符是否标记为有效(P位1),并且界限也有效(大于或者等于0x67)
④检查新任务是否可用,不忙(B=0,对于call/jmp/异常或者中断发起的任务切换)或者忙(B=1,对于以iret发起的任务切换)。
⑤检查当旧任务 新任务的TSS,以及所有在任务切换时用到的段描述符已经安排到系统内存中。
⑥如果任务切换是由jmp或者iret发起的,会将旧任务的B位置0,如果是call 异常 中断发起的,则B位不变。
⑦如果任务是iret发起的,处理器会清除旧任务的TSS中的EFLAGS中的NT位,如果是由call jmp 异常或者中断,则不变。
⑧保存旧任务的状态到TSS中。处理器从TR中找到当前TSS的基地址,然后将:通用寄存器、段寄存器的段选择子、EFLAGS以及EIP LDTR都保存到TSS
⑨如果任务切换时由call 异常中断发起的,处理器将新任务的EFLAGS中的NT位置1;如果是iret或者jmp指令发起的NT标志位则是从新任务TSS中EFLAGS中的NT位。
⑩如果任务切换由call jmp 异常 中断发起的,处理器将新任务的TSS描述符B位置1,如果是iret发起的,则新任务的B位保持原状态不变。
11.新任务的TSS选择子和TSS描述符加载到TR
12.新任务TSS状态数据加载到处理器,包括LDTR、PDBR(控制寄存器CR3)、EFLAGS、EIP、通用寄存器、段选择子。
13.与段选择子对应的描述符经过验证后加载到对应段寄存器的高速缓冲部分。
14.开始执行新任务。
新任务的执行有以下三点注意:
①切换到新任务后,会从TSS中恢复EIP,从EIP对应的那条指令开始执行。(EIP在该任务上一次被挂起时保存的)
②切换到新任务后,新 任务的特权级不是从旧任务继承来的,而是由恢复后的CS里的段选择子低2位决定的,因此是TSS决定的,所以对TSS的填充和修改,最好只能由操作系统和处理器进行。
0x02 检测题
1.该题目要实现的是 先从prgman内核任务 创建并切换到用户任务,这一点用call far TSS选择子即可,然后用户任务通过调用门调用系统例程Shutdown_Task,切换回来(判断NT位看用iret还是jmp far prgman的TSS选择子),需要注意的是,任务切换回去后,再通过call far TSS选择子再次切换回任务,会回到任务程序保存的上次执行到的位置,也就是用户任务的全局空间里的iret之后,所以我们在iret后面还要加上retf,回到用户任务的局部空间调用Shutdown_Task的位置!
返回prgman任务的系统例程修改如下:
terminate_current_task: ;终止当前任务
;注意,执行此例程时,当前任务仍在
;运行中。此例程其实也是当前任务的
;一部分
pushfd
mov edx,[esp] ;获得EFLAGS寄存器内容
add esp,4 ;恢复堆栈指针
mov eax,core_data_seg_sel
mov ds,eax
test dx,0100_0000_0000_0000B ;测试NT位
jnz .b1 ;当前任务是嵌套的,到.b1执行iretd
mov ebx,core_msg1 ;当前任务不是嵌套的,直接切换到
call sys_routine_seg_sel:put_string
jmp far [prgman_tss] ;程序管理器任务
retf;既然有调用门,就要用retf返回,不然切换到prgman后又切换到用户任务
;时,会切换到这里,没有retf就会在全局空间里跑飞
.b1:
mov ebx,core_msg0
call sys_routine_seg_sel:put_string
iretd
retf;既然有调用门,就要用retf返回,不然切换到prgman后又切换到用户任务
;时,会切换到这里,没有retf就会在全局空间里跑飞
prgman任务修改如下:
;现在可认为“程序管理器”任务正执行中
mov ebx,prgman_msg1
call sys_routine_seg_sel:put_string
mov ecx,0x46
call sys_routine_seg_sel:allocate_memory
call append_to_tcb_link ;将此TCB添加到TCB链中
push dword 50 ;用户程序位于逻辑50扇区
push ecx ;压入任务控制块起始线性地址
call load_relocate_program
call far [es:ecx+0x14] ;执行任务切换,用户任务A首次被执行,任务切
;换时要恢复TSS内容,所以在创建任务
;时TSS要填写完整
;重新加载并切换任务 B
mov ebx,prgman_msg2
call sys_routine_seg_sel:put_string
mov ecx,0x46
call sys_routine_seg_sel:allocate_memory
call append_to_tcb_link ;将此TCB添加到TCB链中
push dword 50 ;用户程序位于逻辑50扇区
push ecx ;压入任务控制块起始线性地址
call load_relocate_program
jmp far [es:ecx+0x14] ;执行任务切换,用户任务B首次被执行,任务切
;换时要恢复TSS内容,所以在创建任务
;时TSS要填写完整
;从任务B切换回来,继续执行
mov ebx,prgman_msg3
call sys_routine_seg_sel:put_string
;再次切换到任务A
mov ecx,[tcb_chain]
call far [es:ecx+0x14]
;从任务A切换回来,继续执行
mov ebx,prgman_msg4
call sys_routine_seg_sel:put_string
;再次切换到任务B
mov ecx,[tcb_chain]
mov ecx,[es:ecx];任务B的TCB基地址
call far [es:ecx+0x14]
mov ebx,end_msg
call sys_routine_seg_sel:put_string
hlt
用户程序代码修改如下:
;任务启动时,DS指向头部段,也不需要设置堆栈
mov eax,ds
mov fs,eax
mov eax,[data_seg]
mov ds,eax
mov ebx,message_1
call far [fs:PrintString]
call far [fs:TerminateProgram] ;退出,并将控制权返回到核心
mov eax,[fs:data_seg]
mov ds,eax
mov ebx,message_2
call far [fs:PrintString]
call far [fs:TerminateProgram]
值得注意的是,切换回prgman任务的例程TerminateProgram,在特权级0的全局空间将DS指向了内核数据段,所以返回特权级为3的用户局部空间时,ds会被置0,要使用用户任务里的数据段,要重新给ds赋值
上述代码编译通过,运行正确
2.思路要从prgman切换到任务A,再由A切换到B,所以任务A,B都要先加载好,把TSS描述符安装到GDT。
而任务切换要权限检查,TSS描述符都是特权级0,所以只能在任务A只能在特权级0的全局空间进行切换,所以我们修改TerminateProgram例程。让prgman以jmp far 切换到Task A,然后Task A调用TerminateProgram,在TerminateProgram 里检测到NT位为0,是jmp切换到当前任务(A)的,就直接call far 到Task B TSS描述符选择子,切换到任务B,然后任务B也调用TerminateProgram,TP例程检测NT位为1,是call切换到当前任务(B)的,就直接jmp far 到 prgman TSS描述符选择子。
注意我们不能用prgman call far到Task A再由Task A jmp far到Task B,再Task B切换到prgman,原因在于,我们从Task A切换到Task B的时候,Task A TSS描述符的B位置0了,但是其嵌套于prgman,而prgman的TSS描述符B位还是1,所以最后无法切换回到prgman。因此prgman->Task A必须要用jmp far,以便把prgman TSS描述符B位置0。(如果你愿意,你也可以用TP例程来手动置0,但是不可取,有失公正 :) )
所以我们要在内核数据段新建task_b_tss 用来保存task b的加载后的tss描述符选择子,方便例程TP使用。
TerminateProgram例程修改如下:
;-------------------------------------------------------------------------------
terminate_current_task: ;终止当前任务
;注意,执行此例程时,当前任务仍在
;运行中。此例程其实也是当前任务的
;一部分
pushfd
mov edx,[esp] ;获得EFLAGS寄存器内容
add esp,4 ;恢复堆栈指针
mov eax,core_data_seg_sel
mov ds,eax
test dx,0100_0000_0000_0000B ;测试NT位
jz .b1 ;由jmp far切换来的
mov ebx,core_msg1 ;当前任务不是嵌套的,直接切换到
call sys_routine_seg_sel:put_string
jmp far [prgman_tss] ;程序管理器任务
retf
.b1:
mov ebx,core_msg0
call sys_routine_seg_sel:put_string
call far [task_b_tss];切换到任务B
retf
prgman任务修改如下:
;现在可认为“程序管理器”任务正执行中
mov ebx,prgman_msg1
call sys_routine_seg_sel:put_string
mov ecx,0x46
call sys_routine_seg_sel:allocate_memory
call append_to_tcb_link ;将此TCB添加到TCB链中
push dword 50 ;用户程序位于逻辑50扇区
push ecx ;压入任务控制块起始线性地址
call load_relocate_program
;all far [es:ecx+0x14] ;执行任务切换,用户任务A首次被执行,任务切
;换时要恢复TSS内容,所以在创建任务
;时TSS要填写完整
;重新加载并切换任务
mov ebx,prgman_msg2
call sys_routine_seg_sel:put_string
mov ecx,0x46
call sys_routine_seg_sel:allocate_memory
call append_to_tcb_link ;将此TCB添加到TCB链中
push dword 50 ;用户程序位于逻辑50扇区
push ecx ;压入任务控制块起始线性地址
call load_relocate_program
mov cx,[es:ecx+0x18];TSS选择子
mov [task_b_tss+0x04],cx;任务B已经加载好了但是没有切换
;将其选择子保存在内核数据段的task_b_tss标号对应内存空间
mov ecx,[tcb_chain];拿到TCB链第一个TCB的基地址
jmp far [es:ecx+0x14];切换到第一个TCB对应的任务 task
hlt
任务代码修改如下:
;任务启动时,DS指向头部段,也不需要设置堆栈
mov eax,ds
mov fs,eax
mov eax,[data_seg]
mov ds,eax
mov ebx,message_1
call far [fs:PrintString]
call far [fs:TerminateProgram]
;调用TP例程 切换到TASK B或者
;prgman任务。
代码编译通过运行正确