极客时间已完结课程限时免费阅读

12 | 设置工作模式与环境(下):探查和收集信息

12 | 设置工作模式与环境(下):探查和收集信息-极客时间

12 | 设置工作模式与环境(下):探查和收集信息

讲述:陈晨

时长14:57大小13.67M

你好,我是 LMOS。
上节课我们动手实现了自己的二级引导器。今天这节课我们将进入二级引导器,完成具体工作的环节。
在二级引导器中,我们要检查 CPU 是否支持 64 位的工作模式、收集内存布局信息,看看是不是合乎我们操作系统的最低运行要求,还要设置操作系统需要的 MMU 页表、设置显卡模式、释放中文字体文件。
今天课程的配套代码,你可以点击这里,自行下载。

检查与收集机器信息

如果 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 与内存:程序放在哪儿”那节课,看一下获取内存视图相关的知识点。
init_mem 函数在调用 mmap 函数后,就会得到 e820map 结构数组,其首地址和数组元素个数由 retemp,retemnr 两个变量分别提供。

初始化内核栈

因为我们的操作系统是 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 函数你要注意,这个我已经帮你写好啦,它主要负责判断一个地址空间是否和内存中存放的内容有冲突。
因为我们的内存中已经放置了机器信息结构、内存视图结构数组、二级引导器、内核映像文件,所以在处理内存空间时不能和内存中已经存在的他们冲突,否则就要覆盖他们的数据。0x8f000~(0x8f000+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,也就是说我们要虚拟地址空间:0xffff800000000000~0xffff800400000000 映射到物理地址空间 0~0x400000000。
我们为了简化编程,使用长模式下的 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,这是由 26~30 行的内层循环控制,每执行一次外层循环就要执行 512 次内层循环。
最后,顶级页目录中第 0 项和第 ((KRNL_VIRTUAL_ADDRESS_START) >> KPML4_SHIFT) & 0x1ff 项,指向同一个页目录指针页,这样的话就能让虚拟地址:0xffff800000000000~0xffff800400000000 和虚拟地址:0~0x400000000,访问到同一个物理地址空间 0~0x400000000,这样做是有目的,内核在启动初期,虚拟地址和物理地址要保持相同。

设置图形模式

在计算机加电启动时,计算机上显卡会自动进入文本模式,文本模式只能显示 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 感兴趣,可以自行阅读其规范
这里我们选择使用了 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;
//x,y是像素的位置

串联

好了,所有的实施工作的函数已经完成了,现在我们需要在 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 就行了,运行结果如下 。
代码运行结果示意图
啊哈!终于显示了 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
上述代码中,1~11 行表示加载 70~75 行的 GDT,13~17 行是设置 MMU 并加载在二级引导器中准备好的 MMU 页表,19~30 行是开启长模式并打开 Cache,34~54 行则是初始化长模式下的寄存器,55~61 行是读取二级引导器准备的机器信息结构中的栈地址,并用这个数据设置 RSP 寄存器。
最关键的是 63~66 行,它开始把 8 和 hal_start 函数的地址压入栈中。dw 0xcb48 是直接写一条指令的机器码——0xcb48,这是一条返回指令。这个返回指令有点特殊,它会把栈中的数据分别弹出到 RIP,CS 寄存器,这正是为了调用我们 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() 函数,这个函数到底干了什么?
欢迎你在留言区跟我互动。如果你身边有朋友对手写操作系统有热情,也欢迎你把这节课转发给他。
分享给需要的人,Ta购买本课程,你将得20
生成海报并分享

赞 25

提建议

上一篇
11 | 设置工作模式与环境(中):建造二级引导器
下一篇
13 | 第一个C函数:如何实现板级初始化?
unpreview
 写留言

精选留言(81)

  • neohope
    置顶
    2021-06-05
    大体上整理了一下,有几处没弄清楚【下半部分】: 11、返回到bstartparm.c 调用了chkcpmm.c的init_bstartpages 12、然后调用到了fs.c的move_krlimg函数申请了内存,建立了MMU页表: 顶级页目录,开始于0x1000000 页目录指针目录,开始于0x1001000,,共16项 ,其中每一项都指向一个页目录 页目录,开始于0x1002000, 每页指向512 个物理页,每页2MB【 0x200000】 让物理地址p[0]和虚拟地址p[((KRNL_VIRTUAL_ADDRESS_START) >> KPML4_SHIFT) & 0x1ff],指向同一个页目录指针页,确保内核在启动初期,虚拟地址和物理地址要保持相同 没搞清楚为什么虚拟地址是这个,也暂时没搞清楚为何要指向(u64_t)((u32_t)pdpte | KPML4_RW | KPML4_P) 最后,把页表首地址保存在机器信息结构中 13、返回到bstartparm.c 调用了graph.c的init_graph A、初始化了数据结构 B、调用init_bgadevice 首先获取GBA设备ID 检查设备最大分辨率 设置显示参数,并将参数保存到mbsp结构中 C、如果不是图形模式,要通过BIOS中断进行切换,设置显示参数,并将参数保存到mbsp结构中: 获取VBE模式,通过BIOS中断 获取一个具体VBE模式的信息,通过BIOS中断 设置VBE模式,通过BIOS中断 这三个方法同样用到了realadr_call_entry,调用路径与上面_getmmap类似,不再展开 D、初始化了一块儿内存 感觉会与物理地址与虚拟地址之间转换由一定关系 E、进行logo显示 调用get_file_rpadrandsz定位到位图文件 调用bmp_print,读入像素点,BGRA转换 最后调用write_pixcolor,写入到mbsp->mb_ghparm正确的位置,图像就显示出来了 14、然后一路返回 到bstartparm.c的init_bstartparm 到ldrkrlentry.c的ldrkrl_entry 到ldrkrl32.asm的call ldrkrl_entry 再往下是jmp 0x2000000 这个地址就是IMGKRNL_PHYADR,就是刚才放Cosmos.eki的位置 15、然后就接上了本节最后一部分内容了,不容易啊!哈哈哈!
    展开

    作者回复: 因为有一段 地址是物理地址,二级引导器中,并没有切换到长模式,是没有办法使用高端的物理地址,只能从低端地址切换

    16
  • neohope
    置顶
    2021-06-05
    大体上整理了一下,有几处没弄清楚【上半部分】: 1、bstartparm.c从init_bstartparm函数开始 A、初始化machbstart_t 2、跳转到chkcpmm.c的init_chkcpu函数,检查是否支持CPUID功能 A、init_chkcpu函数 CPU自带检查方式:无法反转 Eflags第21位,表示CPU支持CPUID功能 如果反转成功,说明不支持CPUID,打印内核错误并退出 B、然后调用CPUID功能,判断是否支持长模式 先通过通过0x80000000参数,调用cpuid命令,判断CPU是否支持扩展处理器信息【返回值比0x80000000大】 如果支持,通过0x80000001参数,调用cpuid命令,获取扩展处理器信息,然后检测第29位,判断是否支持长模式 如果不支持,打印内核错误并退出 C、设置mbsp中cpumode为64位 3、返回chkcpmm.c,继续检测内存信息 A、跳转到chkcpmm.c的init_mem函数 B、通过mmap调用realadr_call_entry(RLINTNR(0),0,0) C、实际会执行ldrkrl32.asm的realadr_call_entry D、跳转到save_eip_jmp E、最后在cpmty_mode处,把 0x18:0x1000 [段描述索引:段内的偏移],装入到 CS:EIP 寄存器中 F、而EIP这个地址恰恰是内存中initldrsve.bin的位置,因为之前write_realintsvefile把数据加载到了REALDRV_PHYADR 0x1000【而且在initldrsve.lds好像也指定了段内偏移0x1000】 同时CS中段描述符为k16cd_dsc,说明是16位代码段,可以执行,CPU继续从EIP地址执行 G、而initldrsve.bin是由realintsve.asm编译得到的,所以实际会继续执行realintsve.asm中代码 H、然后到real_entry这里,通过传入的参数ax,判断调用func_table哪个方法 当前参数位0,ax就是0,也就是调用了func_table的第一个函数_getmmap I、_getmmap中,通过BIOS的15h中断,获取内存信息 J、检查内存信息,如果小于128M,打印内核错误并退出 K、设置machbstart_t内存相关参数 L、然后调用了init_acpi 4、在init_acpi中 通过“RSD PTR ”及校验,判断是否支持ACPI功能 不支持则 打印内核错误并退出 5、返回到bstartparm.c 好像是确认了一下initldrsve.bin的状态,获取了一下文件内存地址及大小 6、返回到bstartparm.c,继续调用到chkcpmm.c的init_krlinitstack函数 7、然后调用到了fs.c的move_krlimg函数 首先判断新申请的地址,是否可用 -》如果可用直接使用 -》如果不可用,则判断申请的内存大小是否超出设备物理大小 -》-》如果超出大小,系统打印内核错误并退出 -》-》如果不超出大小,系统会将内存对齐到0x1000后,将initldrsve.bin移动到新位置,并修正地址 整体来说move_krlimg更像是申请内存,但不知道为何要不断驱赶initldrsve.bin文件 8、返回到chkcpmm.c 初始化栈顶和栈大小 9、返回到bstartparm.c 调用fs.c的init_krlfile函数,将Cosmos.eki加载到了IMGKRNL_PHYADR 并填写了mbsp相关内容 10、返回到bstartparm.c 调用了chkcpmm.c的init_meme820函数 然后调用到了fs.c的move_krlimg函数申请了内存,拷贝了一份e820map_t到Cosmos.eki之后的地址,并修正mbsp指向新地址 感觉和内存保护 或 物理地址与虚拟地址之间转换有一定关系
    展开

    作者回复: 是的,你学的很透

    共 6 条评论
    13
  • 言C
    置顶
    2021-10-17
    经过一段时间的摸索,今天终于成功显示了Cosmos的logo界面了,遇到了几个坑,特此来给大家填坑了: 1、出现无法找到Cosmos的错误提示,如下: 正在生成Cosmos内核映像文件,请稍后…… 文件数:6 映像文件大小:4911104 Converting from raw image file="../hd.img" to file="../exckrnl/hd.vdi"... Creating dynamic image with size 104857600 bytes (100MB)... VBoxManage: error: Could not find a registered machine named 'Cosmos' VBoxManage: error: Details: code VBOX_E_OBJECT_NOT_FOUND (0x80bb0001), component VirtualBoxWrap, interface IVirtualBox, callee nsISupports VBoxManage: error: Context: "FindMachine(Bstr(a->argv[0]).raw(), machine.asOutParam())" at line 1044 of file VBoxManageStorageController.cpp make[1]: *** [vbox.mkf:13:idectrnul] 错误 1 make: *** [Makefile:101:VBOXRUN] 错误 2 解决办法:需要自己建立一个虚拟机,命名为 Cosmos 2、运行 make virtualtest 后,virtualbox启动后没有出现logo图片,而出现错误提示:INITKLDR DIE ERROR:S 解决办法:建立虚拟机时,需要选择为 64bit 的系统 3、这个问题是与第二个的关联问题,当你在建立虚拟机时,想选择64bit的系统,却发现只有 32bit 的 解决办法: VirtualBox安装64位的系统需要满足以下条件: 1. 64位的cpu 2. cpu允许硬件虚拟化 先来看第一个条件,64位的CPU,这个嘛,现在的笔记本一般都是64位的了,所以不用担心,除非是好几年之间的电脑。如果你不清楚,可以打开命令行,输入systeminfo,在输出的信息中找到CPU这一行,如果是X86_64的,就是64位CPU; 第二条,是否开启CPU硬件虚拟化1,这个嘛,各大厂商的情况不大相同,有的电脑默认开启了(比如,我的HP),有的没有,所以需要自行开启,开启方法:开机时按某个键进入BIOS设置界面2。 然后,setup==>security==>cpu virtualization,将cpu virtualization这一项由Disable设置为Enable。保存,然后重启电脑,硬件虚拟化就开启成功了。 如果你是通过windows + vmware + virtualbox 的方式实现的,还需要设置 VMware,打开vmware,找到对应的虚拟机,虚拟机设置->硬件->处理器->虚拟化引擎,这里一般会有三个选项,全都勾上 然后在打开该虚拟机,去virtualbox中建立虚拟机时,就会发现有 64bit 的选项了
    展开

    作者回复: 厉害了

    5
  • blentle
    2021-06-04
    首先,这个函数为啥这么命名,没整明白

    作者回复: 这个问题不重要啊

    共 7 条评论
    13
  • pedro
    2021-06-04
    思考题还是挺麻烦的,主要是没有注释啊,很多字段的含义都是靠猜,文章也没有介绍到这些。 首先是 init_mem820 这个函数本身: ```c void init_meme820(machbstart_t *mbsp) { e820map_t *semp = (e820map_t *)((u32_t)(mbsp->mb_e820padr)); // e820数组地址 u64_t senr = mbsp->mb_e820nr; // 个数 e820map_t *demp = (e820map_t *)((u32_t)(mbsp->mb_nextwtpadr)); if (1 > move_krlimg(mbsp, (u64_t)((u32_t)demp), (senr * (sizeof(e820map_t))))) { kerror("move_krlimg err"); } m2mcopy(semp, demp, (sint_t)(senr * (sizeof(e820map_t)))); mbsp->mb_e820padr = (u64_t)((u32_t)(demp)); mbsp->mb_e820sz = senr * (sizeof(e820map_t)); mbsp->mb_nextwtpadr = P4K_ALIGN((u32_t)(demp) + (u32_t)(senr * (sizeof(e820map_t)))); mbsp->mb_kalldendpadr = mbsp->mb_e820padr + mbsp->mb_e820sz; return; } ``` 我们发现这个函数实际上在拷贝内存,即将 semp 指针处的 senr * (sizeof(e820map_t) 内存大小拷贝到 demp 处,而 demp 的地址正是 mb_nextwtpadr,那么这个 mb_nextwtpadr 是怎么来的呢?在init_krlfile函数中可以大致猜测: ```c void init_krlfile(machbstart_t *mbsp) { 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 = P4K_ALIGN(mbsp->mb_krlimgpadr + mbsp->mb_krlsz); mbsp->mb_kalldendpadr = mbsp->mb_krlimgpadr + mbsp->mb_krlsz; return; } ``` 没错,mb_nextwtpadr 正是跳过内核起始地址+内核大小后的第一个页地址,注意需要4k对齐。 那么刚才内存拷贝的意图也很明显,对于初始化后的内存,内核本身的内存映射是不可访问的,必须保护充分内核,所以 init_mem820 函数的作用是跳过内核初始化内存。 由于代码无注释,文中篇幅有限,如有错误,多多指正,望海涵~
    展开

    作者回复: 你猜的很对 看来我确实不用写注释

    11
  • neohope
    2021-06-05
    大体上整理了一下,有几处没弄清楚【补充最后一段,发漏了】 15、然后就接上了本节最后一部分内容了,不容易啊!哈哈哈! Cosmos.bin中【前面写错为Cosmos.eki了】,ld设置的程序入口为init_entry.asm的_start: 16、 init_entry.asm中_start: A、关闭中断 B、通过LGDT命令,指定长度和初始位置,加载GDT C、设置页表,开启PAE【CR4第五位设置为1】,将页表顶级目录放入CR3 D、读取EFER,将第八位设置为1,写回EFER,设置为长模式 E、开启保护模式【CR0第0位设置为1】,开启分页【CR0第31位设置为1】,开启CACHE【CR0第30位设置为0】,开启WriteThrough【CR0第29位设置为0】 F、初始化寄存器们 G、将之前复制到Cosmos.bin之后的mbsp地址,放入rsp H、0入栈,0x8入栈, hal_start 函数地址入栈 I、调用机器指令“0xcb48”,做一个“返回”操作,同时从栈中弹出两个数据[0x8:hal_start 函数地址],到[CS:RIP] 长模式下,指令寄存器为RIP,也就是说下一个指令为hal_start 函数地址 CS中为0x8,对应到ekrnl_c64_dsc,对应到内核代码段,可以执行,CPU继续冲RIP地址执行 17、hal_start.c A、执行hal_start函数
    展开

    作者回复: 嗯 就是这样的

    7
  • 嗣树
    2021-06-04
    回答下思考题,init_mem820() 的作用? 先说结论,init_mem820 函数的作用是将 init_mem() 中获取到的内存信息转存到字体文件之后,其实就是为它找一个安稳的地方存放。 简单分析一下: init_mem() -> mmap() -> realadr_call_entry() 然后进入实模式调用中断,实模式的访址能力是有限,我们为中断处理指定临时的地址(E80MAP_ADRADR) 存放内存信息数组 #define ETYBAK_ADR 0x2000 #define E80MAP_ADRADR (ETYBAK_ADR+68) 接下来在把内核文件加载到指定内存地址,字体文件紧随其后,然后就是我们收集到的信息,我们还要把放在临时位置的信息复制出来。
    展开

    作者回复: 对 的 对的 就是这样的

    共 2 条评论
    6
  • 2021-06-05
    init_mem820()函数作用是把e820map结构数组从低地址处(0x2068)搬到高地址处(0x2000000之上)。 问什么要搬?在实模式下能用的地址空间有限,只能访问低地址。所以e820map结构数组不能一直存在低地址处。

    作者回复: 对 对 对 你理解的很好

    共 4 条评论
    4
  • 一叉树
    2022-02-21
    Mac 主机 + VBox。卡在了文中“你只需要进入代码目录中 make vboxtest 就行了,运行结果如下 。”这一步很久很久。 我是在 Ubuntu 虚拟机中执行 make vboxtest 的,会生成一个 hd.img,同时会有如下报错 恭喜我,系统编译构建完成! ^_^ 恭喜我,系统编译构建完成! ^_^ 正在生成Cosmos内核映像文件,请稍后…… 文件数:6 映像文件大小:4911104 [sudo] password for mie: make[1]: VBoxManage: Command not found make[1]: *** [vbox.mkf:23: tranvdi] Error 127 make: *** [Makefile:101: VBOXRUN] Error 2 这时候需要自己 1. 把生成的 hd.img 搞到外面的 mac 2. 使用第 10 节课用到的命令转化为 vdi 文件: VBoxManage convertfromraw ./hd.img --format VDI ./hd.vdi 3. 新建一个虚拟机,将这个生成的 hd.vdi 当硬盘挂上去 4. 成功运行 ^_^ 曾经郁闷这一步怎么这么跳跃,文中省了这么多步骤。但仔细观察 cosmos/lesson12/Cosmos/build/vbox.mkf 这个文件你会发现,这一切老师早帮你写好了,只是我使用 mac主机 + Ubuntu虚拟机学习 + 另一虚拟机运行结果,用了两个虚拟机而非套娃,所以在 Ubuntu 中报错 VBoxManage: Command not found。老师写的自动拉起虚拟机运行的脚本没能自动运行。
    展开

    作者回复: 66666

    3
  • scutgj2010
    2021-08-15
    我是vmware虚拟机,不支持VBE的118模式,一直在报错vbe mode not 118,手动改成117或者120,还是继续报错,这个怎么解决呀?

    作者回复: 必须要118 否则要改图形驱动的

    共 5 条评论
    4
  • XF-薛峰
    2021-06-20
    这一讲的实验,我的配置是Win10+Virtualbox+Ubuntu用来做make编译,使用make vboxtest命令编译,有关Vbox的报错不用理会(我没在虚拟机下的虚拟机里安装),只要找到hd.vdi文件就好了,退出Ubuntu,另新建虚拟机使用这个hd.vdi就行了。如果只使用make,没有转换成hd.vdi

    作者回复: 你好,你需要转换成 hd.vdi

    共 7 条评论
    3
  • 孙孙孙
    2021-06-06
    正在生成Cosmos内核映像文件,请稍后…… 文件数:6 映像文件大小:4911104 Converting from raw image file="../hd.img" to file="../exckrnl/hd.vdi"... Creating dynamic image with size 104857600 bytes (100MB)... VBoxManage: error: Could not find a registered machine named 'Cosmos' VBoxManage: error: Details: code VBOX_E_OBJECT_NOT_FOUND (0x80bb0001), component VirtualBoxWrap, interface IVirtualBox, callee nsISupports VBoxManage: error: Context: "FindMachine(Bstr(a->argv[0]).raw(), machine.asOutParam())" at line 1044 of file VBoxManageStorageController.cpp make[1]: *** [vbox.mkf:13:idectrnul] 错误 1 make: *** [Makefile:101:VBOXRUN] 错误 2 有人知道这个是为什么吗?
    展开

    作者回复: 你好,你需要先建立一个虚拟机

    3
  • 野欲
    2021-09-26
    跟着写完了代码也不知道cosmos是怎么跑起来的,迷惑

    作者回复: 那就再来一遍

    3
  • zhanghj
    2021-06-13
    各种数据结构放的位置,比如内存视图,内核文件保存的位置,栈顶指针的地址,这些地址值是如何规划的?

    作者回复: 看看内存的布局 进行合理规划

    共 3 条评论
    2
  • 熊光红
    2021-06-06
    用lession12的hd.img制作了个hd.vdi,加载到虚拟机,运行出现out of memory错误,请问如何建立调试环境调试一下,看看哪儿出错了

    作者回复: 你好,你需要给虚拟机配置更大的内存

    共 3 条评论
    2
  • didi迪迪
    2022-09-14 来自湖北
    作者,您好,想请您解释下下面这段话: 顶级页目录中第 0 项和第 ((KRNL_VIRTUAL_ADDRESS_START) >> KPML4_SHIFT) & 0x1ff 项,指向同一个页目录指针页,这样的话就能让虚拟地址:0xffff800000000000~0xffff800400000000 和虚拟地址:0~0x400000000,访问到同一个物理地址空间 0~0x400000000 通过计算,发现 ((KRNL_VIRTUAL_ADDRESS_START) >> KPML4_SHIFT) & 0x1ff为100000000,也就是256,但是为什么第0项和第256项指向一个页目录指针页就可以实现访问同一个物理地址空间呢?
    展开

    作者回复: 可以看MMU是如何转换地址的

    1
  • Geek_504185
    2021-11-18
    7. 然后回到12课代码的根目录有Makefile的目录,利用命令make vboxtest,虚拟机启动了一直提示VT-x不支持。 8.查了资料,从BIOS里查看了已经开启虚拟化了,不知道为啥原因,有说Win10从10月后多Hyper-v需要关闭; 9.关闭了Hyper-V,再次实验还是提示不支持。 10.考虑是否是Virtualbox里没开启VT-x,查了虚拟机Cosmos,设置的system里确实没开启; 11.使用命令开启,开启后还是不行,完全懵了。 12.使用手工创建Cosmos虚拟机,发现Virtualbox就是没有64位系统的选项。 13.这时考虑到Virtualbox的宿主机是VMWare,所以VMWare也开启VT-x!!!! 14.再次运行,需修改Cosmos虚拟机内存,默认128M,改为1024M,实验成功。
    展开

    作者回复: 是的

    1
  • Geek_504185
    2021-11-18
    先说步骤吧: 第一步:开启硬件BIOS里的VT-x; 第二步: 如果和我一样有虚拟机开启虚拟机的VT-x; 第三步:检查Virtualbox创建虚拟机时出现了可以选择创建64位系统的选项就可以了; 第四步:lesson12代码, cd build, make createvm -f vbox.mkf make crtidectr -f vbox.mkf cd .. make vboxtest 其中这几天实验过程记录一下: 我的开始用的环境是:Win10+VMware12 (Ubuntu20.4 64位) 1. 开始从Ubuntu里创建hd.img,安装grub和编译Cosmos.eki放到/boot/目录下; 2. 然后利用StarWindConverter.exe转换成VMware识别的硬盘格式,运行; 3.运行程序提示INITKRL DIE ERROR:not find file ; 4.开始以为是VMWare虚拟机的问题(目前来看应该不是),转战使用Ubuntu20.4 里安装Virtualbox,开启套娃模式。 5.Ubuntu64位操作系统里使用sudo apt-get install virtualbox-6.1提示Package 'virtualbox-6.1' has no installation candidate, 就去官网下载64位virtualbox手动安装,最后提示安装失败,不支持,最后利用Ubuntu自带的App Store安装了Virtualbox6.1; 6.开始利用12课代码做实验,仔细看来makefi
    展开

    作者回复: 哈哈

    1
  • 余生
    2021-09-09
    起动提示INITKLDR DIE ERROR:S ,建虚机时选64位,入坑了很久才爬出来

    作者回复: 怎么会有这个错误的

    共 5 条评论
    1
  • 李城
    2021-09-02
    将Cosmos.eki 更名为HelloOS.eki, 放在./hdisk/boot 目录下,打开虚拟机,报错 not find file, 不知如何下手

    作者回复: 用gitee上课程目录中的代码构建 不要自己替换

    1