写下第一个linux内核

Hello World!

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

x86机器是如何启动的呢?

先看看机器如何启动并将控制转移到内核:

x86CPU 在物理地址[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指令禁用中断,而cliclear-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]的属性字节。

一波操作之后,这将在屏幕中显示字符串。

链接部分

我们将使用NASMkernel.asm链接到目标文件中,然后使用GCC编译kernel.c成为另一个目标文件,现在,我们的工作是将这些对象链接到一个可执行引导内核。

因此,我们使用显示链接脚本器,它可以作为参数传递给ld(我们的链接器)

/*
 * link.ld
 */
OUTPUT_FORMAT(elf32-i386)
ENTRY(start)
SECTIONS
{
	. = 0X100000;
	.text : { *(.text)}
	.data : { *(.data)}
	.bss  : { *(.bss)}
}

首先,我们将输出的格式设置为32位可执行可链接格式(ELF),ELFx86架构上类Unix系统的标准二进制文件格式。

ENTRY有一个参数,他指定我们可执行文件入口的symbol name

SECTIONS使我们最终要的地方,在这里我们定义可执行文件的布局。我们可以指定如何合并不同的部分以及每个部分放置在哪里。

SECTIONS语句后面的大括号内,"句点’.’ '"表示位置计数器,位置计数器在SECTION语句的开头总是始终初始化为[0x0],可以通过给它分配新值以修改它。

记住,早些时候,我告诉过你,内核的代码应该在地址[0x100000]开始,因此,我们将位置计数器设置为[0x100000]

看下一行.text : { *(.text) },星号"*"是一个匹配任何文件名的通配符,因此,表达式*(.text)表示来自所有输入文件的所有.text输入部分。

因此,链接器 在位置计数器地址处 将目标文件的所有文本部分合并到可执行文件中。因此,我们可执行文件的代码部分从[0x100000]开始。

链接器放置文本输出部分后,位置计数器的值变为[0x100000]+文本输出部分的大小。

类似地,databss部分被合并到位置计数器的接下来的值所表示的地址处。

Grub与多重引导

现在,我们已经准备好构建内核的所有文件,但是,由于我们希望使用Grub引导加载程序引导内核,因此还有一步之遥。

已经有一个给不同使用引导加载程序的x86内核的标准,称为"多重引导规范(Multiboot specification)"。

如果我们的内核符合"多重引导规范",那么GRUB将仅加载我们的内核。

根据规范,内核必须在其8千字节内包含引导头(也就是Multiboot header)。

此外,Multiboot header必须包含三个(align 4)的字段:

  • a magic field:包含magic number [0x1BADB002],以识别引导头
  • a flags field:我们不关心此字段,只是简单地将其设置为0
  • a 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.asmkernel.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:

  • 建议您使用虚拟机来进行内核游戏
  • 要在grub2linux新发行版的默认引导程序)上运行,你的配置应该是这样的:
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

发布了29 篇原创文章 · 获赞 8 · 访问量 2709

猜你喜欢

转载自blog.csdn.net/HaoZiHuang/article/details/94176629