任务和任务的创建
01. 任务:概念和组成
内核时对整个计算机系统进行管理,管理软件和硬件。内核可以加载用户程序,对用户程序进行重定位,用户程序终止后还可以回收用户程序的资源,在编程时位用户程序提供API。
将内核和用户程序可以看作一个整体:任务
在一个系统中只有一个内核,但可以有多个用户程序,即多任务系统。
GDT存放内核中的描述符,LDT存放每个任务中用户程序的描述符:
每个任务还需要一个任务状态段:TSS(Task Status Segment)。
02. 使用任务控制块保存任务的基本信息
本节主要介绍了本章程序的结构,主要结构如下,本章程序有三个
- 用户程序:c13_app1.asm
- 用户头部段
- 用户数据段
- 用户栈段
- 用户代码段
- 用户尾部段
- 内核程序:c14_core.asm
- 定义常量存放选择子
- 内核程序头部段
- 内核公共例程段,就是一些例程和以前一样
- 内核数据段,一些文本信息、SALT等
- 内核代码段,有内核入口点start、创建任务控制块TCB(Task Control Block)记录任务的信息
- 内核尾部段
- 引导程序:c13_mbr0.asm
- 创建必要的段描述符
- 进入保护模式
- 加载内核
- 创建内核的段描述符
- 使用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 进行许可。