这是 tss 的结构
struct tss {
uint_32 last_tss;
uint_32* esp0;
uint_32 ss0;
uint_32* esp1;
uint_32 ss1;
uint_32* esp2;
uint_32 ss2;
uint_32 cr3;
uint_32 (*eip)(void);
uint_32 eflags;
uint_32 eax;
uint_32 ecx;
uint_32 edx;
uint_32 ebx;
uint_32 esp;
uint_32 ebp;
uint_32 esi;
uint_32 edi;
uint_32 es;
uint_32 cs;
uint_32 ss;
uint_32 ds;
uint_32 fs;
uint_32 gs;
uint_32 ldt;
uint_32 io_pos;
};
static struct tss tss;
虽然我们只声明了一个 tss ,但问题不大。事实上,我们并不采用cpu自带的多任务切换来实现用户进程,因此只要一个 tss 即可。这个 tss 作为“当前”进程的任务状态段存在,存在的主要意义在于当前进程用户态和内核态之间的切换。
从用户态转到内核态时,CPU会从TSS内读取并加载ss和esp0,使用pcb内的内核栈。tss 中的ss往往不变,而esp0 需要根据当前用户进程变化,方便它切换至相应内核态pcb的栈。
因此,这么大个 tss ,在我们的实现里,只关心里面的 esp0。esp0在当前pcb的顶端。
void update_tss_esp(struct task_struct* pthread) {
tss.esp0 = (uint_32*)((uint_64)(uint_32)pthread + PAGESIZE);
}
在gdt中创建tss描述符、用户数据段和代码段。
重新加载gdt和tss
0xc0000620 的由来: 0x600 是内核被加载的地方,前面的是已经虚拟构成的内核gdt表项。
这是原来的 GDT。
;----------------------------------------------------------
SECTION loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR
GDT_NULL dd 0x00
dd 0x00
CODE_SEG dd 0x0000ffff
dd DESC_CODE_HIGH4
DATA_SEG dd 0x0000ffff
dd DESC_DATA_HIGH4
VIDIO_SEG dd 0x80000007
dd DESC_VIDEO_HIGH4
GDT_SIZE equ $-GDT_NULL
GDT_LIMIT equ GDT_SIZE-1
times 60 dq 0 ;预留空位
这里形成的是用户的一些描述符。
设置了 TSS 的一些必要的表项。
void tss_init(void)
{
put_str("tss init start\n");
uint_32 tss_size = sizeof(tss);
tss.last_tss = 0;
tss.io_pos = tss_size;
tss.ss0 = SELECTOR_K_STACK;
update_tss_esp(running_thread());
// tss 描述符
*((struct gdt_desc*)0xc0000620) = form_gdt_desc(&tss, \
tss_size-1, \
TSS_ATTR_LOW, \
TSS_ATTR_HIGH);
// 用户代码段 描述符
*((struct gdt_desc*)0xc0000628) = form_gdt_desc((void*)0, \
0xfffff, \
USRCODE_ATTR_LOW, \
USRCODE_ATTR_HIGH);
// 用户数据段 描述符
*((struct gdt_desc*)0xc0000630) = form_gdt_desc((void*)0, \
0xfffff, \
USRDATA_ATTR_LOW,
USRDATA_ATTR_HIGH);
uint_64 gdt_op = ((7*8-1) | ((uint_64)(uint_32)0xc0000600)<<16);
asm volatile("lgdt %0"::"m"(gdt_op));
asm volatile("ltr %w0"::"r"(SELECTOR_TSS));
put_str("tss init done\n");
}
本章的内容可分为两块:初始化和执行
void process_execute(char* name,void* filename)
{
void* pcb_addr = get_kernel_pages(1);
struct task_struct* pcb = (struct task_struct*)pcb_addr;
init_thread(pcb,name,DEFALT_PRI);
// 初始化
create_page_dir(pcb);
usr_vaddr_init(pcb);
// 执行
thread_create(pcb,start_process,filename);
enum intr_status old_status = intr_disable();
list_append(&all_thread_list,&pcb->all_list_tag);
list_append(&thread_ready_list,&pcb->wait_tag);
intr_set_status(old_status);
}
用户进程和内核线程的第一个不同之处在于,它们有自己的4GB虚拟内存。具体来说就是,有自己的页目录表和页表,还有自己管理的一块虚拟内存。
PCB 的内容
struct task_struct {
uint_32* kstack_p;
char name[20];
pid_t pid;
struct list_elm wait_tag;
struct list_elm all_list_tag;
uint_32 ticks;
uint_32 elapsed_ticks;
enum task_status status;
uint_32 priority;
/************************************************/
void* pdir;
struct virt_addr usrprog_vaddr;
/************************************************/
uint_32 kmagic;
};
页目录表和虚拟内存池,这两个参数在pcb是存在的。只不过我们以前的是线程,用不上。
这里创建进程就需要在pcb中记录这些信息。
换言之也就是,初始化进程 = 初始化线程 + 初始化进程特有参数并记录至pcb
创造用户进程特有的页目录表,它是用户拥有独立4GB虚拟地址的证明。3GB以上的内核部分是共享的,所以初始化时要从内核页目录表那复制过来,以使第0x300个及之后的pde指向相同的页表。页目录表的最后一项指向自己。
void create_page_dir(struct task_struct* pcb)
{
uint_32* page_dir_vaddr = get_kernel_pages(1);
memcpy((void*)((uint_32)page_dir_vaddr+0x300*4),(void*)0xfffffc00,255*4);
page_dir_vaddr[1023] = v2p(page_dir_vaddr)|PAGE_RW_W|PAGE_US_U|PAGE_P;
pcb->pdir = page_dir_vaddr;
}
用户进程需要自己管理自己的虚拟内存。
虚拟地址起始处:#define USR_VADDR_START 0x8048000
如果你观察Linux下可执行文件的起始位置,可以发现 Entry point address 往往在 0x8048000 左右。当然,你在编译时,得加两个参数,一个是-m32
,因为咱现在是32位系统;还有一个是-no-pie
,如果使用pie的话,没有绝对地址引用所以每次加载的地址也不尽相同。
void usr_vaddr_init(struct task_struct* pcb)
{
uint_32 btmp_pgsize = DIV_ROUND_UP((0xc0000000 - USR_VADDR_START)/PAGESIZE/8,PAGESIZE);
struct virt_addr* usr_vaddr = &pcb->usrprog_vaddr;
// 指定虚拟地址起始处
usr_vaddr->vaddr_start = USR_VADDR_START;
// 初始化vaddr->btmp
usr_vaddr->btmp.bits = get_kernel_pages(btmp_pgsize);
usr_vaddr->btmp.map_size = (0xc0000000-USR_VADDR_START)/PAGESIZE/8;
bit_init(&pcb->usrprog_vaddr.btmp);
}
初始化完了,执行时schedule()必须做相应的 process_activate。
void schedule(void)
{
struct task_struct* cur = running_thread();
if (cur->ticks == 0)
{
cur->ticks = cur->priority;
cur->status = TASK_READY;
list_append(&thread_ready_list,&cur->wait_tag);
}
if (list_empty(&thread_ready_list)) thread_unblock(idle_pcb);
struct list_elm* next_ready_tag = list_pop(&thread_ready_list);
struct task_struct* next = mem2entry(struct task_struct,next_ready_tag,wait_tag);
process_activate(next);
next->status = TASK_RUNNING;
switch_to(cur,next);
}
激活cr3,调整tss内esp0的内容。正式指定用户虚拟地址和内核栈。
void process_activate(struct task_struct* pcb)
{
page_dir_activate(pcb);
update_tss_esp(pcb);
}
void page_dir_activate(struct task_struct* pcb)
{
uint_32 pdir_paddr;
if (pcb->pdir == NULL) {
pdir_paddr = 0x100000;
} else {
pdir_paddr = v2p(pcb->pdir);
}
asm volatile("movl %0,%%cr3"::"r"(pdir_paddr));
}
kernel_thread 执行函数 start_process。
用户进程和内核线程第二个不同在于特权级的差异。用户进程特权级为3,目前特权级为0,ret是不能从高特权级切换到低特权级的。因此,这里我们需要使用 iret 假装从中断返回 。在这个过程中,设置好精心准备的 intr_stack 内容然后弹出恢复寄存器映像。
void start_process(void* filename)
{
void* func = filename;
struct task_struct* cur = running_thread();
cur->kstack_p += sizeof(struct thread_stack);
struct intr_stack* intr = (struct intr_stack*)cur->kstack_p;
intr->vec_no = 0;
intr->esi = intr->edi = intr->ebp = intr->esp_dump = 0;
intr->eax = intr->ebx = intr->ecx = intr->edx = 0;
intr->gs = 0;
intr->ss = intr->ds = intr->es = intr->gs = SELECTOR_U_DATA;
intr->cs = SELECTOR_U_CODE;
intr->eip = func;
intr->eflags = (EFLAG_MBS | EFALG_IOPL_0 | EFLAG_IF_1);
intr->esp = get_a_page(PF_USER,USR_STACK_VADDR);
asm volatile("movl %0,%%esp;jmp int_exit;"::"g"((uint_32)intr):"memory");
}
intr_stack初始化
用户进程不能访问显存,gs=0。
vec_no没有什么作用,不用设置。
作为一个进程的开始,没有任何计算发生,所以8个通用寄存器都置为0即可。
eflags 的IOPL位为0,CPL <= IOPL时当前代码段才能执行I/O操作,要求用户程序默认情况下外设端口不开启。进程需要开中断,时间片到了好换下去,IF位为1。MBS 固定为1。
要为用户进程申请一块地方作为栈, 令esp指向那里。#define USR_STACK_VADDR (0xc0000000-1000)
struct intr_stack{
uint_32 vec_no;
uint_32 edi;
uint_32 esi;
uint_32 ebp;
uint_32 esp_dump;
uint_32 ebx;
uint_32 edx;
uint_32 ecx;
uint_32 eax;
uint_32 gs;
uint_32 fs;
uint_32 es;
uint_32 ds;
uint_32 err_code;
void (*eip)(void);
uint_32 cs;
uint_32 eflags;
void* esp;
uint_32 ss;
};
参考资料:
操作系统真象还原
linker script 简单教学