导语:今天我们来编写一个简单的内核,可以在x86系统上加载GRUB引导加载程序。此内核将在屏幕上显示一条消息,然后挂起。
大家好,今天我们来编写一个简单的内核,可以在x86系统上加载GRUB引导加载程序。此内核将在屏幕上显示一条消息,然后挂起。
x86机器是如何启动的
在我们考虑编写内核之前,让我们看看机器是如何启动并将控制转移到内核的。
上电后,x86 CPU的大多数寄存器都有明确定义的值。指令指针(EIP)寄存器保存处理器正在执行的指令的存储器地址。EIP硬编码为值0xFFFFFFF0,因此,x86 CPU硬连线开始在物理地址0xFFFFFFF0处执行。实际上,它是32位地址空间的最后16个字节。该存储器地址称为复位向量。
现在,芯片组的内存映射确保0xFFFFFFF0映射到BIOS的某个部分,而不是RAM。同时,BIOS将自身复制到RAM以便更快地访问,我们称之为shadowing。地址0xFFFFFFF0只包含一条跳转指令,指向BIOS复制自身的内存中的地址。
到这里,BIOS代码开始执行。BIOS首先按配置的引导设备顺序搜索可引导设备。它会检查某个幻数以确定设备是否可引导(第一扇区的字节511和512是否为0xAA55)。
一旦BIOS找到可引导设备,它就会从物理地址0x7c00开始将设备第一个扇区的内容复制到RAM中,然后跳转到地址并执行刚刚加载的代码,此代码称为引导加载程序。
然后,引导加载程序将内核加载到物理地址0x100000。地址0x100000用作x86计算机上所有大内核的起始地址。
所有x86处理器都以简单的16位模式开始,称为实模式。GRUB引导加载程序通过将CR0寄存器的最低位设置为1来切换到32位保护模式,因此,内核以32位保护模式加载。
请注意,在Linux内核的情况下,GRUB会检测Linux启动协议并以实模式加载Linux内核。Linux内核本身可以切换到保护模式。
我们需要什么?
· 一台x86电脑
· Linux
· NASM汇编程序
· gcc
· ld(GNU链接器)
· grub
源代码
源代码可以在我的Github存储库中找到——https://t.umblr.com/redirect?z=%2F%2Fgithub.com%2Farjun024%2Fmkernel&t=MGJmMmY4Y2U0YWE0NGY1YmMzYjFjYmJkNTQwNTg4ODU2NzEyY2RhZSxJVHNWb3BUUw%3D%3D&b=t%3AlMu0rOs9jGwbQo_qiwGh_A&p=https%3A%2F%2Farjunsreedharan.org%2Fpost%2F82710718100%2Fkernel-101-lets-write-a-kernel&m=1
使用程序集的入口点
我们喜欢用C语言编写所有内容,但我们无法避免一些组装。我们将用x86汇编语言编写一个小文件,以此来作为我们内核的起点。我们所有的汇编文件都会调用一个外部函数,我们将用C编写,然后暂停程序流程。
我们如何确保此汇编代码将作为内核的起点呢?
我们将使用链接脚本来链接目标文件以生成最终的内核可执行文件(稍后将详细解释)。在此链接描述文件中,我们将明确指定我们希望将二进制文件加载到地址0x100000。正如我之前所说,这个地址是内核的预期。因此,引导加载程序将负责触发内核的入口点。
这是汇编代码:
;;kernel.asm bits 32;nasm directive - 32 bit section .text global start extern kmain ;kmain is defined in the c file start: cli ;block interrupts mov esp, stack_space;set stack pointer call kmain hlt ;halt the CPU section .bss resb 8192;8KB for stack stack_space:
第一条指令bits 32不是x86汇编指令,它是NASM汇编程序的一个指令,它指定它应该生成代码,以在32位模式下运行的处理器上运行。在我们的例子中,它并不是强制性的,但是这里包含了它,因为它确实是一个良好实践。
第二行开始文本部分(又名代码部分),这是我们放置所有代码的地方。
global是另一个将源代码中的符号设置为全局的NASM指令,通过这样做,链接器知道符号的start位置,这恰好是我们的切入点。
kmain是我们的函数,将在我们的kernel.c文件中定义。extern声明该函数在其他地方声明。
然后,我们有了start函数,它调用kmain函数并使用hlt指令暂停CPU 。中断可以从hlt指令中唤醒CPU,所以我们事先使用cli指令禁用中断。cli是明显中断的缩写。
理想情况下,我们应该为堆栈留出一些内存并将堆栈指针(esp)指向它。但是,似乎GRUB这样做就已经设置了堆栈指针。但是,为了以防万一,我们将在BSS部分中分配一些空间,并将堆栈指针指向分配的内存的开头。我们使用resb以字节为单位保留内存的指令,在它之后,留下一个标签,指向保留的内存块的边缘。在kmain调用之前,堆栈指针(esp)使用mov指令使指向该空间。
C中的内核
在kernel.asm,我们调用了该函数kmain()。所以我们的C代码将开始执行kmain():
/* * kernel.c */ void kmain(void) { const char *str = "my first kernel"; char *vidptr = (char*)0xb8000; //video mem begins here. unsigned int i = 0; unsigned int j = 0; /* this loops clears the screen * there are 25 lines each of 80 columns; each element takes 2 bytes */ while(j < 80 * 25 * 2) { /* blank character */ vidptr[j] = ' '; /* attribute-byte - light grey on black screen */ vidptr[j+1] = 0x07; j = j + 2; } j = 0; /* this loop writes the string to video memory */ while(str[j] != '\0') { /* the character's ascii */ vidptr[i] = str[j]; /* attribute-byte: give character black bg and light grey fg */ vidptr[i+1] = 0x07; ++j; i = i + 2; } return; }
我们所有的内核都会清除屏幕并写入字符串“我的第一个内核”。
首先,我们创建一个指向vidptr地址0xb8000的指针,该地址是受保护模式下的视频内存的开始。屏幕的文本内存只是我们地址空间中的一块内存,屏幕的内存映射输入/输出从0xb8000开始,支持25行,每行包含80个ASCII字符。
该文本存储器中的每个字符元素由16位(2字节)表示,而不是我们习惯的8位(1字节)。第一个字节应该具有ASCII中的字符表示,第二个字节是attribute-byte。这描述了其特征属性,比如说颜色。
如果要s在黑色背景上打印绿色字符,我们只需要将字符s存储在视频存储器地址的第一个字节中,将值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”的字符被写入视频内存块,每个字符保存一个0x07的属性字节。
这将会在屏幕上显示字符串。
链接部分
我们将kernel.asm与NASM组装成一个目标文件,然后使用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有一个论点,它指定应该是我们的可执行文件的入口点的符号名称。
SECTIONS是我们最重要的部分,在这里,我们定义可执行文件的布局。我们可以指定如何合并不同的部分以及每个部分的放置位置。
在SECTIONS语句后面的大括号内,句点字符(.)表示位置计数器。
在SECTIONS块的开头,位置计数器始终初始化为0x0,这可以通过为其分配新值来修改它。
记住前文中说过的,内核的代码应该从地址0x100000开始,所以我们将位置计数器设置为0x100000。
看看下一行.text:{*(.text)}
星号(*)是一个匹配任何文件名的通配符,因此,表达式 *(.text)表示.text来自所有输入文件的所有输入节。
因此,链接器将目标文件的所有文本部分,合并到位置计数器中存储地址的可执行文件部分,所以我们的可执行文件的代码部分从0x100000开始。
链接器放置文本输出节之后,位置计数器的值将变为0x1000000 +文本输出节的大小。
类似的,数据和bss部分被合并并放置在location-counter的then值处。
Grub和Multiboot
现在,我们准备好构建内核的所有文件。但是,既然我们想用GRUB引导程序引导我们的内核,那么还有一步。
有一个使用引导加载程序加载各种x86内核的标准,称为Multiboot spec(多重引导规范)。
如果GRUB符合Multiboot规范,它将只加载我们的内核。
根据规范,内核必须在其前8个千字节内包含一个头,称为Multiboot header(多引导头)。
此外,此Multiboot标头必须包含3个字节,即4字节对齐,即:
· 一个魔术字段(magic field):包含幻数0x1BADB002,以识别头部。
· 一个标志字段(flags field):我们不关心这个字段,只需将其设置为零。
· 一个校验字段(checksum field):添加“magic”和“flags”的校验字段必须为零。
所以我们的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 defined in the c file start: cli ;block interrupts mov esp, stack_space;set stack pointer call kmain hlt ;halt the CPU section .bss resb 8192;8KB for stack stack_space:
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目录中。(需要超级用户权限才能执行此操作。)
在GRUB配置文件中,grub.cfg应该添加一个条目,例如:
title myKernel root (hd0,0) kernel /boot/kernel-701 ro
不要忘记删除指令hiddenmenu(如果存在的话)。
重新启动计算机,你将获得列出内核名称的列表选择。
选择它,你可以看到:
那就是你的内核!!
PS:
*始终建议给自己设置一个虚拟机,用于各种内核黑客攻击。
*要在grub2 上运行它,这是新发行版的默认引导加载程序,你的配置应如下所示:
menuentry 'kernel 701' { set root='hd0,msdos1' multiboot /boot/kernel-701 ro }
*此外,如果您想在qemu模拟器上运行内核而不是使用GRUB启动内核,你可以通过以下方式执行此操作:
qemu-system-i386 -kernel kernel
参考和感谢:
1. wiki.osdev.org
2. osdever.net
3. Multiboot spec
4. 感谢RubénLaguna对grub2 config的评论