真象还原第十一章

这是 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 简单教学

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容