1. 程式人生 > >作業系統---在核心中重新載入GDT和堆疊

作業系統---在核心中重新載入GDT和堆疊

## 摘要 用BIOS方式啟動計算機後,BIOS先讀取引導扇區,引導扇區再從外部儲存裝置中讀取載入器,載入器讀取核心。進入核心後,把載入器中建立的GDT複製到核心中。 這篇文章的最大價值也許在末尾,對C語言指標的新理解。 ## 是什麼 在BOOT(引導扇區)載入LOADER(載入器)。 在LOADER中初始化GDT、堆疊,把Knernel(核心)讀取到記憶體,然後開啟保護模式,最後進入Knernel並開始執行。作業系統正式開始運行了。 GDT是CPU在保護模式下記憶體定址必定會使用的元素,在Kernel執行過程中也需要用到。 在核心中重新載入GDT和堆疊,是指,把儲存於LOADER所使用的記憶體中的GDT資料和堆疊中的資料複製到Kernel所使用的記憶體中。關鍵點不是Kernel和LOADER所使用的記憶體,而是變數。換句話說,把儲存在LOADER中的變數中的GDT資料和堆疊中的資料複製到Kernel變數中的GDT和堆疊。 LOADER是用匯編寫的,“彙編中的變數”,不知道這種表述是否準確。 ## 為什麼 理由很簡單。LOADER是用匯編語言寫的,Kernel主要用C語言開發。在Kernel中使用GDT,若使用LOADER中定義的那個GDT變數(或者叫標號),光想一想就覺得很混亂。 用一句解釋:C語言中使用C語言中的變數更方便。 ## 怎麼做 ### 流程 1. 在kernel中宣告變數`unsigned short gdt_ptr`,儲存GDT的記憶體地址。 2. 使用`sgdt`指令把GDT的內地址複製到`gdt_ptr`中。 3. 在kernel中建立結構體`gdt`,儲存GDT。 4. 使用記憶體複製函式把GDT從LOADER中設定的記憶體位置複製到kernel中的變數`gdt`表示的記憶體中。 ### memcpy 它是記憶體複製函式。 這樣實現它: 1. 原型是:`memcpy(void *dst, void *src, int size)`。 2. 核心是,把資料從`[ds:esi]`移動到`[es:edi]`。 3. 以位元組為單位來複制資料,複製`size`次。 4. 用`jmp`實現迴圈,不用`loop`。 5. 迴圈終止條件是:size = 0。 ```assembly memcpy: push ebp mov ebp, esp push edi push esi push ecx push eax push ds push es mov es, [ebp + 12] ;dst mov ds, [ebp + 8] ; src mov size, [ebp + 4] ; size mov edi, 0 mov esi, 0 mov ecx, size .1: cmp ecx, 0 jz .2 mov al, [ds:esi] mov [es:edi], al inc esi inc edi dec ecx .2: pop es pop ds pop eax pop ecx pop esi pop edi pop ebp ret ``` ### gdt ```c typedef struct { unsigned short limitLow; unsigned short baseAddressLow; unsigned char baseAddressMid; unsigned char attribute1; unsigned char attribute_limit; unsigned char baseAddressHigh; }Descriptor; Descriptor gdt[128]; ``` ### 堆疊 ```assembly [SECTION .bss] StackSpace resb 2 * 1024 StackTop: mov esp, StackTop ``` 不理解。 ### 程式碼 C語言 ```c // 宣告一個char陣列,儲存GDT的記憶體地址 unsigned char gdt_ptr[6]; ``` nasm彙編 ```assembly ; 使用C語言中宣告的變數gdt_ptr extern gdt_ptr ; 把暫存器gdtr中的資料複製到變數gdt_ptr中 sgdt [gdt_ptr] ``` 然後在C語言中把LOADER中的GDT複製到C語言中的gdt變數中。 ```c memcpy(&gdt, (void *)((*)(int *)(&gdt_ptr[2])), (*)((int *)(&gdt_ptr[0])) ); short *gdt_limit = &gdt_ptr[0]; int *gdt_base = &gdt_ptr[2]; *gdt_limit = 128 * sizeof(Descriptor) - 1; *gdt_base = (int) &gdt; ``` #### 難點解讀 #### memcpy的引數 上面的那段程式碼,理解起來難度不小。 ```assembly memcpy(&gdt, (void *)((*)(int *)(&gdt_ptr[2])), (*)((short *)(&gdt_ptr[0]))+1 ); ``` `memcpy`的第一個引數是目標記憶體地址,是一個指標型別變數,賦值應該是一個記憶體地址,所以用`&`取得變數gdt的記憶體地址。 1. 理解`(void *)((*)(int *)(&gdt_ptr[2]))`: 1. 第二個引數是源資料的記憶體地址,是GDT的實體地址。 2. 它儲存在`gdt_ptr`的後6個位元組中。 3. `&gdt_ptr[2]`獲取gdt_ptr的第3個元素`gdt_ptr[2]`的實體地址。 4. 前面的`(int *)`將這段實體地址強制型別轉換為一個指標,這個指標的資料型別是`int *`。 5. 資料型別是`int *`有三層含義: 1. 這個資料是一個指標。 2. 這個資料的值是一個記憶體地址。 3. 這個記憶體地址是一個4位元組(int型別佔用4位元組)記憶體區域的初始地址。 6. `&gdt_ptr[2]`是一個記憶體地址,用`(int *)`將它包裝成或強制轉換成指標型別。 7. 再用`*`運算子,是獲取這個記憶體地址指向的記憶體區域中的資料。 8. 這個資料是`int`型別,佔用4個位元組。這4個位元組的初始地址是`&gdt_ptr[2]`。這是最關鍵的一句。 9. 為什麼最後還要用`void *`? 1. 因為,這4個位元組中儲存的那個`int`資料又是一個記憶體地址,因此,需要再次包裝成一個指標。 2. 因為,`memcpy`對引數的資料型別要求是`void *`。 3. 究竟是哪個原因,我也不知道。 2. 理解:`(*)((short *)(&gdt_ptr[0]))+1` 1. 為什麼要加1?`gdt_ptr`的低2位儲存的是GDT的位元組偏移量的最大值,是GDT的長度減1。 2. `&gdt_ptr[0])`是`gdt_ptr[0])`的記憶體地址AD。 3. `(short *)&gdt_ptr[0])`用AD建立一個指標變數。 1. 這個指標變數指向一塊記憶體。 2. 這塊記憶體佔用2個位元組。 3. 這塊記憶體的初始地址是`&gdt_ptr[0])`,即`gdt_ptr[0])`的記憶體地址。 4. `(short *)&gdt_ptr[0])`實質是指代`&gdt_ptr[0]、&gdt_ptr[1]`這兩小塊記憶體。 4. `(*)((short *)(&gdt_ptr[0]))`是`&gdt_ptr[0]、&gdt_ptr[1]`這兩小塊記憶體中的值,即`gdt_ptr[0]、gdt_ptr[1]`。 5. 為什麼不需要像第二個引數一樣在前面再加上一個`(void *)`? 1. 因為,第4步的結果是一個short型別的整型數(short能稱之為整型嗎?),不是記憶體地址,不需要強制型別轉換。 #### 其他 ```c short *gdt_limit = &gdt_ptr[0]; int *gdt_base = &gdt_ptr[2]; *gdt_limit = 128 * sizeof(Descriptor) - 1; *gdt_base = (int) &gdt; ``` ```c short *gdt_limit = &gdt_ptr[0]; int *gdt_base = &gdt_ptr[2]; ``` 這段程式碼建立了兩個變數並賦值,獲取了GDT的界限和地址。可是緊接著又有下面兩句,是對GDT的界限重新賦值。 ```c *gdt_limit = 128 * sizeof(Descriptor) - 1; *gdt_base = (int) &gdt; ``` 這兩段程式碼的功能重複了嗎? 讓我們先看另外一段程式碼。 ```c #