任务定义(进程定义)

image-20220903091659190

任务状态指的是任务执行时各个寄存器的状态

进程表示

如何在计算机表示一个进程Task?

使用C语言表示大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct
{
RegValue rv; //寄存器的值,记录任务执行状态
Descriptor ldt[3]; //局部段描述符表,LDT描述局部于每个程序的段,包括代码段、数据段、显存段
ushort ldtSelector; //LDT选择子
ushort tssSelector; //TSS,用于查询内核栈
void (*tmain)(); //任务起始地址
uint id; //任务id
ushort current; //任务已经执行的时间
ushort total; //执行队列中 任务总共允许执行的时间
char name[16]; //任务名
Queue wait; //此任务的等待队列
byte* stack; //任务栈
Event* event; //任务事件
} Task;
  • rv寄存器用于保存进程的状态
  • stack表示任务执行时使用的栈

RegValue

寄存器RegValue结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct {
uint gs;
uint fs;
uint es;
uint ds;
uint edi;
uint esi;
uint ebp;
uint kesp;
uint ebx;
uint edx;
uint ecx;
uint eax;
uint raddr;
uint eip; //指令寄存器
uint cs; //
uint eflags;
uint esp;
uint ss;
} RegValue;

所谓保存任务状态就是保存寄存器的值即RegValue结构体

再论TSS

1
2
3
4
5
6
7
8
9
typedef struct
{
uint previous;
uint esp0;
uint ss0;
uint unused[22];
ushort reserved;
ushort iomb;
} TSS;

image-20220709114738197

既然已经有了TSS存储了各个寄存器的value,为什么还需要RegValue呢?

TSS为intel为了方便操作系统管理进程而加入的一种结构,用法也很简单。TSS是一个段,即一块内存,这里保存要切换的进程的cpu信息,包括各种寄存器的值、局部描述表ldt的段选择子等,切换时cpu会将这段内容存进各自对应的寄存器,然后就完成了切换。

因为进程切换非常的重要,虽然intel给我们提供了切换,只需要使用intel的方案就只需简单的几行语句即可。然而操作系统厂商不想依赖于intel,再加之intel切换速度慢,因此自己设计了进程切换方案,

因此现在进程切换,TSS往往是用于查找栈的信息而不使用这些寄存器的信息了

任务初始化InitTask

  • LDT : x86系统任务使用的私有段描述符表,通常有3个段描述符(代码段,数据段,栈段)
  • TSS: 特权级提升需要,用于寻找高特权级栈
  • RegValue:保存任务执行时的上下文信息
  • Stack:x86系统中的任务使用私有的栈
  • GDT:任务对应的LDT和TSS需要在GDT中注册
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//初始化Task结构体变量
static void InitTask(Task* pt, uint id, const char* name, void(*entry)(), ushort pri)
{
pt->rv.cs = LDT_CODE32_SELECTOR;
pt->rv.gs = LDT_VIDEO_SELECTOR;
pt->rv.ds = LDT_DATA32_SELECTOR;
pt->rv.es = LDT_DATA32_SELECTOR;
pt->rv.fs = LDT_DATA32_SELECTOR;
pt->rv.ss = LDT_DATA32_SELECTOR;

//栈已经在结构体中
pt->rv.esp = (uint)pt->stack + AppStackSize;
pt->rv.eip = (uint)TaskEntry;

pt->rv.eflags = 0x3202;//IF=0,屏蔽外部中断

//
pt->tmain = entry;
pt->id = id;
pt->current = 0;
pt->total = MAX_TIME_SLICE - pri;
pt->event = NULL;

if( name )
{
StrCpy(pt->name, name, sizeof(pt->name)-1);
}
else
{
*(pt->name) = 0;
}

Queue_Init(&pt->wait);

//设置好ldt局部段描述符表
SetDescValue(AddrOff(pt->ldt, LDT_VIDEO_INDEX), 0xB8000, 0x07FFF, DA_DRWA + DA_32 + DA_DPL3);//显存段
SetDescValue(AddrOff(pt->ldt, LDT_CODE32_INDEX), 0x00, KernelHeapBase - 1, DA_C + DA_32 + DA_DPL3);//代码段
SetDescValue(AddrOff(pt->ldt, LDT_DATA32_INDEX), 0x00, KernelHeapBase - 1, DA_DRW + DA_32 + DA_DPL3);//数据段

pt->ldtSelector = GDT_TASK_LDT_SELECTOR;
pt->tssSelector = GDT_TASK_TSS_SELECTOR;
}

每次运行任务之前需要将GDT中的ldt描述符的属性地址指向相应task的ldt描述符表

1
2
3
4
5
6
7
8
9
10
static void PrepareForRun(volatile Task* pt)
{
pt->current++;
//设置TSS 查找0特权级的栈
gTSS.ss0 = GDT_DATA32_FLAT_SELECTOR;
gTSS.esp0 = (uint)&pt->rv + sizeof(pt->rv);
gTSS.iomb = sizeof(TSS);
//设置GDT中的ldt描述符属性
SetDescValue(AddrOff(gGdtInfo.entry, GDT_TASK_LDT_INDEX), (uint)&pt->ldt, sizeof(pt->ldt)-1, DA_LDT + DA_DPL0);
}

GDT动态注册LDT和TSS

进程实现的原材料:

  • LDT: x86系统中的任务使用私有的段描述符表
  • TSS: 特权级提升执行时需要
  • RegValue: 保存任务执行时的上下文信息
  • Stack: x86系统中的任务使用私有的栈
  • GDT: 任务对应的 LDT 和 TSS 需要在 GDT 中注册

kermain.c中获取GDT地址

动态的实现任务必须动态地在 GDT 中注册 LDT 和 TSS,那么就必须通过设置 GDT 中描述符的值完成注册。
要设置 GDT 中的描述符就必须获得 GDT的起始地址;这个起始地址如何获取?

写loader.asm可以直接拿到GDT的地址,然而内核kernel是用C语言完成的,无法直接拿到GDT的地址。那么如何解决?
我们可以在loader和kernel之间通过预定义的内存区(0xA000开始)交换数据。如下图所示

image-20220709164137835

执行loader的时候可以将0xA000设为数据交换的起始的地址。loader可以把关键地址GDT起始地址GDT大小等填入到0xA000处,之后跳转到kernel的时候,交换数据区已经有了具体数据,kernel可以直接从0xA000那里取出GDT等关键信息。

image-20220709165430607

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct {
ushort limit1; //段界限0~15
ushort base1; //基地址0~15
byte base2; //基地址16~23
byte attr1; //属性第1部分
byte attr2_limit2; //属性第2部分和段界限第2部分16~19
byte base3; //基地址24~31
} Descriptor;

//用于注册进GDT,从共享内存可取得值
typedef struct {
Descriptor * const entry;
const int size;
} GdtInfo;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//输入参数 描述符,基址,节限,属性  初始化描述符的基址,界限,shu'xi
int SetDescValue(Descriptor* pDesc, uint base, uint limit, ushort attr)
{
int ret = 0;

if( ret = (pDesc != NULL) )
{
pDesc->limit1 = limit & 0xFFFF;
pDesc->base1 = base & 0xFFFF;
pDesc->base2 = (base >> 16) & 0xFF;
pDesc->attr1 = attr & 0xFF;
pDesc->attr2_limit2 = ((attr >> 8) & 0xF0) | ((limit >> 16) & 0xF);
pDesc->base3 = (base >> 24) & 0xFF;
}

return ret;
}

int GetDescValue(Descriptor* pDesc, uint* pBase, uint* pLimit, ushort* pAttr)
{
int ret = 0;

if( ret = (pDesc && pBase && pLimit && pAttr) )
{
*pBase = (pDesc->base3 << 24) | (pDesc->base2 << 16) | pDesc->base1;
*pLimit = ((pDesc->attr2_limit2 & 0xF) << 16) | pDesc->limit1;
*pAttr = ((pDesc->attr2_limit2 & 0xF0) << 8) | pDesc->attr1;
}

return ret;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
; Shared Value Address
GdtEntry equ BaseOfSharedMemory + 0
GdtSize equ BaseOfSharedMemory + 4
IdtEntry equ BaseOfSharedMemory + 8
IdtSize equ BaseOfSharedMemory + 12
RunTaskEntry equ BaseOfSharedMemory + 16

; GdtPtr存储了GDT的长度和GDT的入口地址
StoreGlobal:
mov eax, dword [GdtPtr + 2] ;GDT的入口地址->eax
mov dword [GdtEntry], eax ;eax->共享Gdt的入口地址

mov dword [GdtSize], GdtLen / 8 ;GdtLen/8字节
ret

测试结果如下:

image-20220709172111328

成功打印除了GDT表的内容,还是比较成功的

如何恢复上下文数据(切换进程)?

修改寄存器的值

  • 通过任务数据结构中的寄存器值恢复上下文
  • 借助 esp 寄存器以及pop指令恢复通用寄存器

将esp指向RegValue,然后pop相应的值将栈顶数据恢复到相应的寄存器,popad可以一次性恢复多个通用寄存器

image-20220709173156997

为什么不使用mov指令恢复相应的寄存器,那是因为可能意外地修改了eflags寄存器的值,pop指令是不会影响eflags的值

启动一个新任务可以看作特殊的任务切换,切换的新任务上下文信息中通用寄存器初始化应该为0,因此这些寄存器(edi,esi,ebp,kesp,ebx,edx,ecx,eax)的值为0

特权级转移高->低

在调用门中, retf 从高特权级返回低特权级; 与此类似, iret 指令也能从高特权级返回低特权级。

  • 将esp指向目标内存位置(eip,cs,eflags,esp,ss)
  • 借助 iret指令降特权级执行,栈变化如下所示,iret通过栈的ip,cs,eflags,sp,ss五个参数修改特权级状态

image-20220709174203593

因此esp指向eip,ip可以实现代码跳转,ss,sp可以修改栈,eflags存储了一些信息。再iret即可由高特权级转移到低特权级并且从指定任务入口(cs:ip)处执行代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct {
uint gs; //低地址
uint fs;
uint es;
uint ds;
uint edi;
uint esi;
uint ebp;
uint kesp;
uint ebx;
uint edx;
uint ecx;
uint eax;
uint raddr;
uint eip; //esp指向eip,然后调用iret即可
uint cs;
uint eflags;
uint esp;
uint ss; //高地址
} RegValue;

观察此结构体,只需要让栈顶指针指向结构体中的eip,然后iret指令就可以实现特权级转移了

总体思路:

  • regvalue结构体中将 eip 指向任务代码入口地址 ,cs 指向 LDT 中的代码段描述符 ( DPL = 3),eflags 指定关键状态 ( IOPL,IF,等),esp 指向任务使用的私有栈,ss 指向 LDT 中的数据段描述符
  • 根据regvalue结构体恢复gs,fs等寄存器的值
  • 让当前栈顶指针指向eip,iret 启动任务 (从任务代码入口处执行)

最终启动新进程的函数RunTask:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//loader.asm

//参数是Task* pt 任务结构的数据地址
RunTask:
push ebp ; 函数调用约定需要遵守
mov ebp, esp

; 取出第一个函数的参数的值,esp指针指向任务数据结构
mov esp, [ebp + 8] ; mov esp, &(pt->rv.gs) esp先指向regvalue起始位置

lldt word [esp + 96] ; lldt pt->ldtSelector 加载局部段描述符表 Task结构的偏移可以计算,ldt位于96
ltr word [esp + 98] ; ltr pt->tssSelector 加载任务状态段
pop gs ; 开始修改寄存器的值
pop fs
pop es
pop ds

popad ; pop edi,esi,ebp,esp,ebx,edx,ecx,eax

add esp, 4 ; 等价于mov esp, &(pt->rv.eip)

mov dx, MASTER_IMR_PORT

in ax, dx

%rep 5
nop
%endrep

and ax, 0xFC

out dx, al

mov eax, cr0
or eax, 0x80000000
mov cr0, eax

iret

总结

  • task任务记录在一个结构体里,尤其针对于结构体变量regvalue记录了任务的状态
  • 任务切换前需要设置动态设置好此任务的ldt局部段描述符表和将regvalue寄存器内的值恢复到当前寄存器
  • 当前栈顶指针指向eip然后iret

image-20220709205843524

ctrl+c输入reg观察寄存器

1
2
3
4
5
6
7
8
9
10
11
<bochs:2> reg
eax: 0x00000015 21
ecx: 0x00000015 21
edx: 0x00000015 21
ebx: 0x00000000 0
esp: 0x0000bc08 48136
ebp: 0x0000bc18 48152
esi: 0x00000000 0
edi: 0x00000000 0
eip: 0x0000b07f
eflags 0x00003093: id vip vif ac vm rf nt IOPL=3 of df if tf SF zf AF pf CF

可以观察到esp=0xbc08,处于上述图中的stack顶和stack底之间。