摘要
用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語言中的變數更方便。
怎麼做
流程
- 在kernel中宣告變數
unsigned short gdt_ptr
,儲存GDT的記憶體地址。 - 使用
sgdt
指令把GDT的內地址複製到gdt_ptr
中。 - 在kernel中建立結構體
gdt
,儲存GDT。 - 使用記憶體複製函式把GDT從LOADER中設定的記憶體位置複製到kernel中的變數
gdt
表示的記憶體中。
memcpy
它是記憶體複製函式。
這樣實現它:
- 原型是:
memcpy(void *dst, void *src, int size)
。 - 核心是,把資料從
[ds:esi]
移動到[es:edi]
。 - 以位元組為單位來複制資料,複製
size
次。 - 用
jmp
實現迴圈,不用loop
。 - 迴圈終止條件是:size = 0。
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
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];
堆疊
[SECTION .bss]
StackSpace resb 2 * 1024
StackTop:
mov esp, StackTop
不理解。
程式碼
C語言
// 宣告一個char陣列,儲存GDT的記憶體地址
unsigned char gdt_ptr[6];
nasm彙編
; 使用C語言中宣告的變數gdt_ptr
extern gdt_ptr
; 把暫存器gdtr中的資料複製到變數gdt_ptr中
sgdt [gdt_ptr]
然後在C語言中把LOADER中的GDT複製到C語言中的gdt變數中。
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的引數
上面的那段程式碼,理解起來難度不小。
memcpy(&gdt,
(void *)((*)(int *)(&gdt_ptr[2])),
(*)((short *)(&gdt_ptr[0]))+1
);
memcpy
的第一個引數是目標記憶體地址,是一個指標型別變數,賦值應該是一個記憶體地址,所以用&
取得變數gdt的記憶體地址。
- 理解
(void *)((*)(int *)(&gdt_ptr[2]))
:- 第二個引數是源資料的記憶體地址,是GDT的實體地址。
- 它儲存在
gdt_ptr
的後6個位元組中。 &gdt_ptr[2]
獲取gdt_ptr的第3個元素gdt_ptr[2]
的實體地址。- 前面的
(int *)
將這段實體地址強制型別轉換為一個指標,這個指標的資料型別是int *
。 - 資料型別是
int *
有三層含義:- 這個資料是一個指標。
- 這個資料的值是一個記憶體地址。
- 這個記憶體地址是一個4位元組(int型別佔用4位元組)記憶體區域的初始地址。
&gdt_ptr[2]
是一個記憶體地址,用(int *)
將它包裝成或強制轉換成指標型別。- 再用
*
運算子,是獲取這個記憶體地址指向的記憶體區域中的資料。 - 這個資料是
int
型別,佔用4個位元組。這4個位元組的初始地址是&gdt_ptr[2]
。這是最關鍵的一句。 - 為什麼最後還要用
void *
?- 因為,這4個位元組中儲存的那個
int
資料又是一個記憶體地址,因此,需要再次包裝成一個指標。 - 因為,
memcpy
對引數的資料型別要求是void *
。 - 究竟是哪個原因,我也不知道。
- 因為,這4個位元組中儲存的那個
- 理解:
(*)((short *)(&gdt_ptr[0]))+1
- 為什麼要加1?
gdt_ptr
的低2位儲存的是GDT的位元組偏移量的最大值,是GDT的長度減1。 &gdt_ptr[0])
是gdt_ptr[0])
的記憶體地址AD。(short *)&gdt_ptr[0])
用AD建立一個指標變數。- 這個指標變數指向一塊記憶體。
- 這塊記憶體佔用2個位元組。
- 這塊記憶體的初始地址是
&gdt_ptr[0])
,即gdt_ptr[0])
的記憶體地址。 (short *)&gdt_ptr[0])
實質是指代&gdt_ptr[0]、&gdt_ptr[1]
這兩小塊記憶體。
(*)((short *)(&gdt_ptr[0]))
是&gdt_ptr[0]、&gdt_ptr[1]
這兩小塊記憶體中的值,即gdt_ptr[0]、gdt_ptr[1]
。- 為什麼不需要像第二個引數一樣在前面再加上一個
(void *)
?- 因為,第4步的結果是一個short型別的整型數(short能稱之為整型嗎?),不是記憶體地址,不需要強制型別轉換。
- 為什麼要加1?
其他
short *gdt_limit = &gdt_ptr[0];
int *gdt_base = &gdt_ptr[2];
*gdt_limit = 128 * sizeof(Descriptor) - 1;
*gdt_base = (int) &gdt;
short *gdt_limit = &gdt_ptr[0];
int *gdt_base = &gdt_ptr[2];
這段程式碼建立了兩個變數並賦值,獲取了GDT的界限和地址。可是緊接著又有下面兩句,是對GDT的界限重新賦值。
*gdt_limit = 128 * sizeof(Descriptor) - 1;
*gdt_base = (int) &gdt;
這兩段程式碼的功能重複了嗎?
讓我們先看另外一段程式碼。
#include <stdio.h>
int main(int argc, char **argv)
{
int b = 8;
printf("b = %d\n", b);
int *a = &b;
*a = 9;
printf("b = %d\n", b);
return 0;
}
執行結果是:
MacBook-Pro:my-note-book cg$ ./test
b = 8
b = 9
第8行*a = 9;
修改*a
的值,同時也修改了b
的值,因為第7行int *a = &b;
。
再回頭看
short *gdt_limit = &gdt_ptr[0];
int *gdt_base = &gdt_ptr[2];
*gdt_limit = 128 * sizeof(Descriptor) - 1;
*gdt_base = (int) &gdt;
*gdt_base
指向gdt_ptr[2]
為初始地址的4個位元組的連續的記憶體空間AD,修改*gdt_base
,實質是修改AD中的資料。int *gdt_base = &gdt_ptr[2];
的作用是讓*gdt_base
指向AD;*gdt_base = (int) &gdt;
是修改AD中的資料,從業務邏輯的角度看,是把gdt的記憶體地址寫入AD中。為什麼要這樣做?回憶一下我們的目的是什麼?把儲存了GDT的C語言中變數的記憶體地址儲存到gdt_ptr中。
意外收穫
在理解上面那個比較複雜的指標引數的過程中,我對指標有了新的理解。
int a;
,要求CPU(不知道執行者是CPU還是作業系統)為a
分配四個位元組的記憶體空間,儲存資料。
int *a;
,要求CPU為a
分配四個位元組(第一片四位元組記憶體空間,記作A),在這四個位元組中儲存一個記憶體地址,這個記憶體地址指向另外一個四位元組的記憶體區域(記作B)。int *a
的含義是,指向B中的int
型別資料。
char days[5];
(short *)(&days[2]);
(short *)(&days[2]);
的含義是:
- 指向一片記憶體區域addr,這片記憶體區域的長度是連續的2個位元組(short是2個位元組)。
- addr的初始地址是days[2]的記憶體地址,所以,這片記憶體是
&days[2],&days[3]
。