gitbook/操作系统实战45讲/docs/381157.md
2022-09-03 22:05:03 +08:00

607 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 12 | 设置工作模式与环境(下):探查和收集信息
你好我是LMOS。
上节课我们动手实现了自己的二级引导器。今天这节课我们将进入二级引导器,完成具体工作的环节。
在二级引导器中我们要检查CPU是否支持64位的工作模式、收集内存布局信息看看是不是合乎我们操作系统的最低运行要求还要设置操作系统需要的MMU页表、设置显卡模式、释放中文字体文件。
今天课程的配套代码,你可以点击[这里](https://gitee.com/lmos/cosmos/tree/master/lesson12/Cosmos),自行下载。
## 检查与收集机器信息
如果ldrkrl\_entry()函数是总裁那么init\_bstartparm()函数则是经理它负责管理检查CPU模式、收集内存信息设置内核栈设置内核字体、建立内核MMU页表数据。
为了使代码更加清晰我们并不直接在ldrkrl\_entry()函数中搞事情而是准备在另一个bstartparm.c文件中实现一个init\_bstartparm()。
下面我们就来动手实现它,如下所示。
```
//初始化machbstart_t结构体清0,并设置一个标志
void machbstart_t_init(machbstart_t* initp)
{
memset(initp,0,sizeof(machbstart_t));
initp->mb_migc=MBS_MIGC;
return;
}
void init_bstartparm()
{
machbstart_t* mbsp = MBSPADR;//1MB的内存地址
machbstart_t_init(mbsp);
return;
}
```
目前我们的经理init\_bstartparm()函数只是调用了一个machbstart\_t\_init()函数在1MB内存地址处初始化了一个机器信息结构machbstart\_t后面随着干活越来越多还会调用更多的函数的。
### 检查CPU
首先要检查我们的CPU因为它是执行程序的关键。我们要搞清楚它能执行什么形式的代码支持64位长模式吗
这个工作我们交给init\_chkcpu()函数来干由于我们要CPUID指令来检查CPU是否支持64位长模式所以这个函数中需要找两个帮工**chk\_cpuid、chk\_cpu\_longmode**来干两件事一个是检查CPU否支持CPUID指令然后另一个用CPUID指令检查CPU支持64位长模式。
下面我们去写好它们,如下所示。
```
//通过改写Eflags寄存器的第21位观察其位的变化判断是否支持CPUID
int chk_cpuid()
{
int rets = 0;
__asm__ __volatile__(
"pushfl \n\t"
"popl %%eax \n\t"
"movl %%eax,%%ebx \n\t"
"xorl $0x0200000,%%eax \n\t"
"pushl %%eax \n\t"
"popfl \n\t"
"pushfl \n\t"
"popl %%eax \n\t"
"xorl %%ebx,%%eax \n\t"
"jz 1f \n\t"
"movl $1,%0 \n\t"
"jmp 2f \n\t"
"1: movl $0,%0 \n\t"
"2: \n\t"
: "=c"(rets)
:
:);
return rets;
}
//检查CPU是否支持长模式
int chk_cpu_longmode()
{
int rets = 0;
__asm__ __volatile__(
"movl $0x80000000,%%eax \n\t"
"cpuid \n\t" //把eax中放入0x80000000调用CPUID指令
"cmpl $0x80000001,%%eax \n\t"//看eax中返回结果
"setnb %%al \n\t" //不为0x80000001,则不支持0x80000001号功能
"jb 1f \n\t"
"movl $0x80000001,%%eax \n\t"
"cpuid \n\t"//把eax中放入0x800000001调用CPUID指令检查edx中的返回数据
"bt $29,%%edx \n\t" //长模式 支持位 是否为1
"setcb %%al \n\t"
"1: \n\t"
"movzx %%al,%%eax \n\t"
: "=a"(rets)
:
:);
return rets;
}
//检查CPU主函数
void init_chkcpu(machbstart_t *mbsp)
{
if (!chk_cpuid())
{
kerror("Your CPU is not support CPUID sys is die!");
CLI_HALT();
}
if (!chk_cpu_longmode())
{
kerror("Your CPU is not support 64bits mode sys is die!");
CLI_HALT();
}
mbsp->mb_cpumode = 0x40;//如果成功则设置机器信息结构的cpu模式为64位
return;
}
```
上述代码中检查CPU是否支持CPUID指令和检查CPU是否支持长模式只要其中一步检查失败我们就打印一条相应的提示信息然后主动死机。**这里需要你留意的是最后设置机器信息结构中的mb\_cpumode字段为64,mbsp正是传递进来的机器信息machbstart\_t结构体的指针。**
### 获取内存布局
好了CPU已经检查完成 ,合乎我们的要求。下面就要获取内存布局信息了,物理内存在物理地址空间中是一段一段的,描述一段内存有一个数据结构,如下所示。
```
#define RAM_USABLE 1 //可用内存
#define RAM_RESERV 2 //保留内存不可使用
#define RAM_ACPIREC 3 //ACPI表相关的
#define RAM_ACPINVS 4 //ACPI NVS空间
#define RAM_AREACON 5 //包含坏内存
typedef struct s_e820{
u64_t saddr; /* 内存开始地址 */
u64_t lsize; /* 内存大小 */
u32_t type; /* 内存类型 */
}e820map_t;
```
获取内存布局信息就是获取这个结构体的数组这个工作我们交给init\_mem函数来干这个函数需要完成两件事一是获取上述这个结构体数组二是检查内存大小因为我们的内核对内存容量有要求不能太小。
下面我们来动手实现这个init\_mem函数。
```
#define ETYBAK_ADR 0x2000
#define PM32_EIP_OFF (ETYBAK_ADR)
#define PM32_ESP_OFF (ETYBAK_ADR+4)
#define E80MAP_NR (ETYBAK_ADR+64)//保存e820map_t结构数组元素个数的地址
#define E80MAP_ADRADR (ETYBAK_ADR+68) //保存e820map_t结构数组的开始地址
void init_mem(machbstart_t *mbsp)
{
e820map_t *retemp;
u32_t retemnr = 0;
mmap(&retemp, &retemnr);
if (retemnr == 0)
{
kerror("no e820map\n");
}
//根据e820map_t结构数据检查内存大小
if (chk_memsize(retemp, retemnr, 0x100000, 0x8000000) == NULL)
{
kerror("Your computer is low on memory, the memory cannot be less than 128MB!");
}
mbsp->mb_e820padr = (u64_t)((u32_t)(retemp));//把e820map_t结构数组的首地址传给mbsp->mb_e820padr
mbsp->mb_e820nr = (u64_t)retemnr;//把e820map_t结构数组元素个数传给mbsp->mb_e820nr
mbsp->mb_e820sz = retemnr * (sizeof(e820map_t));//把e820map_t结构数组大小传给mbsp->mb_e820sz
mbsp->mb_memsz = get_memsize(retemp, retemnr);//根据e820map_t结构数据计算内存大小。
return;
}
```
上面最难写的是mmap函数。不过我们还是有办法破解的。如果你理解了前面调用BIOS的机制就会发现**只要调用了BIOS中断就能获取e820map结构数组**。
为了验证这个结论我们来看一下mmap的函数调用关系
```
void mmap(e820map_t **retemp, u32_t *retemnr)
{
realadr_call_entry(RLINTNR(0), 0, 0);
*retemnr = *((u32_t *)(E80MAP_NR));
*retemp = (e820map_t *)(*((u32_t *)(E80MAP_ADRADR)));
return;
}
```
可以看到mmap函数正是通过前面讲的**realadr\_call\_entry函数**,来调用实模式下的\_getmmap函数的并且在\_getmmap函数中调用BIOS中断的。
```
_getmmap:
push ds
push es
push ss
mov esi,0
mov dword[E80MAP_NR],esi
mov dword[E80MAP_ADRADR],E80MAP_ADR ;e820map结构体开始地址
xor ebx,ebx
mov edi,E80MAP_ADR
loop:
mov eax,0e820h ;获取e820map结构参数
mov ecx,20 ;e820map结构大小
mov edx,0534d4150h ;获取e820map结构参数必须是这个数据
int 15h ;BIOS的15h中断
jc .1
add edi,20
cmp edi,E80MAP_ADR+0x1000
jg .1
inc esi
cmp ebx,0
jne loop ;循环获取e820map结构
jmp .2
.1:
mov esi,0 ;出错处理e820map结构数组元素个数为0
.2:
mov dword[E80MAP_NR],esi ;e820map结构数组元素个数
pop ss
pop es
pop ds
ret
```
如果你不明白上面代码的原理请回到“Cache与内存程序放在哪儿”[那节课](https://time.geekbang.org/column/article/376711),看一下获取内存视图相关的知识点。
init\_mem函数在调用mmap函数后就会得到e820map结构数组其首地址和数组元素个数由retempretemnr两个变量分别提供。
### 初始化内核栈
因为我们的操作系统是C语言写的所以需要有栈下面我们就来给即将运行的内核初始化一个栈。这个操作非常简单就是在机器信息结构machbstart\_t中记录一下栈地址和栈大小供内核在启动时使用。
不过,就算操作再简单,我们也要封装成函数来使用。让我们动手来写出这个函数吧,如下所示。
```
#define IKSTACK_PHYADR (0x90000-0x10)
#define IKSTACK_SIZE 0x1000
//初始化内核栈
void init_krlinitstack(machbstart_t *mbsp)
{
if (1 > move_krlimg(mbsp, (u64_t)(0x8f000), 0x1001))
{
kerror("iks_moveimg err");
}
mbsp->mb_krlinitstack = IKSTACK_PHYADR;//栈顶地址
mbsp->mb_krlitstacksz = IKSTACK_SIZE; //栈大小是4KB
return;
}
```
init\_krlinitstack函数非常简单但是其中调用了一个move\_krlimg函数你要注意这个我已经帮你写好啦它主要负责判断一个地址空间是否和内存中存放的内容有冲突。
因为我们的内存中已经放置了机器信息结构、内存视图结构数组、二级引导器、内核映像文件所以在处理内存空间时不能和内存中已经存在的他们冲突否则就要覆盖他们的数据。0x8f0000x8f000+0x1001正是我们的内核栈空间我们需要检测它是否和其它空间有冲突。
### 放置内核文件与字库文件
放置内核文件和字库文件这一步,也非常简单,甚至放置其它文件也一样。
因为我们的内核已经编译成了一个独立的二进制程序,和其它文件一起被打包到映像文件中了。所以我们必须要从映像中把它解包出来,将其放在特定的物理内存空间中才可以,放置字库文件和放置内核文件的原理一样,所以我们来一起实现。
```
//放置内核文件
void init_krlfile(machbstart_t *mbsp)
{
//在映像中查找相应的文件并复制到对应的地址并返回文件的大小这里是查找kernel.bin文件
u64_t sz = r_file_to_padr(mbsp, IMGKRNL_PHYADR, "kernel.bin");
if (0 == sz)
{
kerror("r_file_to_padr err");
}
//放置完成后更新机器信息结构中的数据
mbsp->mb_krlimgpadr = IMGKRNL_PHYADR;
mbsp->mb_krlsz = sz;
//mbsp->mb_nextwtpadr始终要保持指向下一段空闲内存的首地址
mbsp->mb_nextwtpadr = P4K_ALIGN(mbsp->mb_krlimgpadr + mbsp->mb_krlsz);
mbsp->mb_kalldendpadr = mbsp->mb_krlimgpadr + mbsp->mb_krlsz;
return;
}
//放置字库文件
void init_defutfont(machbstart_t *mbsp)
{
u64_t sz = 0;
//获取下一段空闲内存空间的首地址
u32_t dfadr = (u32_t)mbsp->mb_nextwtpadr;
//在映像中查找相应的文件并复制到对应的地址并返回文件的大小这里是查找font.fnt文件
sz = r_file_to_padr(mbsp, dfadr, "font.fnt");
if (0 == sz)
{
kerror("r_file_to_padr err");
}
//放置完成后更新机器信息结构中的数据
mbsp->mb_bfontpadr = (u64_t)(dfadr);
mbsp->mb_bfontsz = sz;
//更新机器信息结构中下一段空闲内存的首地址
mbsp->mb_nextwtpadr = P4K_ALIGN((u32_t)(dfadr) + sz);
mbsp->mb_kalldendpadr = mbsp->mb_bfontpadr + mbsp->mb_bfontsz;
return;
}
```
以上代码的注释已经很清楚了都是调用r\_file\_to\_padr函数在映像中查找kernel.bin和font.fnt文件并复制到对应的空闲内存空间中。
请注意由于内核是代码数据所以必须要复制到指定的内存空间中。r\_file\_to\_padr函数我已经帮你写好了其中的原理在前面的内容里已经做了说明这里不再展开。
### 建立MMU页表数据
前面解决了文件放置问题我们还要解决另一个问题——建立MMU页表。
我们在二级引导器中建立MMU页表数据目的就是要在内核加载运行之初开启长模式时MMU需要的页表数据已经准备好了。
由于我们的内核虚拟地址空间从0xffff800000000000开始所以我们这个虚拟地址映射到从物理地址0开始大小都是0x400000000即16GB也就是说我们要虚拟地址空间0xffff8000000000000xffff800400000000 映射到物理地址空间00x400000000。
我们为了简化编程,使用**长模式下的2MB分页方式**,下面我们用代码实现它,如下所示。
```
#define KINITPAGE_PHYADR 0x1000000
void init_bstartpages(machbstart_t *mbsp)
{
//顶级页目录
u64_t *p = (u64_t *)(KINITPAGE_PHYADR);//16MB地址处
//页目录指针
u64_t *pdpte = (u64_t *)(KINITPAGE_PHYADR + 0x1000);
//页目录
u64_t *pde = (u64_t *)(KINITPAGE_PHYADR + 0x2000);
//物理地址从0开始
u64_t adr = 0;
if (1 > move_krlimg(mbsp, (u64_t)(KINITPAGE_PHYADR), (0x1000 * 16 + 0x2000)))
{
kerror("move_krlimg err");
}
//将顶级页目录、页目录指针的空间清0
for (uint_t mi = 0; mi < PGENTY_SIZE; mi++)
{
p[mi] = 0;
pdpte[mi] = 0;
}
//映射
for (uint_t pdei = 0; pdei < 16; pdei++)
{
pdpte[pdei] = (u64_t)((u32_t)pde | KPDPTE_RW | KPDPTE_P);
for (uint_t pdeii = 0; pdeii < PGENTY_SIZE; pdeii++)
{//大页KPDE_PS 2MB可读写KPDE_RW存在KPDE_P
pde[pdeii] = 0 | adr | KPDE_PS | KPDE_RW | KPDE_P;
adr += 0x200000;
}
pde = (u64_t *)((u32_t)pde + 0x1000);
}
//让顶级页目录中第0项和第((KRNL_VIRTUAL_ADDRESS_START) >> KPML4_SHIFT) & 0x1ff项指向同一个页目录指针页
p[((KRNL_VIRTUAL_ADDRESS_START) >> KPML4_SHIFT) & 0x1ff] = (u64_t)((u32_t)pdpte | KPML4_RW | KPML4_P);
p[0] = (u64_t)((u32_t)pdpte | KPML4_RW | KPML4_P);
//把页表首地址保存在机器信息结构中
mbsp->mb_pml4padr = (u64_t)(KINITPAGE_PHYADR);
mbsp->mb_subpageslen = (u64_t)(0x1000 * 16 + 0x2000);
mbsp->mb_kpmapphymemsz = (u64_t)(0x400000000);
return;
}
```
这个函数的代码写得非常简单,**映射的核心逻辑由两重循环控制**外层循环控制页目录指针顶只有16项其中每一项都指向一个页目录每个页目录中有512个物理页地址。
物理地址每次增加2MB这是由2630行的内层循环控制每执行一次外层循环就要执行512次内层循环。
最后顶级页目录中第0项和第((KRNL\_VIRTUAL\_ADDRESS\_START) >> KPML4\_SHIFT) & 0x1ff项指向同一个页目录指针页这样的话就能让虚拟地址0xffff8000000000000xffff800400000000和虚拟地址00x400000000访问到同一个物理地址空间00x400000000这样做是有目的**内核在启动初期,虚拟地址和物理地址要保持相同。**
### 设置图形模式
在计算机加电启动时计算机上显卡会自动进入文本模式文本模式只能显示ASCII字符不能显示汉字和图形所以我们要让显卡切换到图形模式。
切换显卡模式依然要用BIOS中断这个调用原理我们前面已经了如指掌。在实模式切换显卡模式的汇编代码我已经帮你写好了下面我们只要写个C函数调用它们就好了代码如下所示。
```
void init_graph(machbstart_t* mbsp)
{
//初始化图形数据结构
graph_t_init(&mbsp->mb_ghparm);
//获取VBE模式通过BIOS中断
get_vbemode(mbsp);
//获取一个具体VBE模式的信息通过BIOS中断
get_vbemodeinfo(mbsp);
//设置VBE模式通过BIOS中断
set_vbemodeinfo();
return;
}
```
上面init\_graph函数中的这些处理VBE模式的代码我已经帮你写好你可以自己在graph.c文件查看。
什么你不懂VBE其实我开始也不懂后来通过搜寻资料才知道。
其实VBE是显卡的一个图形规范标准它定义了显卡的几种图形模式每个模式包括屏幕分辨率像素格式与大小显存大小。调用BIOS 10h中断可以返回这些数据结构。[如果你实在对VBE感兴趣可以自行阅读其规范](https://vesa.org/) 。
这里我们选择使用了VBE的118h模式该模式下屏幕分辨率为1024x768显存大小是16.8MB。显存开始地址一般为0xe0000000。
屏幕分辨率为1024x768即把屏幕分成768行每行1024个像素点但每个像素点占用显存的32位数据4字节红、绿、蓝、透明各占8位。我们只要往对应的显存地址写入相应的像素数据屏幕对应的位置就能显示了。
每个像素点,我们可以用如下数据结构表示:
```
typedef struct s_PIXCL
{
u8_t cl_b; //蓝
u8_t cl_g; //绿
u8_t cl_r; //红
u8_t cl_a; //透明
}__attribute__((packed)) pixcl_t;
#define BGRA(r,g,b) ((0|(r<<16)|(g<<8)|b))
//通常情况下用pixl_t 和 BGRA宏
typedef u32_t pixl_t;
```
我们再来看看屏幕像素点和显存位置对应的计算方式:
```
u32_t* dispmem = (u32_t*)mbsp->mb_ghparm.gh_framphyadr;
dispmem[x + (y * 1024)] = pix;
//xy是像素的位置
```
### 串联
好了所有的实施工作的函数已经完成了现在我们需要在init\_bstartparm()函数中把它们串联起来,即按照事情的先后顺序,依次调用它们完成相应的工作,实现检查、收集机器信息,设置工作环境。
```
void init_bstartparm()
{
machbstart_t *mbsp = MBSPADR;
machbstart_t_init(mbsp);
//检查CPU
init_chkcpu(mbsp);
//获取内存布局
init_mem(mbsp);
//初始化内核栈
init_krlinitstack(mbsp);
//放置内核文件
init_krlfile(mbsp);
//放置字库文件
init_defutfont(mbsp);
init_meme820(mbsp);
//建立MMU页表
init_bstartpages(mbsp);
//设置图形模式
init_graph(mbsp);
return;
}
```
到这里init\_bstartparm()函数就成功完成了它的使命。
## 显示Logo
前面我们已经设置了图形模式,也应该要展示一下了,检查一下工作成果。
我们来显示一下我们内核的logo。其实在二级引导器中我已经帮你写好了显示logo函数而logo文件是个**24位的位图文件**,目前为了简单起见,我们**只支持这种格式的图片文件**。下面我们去调用这个函数。
```
void logo(machbstart_t* mbsp)
{
u32_t retadr=0,sz=0;
//在映像文件中获取logo.bmp文件
get_file_rpadrandsz("logo.bmp",mbsp,&retadr,&sz);
if(0==retadr)
{
kerror("logo getfilerpadrsz err");
}
//显示logo文件中的图像数据
bmp_print((void*)retadr,mbsp);
return;
}
void init_graph(machbstart_t* mbsp)
{
//……前面代码省略
//显示
logo(mbsp);
return;
}
```
在图格式的文件中除了文件头的数据就是图形像素点的数据只不过24位的位图每个像素占用3字节并且位置是倒排的即第一个像素的数据是在文件的最后依次类推。我们只要依次将位图文件的数据按照倒排次序写入显存中这样就可以显示了。
我们需要把二级引导器的文件和logo文件打包成映像文件然后放在虚拟硬盘中。
复制文件到虚拟硬盘中得先mount然后复制最后转换成VDI格式的虚拟硬盘再挂载到虚拟机上启动就行了。这也是为什么要手动建立硬盘的原因打包命令如下。
```
lmoskrlimg -m k -lhf initldrimh.bin -o Cosmos.eki -f initldrsve.bin initldrkrl.bin font.fnt logo.bmp
```
如果手动打命令对你来说还是比较难也别担心我已经帮你写好了make脚本你只需要进入代码目录中make vboxtest 就行了,运行结果如下 。
![](https://static001.geekbang.org/resource/image/c3/y0/c3d4f0b072b837f208fbd52749913yy0.jpg "代码运行结果示意图")
啊哈终于显示了logo。是不是挺有成就感的这至少证明我们辛苦写的代码是正确的。
但是目前我们的代码执行流还在二级引导器中我们的目的是开发自己的操作系统我们是要开发Cosmos。
**后面我们正式用Cosmos命名我们的操作系统。**Cosmos可以翻译成宇宙尽管它刚刚诞生但我对它充满期待所以用了这样一个能够“包括万物包罗万象”的名字。
## 进入Cosmos
我们在调用Cosmos第一个C函数之前我们依然要写一小段汇编代码切换CPU到长模式初始化CPU寄存器和C语言要用的栈。因为目前代码执行流在二级引导器中进入到Cosmos中这样在二级引导器中初始过的东西都不能用了。
因为CPU进入了长模式寄存器的位宽都变了所以需要重新初始化。让我们一起来写这段汇编代码吧我们先在Cosmos/hal/x86/下建立一个init\_entry.asm文件写上后面这段代码。
```
[section .start.text]
[BITS 32]
_start:
cli
mov ax,0x10
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
mov gs,ax
lgdt [eGdtPtr]
;开启 PAE
mov eax, cr4
bts eax, 5 ; CR4.PAE = 1
mov cr4, eax
mov eax, PML4T_BADR ;加载MMU顶级页目录
mov cr3, eax
;开启 64bits long-mode
mov ecx, IA32_EFER
rdmsr
bts eax, 8 ; IA32_EFER.LME =1
wrmsr
;开启 PE 和 paging
mov eax, cr0
bts eax, 0 ; CR0.PE =1
bts eax, 31
;开启 CACHE
btr eax,29 ; CR0.NW=0
btr eax,30 ; CR0.CD=0 CACHE
mov cr0, eax ; IA32_EFER.LMA = 1
jmp 08:entry64
[BITS 64]
entry64:
mov ax,0x10
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
mov gs,ax
xor rax,rax
xor rbx,rbx
xor rbp,rbp
xor rcx,rcx
xor rdx,rdx
xor rdi,rdi
xor rsi,rsi
xor r8,r8
xor r9,r9
xor r10,r10
xor r11,r11
xor r12,r12
xor r13,r13
xor r14,r14
xor r15,r15
mov rbx,MBSP_ADR
mov rax,KRLVIRADR
mov rcx,[rbx+KINITSTACK_OFF]
add rax,rcx
xor rcx,rcx
xor rbx,rbx
mov rsp,rax
push 0
push 0x8
mov rax,hal_start ;调用内核主函数
push rax
dw 0xcb48
jmp $
[section .start.data]
[BITS 32]
x64_GDT:
enull_x64_dsc: dq 0
ekrnl_c64_dsc: dq 0x0020980000000000 ; 64-bit 内核代码段
ekrnl_d64_dsc: dq 0x0000920000000000 ; 64-bit 内核数据段
euser_c64_dsc: dq 0x0020f80000000000 ; 64-bit 用户代码段
euser_d64_dsc: dq 0x0000f20000000000 ; 64-bit 用户数据段
eGdtLen equ $ - enull_x64_dsc ; GDT长度
eGdtPtr: dw eGdtLen - 1 ; GDT界限
dq ex64_GDT
```
上述代码中111行表示加载7075行的GDT1317行是设置MMU并加载在二级引导器中准备好的MMU页表1930行是开启长模式并打开Cache3454行则是初始化长模式下的寄存器5561行是读取二级引导器准备的机器信息结构中的栈地址并用这个数据设置RSP寄存器。
最关键的是6366行它开始把8和hal\_start函数的地址压入栈中。dw 0xcb48是直接写一条指令的机器码——0xcb48这是一条返回指令。这个返回指令有点特殊它会把栈中的数据分别弹出到RIPCS寄存器这正是为了调用我们Cosmos的**第一个C函数hal\_start**。
## 重点回顾
这是我们设置工作模式与环境的最后一课,到此为止我们的二级引导器已经建立起来了,成功从 GRUB手中接过了权柄开始了它自己的一系列工作二级引导器完成的工作不算少我来帮你梳理一下重点如下。
1.二级引导器彻底摆脱了GRUB的控制之后就开始检查CPU获取内存布局信息确认是不是我们要求的CPU和内存大小接着初始化内核栈、放置好内核文件和字库文件建立MMU页表数据和设置好图形模式为后面运行内核做好准备。
2.当二级引导器完成了上述功能后就会显示我们操作系统的logo这标志着二级引导器所有的工作一切正常。
3.进入Cosmos我们的二级引导器通过跳转到Cosmos的入口结束了自己光荣使命Cosmos的入口是一小段汇编代码主要是开启CPU的长模式最后调用了Cosmos的第一个C函数hal\_start。
你想过吗我们的二级引导器还可以做更多的事情其实还可以在二级引导器中获取ACPI表进而获取CPU数量和其它设备信息期待你的实现。
## 思考题
请你想一下init\_bstartparm()函数中的init\_mem820()函数,这个函数到底干了什么?
欢迎你在留言区跟我互动。如果你身边有朋友对手写操作系统有热情,也欢迎你把这节课转发给他。