《x86 汇编语言从实模式到保护模式》
第15章:任务切换
读书笔记
任务切换的原理
任务切换的两种类型
协同式
以这种方式切换任务,需要当前执行的任务主动交出任务的控制权,或者在任务通过调用门执行系统调用的时候,操作系统趁机执行任务切换。
这样的方式要求每个恩物都具有“自律性”,但是如果一个任务中有一个while(1){}
这样的死循环,那么将无法切换的到别的任务中执行。
抢占式
这是比较常用的任务切换方式,操作系统通过定时器中断强制挂起当前任务的执行,从而执行任务切换。
任务切换的具体方法
通过中断执行任务切换
这是现代抢占式多任务的基础。可以通过设置定时时钟中断让每个任务平等地获得执行时间。
实模式下,中断向量表存储在内存地址的最低1KB的位置,中断发生时,将中断号乘以4就是中断服务程序地址在中断向量表中的偏移(乘以4的原因是实模式下的段:偏移地址为4字节,因此中断向量表的每一项都是4字节的)
保护模式下,不再使用内存的最低1KB来存储中断向量,而是使用中断描述符表。中断描述符表和GDT,LDT一样,都是用于存储段描述符,只不过中断描述符表用于存储门描述符,包括中断门、任务门和陷阱门。中断门、任务门和陷阱门和上一章见过的调用门十分类似(用于特权级检查?)
中断发生时,处理器将中断号乘以8(因为保护模式下每个中断门描述符长度为8字节)获得门描述符在中断向量表中的偏移,如果发现是个任务门描述符,那么就要发生任务切换(保护现场,切换到目标任务中执行);如果是其它门描述符,则执行相应的任务内部的控制流转移。
任务门描述符的格式如下:
markdown-img-paste-20171224193725396.png任务门描述符格式十分简单,最重要的就是TSS选择子的字段,毕竟任务门的主要任务就是为了找到将于将要切换到的任务在GDT表中的索引。
书中说,如果任务切换是由中断引起的,那么任务门中的DPL字段就失去作用,处理器将不按特权级施加任何保护;但是如果以非中断的方式调用任务门(任务门描述符也可以安装在GDT或者LDT中),那么就会加入特权级保护
EFLAGS中NT位在中断引起的任务切换中的作用
当中断引起任务切换后(中断号索引-> 任务门中获取TSS选择子->查GDT->找到目标TSS),处理器将会把当前任务的状态保存到当前任务的TSS段中(TSS段基址在TR寄存器中),之后将目标TSS中的内容恢复到当前任务的各个寄存器中,最后将TR寄存器的值改为目标任务的TSS基址,处理器固件将TSS描述符中的B位置1,任务切换完成,处理器进入新任务执行。
我们知道中断描述符表中除了任务门还有陷阱门和中断门,而从每个中断返回时都需要用iret指令,这就引出了问题:
-
从任务门中使用iret返回,需要返回到被中断的任务
-
从陷阱门和中断门返回,是返回到同一任务内的不同代码段
这就需要处理器使用一个标志位来标记当前执行的中断程序是否是一个嵌套的任务。这个标志位就是EFLAGS中的NT(Nested Task Flag)标志位。
NT位为1,标志当前任务是一个嵌套的任务,是由另一个任务通过中断方式切换而来的,如果在这个任务中执行iret指令,将会通过当前任务TSS中的任务链接域找到被中断的任务,进行任务切换到前一个任务继续执行,同时把新任务的TSS描述符B位置1.任务切换时,会将当前任务的EFLAGS中的NT位和B位置0。
NT位为0,则表示这是一个正常的、同一任务内的中断转移,这时iret只要返回到同一个任务上下文中触发中断的语句处继续执行即可。
处理器不论何时遇到iret,都会检查EFLAGS中的NT标志位,以确定是执行任务切换还是任务内跳转。
通过call执行任务切换
由于任务门描述符可以安装在GDT和LDT中,因此可以直接通过call/jmp这样的指令进行调用。
call指令引起的任务切换和中断引起的任务切换一样,是嵌套的。
当程序控制流从旧任务进入新任务时,不会改变旧任务的B位,而是直接将B为存储到旧任务的TSS中,这么做的原因是任务是不可重入的,简单来说就是处理器不允许切换进入TSS中B位为1的任务,因为假设允许这么做,那么就可以从一个任务切换进入一个用户自身,那么TSS将无从存储和恢复。因此,将嵌套任务链中的任务的TSS中的B位都置为1,能够防止新任务切换近旧任务,造成任务链的混乱。
通过call进入的任务切换也需要通过iret指令返回到前一个任务,和中断的情况一样,切换回前一个任务时,当前任务的TSS中的B位和EFLAGS中的NT位都会被清零,以保证别的任务切换进来并能够正常切回。
通过jmp执行任务切换
jmp方法和前面的两种方法不同,它引起的任务切换不会形成任务间的嵌套关系,切换时除了正常的现场保护外,只需要将新任务TSS的B为置1,旧任务的TSS的B为清0即可。
不论是通过jmp、call还是中断进行的任务切换,处理器都会检查TSS的B位来检测重入。
任务切换实例
准备工作(基本和前一章一致)
内核代码基本和上一章一致,只不过在控制流从内核程序进入用户程序是通过任务切换的方式而不是上一章中的伪造调用门返回的方式。
内核首先初始化系统例程的调用门并更新到C-SALT表中,具体过程和上一章一致。因为本章需要从内核以任务切换的方式进入用户任务,因此首先要为内核程序自己建立一个TSS,并将这个TSS记录到GDT中。
markdown-img-paste-20171224222518712.png之后将内核TSS的选择子用ltr指令加载到tr寄存器中,这样可以看作程序已经在内核任务中运行了(因为本来就在内核中运行,只不过用TSS记录了任务切换所需的必要信息)。
ltr指令执行之后,查看TSS的内容如下:
markdown-img-paste-2017122422294247.png看到除了TSS基址和一些标志位外,所有的值都是0.
程序继续执行,先是和上一章一样,调用load_relocate_program
从硬盘中加载用户程序到内存,并且创建用户程序的TSS段注册到GDT表中。不同的是,由于这次要使用任务切换的方式切换到任务程序,因此任务程序的初始化TSS表应该要内容详尽,因为TSS的值在任务切换时将要真正写到实际寄存器中。
使用call指令从内核任务切入用户任务
加载完成后使用call方式进行任务切换:
call far [es:ecx+0x14]
操作数是TCB表中的值,经过查看TCB表结构可知,这是TSS基址和TSS段选择子的位置,实际上这条命令等价于call far TSS段选择子:TSS基址
。这是合法的操作,因为call指令的操作数如果是一个TSS的选择子,那么处理器就会执行任务切换,直接从TSS中恢复现场,而忽略冒号后面的偏移值。
call语句执行后的TSS如下:
markdown-img-paste-20171224231508370.png可以看到此时tr执行的基址已经切换到0x104550(之前是0x100000),任务切换发生,观察到当前tss中各段寄存器中的RPL位都是3,EFLAGS中的值中NT位为0(第14位)(为什么不是1?下面会给出答案),EFLAGS的格式如下:
markdown-img-paste-20171224234616334.png查看TSS对应的内存地址,可以得到任务链接域的值是0x60,对应GDT表中的第14项:
markdown-img-paste-20171224233114883.png当前GDT表内容如下:
markdown-img-paste-20171224232608949.png观察到由于call指令执行的任务切换是嵌套的,因此切换到用户任务后两个TSS段都是busy的,此时除了从嵌套的任务中返回,不允许其它任务切换到这两个任务中。
从使用call指令进入的任务中返回
markdown-img-paste-20171224233723362.png用户程序执行到这里,将会通过调用门进入内核代码段,但是这时并没有发生任务切换
call执行前的TSS:
markdown-img-paste-20171224234000752.pngcall执行后的TSS:
markdown-img-paste-2017122423404012.png因为任务切换需要通过任务门或者对TSS的调用或者从嵌套任务中返回。于是我们来到内核代码:
markdown-img-paste-20171224234157356.png这个代码的作用是结束当前程序,由于现在还是在用户任务中,需要返回到内核任务才能结束用户任务。上面这段代码的主要用途是根据NT位的值判断判断以何种方式切换回内核任务。如果NT位为1,说明当前任务是嵌套的,因此需要iret切换到前一个任务;如果NT为0,说明当前任务不是嵌套的,因此直接jmp(或者call)到内核任务即可。
上面的代码中的prgman_tss字段在初始化内核时初始化,用于存储内核任务的TSS基址。因此可以直接jmp过去而不用填写偏移地址。
markdown-img-paste-20171224234927314.png在执行
pushfd
mov edx,[esp]
add esp,4
之后,edx中的值存储的是当前EFLAGS的值:
markdown-img-paste-20171224235530483.png发现现在EFLAGS的值是0x4046,NT位值为1!
再看了看现在的TSS,发现TSS中EFLAGS字段的值依然是0x46,NT位为0:
markdown-img-paste-20171224235705916.png由此可以得知NT位的值并不是任务切换后直接改写到TSS中的,而是在从当前任务切换到其它任务时才跟着EFLAGS写回到TSS中保存的。
执行完NT值的判断后,程序在屏幕上打出了提示信息:
因此接下来通过iretd切换回内核任务,切换后的TR指向的TSS如下:
markdown-img-paste-20171225000145796.png看到tr寄存器中存储的是GDT表第12项的索引,RPL是0,再观察到当前PC指向的是之前切入用户任务的下一条指令:
切入用户任务前:
markdown-img-paste-20171225000854492.png切入用户任务后:
markdown-img-paste-2017122500072802.png因此确定任务成功切换回内核任务,运行一条call printString
之后,屏幕打出提示信息:
接着程序将用jmp命令切换进入另一个任务中。
需要注意的是,内核中并没有回收第一个用户任务的内存空间(如TSS和TCB以及一些其他的用户段),因此可以从内核或者别的任务中重新切回第一个用户任务。
使用jmp指令进行任务切换
使用allocate_memory
申请一块新内存,作为新任务的TCB,加入TCB链的末尾。
过load_relocate_program
指令加载用户程序到内存,并初始化(TSS的建立和安装,TCB表的填充等),注意到这里是从相同的逻辑扇区读取相同的程序进入内存建立一个新的任务,加深了我对“任务是程序在内存中的副本”这一概念的理解。
新任务建立好之后,查看GDT,发现新的TSS已经安装:
markdown-img-paste-20171225002619565.png顺便观察到此时只有内核任务的TSS是Busy,其余两个都是Available.
通过jmp 目标任务的段选择子
实现任务切换。
切换前TSS:
markdown-img-paste-20171225002716649.png切换后TSS:
markdown-img-paste-20171225002743406.pngtr值显然改变为了新任务的TSS段选择子,查看当前各寄存器的值可以看到EFLAGS中的NT值仍然为0,说明这不是嵌套的任务:
markdown-img-paste-20171225002941552.png观察TSS基址的任务链域,可以看到是0,也表明没有前一个任务:
markdown-img-paste-20171225003055192.png执行新的用户程序,将会在屏幕上打印新的提示信息:
markdown-img-paste-20171225003259114.png从jmp到的任务中切回内核任务
新的用户程序代码执行完成之后,和上面一样会使用调用门进入内核例程(但是还在用户任务中),判断是否是嵌套任务,在这里不是嵌套任务,因此使用jmp切回内核任务,切换前,屏幕中打印出提示:
markdown-img-paste-20171225003725401.png切换前的TSS:
markdown-img-paste-20171225003759261.png切换后的TSS:
markdown-img-paste-20171225003819720.png切换回内核后,控制流到达切换到第二个任务的那条jmp语句的下一条语句:
markdown-img-paste-20171225003915300.png接着是打印提示信息,停机:
markdown-img-paste-20171225003953392.png屏幕中显示:
markdown-img-paste-2017122500401778.png
网友评论