作業系統---在核心中重新載入GDT和堆疊
阿新 • • 發佈:2021-03-04
## 摘要
用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
#