天天看點

《自己動手寫作業系統》第六章:從系統核心到程序A ring0>>ring1(一)1.程序2.與程序切換相關的彙編指令3.程序排程大緻過程4.初始化——ring0>>>ring1

摘要:程序排程可謂是作業系統中最為重要的環節之一,在本文中,我們主要講解程序模型、涉及的資料結構、如何從核心态轉到使用者程序?這一小節主要完成程序資料結構的分析,和從ring0>>ring1的程序開始階段的内容。

1.程序

我們來盤點一下,完成程序切換需要哪些資料結構和程式子產品:

1)首先,一個程序必須有代碼、資料(和堆棧):相關資料有LDT、段描述符、TSS等

2)對于正在休息的程序,我們需要讓它重新醒來的時候記得挂起之前的狀态,進而讓原來的任務能夠繼續執行——是以我們需要儲存程式狀态,這就是PCB(程式控制塊)

3)程序的切換者,是作業系統的程序排程子產品

4)時鐘中斷處理程式,幫助我們完成程序切換

2.與程序切換相關的彙編指令

    pushad:因為程序切換需要儲存程序上下文,使用push來儲存每個寄存器比較麻煩,于是,intel提供了一條指令pushad來儲存所有通用寄存器的值

    IRET 與 IRETD 是相同操作碼的助記符。IRETD 助記符(中斷傳回雙字)用于從使用 32 位操作數大小的中斷傳回;不過大多數彙編器對這兩種操作數大小都互換使用 IRET 助記符。

3.程序排程大緻過程

    PCB:PCB是用來描述程序的,它獨立在程序之外,當我們将上下文壓入PCB之時,已經處在程序管理子產品中了。

    ESP指向:在程序排程子產品中,将會用到堆棧,而寄存器被壓棧到程序表之後,esp指向PCB的某個位置——接下來的堆棧操作将破壞PCB。為了解決上述問題,需要将esp指向專門的核心棧。是以,在程序切換的過程中ESP的指向有三次:程序堆棧——PCB——核心棧。

    特權級變換:從外層到内層次,從TSS中取得SS:ESP;初始化的時候,從ring0>>>ring1,這個和恢複程序執行有點像,我們需要完成上下文的初始化,然後使用iretd指令來完成轉移。

    恢複:首先,我們需要從PCB中恢複寄存器的值,然後指令iretd,設定cs:ip和eflags,這樣程式就回到了程序B。

4.初始化——ring0>>>ring1

為了對從核心到程序的轉化有一個感性的認識,我們來看一下轉化時刻的代碼:

chapter6/i/kernel/kernel.asm
; ====================================================================================
;                                   restart
; ====================================================================================
restart:
    mov esp, [p_proc_ready]  ;esp 指向LPCB,在程序運作的時候就已經準備好了
    lldt    [esp + P_LDT_SEL] 
    lea eax, [esp + P_STACKTOP];eax=esp+P_stacktop
    mov dword [tss + TSS3_S_SP0], eax ;這樣以後,位址tss + TSS3_S_SP 處,存放的是ss0的位址
restart_reenter:
    dec dword [k_reenter]
    pop gs
    pop fs
    pop es
    pop ds
    popad
    add esp, 4
    iretd
           

那麼這一部分從哪裡跳轉過來的呢?

/kernel/mai.c/kernel_main()>>restart()

分析一下代碼執行流程:

1)p_proc_ready,是一個PCB指針,指向下一個将要執行的PCB;

2)好了,這裡,我們去檢視一下PCB的定義和P_LDT_SEL的定義:

PCB的定義:

31 typedef struct s_proc {
 32     STACK_FRAME regs;          /* process registers saved in stack frame */
 33 
 34     u16 ldt_sel;               /* gdt selector giving ldt base and limit */
 35     DESCRIPTOR ldts[LDT_SIZE]; /* local descriptors for code and data */
 36 
 37         int ticks;                 /* remained ticks */
 38         int priority;
 39 
 40     u32 pid;                   /* process id passed in from MM */
 41     char p_name[16];           /* name of the process */
 42 }PROCESS;
           

P_LDT_SEL表示的是ldt_sel的索引,來看看PCB總相關變量的一些列定義:

8 P_STACKBASE equ 0
  9 GSREG       equ P_STACKBASE
 10 FSREG       equ GSREG       + 4
 11 ESREG       equ FSREG       + 4
 12 DSREG       equ ESREG       + 4
 13 EDIREG      equ DSREG       + 4
 14 ESIREG      equ EDIREG      + 4
 15 EBPREG      equ ESIREG      + 4
 16 KERNELESPREG    equ EBPREG      + 4
 17 EBXREG      equ KERNELESPREG    + 4
 18 EDXREG      equ EBXREG      + 4
 19 ECXREG      equ EDXREG      + 4
 20 EAXREG      equ ECXREG      + 4
 21 RETADR      equ EAXREG      + 4
 22 EIPREG      equ RETADR      + 4
 23 CSREG       equ EIPREG      + 4
 24 EFLAGSREG   equ CSREG       + 4
 25 ESPREG      equ EFLAGSREG   + 4
 26 SSREG       equ ESPREG      + 4
 27 P_STACKTOP  equ SSREG       + 4
 28 P_LDT_SEL   equ P_STACKTOP
 29 P_LDT       equ P_LDT_SEL   + 4
 30 
 31 TSS3_S_SP0  equ 4
           

好了,看到這裡,或許你已經明白了PCB結構,如下:

《自己動手寫作業系統》第六章:從系統核心到程式A ring0>>ring1(一)1.程式2.與程式切換相關的彙編指令3.程式排程大緻過程4.初始化——ring0>>>ring1

tss的相關定義:

35 typedef struct s_tss {
 36     u32 backlink;
 37     u32 esp0;       /* stack pointer to use during interrupt */
 38     u32 ss0;        /*   "   segment  "  "    "        "     */
		....
		....
}TSS
           

3)總結一下,前兩句是設定LDT;接着兩句是設定tss的sp0

我們來看看調用代碼的上下文:

73         /* 初始化 8253 PIT */
 74         out_byte(TIMER_MODE, RATE_GENERATOR);
 75         out_byte(TIMER0, (u8) (TIMER_FREQ/HZ) );
 76         out_byte(TIMER0, (u8) ((TIMER_FREQ/HZ) >> 8));
 77 
 78         put_irq_handler(CLOCK_IRQ, clock_handler); /* 設定時鐘中斷處理程式 */
 79         enable_irq(CLOCK_IRQ);                     /* 讓8259A可以接收時鐘中斷 */
 80 
 81     restart();
 82 
 83     while(1){}
           

    結合一下程序表的開始結構圖,我們得出如下結論:

    下一次中斷發生時候,先pop sregs,然後在pop regs,接着跳過retaddr,然後執行iretd。下次中斷發生的時候,需要完成的工作就是:恢複各個寄存器的值、TSS中ss0和設定ldtr。

4.1時鐘中斷處理程式

      時鐘中斷隻是為了完成程序切換,我們這裡不使用複雜的排程,僅僅完成ring0>>ring1,是以使用iret即可。

150 ALIGN   16
151 hwint00:        ; Interrupt routine for irq 0 (the clock).
152     iretd
           

4.2PCB、程序體、GDT和TSS

對于PCB的初始化,我們僅僅需要設定sregs、eip、esp和eflags。另外,cs和ds此時對應的是LDT,是以需要初始化LDT。另外,我們還需要初始化TSS中ss0和esp0。

好了,我們來看一下程序表、PCB、GDT、TSS他們之間的資料關系:(見上圖)

接下來,我們來做這四個部分的初始化工作:

1)程序體:

是一個函數,不停列印字母A:chapter6/a/kernel/main.c

51 void TestA()
 52 {
 53     int i = 0;
 54     while(1){
 55         disp_str("A");
 56         disp_int(i++);
 57         disp_str(".");
 58         delay(1);
 59     }
 60 }
           

        思考一下,TestA僅僅是一個程序,而且是被中斷排程的對象,顯然不是核心的一部分。怎麼将控制權轉移到程序呢?在前面的章節中,kernel_main是核心函數,跳轉過程:

        kernel.asm中有一條jmp kernel_main指令;kernel_main是main.c中的一個函數,kernel.main最後一句是while(1){},是以核心将進入等待模式,将會相應中斷處理子產品和程序排程子產品的請求。

2)程序表

根據上圖程序表示意圖,我們不難定義PCB的相關結構:

9 typedef struct s_stackframe {
 10     u32 gs;     /* \                                    */
 11     u32 fs;     /* |                                    */
 12     u32 es;     /* |                                    */
 13     u32 ds;     /* |                                    */
 14     u32 edi;        /* |                                    */
 15     u32 esi;        /* | pushed by save()                   */
 16     u32 ebp;        /* |                                    */
 17     u32 kernel_esp; /* <- 'popad' will ignore it            */
 18     u32 ebx;        /* |                                    */
 19     u32 edx;        /* |                                    */
 20     u32 ecx;        /* |                                    */
 21     u32 eax;        /* /                                    */
 22     u32 retaddr;    /* return addr for kernel.asm::save()   */
 23     u32 eip;        /* \                                    */
 24     u32 cs;     /* |                                    */
 25     u32 eflags;     /* | pushed by CPU during interrupt     */
 26     u32 esp;        /* |                                    */
 27     u32 ss;     /* /                                    */
 28 }STACK_FRAME;
 29 
 30 
 31 typedef struct s_proc {
 32     STACK_FRAME regs;          /* process registers saved in stack frame */
 33 
 34     u16 ldt_sel;               /* gdt selector giving ldt base and limit */
 35     DESCRIPTOR ldts[LDT_SIZE]; /* local descriptors for code and data */
 36     u32 pid;                   /* process id passed in from MM */
 37     char p_name[16];           /* name of the process */
 38 }PROCESS;
           

知道了資料結構,再來看看它的初始化a/kernel/main.c

26     p_proc->ldt_sel = SELECTOR_LDT_FIRST;
 27     memcpy(&p_proc->ldts[0], &gdt[SELECTOR_KERNEL_CS>>3], sizeof(DESCRIPTOR));
 28     p_proc->ldts[0].attr1 = DA_C | PRIVILEGE_TASK << 5; // change the DPL
 29     memcpy(&p_proc->ldts[1], &gdt[SELECTOR_KERNEL_DS>>3], sizeof(DESCRIPTOR));
 30     p_proc->ldts[1].attr1 = DA_DRW | PRIVILEGE_TASK << 5;   // change the DPL
 31 
 32     p_proc->regs.cs = (0 & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_TASK;
 33     p_proc->regs.ds = (8 & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_TASK;
 34     p_proc->regs.es = (8 & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_TASK;
 35     p_proc->regs.fs = (8 & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_TASK;
 36     p_proc->regs.ss = (8 & SA_RPL_MASK & SA_TI_MASK) | SA_TIL | RPL_TASK;
 37     p_proc->regs.gs = (SELECTOR_KERNEL_GS & SA_RPL_MASK) | RPL_TASK;
 38     p_proc->regs.eip= (u32)TestA;
 39     p_proc->regs.esp= (u32) task_stack + STACK_SIZE_TOTAL;
 40     p_proc->regs.eflags = 0x1202;   // IF=1, IOPL=1, bit 2 is always 1.
 41 
 42     p_proc_ready    = proc_table;
 43     restart();
           

其中,上面用到的宏定義在protect.h之中,參考:a/include/protect.h

67 #define INDEX_DUMMY     0   /* \                         */
 68 #define INDEX_FLAT_C        1   /* | LOADER 裡面已經确定了的 */
 69 #define INDEX_FLAT_RW       2   /* |                         */
 70 #define INDEX_VIDEO     3   /* /                         */
 71 #define INDEX_TSS       4
 72 #define INDEX_LDT_FIRST     5
 73 /* 選擇子 */
 74 #define SELECTOR_DUMMY         0    /* \                         */
 75 #define SELECTOR_FLAT_C     0x08    /* | LOADER 裡面已經确定了的 */
 76 #define SELECTOR_FLAT_RW    0x10    /* |                         */
 77 #define SELECTOR_VIDEO      (0x18+3)/* /<-- RPL=3                */
 78 #define SELECTOR_TSS        0x20    /* TSS                       */
 79 #define SELECTOR_LDT_FIRST  0x28
 80 
 81 #define SELECTOR_KERNEL_CS  SELECTOR_FLAT_C
 82 #define SELECTOR_KERNEL_DS  SELECTOR_FLAT_RW
 83 #define SELECTOR_KERNEL_GS  SELECTOR_VIDEO
 84 
 85 /* 每個任務有一個單獨的 LDT, 每個 LDT 中的描述符個數: */
 86 #define LDT_SIZE        2
 87 
 88 /* 選擇子類型值說明 */
 89 /* 其中, SA_ : Selector Attribute */
 90 #define SA_RPL_MASK 0xFFFC
 91 #define SA_RPL0     0
 92 #define SA_RPL1     1
 93 #define SA_RPL2     2
 94 #define SA_RPL3     3
 95 
 96 #define SA_TI_MASK  0xFFFB
 97 #define SA_TIG      0
 98 #define SA_TIL      4
           

填充GDT中程序LDT的描述符:a/kernel/protect.c

109     init_descriptor(&gdt[INDEX_LDT_FIRST],
110         vir2phys(seg2phys(SELECTOR_KERNEL_DS), proc_table[0].ldts),
111         LDT_SIZE * sizeof(DESCRIPTOR) - 1,
112         DA_LDT);
           

這個函數的實作:

149 PRIVATE void init_descriptor(DESCRIPTOR *p_desc,u32 base,u32 limit,u16 attribute)
150 {
151     p_desc->limit_low   = limit & 0x0FFFF;
152     p_desc->base_low    = base & 0x0FFFF;
153     p_desc->base_mid    = (base >> 16) & 0x0FF;
154     p_desc->attr1       = attribute & 0xFF;
155     p_desc->limit_high_attr2= ((limit>>16) & 0x0F) | (attribute>>8) & 0xF0;
156     p_desc->base_high   = (base >> 24) & 0x0FF;
157 }
           

3)準備GDT和TSS

現在,剩下的就是TSS的初始化和對應描述符在GDT中的填充了:

初始化TSS:

 99 /* 填充 GDT 中 TSS 這個描述符 */
100     memset(&tss, 0, sizeof(tss));
101     tss.ss0 = SELECTOR_KERNEL_DS;
102     init_descriptor(&gdt[INDEX_TSS],
103             vir2phys(seg2phys(SELECTOR_KERNEL_DS), &tss),
104             sizeof(tss) - 1,
105             DA_386TSS);
106     tss.iobase = sizeof(tss); /* 沒有I/O許可位圖 */
下面,填寫tr:
130     xor eax, eax
131     mov ax, SELECTOR_TSS
132     ltr ax
           

4.3iretd

這裡,我們先使用一個簡單的restart函數:

294 restart:
295     mov esp, [p_proc_ready]
296     lldt    [esp + P_LDT_SEL]
297     lea eax, [esp + P_STACKTOP]
298     mov dword [tss + TSS3_S_SP0], eax
299 
300     pop gs
301     pop fs
302     pop es
303     pop ds
304     popad
305 
306     add esp, 4
307 
308     iretd
           

好了,使用iretd将加載CS:IP,想一想,CS和IP的值是多少?注意,編譯以後的main.c中Test函數是位于32b代碼段中,這個我們需要用反彙編研究一下。

4.4程序啟動與回顧

讓我們來回想一下第一個程序的啟動過程:

初始化程序:testA;初始化GDT中的TSS和LDT的兩個字元,初始化TSS(在init_prot()之中);準備程序表(在kernel.main());完成跳轉(kernel.asm)

不過,我們現在僅僅完成了從核心到使用者程序;但是如何完成程序切換,顯然,我們需要打開時鐘中斷和設定8259A的EOI位。

總結一下:

kernel的工作流程:

kernel.asm:

    _start:核心入口,順序往下執行

     cstart():将loader中的GDT複制到核心、設定gdt和ldt,初始化中斷向量表

     init_prot():初始化8259A,初始化各個中斷門 

     設定TR

main.c/tinix_main():

設定PCB資訊

restart():lldt、ss0、恢複段寄存器和通用寄存器、進入ring1(iretd),執行TestA——無限循環

while(1)

這裡,我們來介紹一個技巧:如何調試系統核心?

我們原來的調試,都是在彙程式設計式的狀态,如何按照C語言的行級别來調試核心呢?這裡,我們挖一個坑,以後再回填這個地方。