真象还原第十一章

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,122评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,070评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,491评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,636评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,676评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,541评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,292评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,211评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,655评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,846评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,965评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,684评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,295评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,894评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,012评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,126评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,914评论 2 355

推荐阅读更多精彩内容