快一個多月了,一直想要在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)
最後的源代碼