Bare Bones

来自osdev
Zhang3讨论 | 贡献2021年12月27日 (一) 08:03的版本 (创建页面,内容为“{{BeginnersWarning}} {{Rating|1}} {{Template:Kernel designs}} 在本教程中,你将为 32位x86 编写一个简单的内核并启动它。 这是创建自己的操作系统的第一步。 本教程是如何创建最小系统的示例,但不是如何正确构建项目的示例。 这些说明经过社区审查,并出于充分的理由遵循当前的建议。 提防许多其他在线教程,因为它们不遵循现代建议…”)
(差异) ←上一版本 | 最后版本 (差异) | 下一版本→ (差异)
跳到导航 跳到搜索

等等!你是否已阅读过 起步指南, 起步错误, 以及一些相关的 操作系统理论

难度等级
Difficulty 1.png
初学者
内核设计
模型
其它概念

在本教程中,你将为 32位x86 编写一个简单的内核并启动它。 这是创建自己的操作系统的第一步。 本教程是如何创建最小系统的示例,但不是如何正确构建项目的示例。 这些说明经过社区审查,并出于充分的理由遵循当前的建议。 提防许多其他在线教程,因为它们不遵循现代建议,并且是由经验不足的人编写的。

您即将开始开发新的操作系统。 也许有一天,您的新操作系统可以在其自身下开发。 这是一个被称为引导或自我托管的过程。 今天,您将简单地设置一个系统,该系统可以从现有的操作系统编译您的新操作系统。 这个过程被称为 交叉编译,它是操作系统开发的第一步。

本教程使用现有技术让你开始并直接进入 kernel 开发,而不是开发你自己的 编程语言 、你自己的 compiler 和你自己的 bootloader。 在本教程中,您将使用:

本文假设您正在使用类似Unix的操作系统,例如Linux,它很好地支持操作系统开发。 Windows用户应该能够在 WSLMinGWCygwin 环境中完成它。

成功进行操作系统开发需要成为专家,有耐心并非常仔细地阅读所有说明。 在继续之前,您需要阅读本文中的所有内容。 如果遇到问题,则需要更仔细地重新阅读该文章,然后再进行三次以获得良好的效果。 如果你仍然有问题,OSDev社区将经验丰富,很乐意在 [1] IRC 上提供帮助。

构建交叉编译器

主条目: GCC Cross-Compiler为什么我需要一个交叉编译器?

你应该做的第一件事是为 i686-elf 设置一个 GCC交叉编译器。 你尚未修改您的编译器以了解您的操作系统的存在,因此你将使用一个名为i686-elf的通用目标,它为你提供了一个针对系统V ABI的工具链。 此设置经过osdev社区的充分测试和理解,将允许您使用GRUB和Multiboot轻松设置可启动内核。 (请注意,如果您已经在使用ELF平台,例如Linux,则可能已经有一个生成ELF程序的GCC。 这不适合osdev的工作,因为此编译器将为Linux生成程序,并且您的操作系统是 不是 Linux,无论它多么相似。 如果你不使用交叉编译器,你肯定会遇到麻烦。)

您将 “无法” 在没有交叉编译器的情况下正确编译您的操作系统。

你将 “无法” 使用x86_64-elf的交叉编译器正确完成本教程,因为GRUB只能加载32位多引导内核。 如果这是你的第一个操作系统项目,你应该先做一个32位内核。 如果您改用x86_64编译器,并且以某种方式绕过了以后的健全性检查,则最终将得到GRUB不知道如何引导的内核。

概述

到现在为止,您应该已经为i686-elf设置了 交叉编译器 (如上所述)。 本教程提供了为x86创建操作系统的最小解决方案。 它不作为项目结构的推荐框架,而是作为一个最小内核的例子。 在这个简单的情况下,你只需要三个输入文件:

  • boot.s - 设置处理器环境的内核入口点
  • kernel.c - 您实际的内核例程
  • linker.ld - 用于链接上述文件

引导操作系统

要启动操作系统,将需要一个现有的软件来加载它。 这被称为引导加载程序,在本教程中,您将使用 GRUB。 编写自己的引导加载程序是一个高级主题,但通常会这样做。 我们稍后将配置引导加载程序,但是当引导加载程序将控制权传递给它时,操作系统需要处理。 内核传递了一个非常小的环境,其中堆栈尚未设置,虚拟内存尚未启用,硬件尚未初始化,等等。

您将处理的第一个任务是引导加载程序如何启动内核。 OSDevers很幸运,因为存在多引导标准,该标准描述了引导加载程序和操作系统内核之间的简单接口。 它的工作原理是在一些全局变量 (称为multiboot头) 中放入一些魔术值,这些变量由引导加载程序搜索。 当它看到这些值时,它将内核识别为multiboot兼容,并且它知道如何加载我们,甚至可以向我们转发重要信息,例如内存映射,但是您还不需要。

由于还没有堆栈,并且您需要确保全局变量设置正确,因此您将在汇编中执行此操作。

自举组件

或者,您可以使用 NASM的裸骨 EPLOS 作为您的汇编程序。

您现在将创建一个名为boot.s的文件,并讨论其内容。 在这个例子中,您使用的是GNU汇编程序,它是您之前构建的交叉编译器工具链的一部分。 这个汇编器与GNU工具链的其余部分集成得非常好。

创建最重要的部分是multiboot标头,因为它必须在内核二进制文件中很早,否则引导加载程序将无法识别我们。

/* Declare constants for the multiboot header. */
.set ALIGN,    1<<0             /* align loaded modules on page boundaries */
.set MEMINFO,  1<<1             /* provide memory map */
.set FLAGS,    ALIGN | MEMINFO  /* this is the Multiboot 'flag' field */
.set MAGIC,    0x1BADB002       /* 'magic number' lets bootloader find the header */
.set CHECKSUM, -(MAGIC + FLAGS) /* checksum of above, to prove we are multiboot */

/*
声明一个多引导头,将程序标记为内核。 这些是multiboot标准中记录的魔术值。 引导加载程序将在内核文件的前8 KiB中搜索此签名,并在32位边界处对齐。 签名位于其自己的部分中,因此可以将标头强制设置在内核文件的前8 KiB中。
*/
.section .multiboot
.align 4
.long MAGIC
.long FLAGS
.long CHECKSUM

/*
multiboot标准没有定义堆栈指针寄存器 (esp) 的值,由内核提供堆栈。 这为小堆栈分配空间,方法是在其底部创建一个符号,然后为其分配16384个字节,最后在顶部创建一个符号。 堆栈在x86上向下增长。 堆栈位于其自己的部分中,因此可以将其标记为nobits,这意味着内核文件较小,因为它不包含未初始化的堆栈。 x86上的堆栈必须根据System V ABI标准和事实上的扩展进行16字节对齐。 编译器将假定堆栈已正确对齐,并且无法对齐堆栈将导致未定义的行为。
*/
.section .bss
.align 16
stack_bottom:
.skip 16384 # 16 KiB
stack_top:

/*
The linker script specifies _start as the entry point to the kernel and the
bootloader will jump to this position once the kernel has been loaded. 当引导加载程序消失时,从这个函数返回是没有意义的。
*/
.section .text
.global _start
.type _start, @function
_start:
/*
The bootloader has loaded us into 32-bit protected mode on a x86
machine. Interrupts are disabled. Paging is disabled. The processor
state is as defined in the multiboot standard. The kernel has full
control of the CPU. The kernel can only make use of hardware features
and any code it provides as part of itself. There's no printf
function, unless the kernel provides its own <stdio.h> header and a
printf implementation. There are no security restrictions, no
safeguards, no debugging mechanisms, only what the kernel provides
itself. It has absolute and complete power over the
machine.
*/

	/*
	To set up a stack, we set the esp register to point to the top of the
	stack (as it grows downwards on x86 systems). This is necessarily done
	in assembly as languages such as C cannot function without a stack.
	*/
	mov $stack_top, %esp

	/*
	This is a good place to initialize crucial processor state before the
	high-level kernel is entered. It's best to minimize the early
	environment where crucial features are offline. Note that the
	processor is not fully initialized yet: Features such as floating
	point instructions and instruction set extensions are not initialized
	yet. The GDT should be loaded here. Paging should be enabled here.
	C++ features such as global constructors and exceptions will require
	runtime support to work as well.
	*/

	/*
	Enter the high-level kernel. The ABI requires the stack is 16-byte
	aligned at the time of the call instruction (which afterwards pushes
	the return pointer of size 4 bytes). The stack was originally 16-byte
	aligned above and we've pushed a multiple of 16 bytes to the
	stack since (pushed 0 bytes so far), so the alignment has thus been
	preserved and the call is well defined.
	*/
	call kernel_main

	/*
	如果系统无事可做,则将计算机置于无限循环中。 To do that:
	1) 使用cli禁用中断 (在eflags中清除中断启用)。
	   它们已经被引导加载程序禁用,因此不需要这样做。
	   请注意,您以后可能会启用中断并从kernel_main返回 (这有点荒谬)。
	2) 等待下一个中断以hlt (halt指令) 到达。
	   由于它们被禁用,这将锁定计算机。
	3) 如果由于发生不可屏蔽的中断或由于系统管理模式而唤醒hlt指令,请跳至hlt指令。
	*/
	cli
1:	hlt
jmp 1b

/*
将 _start符号的大小设置为当前位置 “.” 减去它的开始。
这在调试或实现呼叫跟踪时很有用。
*/
.size _start, . - _start

然后,您可以使用以下方法组装boot.s:

i686-elf-as boot.s -o boot.o

实现内核

So far you have written the bootstrap assembly stub that sets up the processor such that high level languages such as C can be used. It is also possible to use other languages such as C++.

独立式和托管环境

如果您在用户空间中进行了C或C编程,则使用了所谓的托管环境。 托管意味着有一个C标准库和其他有用的运行时功能。 另外,还有独立式版本,这就是您在此处使用的版本。 独立式意味着没有C标准库,只有你自己提供的。 但是,有些头文件实际上不是C标准库的一部分,而是编译器。 即使在独立的C源代码中,这些仍然可用。 在这种情况下,您使用 <stdbool.h> 来获取bool数据类型,<stddef.h> 来获取size_t和NULL, 和 <stdint.h> 获取对于操作系统开发非常宝贵的intx_t和uintx_t数据类型,您需要确保变量的大小准确 (如果您使用的是short而不是uint16_t,并且short的大小已更改,则此处的VGA驱动程序将损坏!)。 此外,您还可以访问 <float.h>,<iso646.h>,<limits.h> 和 <stdarg.h> 标头,因为它们也是独立的。 GCC实际上还运送了一些标头,但这些都是特殊用途。

用C编写内核

下面演示如何在C ++ 中创建一个简单的内核。 此内核使用VGA文本模式缓冲区 (位于 0xB8000) 作为输出设备。 它设置了一个简单的驱动程序,该驱动程序可以记住此缓冲区中下一个字符的位置,并提供用于添加新字符的原语。 值得注意的是,不支持换行符 ('\ n') (并且写入该字符将显示一些VGA特定的字符),并且在屏幕填满时不支持滚动。 添加这将是你的第一个任务。 请花点时间理解代码。

IMPORTANT NOTE: the VGA text mode (as well as the BIOS) is deprecated on newer machines, and UEFI only supports pixel buffers. 为了实现前向兼容性,您可能需要从这一点开始。 Ask GRUB to set up a framebuffer using appropriate Multiboot flags or call VESA VBE yourself. Unlike VGA text mode, a framebuffer has pixels, so you have to draw each glyph yourself. 这意味着你需要一个不同的 terminal_putchar,你需要一个字体 (每个字符的位图图像)。 所有Linux发行版都附带您可以使用的 PC屏幕字体,并且wiki文章有一个简单的putchar() 示例。 否则,这里描述的所有其他内容仍然存在 (您必须跟踪光标位置,实现换行符和滚动等)。)

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>

/* 检查编译器是否认为您针对的是错误的操作系统。*/
#if defined(__linux__)
#error "You are not using a cross-compiler, you will most certainly run into trouble"
#endif

/* 本教程仅适用于32位ix86目标。*/
#if !defined(__i386__)
#error "This tutorial needs to be compiled with a ix86-elf compiler"
#endif

/* 硬件文本模式颜色常量。*/
enum vga_color {
VGA_COLOR_BLACK = 0,
VGA_COLOR_BLUE = 1,
VGA_COLOR_GREEN = 2,
VGA_COLOR_CYAN = 3,
VGA_COLOR_RED = 4,
VGA_COLOR_MAGENTA = 5,
VGA_COLOR_BROWN = 6,
VGA_COLOR_LIGHT_GREY = 7,
VGA_COLOR_DARK_GREY = 8,
VGA_COLOR_LIGHT_BLUE = 9,
VGA_COLOR_LIGHT_GREEN = 10,
VGA_COLOR_LIGHT_CYAN = 11,
VGA_COLOR_LIGHT_RED = 12,
VGA_COLOR_LIGHT_MAGENTA = 13,
VGA_COLOR_LIGHT_BROWN = 14,
VGA_COLOR_WHITE = 15,
};

static inline uint8_t vga_entry_color(enum vga_color fg, enum vga_color bg)
{
return fg | bg << 4;
}

static inline uint16_t vga_entry(unsigned char uc, uint8_t color)
{
return (uint16_t) uc | (uint16_t) color << 8;
}

size_t strlen(const char* str)
{
size_t len = 0;
while (str[len])
len++;
return len;
}

static const size_t VGA_WIDTH = 80;
static const size_t VGA_HEIGHT = 25;

size_t terminal_row;
size_t terminal_column;
uint8_t terminal_color;
uint16_t* terminal_buffer;

void terminal_initialize(void)
{
terminal_row = 0;
terminal_column = 0;
terminal_color = vga_entry_color(VGA_COLOR_LIGHT_GREY, VGA_COLOR_BLACK);
terminal_buffer = (uint16_t*) 0xB8000;
for (size_t y = 0; y < VGA_HEIGHT; y++) {
for (size_t x = 0; x < VGA_WIDTH; x++) {
const size_t index = y * VGA_WIDTH + x;
terminal_buffer[index] = vga_entry(' ', terminal_color);
}
}
}

void terminal_setcolor(uint8_t color)
{
terminal_color = color;
}

void terminal_putentryat(char c, uint8_t color, size_t x, size_t y)
{
const size_t index = y * VGA_WIDTH + x;
terminal_buffer[index] = vga_entry(c, color);
}

void terminal_putchar(char c)
{
terminal_putentryat(c, terminal_color, terminal_column, terminal_row);
if (++terminal_column == VGA_WIDTH) {
terminal_column = 0;
if (++terminal_row == VGA_HEIGHT)
terminal_row = 0;
}
}

void terminal_write(const char* data, size_t size)
{
for (size_t i = 0; i < size; i++)
terminal_putchar(data[i]);
}

void terminal_writestring(const char* data)
{
terminal_write(data, strlen(data));
}

void kernel_main(void)
{
/* Initialize terminal interface */
terminal_initialize();

	/* 换行支持保留为练习。*/
	terminal_writestring("Hello, kernel World!\n");
}

请注意在代码中您希望如何使用通用C函数 strlen, 但是这个函数是C标准库的一部分,你没有。 相反,您依靠独立的标头 <stddef.h> 来提供 size_t,并且只需声明自己的 strlen 实现即可。 你将必须为您希望使用的每个函数执行此操作 (因为独立标头仅提供宏和数据类型)。

Compile using:

i686-elf-gcc -c kernel.c -o kernel.o -std=gnu99 -ffreestanding -O2 -Wall -Wextra

请注意,上面的代码使用了一些扩展,因此您将其构建为c99的GNU版本。

用C编写内核

C 中编写内核很容易。 请注意,并非该语言的所有功能都可用。 例如,异常支持需要特殊的运行时支持,内存分配也需要特殊的运行时支持。 要在C中编写内核,只需采用上面的代码: 在main方法中添加extern “C” 声明。 请注意如何使用C链接声明kernel_main函数,否则编译器将在程序集名称 (名称mangling) 中包含类型信息。 这使得从上面的程序集存根中调用函数变得复杂,因此您使用C链接,其中符号名称与函数名称相同 (没有其他类型信息)。 将代码保存为内核。c (或您最喜欢的C文件名扩展名是什么)。

您可以使用以下方法编译文件kernel.c:

i686-elf-g++ -c kernel.c++ -o kernel.o -ffreestanding -O2 -Wall -Wextra -fno-exceptions -fno-rtti

请注意,您还必须为此工作构建了一个交叉C编译器。

链接内核

您现在可以组装boot.s并编译kernel.c. 这会产生两个对象文件,每个文件都包含内核的一部分。 要创建完整的和最终的内核,您必须将这些对象文件链接到引导加载程序可使用的最终内核程序中。 在开发用户空间程序时,您的工具链附带了用于链接此类程序的默认脚本。 但是,这些不适合内核开发,您需要提供自己的自定义链接器脚本。 在linker.ld中保存以下内容:

/* The bootloader will look at this image and start execution at the symbol
   designated as the entry point. */
ENTRY(_start)

/* 告诉目标文件的各个部分将放在最终内核映像中的位置。*/
SECTIONS
{
/* 开始在1 MiB处放置部分,这是引导加载程序加载内核的常规位置。*/
. = 1M;

	/* First put the multiboot header, as it is required to be put very early
	   early in the image or the bootloader won't recognize the file format.
	   Next we'll put the .text section. */
	.text BLOCK(4K) : ALIGN(4K)
	{
		*(.multiboot)
		*(.text)
	}

	/* Read-only data. */
	.rodata BLOCK(4K) : ALIGN(4K)
	{
		*(.rodata)
	}

	/* Read-write data (initialized) */
	.data BLOCK(4K) : ALIGN(4K)
	{
		*(.data)
	}

	/* Read-write data (uninitialized) and stack */
	.bss BLOCK(4K) : ALIGN(4K)
	{
		*(COMMON)
		*(.bss)
	}

	/* The compiler may produce other sections, by default it will put them in
	   a segment with the same name. Simply add stuff here as needed. */
}

With these components you can now actually build the final kernel. We use the compiler as the linker as it allows it greater control over the link process. Note that if your kernel is written in C++, you should use the C++ compiler instead.

You can then link your kernel using:

i686-elf-gcc -T linker.ld -o myos.bin -ffreestanding -O2 -nostdlib boot.o kernel.o -lgcc

Note: Some tutorials suggest linking with i686-elf-ld rather than the compiler, however this prevents the compiler from performing various tasks during linking.

The file myos.bin is now your kernel (all other files are no longer needed). Note that we are linking against libgcc, which implements various runtime routines that your cross-compiler depends on. Leaving it out will give you problems in the future. If you did not build and install libgcc as part of your cross-compiler, you should go back now and build a cross-compiler with libgcc. The compiler depends on this library and will use it regardless of whether you provide it or not.

验证多引导

If you have GRUB installed, you can check whether a file has a valid Multiboot version 1 header, which is the case for your kernel. It's important that the Multiboot header is within the first 8 KiB of the actual program file at 4 byte alignment. This can potentially break later if you make a mistake in the boot assembly, the linker script, or anything else that might go wrong. If the header isn't valid, GRUB will give an error that it can't find a Multiboot header when you try to boot it. This code fragment will help you diagnose such cases:

grub-file --is-x86-multiboot myos.bin

grub-file is quiet but will exit 0 (successfully) if it is a valid multiboot kernel and exit 1 (unsuccessfully) otherwise. You can type echo $? in your shell immediately afterwards to see the exit status. You can add this grub-file check to your build scripts as a sanity test to catch the problem at compile time. Multiboot version 2 can be checked with the --is-x86-multiboot2 option instead. If you invoke the grub-file command manually in a shell, it is convenient to wrap it in a conditional to easily see the status. This command should work now:

if grub-file --is-x86-multiboot myos.bin; then
  echo multiboot confirmed
else
  echo the file is not multiboot
fi

启动内核

In a few moments, you will see your kernel in action.

构建可启动的cdrom映像

您可以使用程序 GRUB-mkrescue 轻松创建包含grub引导加载程序和内核的可引导cd-rom映像。 您可能需要安装GRUB实用程序和程序 xorriso (版本0.5.6或更高版本)。 首先,您应该创建一个名为grub.cfg的文件,其中包含内容:

menuentry "myos" {
	multiboot /boot/myos.bin
}

请注意,括号必须按此处所示放置。 现在,您可以通过键入以下命令来创建操作系统的可引导映像:

mkdir -p isodir/boot/grub
cp myos.bin isodir/boot/myos.bin
cp grub.cfg isodir/boot/grub/grub.cfg
grub-mkrescue -o myos.iso isodir

恭喜你! 你现在已经创建了一个名为myos.iso的文件,其中包含您的Hello World操作系统。 如果您没有安装程序 grub-mkrescue,现在是安装GRUB的好时机。 它应该已经安装在Linux系统上了。 如果没有本地grub-mkrescue程序可用,Windows用户可能会希望使用Cygwin变体。

警告: GNU GRUB,grub-mkrescue 使用的引导加载程序,根据GNU通用公共许可证获得许可。 您的iso文件包含该许可下的受版权保护的材料,并且违反GPL将其重新分发构成版权侵权。 GPL要求您发布与引导加载程序相对应的源代码。 您需要在调用 GRUB-mkrescue 时获取与您从发行版安装的grub包相对应的确切源包 (因为发行版包偶尔会更新)。 然后,您需要与ISO一起发布该源代码,以满足GPL的要求。 替代方案,您可以自己从源代码构建GRUB。 从萨凡纳 (savannah) 克隆最新的GRUB git (不要使用其上一个发行版2012年,它已严重过时)。运行autogen.sh,。/配置并制作dist。 这就是一个g。 将其提取到某个地方,然后从中构建GRUB,并将其安装在隔离的前缀中。 将其添加到您的路径中,并确保其 grub-mkrescue 程序用于生成iso。 然后发布您自己制作的GRUB tarball以及您的操作系统版本。 您根本不需要发布操作系统的源代码,只需要发布iso内部的引导加载程序的代码。

测试您的操作系统 (QEMU)

虚拟机对于开发操作系统非常有用,因为它们允许您快速测试代码并在执行过程中访问源代码。 否则,您将无休止地重新启动,这只会让您烦恼。 它们启动非常快,尤其是与您这样的小型操作系统结合使用。

在本教程中,我们将使用QEMU。 如果你愿意,你也可以使用其他虚拟机。 只需将ISO添加到空虚拟机的CD驱动器即可解决问题。

从您的存储库安装QEMU,然后使用以下命令启动您的新操作系统。

qemu-system-i386 -cdrom myos.iso

这应该会启动一个新的虚拟机,该虚拟机仅包含您的ISO作为cdrom。 如果一切顺利,您将获得引导加载程序提供的菜单。 只需选择myos,如果一切顺利,您应该会看到快乐的话 “您好,内核世界!” 后面跟着一些神秘的角色。

此外,QEMU支持直接引导多引导内核,而无需可引导介质:

qemu-system-i386 -kernel myos.bin

测试您的操作系统(真是硬件)

程序grub-mkrescue很好,因为它制作了一个可在真实计算机和虚拟机上运行的可引导ISO。 然后,您可以构建一个ISO并在任何地方使用它。 要在本地计算机上启动内核,您可以将myos.bin安装到/boot目录中,并适当地配置引导加载程序。

或者,您可以将其刻录到USB记忆棒 (擦除其上的所有数据!)。 要做到这一点,只需找出USB块设备的名称,在我的情况下/dev/sdb但这可能会有所不同,并且使用错误的块设备 (您的硬盘,喘气!) 可能是灾难性的。 如果您使用的是Linux和/dev/sdx是您的块名,只需:

sudo dd if=myos.iso of=/dev/sdx && sync

然后,您的操作系统将安装在您的u盘上。 如果您将BIOS配置为首先从USB启动,则可以插入USB记忆棒,计算机应启动操作系统。

或者,。iso是正常的cdrom图像。 如果您想在几千字节的大内核上浪费其中一个,只需将其刻录到CD或DVD即可。

向前迈进

现在,您可以运行新的闪亮操作系统,恭喜! 当然,取决于您对此感兴趣的程度,这可能只是开始。 这里有几件事要做。

为终端驱动程序添加对换行符的支持

当前终端驱动程序不处理换行符。 VGA文本模式字体在该位置存储另一个字符,因为换行符永远不会实际呈现: 它们是逻辑实体。 Rather, in terminal_putchar check if c == '\n' and increment terminal_row and reset terminal_column.

实现终端滚动

如果终端已满,它将返回到屏幕顶部。 这对于正常使用是不可接受的。 相反,它应该将所有行向上移动一行并丢弃最上面的行,并在底部留下空白行,准备用字符填充。 实施这个。

渲染彩色ASCII艺术

使用现有的终端驱动程序来渲染一些漂亮的东西,你有所有光荣的16种颜色。 请注意,背景颜色可能只有8种颜色可用,因为默认情况下,条目中的最上面的位表示背景颜色以外的其他颜色。 你需要一个真正的VGA驱动程序来解决这个问题。

调用全局构造器

正文: Calling Global Constructors

本教程展示了如何为C和C内核创建最小环境的一个小示例。 不幸的是,你还没有一切都准备好了。 例如,带有全局对象的C不会调用它们的构造函数,因为您永远不会这样做。 编译器使用一种特殊的机制,用于通过 crt *.o 对象在程序初始化时执行任务,即使对于C程序员来说,这也可能是有价值的。 如果正确组合 crt *.o 文件,则将创建一个调用所有程序初始化任务的 _init 函数。 然后,您的boot.o对象文件可以在调用 kernel_main 之前调用 _ init

肉骨架

正文: Meaty Skeleton

本教程是一个最小的例子,给不耐烦的初学者一个快速的hello world操作系统。 它是故意最小化的,并且没有显示有关如何组织操作系统的最佳实践。 Meaty Skeleton教程展示了一个示例,该示例说明了如何使用内核组织最小操作系统,为标准库的增长空间以及为出现的用户空间做准备。

走得更远

正文: Going Further on x86

本指南旨在概述该怎么做,因此您可以为更多功能准备一个内核,而无需在添加它们时从根本上重新设计它。

Bare Bones II

使您的操作系统自托管,然后按照所有说明在您自己的操作系统下完成裸露的骨骼。 这是一个五星级的练习,你可能需要几年的时间来解决它。

常见问题

Why the Multiboot header? Wouldn't a pure ELF file be loadable by GRUB anyway?
GRUB is capable of loading a variety of formats. 但是,在本教程中,我们正在创建一个兼容Multiboot的内核,该内核可以由任何其他兼容的引导加载程序加载。 为了实现这一点,multiboot标头是强制性的。
Is the AOUT kludge required for my kernel?
The AOUT kludge is not necessary for kernels in ELF format: 兼容multiboot的加载程序将识别ELF可执行文件,并使用程序标头将内容加载到适当的位置。 您可以为您的ELF内核提供一个AOUT kludge,在这种情况下,ELF文件的标头将被忽略。 然而,对于任何其他格式,如AOUT、COFF或PE内核,AOUT kludge是必需的。
Can the Multiboot header be anywhere in the kernel file, or does it have to be in a specific offset?
The Multiboot header must be in the first 8kb of the kernel file and must be aligned to a 32-bit (4 byte) boundary for GRUB to find it. 您可以通过将标头放在自己的源代码文件中并将其作为第一个对象文件传递给LD来确保这种情况。
Will GRUB wipe the BSS section before loading the kernel?
Yes. For ELF kernels, the .bss section is automatically identified and cleared (despite the Multiboot specification being a bit vague about it). 对于其他格式,如果您礼貌地要求它这样做,也就是说,如果您使用Multiboot标头 (标志 #16) 中的 “地址覆盖” 信息,并为bss_end_addr字段提供非零值。 请注意,将 “地址覆盖” 与ELF内核一起使用将禁用默认行为,并执行 “地址覆盖” 标头所描述的操作。
What is the state of registers/memory/etc. when GRUB calls my kernel?
GRUB is an implementation of the Multiboot specification. 未指定的任何内容都有 “未定义的行为”,这应该与C/C程序员一起响铃 (不仅如此)... 最好检查Multiboot文档的机器状态部分,不要假设其他任何事情。
I still get Error 13: Invalid or unsupported executable format from GRUB...
Chances are the Multiboot header is missing from the final executable, or it is not at the right location.
If you are using some other format than ELF (such as PE), you should specify the AOUT kludge in the Multiboot header. The grub-file program describe aboveand "objdump -h" should give you more hints about what is going on.
It may also happen if you use an ELF object file instead of an executable (e.g. you have an ELF file with unresolved symbols or unfixable relocations). 尝试将您的ELF文件链接到二进制可执行文件,以获取更准确的错误消息。
当内核大小增加时,一个常见的问题是Multiboot标头不再出现在输出二进制文件的开头。 The common solutions is to put the Multiboot header in a separate section and make sure that section is first in the output binary, or to include the Multiboot header itself in the linker script.
I get Boot failed: Could not read from CD-ROM (code 0009) when trying to boot the iso image in QEMU
If your development system is booted from EFI it may be that you don't have the PC-BIOS version of the grub binaries installed anywhere. If you install them then grub-mkrescue will by default produce a hybrid ISO that will work in QEMU. On Ubuntu this can be achieved with: apt-get install grub-pc-bin.

另见

文章

外部链接