用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]
。