在这篇文章中,我们将从零开始,动手编写一个可以用GRUB来引导的简单x86内核,该内核会在屏幕上打印一条信息,然后——挂起!
one-does-not-kernel
一个人写一个内核是一件简单的事情
X86机器是怎样启动的?
在我们思考怎样写一个内核之前,让我们先看一下x86机器从启动到把控制权交给内核的过程是怎样的:
x86 CPU在机器启动之后就会从地址 [0xFFFFFFF0]处开始执行,这个地址就是在32位寻址空间中的最后16个字节处,这里存放了一条跳转指令,会跳转到内存中BIOS代码起始处。
接着,cpu就开始开始执行BIOS代码块了,BIOS首先会在我们配置好的启动设备序列中,通过检查一个特定的魔数,找到第一个可以引导的设备。
一旦BIOS找到一个可以引导的设备后,它就会把该设备第一个扇区的代码复制到物理内存的[0x7c00]的位置,然后跳转到这个地址开始执行这一段代码,我们习惯把这一段代码叫作bootloader。
Bootloader会将内核代码加载到物理内存[0x100000]的位置,[0x100000]这个地址是所有x86机器宏内核代码的起始地址。
我们需要哪一些工具?
* 一个x86构架的计算机
* Linux
* NASM 汇编器
* GCC
* LD(GNU 连接器)
* GRUB
源码
源代码可以在我的Github上找到: Github repository - mkernel
用汇编代码来编写内核入口
我们喜欢用c来做所有的事情,但是我们无可避免地需要用到一点儿汇编,我们将会写一小段x86的汇编代码来作为内核入口,这一段汇编代码会在调用我们的c代码后停止整个程序流程。
我们怎样确认汇编代码会作为内核的起始点呢?
我们将用一个连接器脚本将这些目标文件链接成我们最终的内核程序(稍后解释更多),在连接器脚本里,我们指定了这段二进制代码会被加载到内存 [0x100000]处。这个地址就是我之前说过的,内核所希望的起始地址。
汇编代码如下:
1. ;;kernel.asm2. bits 32 ;nasm directive -32 bit3. section .text4. 5. global start6. extern kmain ;kmain isdefinedin the c file7. 8. start:9. cli ;block interrupts10. call kmain11. hlt ;halt the CPU
第一行指令 bit32 不是x86汇编指令,它是一条NASM 指令,指定nasm汇编器产生32位的程序,这条语句并不是必不可少的,但加上它是一个好的编程习惯。
第二行是text段(代码段)的开始,在这里存放着我们的代码块。
global是另外一个NASM指令,用将一个符号设置为全局符号。这样做连接器才会知道符号start在哪儿开始,start是我们程序的入口地址。
kmain是我们定义在kernel.c文件中的函数,extern关键字声明了该函数定义在别的文件中。
到这里,我们的函数start调用kmian函数之后就会使用hlt指令将CPU挂起,中断会cpu从hlt 指令中唤醒,我们要在挂起之前用cli指令来关闭系统的中断响应,cli指令是清除中断(clear-interrupts)的缩写。
用C实现的内核
在kernle.asm中,我们调用了kmain()函数,所以我们的c代码将会在kmain()中开始运行:
1. /*2. * kernel.c3.*/4. void kmain(void)5.{6. char*str ="my first kernel";7. char*vidptr =(char*)0xb8000; //video mem begins here.8. unsignedint i =0;9. unsignedint j =0;10. //clear all11. while(j <80*25*2){12. //blank character13. vidptr[j]=' ';14. //attribute-byte: light grey on black screen 15. vidptr[j+1]=0x07; 16. j = j +2;17. }18. j =0;19. while(str[j]!='