问题
内核与应用真的分离开了吗?
下面的代码合理吗?运行后会发生什么?
0xE000是内核地址空间,应用程序通过这个地址向内核写数据,这显然是不合理的,但程序还是正常的运行了,并没有出异常。
当前设计的缺陷
系统基于平坦内存模型 (内核与应用均可访问内存的任意角落)
内核与应用执行于不同的特权级,但未利用特权级保护机制
因此,应用程序可以"恶意"修改内核空间,进而导致整个操作系统奔溃
如何改进?
需求:当应用程序对内核空间写入数据时,直接将应用程序强行结束。
解决思路
启动虚存机制 (页式内存管理) 对内存区域设置属性
利用内核与应用特权级的不同:
- 内核 (DPL0) => 可读写内存任意区域
- 应用 (DPL3) => 不可写入内核所在区域
具体实现方案
1. 以线性方式建立页表
- 即:y = f(x) = x,其中,x为虚地址,y为实地址
2. 在内核中修改页表属性
- 使得内核空间在3特权级无法写入
3. 在第一个任务执行前启动页表机制
再论页表属性
由于物理页面的地址必须按照 4K 字节对齐
页表项可使用低12位描述内存所具备的属性
只读内存页的进一步说明
CR0 寄存器中的 WP 位 (bit16) 用于全局控制内存也是否可写入
当 R/W = 0时
- 若 CR0.WP = 1,则:任何特权级都无法对内存页写数据
- 若 CR0.WP = 0,则:0,1,2 特权级下可对内存页写数据
修改页表属性
BaseOfApp / 0x1000 - 1 算出内核的最后一个页面,然后将页面 0 - 页面 index 的页属性的 R/W 位设置为0,在3特权级下就不能对内核所在的内存空间进行写操作。
启动虚存机制
loader.asm
%include "blfunc.asm"
%include "common.asm"
org BaseOfLoader
BaseOfStack equ BaseOfLoader
Kernel db "KERNEL "
KnlLen equ ($-Kernel)
App db "APP "
AppLen equ ($-App)
[section .gdt]
; GDT definition
; Base, Limit, Attribute
GDT_ENTRY : Descriptor 0, 0, 0
CODE32_DESC : Descriptor 0, Code32SegLen - 1, DA_C + DA_32 + DA_DPL0
VIDEO_DESC : Descriptor 0xB8000, 0x07FFF, DA_DRWA + DA_32 + DA_DPL0
CODE32_FLAT_DESC : Descriptor 0, 0xFFFFF, DA_C + DA_32 + DA_DPL0
DATA32_FLAT_DESC : Descriptor 0, 0xFFFFF, DA_DRW + DA_32 + DA_DPL0
TASK_LDT_DESC : Descriptor 0, 0, 0
TASK_TSS_DESC : Descriptor 0, 0, 0
PAGE_DIR_DESC : Descriptor PageDirBase, 4095, DA_32 + DA_DRW
PAGE_TBL_DESC : Descriptor PageTblBase, 1023, DA_32 + DA_DRW + DA_LIMIT_4K
; GDT end
GdtLen equ $ - GDT_ENTRY
GdtPtr:
dw GdtLen - 1
dd 0
; GDT Selector
Code32Selector equ (0x0001 << 3) + SA_TIG + SA_RPL0
VideoSelector equ (0x0002 << 3) + SA_TIG + SA_RPL0
Code32FlatSelector equ (0x0003 << 3) + SA_TIG + SA_RPL0
Data32FlatSelector equ (0x0004 << 3) + SA_TIG + SA_RPL0
PageDirSelector equ (0x0007 << 3) + SA_TIG + SA_RPL0
PageTblSelector equ (0x0008 << 3) + SA_TIG + SA_RPL0
; end of [section .gdt]
[section .idt]
align 32
[bits 32]
IDT_ENTRY:
; IDT definition
; Selector, Offset, DCount, Attribute
%rep 256
Gate Code32Selector, DefaultHandler, 0, DA_386IGate + DA_DPL0
%endrep
IdtLen equ $ - IDT_ENTRY
IdtPtr:
dw IdtLen - 1
dd 0
; end of [section .idt]
[section .s16]
[bits 16]
BLMain:
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, SPInitValue
; initialize GDT for 32 bits code segment
mov esi, CODE32_SEGMENT
mov edi, CODE32_DESC
call InitDescItem
; initialize GDT pointer struct
mov eax, 0
mov ax, ds
shl eax, 4
add eax, GDT_ENTRY
mov dword [GdtPtr + 2], eax
; initialize IDT pointer struct
mov eax, 0
mov ax, ds
shl eax, 4
add eax, IDT_ENTRY
mov dword [IdtPtr + 2], eax
; load app
push Buffer
push BaseOfApp / 0x10
push BaseOfApp
push AppLen
push App
call LoadTarget
add sp, 10
cmp dx, 0
jz AppErr
; reset es register
mov ax, cs
mov es, ax
; load kernel
push Buffer
push BaseOfKernel / 0x10
push BaseOfKernel
push KnlLen
push Kernel
call LoadTarget
add sp, 10
cmp dx, 0
jz KernelErr
call StoreGlobal
; 1. load GDT
lgdt [GdtPtr]
; 2. close interrupt
; load IDT
; set IOPL to 3
cli
lidt [IdtPtr]
pushf
pop eax
or eax, 0x3000
push eax
popf
; 3. open A20
in al, 0x92
or al, 00000010b
out 0x92, al
; 4. enter protect mode
mov eax, cr0
or eax, 0x01
mov cr0, eax
; 5. jump to 32 bits code
jmp dword Code32Selector : 0
KernelErr:
mov bp, NoKernel
mov cx, NKLen
jmp output
AppErr:
mov bp, NoApp
mov cx, NALen
jmp output
output:
mov ax, cs
mov es, ax
mov dx, 0
mov ax, 0x1301
mov bx, 0x0007
int 0x10
jmp $
; esi --> code segment label
; edi --> descriptor label
InitDescItem:
push eax
mov eax, 0
mov ax, cs
shl eax, 4
add eax, esi
mov word [edi + 2], ax
shr eax, 16
mov byte [edi + 4], al
mov byte [edi + 7], ah
pop eax
ret
;
;
StoreGlobal:
mov dword [RunTaskEntry], RunTask
mov dword [LoadTaskEntry], LoadTask
mov dword [InitInterruptEntry], InitInterrupt
mov dword [EnableTimerEntry], EnableTimer
mov dword [SendEOIEntry], SendEOI
mov eax, dword [GdtPtr + 2]
mov dword [GdtEntry], eax
mov dword [GdtSize], GdtLen / 8
mov eax, dword [IdtPtr + 2]
mov dword [IdtEntry], eax
mov dword [IdtSize], IdtLen / 8
ret
[section .sfunc]
[bits 32]
;
;
Delay:
%rep 5
nop
%endrep
ret
;
;
Init8259A:
push ax
; master
; ICW1
mov al, 00010001B
out MASTER_ICW1_PORT, al
call Delay
; ICW2
mov al, 0x20
out MASTER_ICW2_PORT, al
call Delay
; ICW3
mov al, 00000100B
out MASTER_ICW3_PORT, al
call Delay
; ICW4
mov al, 00010001B
out MASTER_ICW4_PORT, al
call Delay
; slave
; ICW1
mov al, 00010001B
out SLAVE_ICW1_PORT, al
call Delay
; ICW2
mov al, 0x28
out SLAVE_ICW2_PORT, al
call Delay
; ICW3
mov al, 00000010B
out SLAVE_ICW3_PORT, al
call Delay
; ICW4
mov al, 00000001B
out SLAVE_ICW4_PORT, al
call Delay
pop ax
ret
; al --> IMR register value
; dx --> 8259A port
WriteIMR:
out dx, al
call Delay
ret
; dx --> 8259A
; return:
; ax --> IMR register value
ReadIMR:
in ax, dx
call Delay
ret
;
; dx --> 8259A port
WriteEOI:
push ax
mov al, 0x20
out dx, al
call Delay
pop ax
ret
[section .gfunc]
[bits 32]
;
; parameter ===> Task* pt
RunTask:
push ebp
mov ebp, esp
mov esp, [ebp + 8]
lldt word [esp + 96]
ltr word [esp + 98]
pop gs
pop fs
pop es
pop ds
popad
add esp, 4
mov dx, MASTER_IMR_PORT
in ax, dx
%rep 5
nop
%endrep
and ax, 0xFE
out dx, al
%rep 5
nop
%endrep
mov eax, PageDirBase
mov cr3, eax
mov eax, cr0
or eax, 0x80000000
mov cr0, eax
iret
; void LoadTask(Task* pt);
;
LoadTask:
push ebp
mov ebp, esp
mov eax, [ebp + 8]
lldt word [eax + 96]
leave
ret
;
;
InitInterrupt:
push ebp
mov ebp, esp
push ax
push dx
call Init8259A
sti
mov ax, 0xFF
mov dx, MASTER_IMR_PORT
call WriteIMR
mov ax, 0xFF
mov dx, SLAVE_IMR_PORT
call WriteIMR
pop dx
pop ax
leave
ret
;
;
EnableTimer:
push ebp
mov ebp, esp
push ax
push dx
mov dx, MASTER_IMR_PORT
call ReadIMR
and ax, 0xFE
call WriteIMR
pop dx
pop ax
leave
ret
; void SendEOI(uint port);
; port ==> 8259A port
SendEOI:
push ebp
mov ebp, esp
mov edx, [ebp + 8]
mov al, 0x20
out dx, al
call Delay
leave
ret
[section .s32]
[bits 32]
CODE32_SEGMENT:
mov ax, VideoSelector
mov gs, ax
mov ax, Data32FlatSelector
mov ds, ax
mov es, ax
mov fs, ax
mov ax, Data32FlatSelector
mov ss, ax
mov esp, BaseOfLoader
call SetupPage
jmp dword Code32FlatSelector : BaseOfKernel
;
;
SetupPage:
push es
push eax
push ecx
push edi
mov ax, PageDirSelector
mov es, ax
mov edi, 0
mov ecx, 1024 ; 1K sub page tables
mov eax, PageTblBase | PG_P | PG_USU | PG_RWW
cld
stdir:
stosd
add eax, 4096
loop stdir
mov ax, PageTblSelector
mov es, ax
mov edi, 0
mov ecx, 1024 * 1024 ; 1M pages
mov eax, PG_P | PG_USU | PG_RWW
cld
sttbl:
stosd
add eax, 4096
loop sttbl
pop edi
pop ecx
pop eax
pop es
ret
;
;
DefaultHandlerFunc:
iret
DefaultHandler equ DefaultHandlerFunc - $$
Code32SegLen equ $ - CODE32_SEGMENT
NoKernel db "No KERNEL"
NKLen equ ($-NoKernel)
NoApp db "No KERNEL"
NALen equ ($-NoApp)
Buffer db 0
common.asm
; Page Base
PageDirBase equ 0x50000
PageTblBase equ 0x51000
const.h
#define PageDirBase 0x50000
#define PageTblBase 0x51000
kernel.c
void ConfigPageTable()
{
uint* TblBase = (uint*)PageTblBase;
uint index = BaseOfApp / 0x1000 - 1;
uint i = 0;
for(i = 0; i <= index; i++)
{
uint* addr = TblBase + i;
uint value = *addr;
value &= 0xFFFFFFFD;
*addr = value;
}
}
task.c
#include "utility.h"
#include "task.h"
#include "app.h"
#define MAX_TASK_NUM 4
#define MAX_RUNNING_TASK 2
#define MAX_READY_TASK (MAX_TASK_NUM - MAX_RUNNING_TASK)
static AppInfo* (*GetAppToRun)(uint index) = NULL;
static uint (*GetAppNum)() = NULL;
void (* const RunTask)(volatile Task* pt) = NULL;
void (* const LoadTask)(volatile Task* pt) = NULL;
volatile Task* gCTaskAddr = NULL;
// static TaskNode gTaskBuff[MAX_TASK_NUM] = {0};
static TaskNode* gTaskBuff = NULL;
static TSS gTSS = {0};
static Queue gFreeTaskNode = {0};
static Queue gReadyTask = {0};
static Queue gRunningTask = {0};
static Queue gWaitingTask = {0};
// static TaskNode gIdleTask = {0};
static TaskNode* gIdleTask = NULL;
static uint gAppToRunIndex = 0;
static uint gPid = 10;
void TaskEntry()
{
if(gCTaskAddr != NULL)
{
gCTaskAddr->tmain();
}
// to destory current task here
asm volatile(
"movw $0, %ax \n"
"int $0x80 \n"
);
while(1); // TODO: schedule next task to run
}
void IdleTask()
{
while(1);
}
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 + sizeof(pt->stack);
pt->rv.eip = (uint)TaskEntry;
pt->rv.eflags = 0x3202;
pt->id = id;
StrCpy(pt->name, name, sizeof(pt->name) - 1);
pt->tmain = entry;
pt->current = 0;
pt->total = 256 - pri;
gTSS.ss0 = GDT_DATA32_FLAT_SELECTOR;
gTSS.esp0 = (uint)&pt->rv + sizeof(pt->rv);
gTSS.iomb = sizeof(TSS);
SetDescValue(AddrOff(pt->ldt, LDT_VIDEO_INDEX), 0xB8000, 0x07FFF, DA_DRWA + DA_32 + DA_DPL3);
SetDescValue(AddrOff(pt->ldt, LDT_CODE32_INDEX), 0x00, 0xFFFFF, DA_C + DA_32 + DA_DPL3);
SetDescValue(AddrOff(pt->ldt, LDT_DATA32_INDEX), 0x00, 0xFFFFF, DA_DRW + DA_32 + DA_DPL3);
pt->ldtSelector = GDT_TASK_LDT_SELECTOR;
pt->tssSelector = GDT_TASK_TSS_SELECTOR;
}
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);
}
static void CreateTask()
{
int num = GetAppNum();
while((gAppToRunIndex < num) && (Queue_Length(&gReadyTask) < MAX_READY_TASK))
{
TaskNode* tn = (TaskNode*)Queue_Remove(&gFreeTaskNode);
if(tn != NULL)
{
AppInfo* app = GetAppToRun(gAppToRunIndex);
InitTask(&tn->task, gPid++, app->name, app->tmain, app->priority);
Queue_Add(&gReadyTask, &tn->head);
}
else
{
break;
}
gAppToRunIndex++;
}
}
static void ReadyToRunning()
{
QueueNode* node = NULL;
if(Queue_Length(&gReadyTask) < MAX_READY_TASK)
{
CreateTask();
}
while((Queue_Length(&gReadyTask) > 0) && (Queue_Length(&gRunningTask) < MAX_RUNNING_TASK))
{
node = Queue_Remove(&gReadyTask);
((TaskNode*)node)->task.current = 0;
Queue_Add(&gRunningTask, node);
}
}
static void RunningToReady()
{
if(Queue_Length(&gRunningTask) > 0)
{
TaskNode* tn = (TaskNode*)Queue_Front(&gRunningTask);
if(!IsEqual(tn, gIdleTask))
{
if(tn->task.current == tn->task.total)
{
Queue_Remove(&gRunningTask);
Queue_Add(&gReadyTask, (QueueNode*)tn);
}
}
}
}
static void CheckRunningTask()
{
if(Queue_Length(&gRunningTask) == 0)
{
Queue_Add(&gRunningTask, &gIdleTask->head);
}
else if(Queue_Length(&gRunningTask) > 1)
{
if(Queue_Front(&gRunningTask) == &gIdleTask->head)
{
Queue_Remove(&gRunningTask);
}
}
}
void TaskModInit()
{
int i = 0;
gTaskBuff = (void*)0x20000;
gIdleTask = (void*)AddrOff(gTaskBuff, MAX_TASK_NUM);
GetAppToRun = (void*)(*((uint*)GetAppToRunEntry));
GetAppNum = (void*)(*((uint*)GetAppNumEntry));
Queue_Init(&gFreeTaskNode);
Queue_Init(&gReadyTask);
Queue_Init(&gRunningTask);
Queue_Init(&gWaitingTask);
for(i = 0; i < MAX_TASK_NUM; i++)
{
Queue_Add(&gFreeTaskNode, (QueueNode*)AddrOff(gTaskBuff, i));
}
SetDescValue(AddrOff(gGdtInfo.entry, GDT_TASK_TSS_INDEX), (uint)&gTSS, sizeof(gTSS)-1, DA_386TSS + DA_DPL0);
InitTask(&(gIdleTask->task), 0, "IdleTask", IdleTask, 255);
ReadyToRunning();
CheckRunningTask();
}
void LaunchTask()
{
gCTaskAddr = &((TaskNode*)Queue_Front(&gRunningTask))->task;
PrepareForRun(gCTaskAddr);
RunTask(gCTaskAddr);
}
void Schedule()
{
RunningToReady();
ReadyToRunning();
CheckRunningTask();
Queue_Rotate(&gRunningTask);
gCTaskAddr = &((TaskNode*)Queue_Front(&gRunningTask))->task;
PrepareForRun(gCTaskAddr);
LoadTask(gCTaskAddr);
}
void KillTask()
{
QueueNode* node = Queue_Remove(&gRunningTask);
Queue_Add(&gFreeTaskNode, node);
Schedule();
}
kamin.c
#include "task.h"
#include "interrupt.h"
#include "screen.h"
void KMain()
{
void (*AppModInit)() = (void*)BaseOfApp;
int n = PrintString("D.T.OS\n");
PrintString("GDT Entry: ");
PrintIntHex((uint)gGdtInfo.entry);
PrintChar('\n');
PrintString("GDT Size: ");
PrintIntDec((uint)gGdtInfo.size);
PrintChar('\n');
PrintString("IDT Entry: ");
PrintIntHex((uint)gIdtInfo.entry);
PrintChar('\n');
PrintString("IDT Size: ");
PrintIntDec((uint)gIdtInfo.size);
PrintChar('\n');
AppModInit();
TaskModInit();
IntModInit();
ConfigPageTable();
LaunchTask();
}
由于我们要进行内存保护,所以要开启内存分页机制。
我们在 common.asm 中定义了 页目录的起始地址为 0x50000,子页表的起始地址为 0x51000。
在 loader.asm 中,我们定义了页目录和子页表的全局段描述符和选择子,并且定义了 SetupPage 函数,这个函数初始化页目录和子页表,以线性方式初始化页表。并且在执行任务前开启内存分页机制。
在 kernel.c 中,我们定义了 ConfigPageTable 函数,这个函数先将内核最后的内存页 index 计算出来,然后将内存页 0 - 内存页 index 的页属性 R/W位 (第1位) 设置为0,即:在特权级为3时,不能对物理页进行写操作。
我们在 kmian.c 中调用 ConfigPageTable 函数,但当应用程序没有向内核使用的内存写数据时,仍然发生异常。
我们找到了问题的原因:TaskEntry 是每个任务函数的入口,进入这个函数时,特权级就从0变为3了,在 TaskEntry 函数中,又会调用 tmain 函数,这就会使用栈,而使用的这个栈,我们是定义在 task.c 中,这个栈所使用的内存范围是在内核的内存范围内的,所以我们就得将每个 TaskNode 的内存放在应用程序的内存当中,我们就定义 TaskNode 的指针,指向应用程序所使用的内存空间,这样应用程序在进行函数调用时,修改的就是应用程序的内存空间,而不是内核的内存空间了。
我们并没有设置 CR0 的 WP 位,是因为它的初始值就为0。
现在如果应用程序想要向内核所在的区域写入数据,就会出现异常。