天天看點

在ELF格式核心中設定GDT、IDT等相關

快一個多月了,一直想要在ELF格式核心中實作中斷,參考的是兩本書,一本是于淵的orange’s,另一本是川合秀實的30天自制。。。前期,使用的是于淵的方法進入保護模式,加載并運作ELF核心;進入ELF核心後,變使用川合秀實的方式實作了圖形界面(僅僅隻是顯示圖形功能),發現各種錯誤(由其是中斷向量号為13的#GP錯誤,正常保護異常)

因為于淵的方法是,在loader裡面加載ELF,然後跳轉到ELF執行,跳轉過後,在我們編輯代碼的時候已經用C語言了,沒有辦法使用loader彙編檔案裡面定義的那個GDT,是以于老師為了友善管理,就把原來的GDT複制到核心空間,那樣就還可以用。

川合秀實則不一樣,前期他用他自己做的工具,跳過了ELF格式,直接可以用C語言編譯,連結,是以他第一次建立GDT就是在C語言環境下。這樣就不會出現跳轉到ELF後再重建立立GDT就不知道目前運作的代碼所在的段了。

是以我的做法是,先加載ELF,跳轉到ELF核心運作,然後用C語言重新寫一個新的GDT和IDT,然後強行跳轉到新的GDT裡面的某一個代碼段運作(這個代碼段就當做是核心裡面最起始的代碼段了),這樣,我們運作的程式就在新的GDT裡面有對應的描述符可以找到了。

這是剛跳轉如ELF核心的彙編代碼

kernel_start:
    mov edi, ( *  + ) *     /* 嘗試顯示一個字元 */
    mov ah, 
    mov al, 'L'
    mov [gs:edi], ax

;   mov esi, interrupt_handler    /* 這一段代碼可以先不管 */
;   mov edi, 0x28000              /* 我是想把中斷處理函數複制到記憶體指定位置 */
;   mov ecx, handler_length
;   handler_cp:
;       cmp ecx, 0
;       jz handler_cp_end
;       dec ecx
;       mov al, byte [cs:esi]
;       mov byte [es:edi], al
;       inc esi
;       inc edi
;       jmp handler_cp
;   handler_cp_end:

    mov esp, Stack0Top    /* 使用新的堆棧 */

    call kstart    /* 跳轉到C語言去,準備用C語言建立新的GDT,和一些其它準備工作 */

;------------------------------------------------------------------------------
    /* 在C語言函數kstart裡面建立了GDT後,選擇子為1*8(對應第一個段描述符)對應的段為
       一個基址為0,範圍是整個記憶體空間的可讀可執行的代碼段,特權級為0
    */
    jmp :now    /* 這行代碼極為重要,它使程式強行運作在了新定義的GDT對應的段裡面 */
now:
    call kmain    /* 這時我們可以跳轉到C語言環境去執行自己想做的事了 */

    mov edi, ( *  + ) *    /* 再次顯示字元,看看會不會出錯 */
    mov ah, 
    mov al, '!'
    mov [gs:edi], ax

    mov ax, *    /* 加載TSS,任務狀态段,現在還用不到 */
    ltr ax

;   mov edi, (80 * 12 + 4) * 2

    jmp $
           

上面我覺得最重要的就是jmp 8:now那一行了,它使目前程式強制用新的GDT對應的選擇子,這樣在後續的程式調用,中斷處理等地方就不會出現調用傳回時,因為傳回的段的選擇子在GDT中找不到對應段而出現GP異常了。

現在說說在kstart裡面幹的事情

void kstart(void)
{
    int i = ;
    unsigned short *p = (unsigned short *);

    *(p +  *  + ) = () | ('A');
    *(p +  *  + ) = () | ('l');
    *(p +  *  + ) = () | ('l');
    *(p +  *  + ) = () | ('e');
    *(p +  *  + ) = () | ('n');

    io_cli();
    init_gdt_idt();
    init_pic();

    io_sti();

}
           

在kstart裡面,首先嘗試輸出幾個字元,然後我把中斷關閉了,設定了新的GDT和IDT,還有初始化了8259A中斷控制器,然後又打開了中斷,準備接受中斷請求了。

其中的init_gdt_idt是比較重要的,裡面定義了幾個段和中斷門,然後重新加載GDT和IDT

void init_gdt_idt(void)
{
    struct Segment_Descriptor *gdt = (struct Segment_Descriptor *);
    struct Gate_Desciptor     *idt = (struct Gate_Desciptor     *);
    int i;

    for(i = ; i < ; i++)
        set_segdesc(gdt + i, , , );
    for(i = ; i < ; i++)
        set_gatedesc(idt + i, (int)(vector_others), *, );

    set_segdesc((struct Segment_Descriptor *)( + *), , , (DA_32 | DA_CR));  /* 範圍是整塊記憶體的代碼段 */
    set_segdesc((struct Segment_Descriptor *)( + *), , , (DA_32 | DA_DRW)); /* 範圍是整塊記憶體的資料段 */
    set_segdesc((struct Segment_Descriptor *)( + *), , (int)stack_ring0, (DA_32 | DA_DRW | DA_DPL0));  /* ring0的堆棧段 */
    set_segdesc((struct Segment_Descriptor *)( + *), , (int)stack_ring1, (DA_32 | DA_DRW | DA_DPL1));  /* ring1的堆棧段 */
    set_segdesc((struct Segment_Descriptor *)( + *), , (int)stack_ring2, (DA_32 | DA_DRW | DA_DPL2));  /* ring2的堆棧段 */
    set_segdesc((struct Segment_Descriptor *)( + *), , (int)stack_ring3, (DA_32 | DA_DRW | DA_DPL3));  /* ring3的堆棧段 */
    set_segdesc((struct Segment_Descriptor *)( + *), , , (DA_DRW | DA_DPL3));  /* 顯存的段 */
    set_segdesc((struct Segment_Descriptor *)( + *), , , (DA_32 | DA_CR));    /* handler的段 */
    set_segdesc((struct Segment_Descriptor *)( + *), , (int)LABEL_TSS, DA_386TSS);  /* 用來存放TSS */


    set_gatedesc((struct Gate_Desciptor     *)( + *), (int)(vector13_handler), *, );
    set_gatedesc((struct Gate_Desciptor     *)( + *), (int)(vector33_handler), *, );

    load_gdt(*, );
    load_idt(*, );
}
           

寫GDT描述符和IDT描述符的方法,兩位老師的都可以用,隻是一個資料結構罷了。

其實,也沒做多少事,就跳回彙程式設計式裡面了。

在之後的彙程式設計式裡面則寫了中斷handler和一些cli,sti等必須用彙編寫的代碼,給一個中斷處理程式的例子:

vector33_handler:
pushad
    mov ax, *
    mov gs, ax
    mov edi, ( *  + ) * 
    mov al, 
    out , al    ;響應中斷請求,為下一次中斷做準備
    in al,     ;擷取鍵盤讀入的鍵盤編碼(不是ASCII碼)
    mov dl, al
    mov cl, 
    .disp_loop:    ;把鍵盤編碼顯示出來
        cmp cl, 
        jz .disp_loop_end
        dec ecx
        mov al, dl
        shr al, cl
        and al, 
        add al, '0'
        mov ah, 
        mov [gs:edi], ax
        add edi, 
        jmp .disp_loop
    .disp_loop_end:
    call write_vedio    ;嘗試調用函數,看看會不會出錯
    call kmain
popad
iretd    ;中斷處理函數的傳回
nop
           

在這裡,注意處理函數所在的段,我這裡把它暫時放到了最開始的段,因為這樣就不用在記憶體中複制代碼了,如果想要單獨建立一個段來存中斷處理函數(一般是要這麼做的),就需要把我們的代碼複制到那個段所在的位置(包括彙編代碼和C語言代碼)。

還有一點就是鍵盤編碼,我們通過0x60端口讀到的按鍵資料,不是ASCII碼(一開始我以為是ASCII,結果這麼也對不上),它是IBM PC鍵盤掃描碼,它是在鍵盤上每一個鍵都對應一個8位二進制數(包括小鍵盤和Fn等,凡是你在鍵盤上能按到的,都有一個編碼,而且按下,按住和彈起對應最高位不同)

以前我在看其它一些工程代碼的時候,在最開始的地方總是會有一個start.c和main.c,當時以為兩個作用相同,都隻是程式剛開始的地方,和成一個寫也沒關系。但就最近的學習來看,不是的,而是必須寫成兩個.c檔案,一開始的start.c使用C語言編寫了一寫後面操作要用到的東西,比如GDT,IDT。然後,在start.c的内容執行完後,我們必須要執行一段彙編代碼,來初始化一些東西,如把某一個段的内容寫進新的GDT對應的段,然後才可以正常跳轉到C語言環境繼續執行(雖然有時候,我們跳轉到start.c後就加一個while1死循環,不會出錯,但涉及到段間跳轉就可能會出錯,這時候就需要那個jmp 8:now)

最後的源代碼

繼續閱讀