共计 2790 个字符,预计需要花费 7 分钟才能阅读完成。
系列目录
- 序篇
- 筹备工作
- BIOS 启动到实模式
- GDT 与保护模式
- 虚拟内存初探
- 加载并进入 kernel
- 显示与打印
- GDT 和 IDT,中断解决
- 虚拟内存欠缺
- 实现堆和 malloc
- 创立第一个内核线程
- 多线程运行与切换
- 锁与多线程同步
- 过程的实现
- 进入用户态
- 一个简略的文件系统
- 加载可执行程序
- 零碎调用的实现
- 键盘驱动
- 运行 shell
kernel 磁盘镜像
接上一篇 虚拟内存初探,本篇将正式加载并启动 kernel,也就是图中绿色的局部:
当然 kernel 镜像要从磁盘上读取加载,所以这里回顾一张老图,是 disk
和 memory
(物理内存)的数据对应关系:
顺便提一下,上图中斜线暗影打问号的局部,就是上一章讲的 kernel page tables
,即第一张图的橙色局部,共 256 张占地 1MB。
编写 kernel
回到 kernel
,即图中绿色局部,它当初实际上还不存在,所以首先咱们须要实现、编译一个简略的 demo 性质的 kernel。如果对 kernel 是什么还没有概念的同学,可能会问:到底 kernel 长什么样?
答案非常简单:kernel 和你平时用 C 语言写的可执行程序简直没有任何区别,也是从一个 main 函数开始。
上面咱们就实现咱们的第一个 kernel:
void main() {while (1) {}}
就是这样简略,除了一个 while
循环,没有任何其它货色,但它足以用作咱们这里的 demo。
编译 kernel
这里有很多编译参数,例如以 32 位编码,禁用 C 规范库等(这是咱们本人定制的 OS,和 C 规范库不可能兼容)。
gcc -m32 -nostdlib -nostdinc -fno-builtin -fno-stack-protector -no-pie -fno-pic -c main.c -o main.o
链接 kernel:
ld -m elf_i386 -Tlink.ld -o kernel main.o
这里会用到一个 link 配置文件 link.ld
:
ENTRY(main)
SECTIONS
{
.text 0xC0800000:
{
code = .; _code = .; __code = .;
*(.text)
}
.data ALIGN(4096):
{
data = .; _data = .; __data = .;
*(.data)
*(.rodata)
}
.bss ALIGN(4096):
{
bss = .; _bss = .; __bss = .;
*(.bss)
. = ALIGN(4096);
}
end = .; _end = .; __end = .;
}
这里最重要的就是定义了 text
段的起始地址 0xC0800000
,也是整个 kernel 编址的起始。如果你还记得上一篇的内容,咱们布局了 kernel 空间的虚拟内存散布:
0xC0800000
将是 kernel 的入口地址,因为 text
段会被加载到此处,往后顺次是 data
,bss
等段。loader
完结后将会跳转到该地址。
另外下面还定义了整个可执行文件的入口函数为 main
。
编译链接后的 kernel 是一个 ELF 格局的二进制,咱们无妨将它反汇编 dump 看一下:
objdump -dsx kernel
能够看到 main
函数的地址为 0xC080000
,这是进入 kernel 后的第一条指令。
制作 kernel 镜像
dd if=kernel of=scroll.img bs=512 count=2048 seek=9 conv=notrunc
seek=9
是因为后面 mbr
和 loader
曾经在磁盘上占据了前 9 个 sectors。这里 kernel 大小为 2048 个 sectors 共 1MB,对于咱们这个我的项目而言曾经足够大了,齐全够用。
当初磁盘镜像终于变成了这样:
读取并加载 kernel
镜像筹备结束,接下来就能够将 kernel 读取并且加载了。首先还是给出代码链接 init_kernel,供你参考。
和之前 mbr
和 loader
的 加载
不同,这里将 读取
和加载
两个词离开,是因为它们是两个步骤:
- 读取:是将 kernel 磁盘镜像的 原始二进制 复制到内存中某闲暇处,这里的二进制是 ELF 格局的;
- 加载:是将前一步失去的 ELF 可执行二进制进行解析,将每一个
section
复制到它们被 编址 的中央;
首先来看第一步“读取”。咱们抉择的是虚拟内存顶部的 1MB,即 (0xFFFFFFFF - 1MB) ~0xFFFFFFFF
的 1MB 空间作为二进制镜像的寄存地址。当然也要为它调配相应的物理页 frames
,在 page table
中建设映射。而后就能够像之前读取 mbr 和 loader 一样,将 kernel 镜像读取进来。
接下来是第二步“加载”。这里波及到了依据 ELF 文件格式的标准进行解析,次要就是从 program header table
中获取每个 section
的地位和大小,以及加载的内存地址(当然是 virtual 地址),而后将数据 copy 过来。这一次加载的内存地址,才是 0xC0800000
开始的地位。当然在 copy 之前,当然要为它们事后调配好 frames 并且在 page table
中建设好内存映射。这所有工作都在 allocate_pages_for_kernel 这个函数中提前完成了。
进入 kernel
所有准备就绪,接下来就能够真正进入 kernel 了:
init_kernel:
call allocate_pages_for_kernel
call load_hd_kernel_image
call do_load_kernel
; init floating point unit before entering the kernel
finit
; move stack to 0xF0000000
mov esp, KERNEL_STACK_TOP - 16
mov ebp, esp
; let's jump to kernel entry :)
jmp eax
ret
首先初始化了 CPU 的浮点数单元,避免它前面异样。
而后我将 stack
移到了比拟高的地址 0xF0000000
地位,这当然不是必须的,以后的 stack 地位其实也不错(大概在 0x7B00
以下左近的地位,这是在 mbr 中转移过来的,如果你还记得的话)。只是我心愿前面的 stack 地位能被移到 0xC0000000
以上的 kernel 空间中,所以才这么做了一步。stack 的地位是比拟灵便的,只有是一个闲置的,不会受到烦扰的中央就能够。
而后非常简单,jmp eax
一条指令跳到了 kernel
入口处。
为什么是 eax
?这是下面函数 do_load_kernel
的返回值,这个函数就是咱们解析加载 kernel 的 ELF 二进制的函数,它会返回值 kernel 的入口地址,即 main
函数地址,这个地址是由 ELF 文件中 ELF Header
的 e_entry
字段给出的。ELF 可执行二进制的入口地址是在链接阶段确定的,它实际上是由之前的 link.ld
里的 ENTRY(main)
指定的。
顺利的话,运行的后果如下:
程序曾经胜利地进入 kernel 并且运行到了 0xC0800003
处,就是那个 while 循环的地位,这将是 kernel 征途的真正开篇:)