从零开始写 OS 内核 - 加载并进入 kernel
作者:hyuan
来源:SegmentFault 思否社区
系列目录
序篇 准备工作 BIOS 启动到实模式 GDT 与保护模式 虚拟内存初探 加载并进入 kernel 显示与打印 GDT 和 IDT,中断处理 虚拟内存完善 实现堆和 malloc 创建第一个内核线程 多线程运行与切换 锁与多线程同步 进程的实现 进入用户态 一个简单的文件系统 加载可执行程序 系统调用的实现 键盘驱动 运行 shell
kernel 磁盘镜像
disk
和 memory
(物理内存)的数据对应关系:kernel page tables
,即第一张图的橙色部分,共 256 张占地 1MB。编写 kernel
kernel
,即图中绿色部分,它现在实际上还不存在,所以首先我们需要实现、编译一个简单的 demo 性质的 kernel。如果对 kernel 是什么还没有概念的同学,可能会问:到底 kernel 长什么样?main() {
while (1) {}
}
while
循环,没有任何其它东西,但它足以用作我们这里的 demo。编译 kernel
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.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
。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
mbr
和 loader
的加载
不同,这里将读取
和加载
两个词分开,是因为它们是两个步骤:读取:是将 kernel 磁盘镜像的 原始二进制 复制到内存中某空闲处,这里的二进制是 ELF 格式的; 加载:是将前一步得到的 ELF 可执行二进制进行解析,将每一个 section
复制到它们被 编址 的地方;
(0xFFFFFFFF - 1MB) ~0xFFFFFFFF
的 1MB 空间作为二进制镜像的存放地址。当然也要为它分配相应的物理页 frames
,在 page table
中建立映射。然后就可以像之前读取 mbr 和 loader 一样,将 kernel 镜像读取进来。program header table
中获取每个 section
的位置和大小,以及加载的内存地址(当然是 virtual 地址),然后将数据 copy 过去。这一次加载的内存地址,才是 0xC0800000
开始的位置。当然在 copy 之前,当然要为它们预先分配好 frames 并且在 page table
中建立好内存映射。这一切工作都在 allocate_pages_for_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
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)
指定的。0xC0800003
处,就是那个 while 循环的位置,这将是 kernel 征途的真正开篇:)评论