设为首页 加入收藏

TOP

内核代号101 — 动手写自己的内核
2014-11-23 23:41:55 来源: 作者: 【 】 浏览:18
Tags:内核 代号 101 手写 自己

Hi, 大家好。


在这篇文章中,我们将从零开始,动手编写一个可以用GRUB来引导的简单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]处。这个地址就是我之前说过的,内核所希望的起始地址。


汇编代码如下:


第一行指令 bit32 不是x86汇编指令,它是一条NASM 指令,指定nasm汇编器产生32位的程序,这条语句并不是必不可少的,但加上它是一个好的编程习惯。


第二行是text段(代码段)的开始,在这里存放着我们的代码块。


global是另外一个NASM指令,用将一个符号设置为全局符号。这样做连接器才会知道符号start在哪儿开始,start是我们程序的入口地址。


kmain是我们定义在kernel.c文件中的函数,extern关键字声明了该函数定义在别的文件中。


到这里,我们的函数start调用kmian函数之后就会使用hlt指令将CPU挂起,中断会cpu从hlt 指令中唤醒,我们要在挂起之前用cli指令来关闭系统的中断响应,cli指令是清除中断(clear-interrupts)的缩写。


kernle.asm中,我们调用了kmain()函数,所以我们的c代码将会在kmain()中开始运行:


我们的内核首先会清空整个屏幕,然后打印出字符串。


首先,我们用一个vidptr指针,指向地址[0xb8000] , 这个地址是保护模式下显存的起始地址。屏幕的文本内容对应着的内存空间中一个内存段,即屏幕的输出输出映射到了内存中地址[0xb8000]的地方,整个屏幕共支持25行,每行80个ASCII字符。


在文本内存中每一个字符由16bits(2个字节)表示,这不像我们以前使用8bits来定义。其中第一个字节是该字符的ASCII码,第二个字节是属性字节, 它描述了字符的表现形式,包括了字符颜色等属性。


为了在黑色的背景下打印绿色字符’s‘,我们将字符’s‘放在显存中的第一个字节,接着将[0x02]放在第二个字节中, 其中 0表示黑色背景,2表示绿色前景。


下面是不同颜色的定义:


在我们的内核中,我们将字符颜色设置为灰色,将背景颜色设定为黑色,因此我们的属性字节的值是[0x07].


在第一个while循环中,程序将属性值为[0x07]的空格字符(‘ ’)写到整个屏幕中(共25行,每行80个字符),这样就会将整个屏幕清空了。


在第二个while循环中,我们将null结尾的字符串 “my first kernel” ,从显存的起始处开始写入。


这样字符串就打印在屏幕上了


我们用NASM,GCC分别将kernale.asm,kernel.c编译成目标文件,接着将这些目标文件链接成一个可引导的内核程序。


我们指定ld连接器按照我们脚本规定来进行链接。


脚本指定了输出格式为 32位的ELF文件格式. ELF(Executable and Linkable Format)是x86构架的类Unix系统标准的二进制格式。


ENTRY 接收一个参数。它指定了可执行文件的入口符号。


SECTIONS 对我们来讲是最重要的。在这里,我们定义即将生成的可执行文件的布局。我们可以定义各个段链接融合的方式以及放置的位置。


SECTIONS 后的花括号中,符号 (.) 表示的是一个位置计数器。它通常会被初始化为[0x0],作为SECTIONS 块的起始地址 ,它的值是可以被修改的。 之前我说过,内核代码需要在地址[0x100000]处,所以我们将它修改为[0x100000]。


接着看下一行的 .text : { *(.text) }


星号( * )是一个通配符,表示所有的文件名。*(.text)表示将所有输入文件的 .text


因此,按照这个设定,连接器将所有目标文件的text段融合到最终可执行文件的text 段中,即在位置计数器所标识的地址处 ([0x100000])。


在连接器将处理好输出的text段后,地址计数器的值会变为[0x100000]+text段的长度。


类似的,data段和bss段也会相应得融合后放置到地址计数器所标识的位置。


现在我们已经准备好所有制作内核所需的文件了,但我们还有一步工作,我们还需要用grub Bootloader来启动我们的内核。


在按照Mutileboot 规范来编译我们的内核后,它就可以被GRUB引导了。


按照Mutileboot 的规范说明,内核必须在起始的8KB中包含这一个多引导项头(Multiboot header)。


而且,这个多引导项头里面必须有3个4字节对齐的块。


一个魔术块:包含了魔数[0x1BADB002],是多引导项头结构的定义值。


一个标志块:我们不关心这个块的内容,我们简单设定为0。


一个校检块:校检块,魔术块和标志块的数值的总和必须是0。


因此,我们的内核代码如下:


dd 指令定义了个4字节的双字。


】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
分享到: 
上一篇Bash脚本15分钟进阶教程 下一篇Java 面试题及答案

评论

帐  号: 密码: (新用户注册)
验 证 码:
表  情:
内  容: