任务和任务的创建

xiaoeryu Lv5

01. 任务:概念和组成

内核时对整个计算机系统进行管理,管理软件和硬件。内核可以加载用户程序,对用户程序进行重定位,用户程序终止后还可以回收用户程序的资源,在编程时位用户程序提供API。

将内核和用户程序可以看作一个整体:任务

在一个系统中只有一个内核,但可以有多个用户程序,即多任务系统。

GDT存放内核中的描述符,LDT存放每个任务中用户程序的描述符:

每个任务还需要一个任务状态段:TSS(Task Status Segment)

02. 使用任务控制块保存任务的基本信息

本节主要介绍了本章程序的结构,主要结构如下,本章程序有三个

  • 用户程序:c13_app1.asm
    1. 用户头部段
    2. 用户数据段
    3. 用户栈段
    4. 用户代码段
    5. 用户尾部段
  • 内核程序:c14_core.asm
    1. 定义常量存放选择子
    2. 内核程序头部段
    3. 内核公共例程段,就是一些例程和以前一样
    4. 内核数据段,一些文本信息、SALT
    5. 内核代码段,有内核入口点start、创建任务控制块TCB(Task Control Block)记录任务的信息
    6. 内核尾部段
  • 引导程序:c13_mbr0.asm
    1. 创建必要的段描述符
    2. 进入保护模式
    3. 加载内核
    4. 创建内核的段描述符
    5. 使用jmp far 指令跳转执行内核

03. 将任务控制块加入任务控制块链表

本节主要讲解内核代码段中的append_to_tcb_link,在内核数据段定义标号tcb_chain dd 0,是一个指针,用来保存第一个任务控制块的线性基地址,称为头指针,为0表示链表为空没有任务。

其中任务控制块TBC的结构如下:

任务控制链表:

清除TCB指针域:表明当前TCB是最后一个

判断链表中是否为空,为空表示这是第一个TCB

若链表不为空,顺着链表找到最后一个TCB

找到最后一个TCB,指向新的TCB,然后恢复压栈的内容并返回调用者:

04. 通过栈传递例程参数和立即数的压栈指令

接上一节,在创建TCB之后接着加载和重定位用户程序并创建一个任务。使用callload_relocate_program调用例程load_relocate_program,使用栈传递参数,此时的栈是在引导程序中定义的4K字节、基地址为0x7c00的栈。
代码如下:

         push dword 50                      ;用户程序位于逻辑50扇区
         push ecx                           ;压入任务控制块起始线性地址 
       
         call load_relocate_program  

其中立即数的压栈指令:

push imm8:

push imm16:

push imm32:

压栈可以不需要容器,即直接使用立即数,但是出栈需要有容器接着:

其中处理器使用32位操作尺寸,压入一个双字,0x00000055。出栈使用32位寄存器EDX或者是一个dword修饰的内存地址。这里压栈和出栈的尺寸必须一致,否则会影响栈平衡。

05. 段寄存器的压栈和出栈以及栈的随机访问机制

接上一节,进入load_relocate_program例程之后进行段寄存器的压栈操作:

load_relocate_program:                      ;加载并重定位用户程序
                                            ;输入: PUSH 逻辑扇区号
                                            ;      PUSH 任务控制块基地址
                                            ;输出:无 
         pushad
      
         push ds
         push es 

其中段寄存器的压栈操作:

段寄存器的出栈操作:pop cs指令非法

接着执行命令:
mov ebp,esp ;为访问通过堆栈传递的参数做准备

此时栈的分布:

使用EBP记录ESP的值,用来访问战中的参数,可以在不破坏栈顶指针ESP的情况下去访问栈中的其他数据

其中若使用BP或者EBP访问栈,那么默认的前缀是栈段SS

06. 创建任务的局部描述符LDT

任务共有的部分安装在GDT,私有的部分安装在LDT中。

接上一节,在load_relocate_program例程中:申请创建LDT所需要的内存、加载用户程序、判断整个用户程序的尺寸,之后建立用户头部段描述符、使用例程fill_descriptor_in_ldt将描述符安装在LDT中。

申请LDT内存:LDT的大小可以自己设置,最大也是64K字节

         ;以下申请创建LDT所需要的内存
         mov ecx,160                        ;允许安装20个LDT描述符
         call sys_routine_seg_sel:allocate_memory
         mov [es:esi+0x0c],ecx              ;登记LDT基地址到TCB中
         mov word [es:esi+0x0a],0xffff      ;登记LDT初始的界限到TCB中  

申请过的LDT可以将之保存在TCB

登记过LDT的线性地址和界限值之后,接下来我们要从硬盘读取用户程序:将加载的用户程序的首地址也保存在TCB中。

 ;以下开始加载用户程序 
         mov eax,core_data_seg_sel
         mov ds,eax                         ;切换DS到内核数据段
       
         mov eax,[ebp+12*4]                 ;从堆栈中取出用户程序起始扇区号 
         mov ebx,core_buf                   ;读取程序头部数据     
         call sys_routine_seg_sel:read_hard_disk_0

         ;以下判断整个程序有多大
         mov eax,[core_buf]                 ;程序尺寸
         mov ebx,eax
         and ebx,0xfffffe00                 ;使之512字节对齐(能被512整除的数低 
         add ebx,512                        ;9位都为0 
         test eax,0x000001ff                ;程序的大小正好是512的倍数吗? 
         cmovnz eax,ebx                     ;不是。使用凑整的结果
      
         mov ecx,eax                        ;实际需要申请的内存数量
         call sys_routine_seg_sel:allocate_memory
         mov [es:esi+0x06],ecx              ;登记程序加载基地址到TCB中 

然后创建用户程序头部段描述符,将之安装在LDT中

         mov ebx,ecx                        ;ebx -> 申请到的内存首地址
         xor edx,edx
         mov ecx,512
         div ecx
         mov ecx,eax                        ;总扇区数 
      
         mov eax,mem_0_4_gb_seg_sel         ;切换DS到0-4GB的段
         mov ds,eax

         mov eax,[ebp+12*4]                 ;起始扇区号 
  .b1:
         call sys_routine_seg_sel:read_hard_disk_0
         inc eax
         loop .b1                           ;循环读,直到读完整个用户程序

         mov edi,[es:esi+0x06]              ;获得程序加载基地址

         ;建立程序头部段描述符
         mov eax,edi                        ;程序头部起始线性地址
         mov ebx,[edi+0x04]                 ;段长度
         dec ebx                            ;段界限
         mov ecx,0x0040f200                 ;字节粒度的数据段描述符,特权级3 
         call sys_routine_seg_sel:make_seg_descriptor
      
         ;安装头部段描述符到LDT中 
         mov ebx,esi                        ;TCB的基地址
         call fill_descriptor_in_ldt               

07. 在当前任务的LDT中安装描述符

接上一节,在LDT中安装一个新的描述符

SECTION core_code vstart=0
;-------------------------------------------------------------------------------
fill_descriptor_in_ldt:                     ;在LDT内安装一个新的描述符
                                            ;输入:EDX:EAX=描述符
                                            ;          EBX=TCB基地址
                                            ;输出:CX=描述符的选择子  

fill_descriptor_in_ldt例程中运行。

PS:CX在计算过程中溢出的进位不会影响ECX的高16位,而是会被丢弃。

更新TI位为1,表示在LDT中:

08. LDT描述符的格式和创建

描述符的分类:

GDT的线性基地址存放在GDTR寄存器中,LDT=和TSS的线性基地址存放在GDT中,即LDT、TSS也有自己的段描述符,需要安装在GDT中。

描述符中的S位TYPE字段:

  • S=0表示系统描述符中,在系统描述符中21、22位是没有意义的置零即可
  • TYPE = 0010表示为LDT描述符

GDT中登记LDT描述符:

         ;在GDT中登记LDT描述符
         mov eax,[es:esi+0x0c]              ;LDT的起始线性地址
         movzx ebx,word [es:esi+0x0a]       ;LDT段界限
         mov ecx,0x00408200                 ;LDT描述符,特权级0
         call sys_routine_seg_sel:make_seg_descriptor
         call sys_routine_seg_sel:set_up_gdt_descriptor
         mov [es:esi+0x10],cx               ;登记LDT选择子到TCB中

其中:

  • S = 0:表示系统描述符;
  • TYPE = 0010:表示LDT描述符;
  • P = 1:表示段存在;
  • G = 0:表示段界限以字节为单位;

09. 创建任务状态段TSS\

除了局部描述符表LDT,每个任务还应该有一个任务状态段TSS,TSS的基本长度是104个字节

接上一节,本节创建TSS,代码如下:

         ;创建用户程序的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的内存分布:

T位为1,在多任务环境中每次切换到该任务时,将引发一个调试异常中断,所以我们将此为置零。

然后创建完TSS就将LDT选择子从TCB任务控制块中写入TSS

         mov dx,[es:esi+0x10]               ;登记任务的LDT选择子
         mov [es:ecx+96],dx                 ;到TSS中 

10. TSS描述符的格式和创建

接上一节,本节在GDT中登记TSS描述符。
将创建好的TSS描述符登记到GDT中,GDT登记完成后返回TSS选择子再存入TCB任务控制块中。

         ;在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 

TSS描述符格式:

  • S = 0:表示为系统描述符
  • TYPE中的B位(Busy):此位由处理器根据任务的状态实时修改,我们创建时置零就好。

11. 用带参数的RET指令返回调用者

接上一节,本节从例程load_relocate_program返回调用者,在返回之前栈的状态:

在执行下列出栈指令之后:

         pop es                             ;恢复到调用此过程前的es段 
         pop ds                             ;恢复到调用此过程前的ds段
      
         popad 

栈的状态为:

然后执行返回指令:

         ret 8                              ;丢弃调用本过程前压入的参数  

栈的状态:

其中ret imm、retf imm指令:

到目前为止的内存布局:

12. 加载任务寄存器TR和局部描述符表寄存器LDTR

接上一节,接下来从load_relocate_program返回,之后要转到任务私有部分(用户程序)执行。

在多任务系统中,有多个任务、多个TSS、LDT。

8086使用寄存器TR&LDTR来存储TSS&LDT的基地址和界限

TR&LDTR结构:

TR&LDTR可以使用ltr&lldt指令加载:

LDTR加载过程:

  • lldt r/m16指令执行时,将选择子送入LDTR的LDT选择器中;
  • 处理器从GDT中取出LDT描述符;
  • LDT描述符中的LDT线性基地址送入LDT描述符高速缓存器中的32位线性基地址部分;
  • 之后LDTR就指向LDT

TR加载过程:

  • ltr r/m16指令执行时,将选择子送入TR的TSS选择器中;
  • 处理器从GDT中取出TSS描述符;
  • TR描述符中的TSS线性基地址送入TR描述符高速缓存器中的32位线性基地址部分;
  • 之后TR就指向TSS

加载完之后就跳转用户程序执行。

13. 在虚拟机上验证人物的执行

写数据并执行:

Bochs虚拟机:
在0x7c00(主引导程序)断下来之后设置modbp断点,会在从实模式切换到保护模式的时候断下来。

进入内核:

执行完lldt指令之后看一下寄存器的状态:ldtr和gdtr的内容

查看GDT内容:info gdt

查看描述符详细信息:info gdt 5

查看LDT内容以及每个描述符的详细信息:info ldt

查看TSS信息:info tss

运行结果:正常执行

  • 标题: 任务和任务的创建
  • 作者: xiaoeryu
  • 创建于 : 2022-12-11 22:45:00
  • 更新于 : 2023-10-03 12:59:58
  • 链接: https://github.com/xiaoeryu/2022/12/11/27-任务和任务的创建/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论