导语:今天我们来编写一个简单的内核,可以在x86系统上加载GRUB引导加载程序。此内核将在屏幕上显示一条消息,然后挂起。

大家好,今天我们来编写一个简单的内核,可以在x86系统上加载GRUB引导加载程序。此内核将在屏幕上显示一条消息,然后挂起。

微信图片_20180827212455.jpg

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(如果存在的话)。

重新启动计算机,你将获得列出内核名称的列表选择。

选择它,你可以看到:

微信图片_20180827212445.png

那就是你的内核!!

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的评论

源链接

Hacking more

...