任务定义(进程定义)
任务状态指的是任务执行时各个寄存器的状态
进程表示 如何在计算机表示一个进程Task? 使用C语言表示大致如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 typedef struct { RegValue rv; Descriptor ldt[3 ]; ushort ldtSelector; ushort tssSelector; void (*tmain)(); uint 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;
既然已经有了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 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 ; 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); 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++; gTSS.ss0 = GDT_DATA32_FLAT_SELECTOR; gTSS.esp0 = (uint)&pt->rv + sizeof (pt->rv); gTSS.iomb = sizeof (TSS); 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开始)交换数据。如下图所示
执行loader的时候可以将0xA000设为数据交换的起始的地址。loader可以把关键地址GDT起始地址GDT大小等填入到0xA000处,之后跳转到kernel的时候,交换数据区已经有了具体数据,kernel可以直接从0xA000那里取出GDT等关键信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 typedef struct { ushort limit1; ushort base1; byte base2; byte attr1; byte attr2_limit2; byte base3; } Descriptor; 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 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
测试结果如下:
成功打印除了GDT表的内容,还是比较成功的
如何恢复上下文数据(切换进程)? 修改寄存器的值
通过任务数据结构中的寄存器值恢复上下文
借助 esp 寄存器以及pop指令恢复通用寄存器
将esp指向RegValue,然后pop相应的值将栈顶数据恢复到相应的寄存器,popad可以一次性恢复多个通用寄存器
为什么不使用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五个参数修改特权级状态
因此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; 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
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底之间。