写完这篇笔记之后,我对操作系统的集中学习时间也告一段落,一个半月以来我学会了许多主要知识,像虚拟内存、物理内存、分页机制、用户态、中断等,但也有一部分没来得及学,如文件系统,多任务等,这留给我以后不断完善。

我们之前一直都在内核状态下工作,特权级处于ring0,但我们平时在Windows或Linux所运行的程序,其实都属于用户进程,它运行在ring3下,所以用户能做的事很有限,比如不能访问内核的段,不能执行特权指令(像hlt),否则病毒就可以把你的操作系统卸载,然后把它自己变成操作系统:)

为了安全考虑,操作系统提供了syscall,使用户可以受限的访问内核级功能。玩Linux的都知道,可以用int 80来调用syscall。这次我们将进入用户模式下,并写一个syscall供用户调用。

切换到用户态

switch_to_user_mode 专门用来切换模式,从内核态切换到用户态。原理也比较好理解,就是更改cs,ds,ss等寄存器的值为用户段的选择子。

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
/*
syscall/task.c
*/
void switch_to_user_mode()
{
// 从用户模式进入内核模式时使用的栈(写入到tss的esp0处)
set_kernel_stack(0x300000);

// 关中断。将各寄存器改成用户数据段,将cs改成用户代码段。
// 用户数据段的选择子是0x20,但这里写的是0x23,是因为要将段选择器中RPL级别设置为3,表示用户级别。
asm volatile(" \
cli; \
mov $0x23, %ax; \
mov %ax, %ds; \
mov %ax, %es; \
mov %ax, %fs; \
mov %ax, %gs; \
\
\
mov %esp, %eax; \
pushl $0x23; \
pushl %esp; \
pushf; \
pushl $0x1B; \
push $1f; \
iret; \
1: \
");

}

任务切换

在x86体系结构下想要进行任务切换,必须要用TSS,这东西说白了就是为了保护现场,每个任务都有一个。

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
/*
include/gdt.h
*/
struct tss_entry_struct
{
u32int prev_tss; // 前一个tss
u32int esp0; // 进入内核态时使用的栈指针
u32int ss0; // 进入内核态时使用的选择子
u32int esp1; // 未使用...
u32int ss1;
u32int esp2;
u32int ss2;
u32int cr3;
u32int eip;
u32int eflags;
u32int eax;
u32int ecx;
u32int edx;
u32int ebx;
u32int esp;
u32int ebp;
u32int esi;
u32int edi;
u32int es; // The value to load into ES when we change to kernel mode.
u32int cs; // The value to load into CS when we change to kernel mode.
u32int ss; // The value to load into SS when we change to kernel mode.
u32int ds; // The value to load into DS when we change to kernel mode.
u32int fs; // The value to load into FS when we change to kernel mode.
u32int gs; // The value to load into GS when we change to kernel mode.
u32int ldt; // Unused...
u16int trap;
u16int iomap_base;
} __attribute__((packed));

typedef struct tss_entry_struct tss_entry_t;

安装TSS,在GDT中安装tss描述符。

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
/*
gdt/gdt.c
*/
void init_gdt(){

gdtr.limit = (sizeof(struct gdt_descriptor) * 7 - 1);
gdtr.base = (u32)&gdt;

gdt_set_descriptor(0,0,0,0,0);
gdt_set_descriptor(1,0,0xFFFFFFFF,0x9A,0xCF);
gdt_set_descriptor(2,0,0xFFFFFFFF,0x92,0xCF);
gdt_set_descriptor(3,0,0xFFFFFFFF,0xFA,0xCF);
gdt_set_descriptor(4,0,0xFFFFFFFF,0xF2,0xCF);
//向gdt中写入tss描述符。
write_tss(5,0x10,0x0);


gdt_flush((u32)&gdtr);
// 更新TR寄存器,指向该TSS。
tss_flush();
}


static void write_tss(u32 num,u16 ss0,u32 esp0){
u32 base = (u32) &tss_entry;
u32 limit = base + sizeof(tss_entry);

//写入tss描述符
gdt_set_descriptor(num,base,limit,0xE9,0x00);

memset(&tss_entry,0,sizeof(tss_entry));


tss_entry.ss0 = ss0;
tss_entry.esp0 = esp0;

//从用户模式切换回内核时需要使用的内核段选择子。
tss_entry.cs = 0x0b;
tss_entry.ss = tss_entry.ds = tss_entry.es = tss_entry.fs = tss_entry.gs = 0x13;
}

更新TR寄存器:

1
2
3
4
5
6
7
8
9
/*
gdt/gdt_s.s
*/
[GLOBAL tss_flush]
tss_flush:
mov ax, 0x2B ;tss的选择子。

ltr ax ;更新选择子到TR寄存器。
ret

syscall

然后实现syscall,类似于Linux中的write,read,execve。
为了简单,这里只实现一个print功能的syscall。

initialise_syscalls 安装syscall的调用函数到0x80号中断。
syscall_handler 调用syscalls[regs->eax]指向的函数,这里我指向的是printk。

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
43
44
45
46
47
48
49
50
51
52
/*
syscall/syscall.c
*/

#include "syscall.h"
#include "isr.h"
#include "gdt.h"
#include "console.h"

static void syscall_handler(registers_t *regs);
DEFN_SYSCALL1(printk,0, const char*);

//syscall数组
static void *syscalls[1] =
{
&printk,
};

//syscall的总数
u32 num_syscalls = 1;

//安装0x80中断处理程序
void initialise_syscalls(){
register_interrupt_handler(0x80,&syscall_handler);
}


void syscall_handler(registers_t *regs){

if(regs->eax >= num_syscalls){
return;
}

void *location = syscalls[regs->eax];
int ret;

//因为不知道函数需要多少个参数,所以把他们全部压到栈中。
asm volatile (" \
push %1; \
push %2; \
push %3; \
push %4; \
push %5; \
call *%6; \
pop %%ebx; \
pop %%ebx; \
pop %%ebx; \
pop %%ebx; \
pop %%ebx; \
" : "=a" (ret) : "r" (regs->edi), "r" (regs->esi), "r" (regs->edx), "r" (regs->ecx), "r" (regs->ebx), "r" (location));
regs->eax = ret;
}

这些宏是为了创建给用户调用的函数,比如创建一个叫syscall_printk的函数,用户只能通过调用它,然后内部会执行int 0x80,触发中断处理程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
include/syscall.h
*/
#define DECL_SYSCALL1(fn,p1) int syscall_##fn(p1);

//定义syscall函数
#define DEFN_SYSCALL1(fn, num, P1) \
int syscall_##fn(P1 p1) \
{ \
int a; \
asm volatile("int $0x80" : "=a" (a) : "0" (num), "b" ((int)p1)); \
return a; \
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
init/kernel.c
*/
int kmain(void){

console_clear();
init_gdt();
init_idt();
printk("Hello,kernel;\n");
asm volatile ("sti");
initialise_paging();

//安装0x80中断
initialise_syscalls();

//切换为用户模式
switch_to_user_mode();

//调用系统调用prink
syscall_printk("Hello, User World.");

return 0;
}

参考资料
User mode (and syscalls)