@会网络的老鼠

涂飞平的博客空间

写一个极小的操作系统内核

2 年前 0

最近工作太多,需要做点Java之外的东西来调节一下。
偶然看到一篇文章Let's write a kernel,虽然这个标题看起来有点大,但它确实是一个实实在在的内核。通过这个极小内核,我们可以继续完成诸如cpu分层管理,虚拟机内存管理等。
不过在这里,demo仅仅显示几个字符。
本文没有什么原创的东西,都是按照文章的内容,一步一步做,完成实验,理论很简单,结果也很简单。整个过程做下来,读者能了解到以下几个事实:
1、Linux下面各种工具真是应有尽有;
2、用C语言编写系统代码远比汇编简单;
3、越底层,对特定地址的依赖越大,你甚至需要在链接程序中指定地址;
4、链接程序的重要性比应用层开发更突出(没有了虚拟地址,地址需要干预);
5、Linux确实适合学习,实验操作系统原理。

下面按照文章的步骤,我们一步一步学着做。
1、准备环境,我们这里使用虚拟机(VirtualBox),安装Debian 3.19.3 操作系统
Screenshot1.png
2、进入Debian系统,下载nasm编译工具,gcc系统默认已经有了,不用下载;
使用命令 apt-get install nasm 下载安装完毕后,开始进入编码阶段

3、编写一小段汇编代码: bootload.asm
bits 32 ;告诉nasm编译器,这里需要生成32位指令
section .text ;代码段
align 4 ; 这里是为gurb引导特殊的设置
;gurb2要求一个魔数来表明这是内核载入代码
db 0x1BADB002 ;1BADB002就是这个Magic Number了
db 0x00 ; 魔数的结束边界
dd - (0x1BADB002 + 0x00) ; 这段魔数定义的长度,会体现在代码段中
global start ;导出start到符号表,以便链接工具读取
extern kmain ;从符号表载入kmain地址,需要链接工具写入

start:
cli ;在引导时候,关闭cpu中断
call kmain ;调用kmain函数,这个函数是用c语言编写的
hlt ; cpu停机指令

以上都是很简单的汇编程序,加上注释,应该可以看明白。
真正的逻辑,用c语言来实现,可以用汇编来实现吗?当然可以,但需要写的汇编指令比较多!那上面这段代码用c来写可以吗?答案是不可以,因为这里通过dd定义,可以准确的构建出loader的镜像结构(text段的前面4个字节必须是1BADB002,grub要求的,否则,grub会认为其不是正确的内核,并拒绝引导到该系统)。

4、用c语言继续完成我们的内核逻辑(kernel.c),这里比较简单,就显示一段文字;
void kmain(void) {
char* str = "Test os loader"; // 需要显示的文字
char* vidptr = (char*) 0xb8000; // 0xb8000是实模式下显存地址
unsigned int i = 0;
unsigned int j = 0;
while ( j < 80 * 25 * 2) {
//刷新显存,为什么是80*25?因为实模式下的屏幕就是25行,
//每行80字符,每个字符占用2字节,一个字节是字符,一个
//字节是属性(比如颜色信息)
vidptr[j] = ' '; //都刷新为空格
vidptr[j + 1] = 0x07; //0x07是浅白色字
j = j + 2;
}
j = 0;
while (str[j] != '\0') {
//将我们定义的字符写入到显存中
vidptr[i] = str[j];
vidptr[i + 1] = 0x07;
++j;
i = i + 2; // 每个字符两字节
}
return;
}


5、分别编译汇编和c程序:
nasm -f elf32 bootload.asm -o bootload.o
gcc -m32 -c kernel.c -o kc.o

这里需要注意的是参数,nasm的-f指定编译器生成的目标文件格式为elf32,而gcc的编译使用-c参数,告诉编译器仅仅完成编译,并不做链接操作,我们稍后会把这两个文件通过链接工具合并为一个内核程序;

6、链接文件
这一步是最关键的,平时写应用层系统,一般都是IDE直接调用ld程序把不同模块的文件链接到一个执行程序中,而现在我们需要介入链接程序,告诉它如何将两个部分结合起来,并将不同段放在不同的位置。
OUTPUT_FORMAT(elf32-i386) //链接目标格式
ENTRY(start) //入口函数
SECTIONS // 各节信息
{
. = 0x100000; //镜像起始地址,以下所有路径都以这个路径为基点
.text : {*(.text)} //所有文件的代码节合并为一个.text节
.data : {*(.data)} //所有文件的数据节合并
.bss : {*(.bss)} //所有文件的全局变量数据节合并
}

以上的地址0x100000是真实的物理地址,因为我们编译的内核最后会被载入到0x100000地址中,所以,所有内核中的地址的物理地址都是需要加上0x100000的,告知连接器这个数字,连接器会在之后的符号表地址确定的时候给出正确的物理地址。还要注意这里定义的顺序,.text段在最前面,然后是.data和.bss,在实际生成的镜像中,也是这样布置的。
运行下面的命令,将c语言和汇编语言输出的中间文件链接到kernel文件中。
ld -m elf_i386 -T link.ld -o kernel bootload.o kc.o


7、将kernel拷贝到/boot/目录下面,并改名为kernel-801
cp kernel /boot/
mv /boot/kernel /boot/kernel-801

注意,这里需要切换到root用户才有权限做拷贝,改名的操作

8、修改grub.cfg,然后进入我们的内核,用root用户编辑/boot/grub/grub.cfg文件,找到最后一个menuentry节点后面,再添加下面的内容,我的grub.cfg这个位置是144行,这个位置可能不一样,仅供参考。
menuenty 'kernel 801' {
set root='hd0,msdos1'
multiboot /boot/kernel-801 ro
}

然后重启虚拟机,如果按照步骤,都没有问题的话,应该可以看到这个界面了。
Screenshot2.png
选择Kernel-801后,就进入到我们的内核了,没错,显示的就是"Test os loader"
Screenshot3.png
当然,你也可以将文字显示得更绚一点,比如,来个红色的~~
Screenshot4.png

到这里,系统的控制权已经交给我们的代码了,完成一个操作系统的工作漫漫长路才刚刚开始,接下来建立操作系统数据结构,初始化vmm环境,跳入保护模式,载入各种设备驱动等等过程都需要大量代码来完成(据初步估计,一个类Linux的完整操作系统内核,c语言代码量大概在1000万行上下)!
目前我们写的这个还是比较简单,两个地址,实模式显存地址0xb8000,在当年写dos程序的时候,也经常往这个地址里面刷字符~~至于0x100000地址是什么,这是BIOS的约定,它在启动后,会将内核代码载入到内存物理地址0x100000的位置。

作者在其博客http://arjunsreedharan.org/ 中的第二部分加入了键盘响应,完成基本的输入输出(IO),有兴趣的可以继续尝试!

编写评论