Hello World!
让我们编写一个简单的内核,可以在X86
系统上加载GRUB
引导程序。此内核将在屏幕上显示一条消息,然后挂起。
x86
机器是如何启动的呢?
先看看机器如何启动并将控制转移到内核:
x86
CPU 在物理地址[0xFFFFFFF0]
处开始执行。实际上,它是32位地址空间的最后16个字节。该地址只含一条地址跳转指令,指向BIOS
复制自身的内存中的地址。因此,
BIOS
代码开始执行,BIOS
首先按照配置过的引导设备顺序搜寻可引导设备,它会检查某个magic number
去确定设备是否可引导。一旦
BIOS
找到可引导设备,它就会从物理地址[0x7c00]
处开始将设备第一个扇区的内容复制到RAM
中。然后然后跳转到该地址并执行刚刚加载的代码——此代码称为引导加载程序。接着,引导加载程序将内核加载到物理地址
[0x100000]
,地址[0x100000]
用作x86
计算机上所有大内核的起始地址。
我们需要什么?
- 一台
x86
的计算机 Linux
NASM
汇编程序gcc
ld (GNU Linker)
grub
源代码
在 **mkernel
**中
使用汇编语言写程序入口
我们习惯用C语言编写所有的内容,但是我们无法避免一点儿汇编语言。我们将用x86
汇编语言编写一个小文件,作为我们内核的起点。我们所有的汇编文件将会调用一个外部函数,我们将用C语言编写,然后暂停程序流。
那么我们如何确保此汇编代码作为内核的起点呢?
我们将使用链接脚本来链接目标文件以生成最终的内核可执行文件(后面会详细说明)。在此链接脚本文件中,我们将明确指定我们希望将二进制文件加载到地址[0x100000]
。正如我们之前所说的那样,这个地址是内核预期的地方。因此,引导加载程序将负责触发内核的入口。
以下是汇编代码:
;;kernel.asm
bits 32 ;nasm directive(指令) - 32位
section .text
globel start
extern kmain ;kmain is defined in the c file
start:
cli ;block interrupts
call kmain
hlt ;CPU暂停
第一个指令bits 32
不是x86
汇编指令,它是NASM
汇编程序的第一个指令,它指定应该在32位处理器上运行代码。在我们的实例中这并非强制要求,但这里包含它确实是个好习惯。
第二行开始文本部分(aka
代码部分),这是我们放置所有代码的部分。
global
是另一个将源代码中的符号设置为全局变量的NASM
指令,这样,链接器知道符号开始的位置,这恰好是我们的切入点。
kmain
是我们将在kernel.c
文件中定义的函数,extern
声明该函数在其他地方声明。
然后,我们有start
函数,它调用kmain
函数并使用hlt
指令暂停CPU
,而中断可以从hlt
指令中唤醒CPU
,所以我们事先使用cli
指令禁用中断,而cli
是clear-interrupts
的缩写。
C语言部分的kernel
在文件kernel.asm
中,我们调用了函数kmain()
,所以我们的C代码将开始在kmain()
处执行
/*
* kernel.c
*/
void kmain(void)
{
char *str="my first kernel";
char *vidptr=(char*)0xb8000; //video mem begins here
unsigned int i = 0;
unsigned int j = 0;
// clear all
while(j<80 * 25 * 2)
{
//blank character
vidptr[j] = ''; //attribute-byte:黑色屏幕上的浅灰色
vidptr[j+1] = 0x07;
j += 2;
}
j = 0;
while(str[j] != '\0')
{
vidptr[i] = str[j];
vidptr[i+1] = 0x07;
++j;
i += 2;
}
return;
}
我们所有的内核都会清除屏幕并写入字符串"my first kernel"。
首先,我们创建一个指向地址[0xb8000]
的指针vidpri
,这个地址是保护模式下video memory
的开始。屏幕的文本内存只是我们地址空间中的一块内存,而屏幕的输入/输出从[0xb8000]
开始,支持25行,每行包含80个ascii
字符。
在该文本缓存中每个字符元素由16位(2字节)表示,而不是我们熟知的8位(1字节)。第一个字节应该有ASCII
中的有的字符,第二个字节是属性字节。这描述了字符的格式,包括颜色等属性。
要在黑色背景上打印绿色字符s
,我们将字符s
存储在video memory
地址的第一个字节,将值[0x02]
存储在第二个字节,0
表示黑色背景,2
表示绿色背景
可以查看下表中的不同颜色:
0 - Black, 1 - Blue, 2 - Green, 3 - Cyan, 4 - Red, 5 - Magenta, 6 - Brown, 7 - Light Grey, 8 - Dark Grey, 9 - Light Blue, 10/a - Light Green, 11/b - Light Cyan, 12/c - Light Red, 13/d - Light Magenta, 14/e - Light Brown, 15/f – White.
在我们的内核中,我们将在黑色背景上使用浅灰色字符,所以我们的属性字节值必须为[0x07]
在第一个while
循环中,程序在25行80列中写入[0x07]
属性的空白字符,这样可以清除屏幕。
在第二个while
循环中,空终止字符串"my first kernel"
的字符被写入video memory
块中,每个字符有[0x07]
的属性字节。
一波操作之后,这将在屏幕中显示字符串。
链接部分
我们将使用NASM
将kernel.asm
链接到目标文件中,然后使用GCC
编译kernel.c
成为另一个目标文件,现在,我们的工作是将这些对象链接到一个可执行引导内核。
因此,我们使用显示链接脚本器,它可以作为参数传递给ld
(我们的链接器)
/*
* link.ld
*/
OUTPUT_FORMAT(elf32-i386)
ENTRY(start)
SECTIONS
{
. = 0X100000;
.text : { *(.text)}
.data : { *(.data)}
.bss : { *(.bss)}
}
首先,我们将输出的格式设置为32位可执行可链接格式(ELF),ELF
是x86
架构上类Unix
系统的标准二进制文件格式。
ENTRY
有一个参数,他指定我们可执行文件入口的symbol name
SECTIONS
使我们最终要的地方,在这里我们定义可执行文件的布局。我们可以指定如何合并不同的部分以及每个部分放置在哪里。
在SECTIONS
语句后面的大括号内,"句点’.’ '"表示位置计数器,位置计数器在SECTION
语句的开头总是始终初始化为[0x0]
,可以通过给它分配新值以修改它。
记住,早些时候,我告诉过你,内核的代码应该在地址[0x100000]
开始,因此,我们将位置计数器设置为[0x100000]
看下一行.text : { *(.text) }
,星号"*"是一个匹配任何文件名的通配符,因此,表达式*(.text)
表示来自所有输入文件的所有.text
输入部分。
因此,链接器 在位置计数器地址处 将目标文件的所有文本部分合并到可执行文件中。因此,我们可执行文件的代码部分从[0x100000]
开始。
链接器放置文本输出部分后,位置计数器的值变为[0x100000]
+文本输出部分的大小。
类似地,data
和bss
部分被合并到位置计数器的接下来的值所表示的地址处。
Grub与多重引导
现在,我们已经准备好构建内核的所有文件,但是,由于我们希望使用Grub
引导加载程序引导内核,因此还有一步之遥。
已经有一个给不同使用引导加载程序的x86
内核的标准,称为"多重引导规范(Multiboot specification
)"。
如果我们的内核符合"多重引导规范",那么GRUB
将仅加载我们的内核。
根据规范,内核必须在其8千字节内包含引导头(也就是Multiboot header
)。
此外,Multiboot header
必须包含三个(align 4)
的字段:
a magic field
:包含magic number [0x1BADB002]
,以识别引导头a flags field
:我们不关心此字段,只是简单地将其设置为0a checksum field
:当加上前两个字段时,该字段必须为0
所以我们的kernel.asm
文件改为:
;;kernel.asm
;nasm directive - 32 bit
bits 32
section .text
;multiboot spec
align 4
dd 0x1BADB002 ;magic
dd 0x00 ;flags
dd - (0x1BADB002 + 0x00) ;checksum. m+f+c should be zero
global start
extern kmain ;kmain is define in the c file
start:
cli ;block interrupts
call kmain
hlt
dd
指令定义了大小为4字节的双精度变量。
构建内核
我们现在将使用kernel.asm
和kernel.c
创建目标文件,然后使用我们的链接脚本链接。
nasm -f elf32 kernel.asm -o kasm.o
这条指令将运行汇编程序以ELF-32
格式创建目标文件kasm.o
gcc -m32 -c kernel.c -o kc.o
-c
选项确保在编译之后,链接不会隐式发生
ld -m elf_i386 -T link.ld -o kernel kasm.o kc.o
这条指令将使用我们的链接脚本运行链接器并生成名为kernel
的可执行文件。
配置GRUB并运行内核
GRUB
要求您的内核名称为kernel-<version>
的格式,所以,我们得重命名内核,重命名为kernel-701
现在将它放置到/boot
目录中,你需要root
权限才可以执行此操作。
在GRUB
的配置文件grub.cfg
中,添加一个条目:
title mykernel
root (hd0, 0)
kernel /boot/kernel-701 ro
不要忘记删除指令hiddenmenu
(如果存在的话),重启计算机,您将获得列出内核名称的列表选择。
选择你新建的内核,你讲看到:
这就是你的内核!!!
PS:
- 建议您使用虚拟机来进行内核游戏
- 要在
grub2
(linux
新发行版的默认引导程序)上运行,你的配置应该是这样的:
menuentry 'kernel 7001' {
set root='hd0,msdosl'
multiboot /boot/kernel-70001 ro
}
- 此外,如果要在
qemu
仿真器上运行内核而不是使用GRUB
启动,可以通过一下方式执行此操作:
qemu-system-i386 -kernel kernel
这是英文原版地址:https://download.csdn.net/download/haozihuang/11264520