用BIOS方式啓動計算機後,BIOS先讀取引導扇區,引導扇區再從外部存儲設備中讀取加載器,加載器讀取內核。進入內核後,把加載器中創建的GDT複製到內核中。shell
這篇文章的最大價值也許在末尾,對C語言指針的新理解。數組
在BOOT(引導扇區)加載LOADER(加載器)。函數
在LOADER中初始化GDT、堆棧,把Knernel(內核)讀取到內存,而後開啓保護模式,最後進入Knernel並開始執行。操做系統正式開始運行了。oop
GDT是CPU在保護模式下內存尋址一定會使用的元素,在Kernel執行過程當中也須要用到。操作系統
在內核中從新加載GDT和堆棧,是指,把存儲於LOADER所使用的內存中的GDT數據和堆棧中的數據複製到Kernel所使用的內存中。關鍵點不是Kernel和LOADER所使用的內存,而是變量。換句話說,把存儲在LOADER中的變量中的GDT數據和堆棧中的數據複製到Kernel變量中的GDT和堆棧。指針
LOADER是用匯編寫的,「彙編中的變量」,不知道這種表述是否準確。code
理由很簡單。LOADER是用匯編語言寫的,Kernel主要用C語言開發。在Kernel中使用GDT,若使用LOADER中定義的那個GDT變量(或者叫標號),光想想就以爲很混亂。ip
用一句解釋:C語言中使用C語言中的變量更方便。內存
unsigned short gdt_ptr
,存儲GDT的內存地址。sgdt
指令把GDT的內地址複製到gdt_ptr
中。gdt
,存儲GDT。gdt
表示的內存中。它是內存複製函數。開發
這樣實現它:
memcpy(void *dst, void *src, int size)
。[ds:esi]
移動到[es:edi]
。size
次。jmp
實現循環,不用loop
。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
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(&gdt, (void *)((*)(int *)(&gdt_ptr[2])), (*)((short *)(&gdt_ptr[0]))+1 );
memcpy
的第一個參數是目標內存地址,是一個指針類型變量,賦值應該是一個內存地址,因此用&
取得變量gdt的內存地址。
(void *)((*)(int *)(&gdt_ptr[2]))
:
gdt_ptr
的後6個字節中。&gdt_ptr[2]
獲取gdt_ptr的第3個元素gdt_ptr[2]
的物理地址。(int *)
將這段物理地址強制類型轉換爲一個指針,這個指針的數據類型是int *
。int *
有三層含義:
&gdt_ptr[2]
是一個內存地址,用(int *)
將它包裝成或強制轉換成指針類型。*
運算符,是獲取這個內存地址指向的內存區域中的數據。int
類型,佔用4個字節。這4個字節的初始地址是&gdt_ptr[2]
。這是最關鍵的一句。void *
?
int
數據又是一個內存地址,所以,須要再次包裝成一個指針。memcpy
對參數的數據類型要求是void *
。(*)((short *)(&gdt_ptr[0]))+1
gdt_ptr
的低2位保存的是GDT的字節偏移量的最大值,是GDT的長度減1。&gdt_ptr[0])
是gdt_ptr[0])
的內存地址AD。(short *)&gdt_ptr[0])
用AD建立一個指針變量。
&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 *)
?
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]);
的含義是:
&days[2],&days[3]
。