0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

编写一个可以用GRUB来引导的简单x86内核

C语言专家集中营 2018-01-21 09:12 次阅读

在这篇文章中,我们将从零开始,动手编写一个可以用GRUB来引导的简单x86内核,该内核会在屏幕上打印一条信息,然后——挂起!

one-does-not-kernel

一个人写一个内核是一件简单的事情

X86机器是怎样启动的?

在我们思考怎样写一个内核之前,让我们先看一下x86机器从启动到把控制权交给内核的过程是怎样的:

x86CPU在机器启动之后就会从地址[0xFFFFFFF0]处开始执行,这个地址就是在32位寻址空间中的最后16个字节处,这里存放了一条跳转指令,会跳转到内存中BIOS代码起始处。

接着,cpu就开始开始执行BIOS代码块了,BIOS首先会在我们配置好的启动设备序列中,通过检查一个特定的魔数,找到第一个可以引导的设备。

一旦BIOS找到一个可以引导的设备后,它就会把该设备第一个扇区的代码复制到物理内存的[0x7c00]的位置,然后跳转到这个地址开始执行这一段代码,我们习惯把这一段代码叫作bootloader。

Bootloader会将内核代码加载到物理内存[0x100000]的位置,[0x100000]这个地址是所有x86机器宏内核代码的起始地址。

我们需要哪一些工具?

* 一个x86构架的计算机

*Linux

*NASM汇编

*GCC

*LD(GNU连接器)

*GRUB

源码

源代码可以在我的Github上找到:Githubrepository-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]!=''){20.                               vidptr[i]= str[j];21.                               vidptr[i+1]=0x07;22.                                ++j;23.                                i = i +2;24.                       }25.            return;26.    }

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

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

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

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

下面是不同颜色的定义:

1. 0-Black,1-Blue,2-Green,3-Cyan,4-Red,5-Magenta,6-Brown,7-LightGrey,8-DarkGrey,9-LightBlue,10/a -LightGreen,11/b -LightCyan,12/c -LightRed,13/d -LightMagenta,14/e -LightBrown,15/f –White.

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

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

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

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

链接部分

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

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

1. /*2. * link.ld3. */4. OUTPUT_FORMAT(elf32-i386)5. ENTRY(start)6. SECTIONS7. {8. .=0x100000;9. .text :{*(.text)}10. .data :{*(.data)}11. .bss :{*(.bss)}12. }

脚本指定了输出格式为 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和多重引导

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

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

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

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

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

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

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

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

1.;;kernel.asm2.3.;nasm directive -32 bit4.bits 325.section .text6.;multiboot spec7.align 48.dd0x1BADB002 ;magic9.dd0x00 ;flags10.dd-(0x1BADB002+0x00);checksum. m+f+c should be zero11.12.global start13.extern kmain ;kmain isdefinedin the c file14.15.start:16.cli ;block interrupts17.call kmain18.hlt ;halt the CPU

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

生成内核

我们现在开始将kernel.asm和kernel.c编译成目标文件,接着将它们根据我们的连接器脚本的设定链接到一起:

1. nasm -f elf32 kernel.asm-o kasm.o

启动NASM汇编器将kernel.asm编译成ELF-32位格式的目标文件。

1. gcc-m32 -c kernel.c -o kc.o

-c选项告知GCC编译器在将源文件编译成目标文件后,不要对它们进行链接。

1. ld-m elf_i386 -T link.ld-o kernel kasm.o kc.o

启动链接器,根据我们的链接脚本生成一个名为kernel的可执行的文件。

配置grub,启动内核

GRUB需要以kernel-的形式来命名内核程序,所以,我将它重名为kernel-701.

接着将它放在/boot目录下,这一步需要你需要拥有超级用户权限才能够进行操作。

在你的GRUB配置文件grub.cfg中加上一个引导入口,如下:

1. title myKernel2. root (hd0,0)3. kernel /boot/kernel-701 ro

如果存在一个“hiddenmenu”的指令,记得要把它移除掉。

重启电脑,你就能够看到你的内核也在启动选择项列表中了。

选择启动它之后,结果如下:

mkernel

成功显示出来了。

”这是你的内核“

”不,是你的内核“。

PS:

* 建议你在虚拟机中进行你所有内核hacking。

* 在一些新的发行版中,使用了grub2作为默认的bootloader,你需要向下面这样来配置你的配置文件。

(感谢RubénLaguna提供了grub2的配置)

1. menuentry 'kernel 7001'{2. set root='hd0,msdos1'3. multiboot /boot/kernel-7001 ro4. }

* 如果你想用qemu模拟器代替GRUB来启动你的内核程序的话,你可以怎么做:

1. qemu-system-i386 -kernel kernel

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 内核
    +关注

    关注

    3

    文章

    1372

    浏览量

    40288
  • Linux
    +关注

    关注

    87

    文章

    11302

    浏览量

    209432
  • X86
    X86
    +关注

    关注

    5

    文章

    294

    浏览量

    43458

原文标题:内核代号001 — 动手写自己的内核

文章出处:【微信号:C_Expert,微信公众号:C语言专家集中营】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    X86与ARM,江湖厮杀鹿死谁手?

    关于X86架构和ARM架构这两者谁将统市场的争执直都有,但是也有人说这两者根本不具备可比性,X86无法做到 ARM的功耗,而ARM也无法做到X8
    发表于 08-04 10:20 4152次阅读

    Galileo 开发板的体验、分析和应用——x86 版的 Arduino

    :使用 Locale Emulator 启动 Arduino IDE,解决闪退问题 2.3 发热与功耗功耗直是困扰 x86 平台在移动嵌入式领域应用的重要问题,我对 Intel Galileo 做了几个简单的测试
    发表于 12-17 11:43

    解读x86、ARM和MIPS三种主流芯片架构

    的精简,所以许多工作必须组合简单的指令,而针对复杂组合的工作便需要由编译程序执行。而CISC体系的x86指令集因为硬件所提供的指令集较多,所以许多工作都能够以
    发表于 05-25 16:09

    CPLD和X86的low pin count问题!

    到AE。但是,是有问题的,这个时候应该x86也开始扫描LPC了,导致过不去,USB就不可以使用2:我就讲CPLD的连接LPC的这几个引脚,不管设置为输入,还是输出,,还是输入三态到
    发表于 05-31 10:11

    grub.conf配置核心知识详解

    引导加载程序(Boot loader)是在计算机在加载操作系统内核之前运行的段小程序。通过这段小程序,可以初始化硬件设备、建立内存空间的映射图,从而将系统的软硬件环境加载到
    发表于 04-08 16:52

    分钟掌握Linux系统grub.conf配置核心知识

    引导文件)。引导加载程序读取grub.conf文件的配置信息,然后根据对应配置信息启动不同的操作系统, 管理员可以从三
    发表于 08-15 17:08

    Powerpc架构与X86架构的区别

    目录1、ARM1.1 ARM历史1.2 ARM内核系列2、MIPS应用范围发展历史3、PowerPC三巨头4、X86架构X86历史5、PowerPC架构相比于ARM的优势6、Powerpc架构
    发表于 07-26 06:16

    如果arm CHIP內建x86 decoder會能跑x86

    如果arm CHIP內建 x86 decoder 會能跑 x86?現在X86 cpu 有些都變 micro code .. risc
    发表于 06-14 11:38

    x86构架的SoC及STPC的种应用

    讲述x86 构架的SoC 的发展及近况,并描述种基于STPC-Industrial 芯片的网络终端设备的设计与实现。这种网络终端设备具有体积小、结构简单、功能强、软件适应性强的特点。
    发表于 04-16 10:38 20次下载

    x86 构架的SoC 及STPC 的种应用

    讲述x86 构架的SoC 的发展及近况,并描述种基于STPC-Industrial 芯片的网络终端设备的设计与实现。这种网络终端设备具有体积小、结构简单、功能强、软件适应性强的特点。
    发表于 05-15 13:25 12次下载

    针对英特尔Atom,AMD两款x86内核计划出炉

    针对英特尔Atom,AMD两款x86内核计划出炉 AMD描述了两款新x86内核的概貌——其中款针对英特尔Atom——计划于2011年投产
    发表于 11-26 09:06 821次阅读

    32位x86汇编设计8051模拟器

    32位x86汇编设计8051模拟器,有兴趣的同学可以下载学习
    发表于 05-03 16:36 19次下载

    什么是x86嵌入式工控主板,x86嵌入式主板该如何选择

    什么是x86嵌入式工控主板?x86嵌入式主板就是嵌入式工控机内常用到的主板,它跟嵌入式工控机样,体积比较小,主板上的CPU、内存都是直接焊在主板上,不像普通工控机主板上的CPU和内存可以
    发表于 12-09 12:50 3294次阅读

    什么是X86主板,X86嵌入式主板该如何挑选

    起来了解几个关于X86嵌入式主板的相关文字图片内容吧。大家将为大家详细介绍嵌入式主板的介绍、嵌入式主板的分类以及X86主板的相关信息这3方面的信息。
    发表于 04-12 15:49 5525次阅读

    X86架构与Arm架构区别

    X86架构(The X86 architecture)是微处理器执行的计算机语言指令集,指intel通用计算机系列的标准编号缩写,也标识
    的头像 发表于 02-22 09:37 6965次阅读