OSDev——Bare Bones

卫建义
2023-12-01

原文链接:https://wiki.osdev.org/Bare_Bones

主页:https://blog.csdn.net/qq_37422196/article/details/122591214

下面的链接如果指向原网站的话,大概是还没有翻译

在赶了在赶了……


等等!你是否阅读过入门初学者易犯错误和一些相关的操作系统理论

难度等级:入门

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

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

本教程使用现有技术帮助你开始并直接进入内核开发,而不是开发你自己的编程语言、你自己的编译器和你自己的引导加载程序。在本教程中,你将使用:

本文假设你使用的是类Unix操作系统,例如Linux,它对操作系统开发支持得很好。Windows 用户应该能够从WSLMinGWCygwin环境中完成它

在操作系统开发上取得成功需要精通知识,要有耐心,并非常仔细地阅读所有说明。在继续之前,你需要阅读本文中的所有内容。如果遇到问题,你需要更仔细地重新阅读文章,然后再重复三次。如果你仍有问题,OSDev社区经验丰富,很乐意在论坛或IRC上提供帮助

构建交叉编译器

主条目:GCC交叉编译器为什么需要交叉编译器

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

如果没有交叉编译器,你将无法正确编译你的操作系统

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

概述

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

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

引导操作系统

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

你将处理的第一个任务是引导加载程序如何启动内核。操作系统开发人员很幸运,因为multiboot标准的存在。multiboot描述了引导加载程序和操作系统内核之间的简单接口。它通过在一些全局变量(称为multiboot header)中放置一些魔数来工作。当引导加载程序看到这些值时,它会识别出内核是兼容multiboot的,它知道如何加载我们,它甚至可以向我们转发重要的信息,例如内存分布,但你还不需要这些

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

引导汇编程序

作为替代,你可以使用NASMEPLOS作为你的汇编编译器

你现在将创建一个名为boot.s的文件并编辑其内容。在本例中,你使用的是GNU汇编编译器,它是你之前构建的交叉编译器工具链的一部分。该汇编器与GNU工具链的其余部分很好地集成在一起

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

/* 声明multiboot header相关的常量 */
.set ALIGN,    1<<0             /* 在页面边界上对齐加载的模块 */
.set MEMINFO,  1<<1             /* 提供内存分布 */
.set FLAGS,    ALIGN | MEMINFO  /* 这是Multiboot的'flag'域 */
.set MAGIC,    0x1BADB002       /* "魔数"让引导加载程序找到header */
.set CHECKSUM, -(MAGIC + FLAGS) /* 以上的校验和,以证明我们是multiboot */
 
/*
声明一个将程序标记为内核的multiboot header。这些是multiboot标准中记录的魔数
引导加载程序将在内核文件的前8KiB中搜索此32位对齐的签名
签名在一个单独的节中,因此可以强制header位于内核文件的前8KiB内
*/
.section .multiboot
.align 4
.long MAGIC
.long FLAGS
.long CHECKSUM
 
/*
multiboot标准没有定义堆栈指针寄存器(esp)的值,而由内核提供堆栈
这通过在其底部创建一个符号,然后为其分配16384字节,最后在顶部创建一个符号来为小堆栈分配空间
堆栈在x86是向低地址增长的。堆栈位于单独的节中,因此可以标记为nobits,这意味着内核文件更小,
因为它不包含未初始化的堆栈。根据System V ABI标准和事实上的扩展,x86上的堆栈必须是16字节对齐的
编译器将假定堆栈已正确对齐,未对齐堆栈将导致未定义的行为
*/
.section .bss
.align 16
stack_bottom:
.skip 16384 # 16 KiB
stack_top:
 
/*
链接描述文件将_start指定为内核的入口点,一旦内核被加载,引导加载程序将跳转到该位置
当引导加载程序消失时,从此函数返回没有意义
*/
.section .text
.global _start
.type _start, @function
_start:
	/*
	引导加载程序已将我们加载到x86机器上的32位保护模式。中断被禁用。分页被禁用
	处理器状态在multiboot标准中定义。内核完全控制CPU
	内核只能利用硬件功能和它自己提供的所有代码
	没有printf函数,除非内核提供自己的<stdio.h>头文件和printf实现
	没有安全限制,没有安全措施,没有调试机制,只有内核自己提供的
	它对机器拥有绝对和完整的权力
	*/
 
	/*
	要设置堆栈,我们将esp寄存器设置为指向堆栈的顶部(因为栈在x86系统向低地址增长)
	这必须在汇编中完成,因为诸如C之类的语言在没有堆栈的情况下无法运行
	*/
	mov $stack_top, %esp
 
	/*
	这是在进入高级内核之前初始化关键处理器状态的好地方
	最好尽量最小化关键功能未启用的早期环境。请注意,处理器尚未完全初始化:
	浮点指令和指令集扩展等功能尚未初始化。GDT应该在这里加载。应在此处启用分页
	全局构造函数和异常等C++特性也需要运行时支持才能正常工作
	*/
 
	/*
	进入高级内核
	ABI要求堆栈在执行call指令时是16字节对齐的(之后会压入大小4字节的返回指针)
	堆栈最初在是16字节对齐的,
	并且我们已经将16的倍数字节压入堆栈(到目前为止已压入0个字节),
	因此对齐被保留并且call定义良好
	*/
	call kernel_main
 
	/*
	如果系统完成任务,就让计算机死循环。要做到这一点:
	1) 使用cli禁用中断(在eflags中清除中断启用)
	   它们已被引导加载程序禁用,因此似乎这不是必须的
	   但请注意,你稍后可能会启用中断并从kernel_main返回(尽管听上去有点荒谬)
	2) 使用hlt(停止指令)等待下一个中断到达。由于中断被禁用,这将锁定计算机
	3) 如果由于发生不可屏蔽中断或系统管理模式而唤醒了hlt指令,则跳转到该指令
	*/
	cli
1:	hlt
	jmp 1b
 
/*
将_start符号的大小设置为当前位置'.'减去它的起始地址。这在调试或实现调用跟踪时很有用
*/
.size _start, . - _start

然后,你可以通过以下命令来编译boot.s:

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

实现内核

到目前为止,你已经编写了设置处理器的引导汇编程序,以便可以使用诸如C之类的高级语言。也可以使用其他语言,例如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指定的字符)并且不支持在屏幕填满时滚动。添加这将是你的第一个任务。请花一些时间来理解代码

重要说明:VGA文本模式(以及BIOS)在较新的机器上已弃用,UEFI仅支持像素缓冲区。为了向前兼容,你可能希望从文本模式开始。让GRUB使用适当的Multiboot标志设置帧缓冲区或自己调用VESA VBE。与VGA文本模式不同,帧缓冲区具有像素,因此你必须自己绘制每个字形。这意味着你将需要一个不同的terminal_putchar,并且你将需要一个字体(每个字符的位图图像)。所有Linux发行版都提供你可以使用的PC屏幕字体,并且wiki文章有一个简单的putchar()示例。否则这里描述的所有其他内容仍然存在(你必须跟踪光标位置,实现换行和滚动等)

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
 
/* 检查编译器是否认为你的目标系统是Linux操作系统 */
#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) 
{
	/* 初始化终端界面 */
	terminal_initialize();
 
	/* 换行支持留作练习 */
	terminal_writestring("Hello, kernel World!\n");
}

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

执行以下命令来编译:

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

请注意,上面的代码使用了一些扩展功能,因此你构建为C99的GNU版本

用C++编写内核

C++编写内核很容易。请注意,并非该语言的所有功能都可用。例如,异常支持需要特殊的运行时支持,内存分配也是如此。要用 C++ 编写内核,只需采用上面的代码:在main方法中添加一个extern "C"声明。注意kernel_main函数必须使用C链接声明,否则编译器将在汇编名称中包含类型信息(name mangling)。这使得从上述汇编程序调用函数变得复杂,因此你使用C链接,其中符号名称与函数名称相同(没有额外的类型信息)。将代码另存为kernel.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中:

/* 引导加载程序将查看此映像并在指定为入口点的符号处开始执行 */
ENTRY(_start)
 
/* 指定目标文件的各个部分在最终内核映像中的位置 */
SECTIONS
{
	/* 将首节放在1MiB处,这是引导加载程序加载内核的常规位置 */
	. = 1M;
 
	/* 首先放置multiboot header,因为它需要在映像的前部放置,否则引导加载程序将无法识别文件格式
	   接下来我们将放置.text节 */
	.text BLOCK(4K) : ALIGN(4K)
	{
		*(.multiboot)
		*(.text)
	}
 
	/* 只读数据 */
	.rodata BLOCK(4K) : ALIGN(4K)
	{
		*(.rodata)
	}
 
	/* 可读写数据(初始化) */
	.data BLOCK(4K) : ALIGN(4K)
	{
		*(.data)
	}
 
	/* 可读写数据(未初始化)和堆栈 */
	.bss BLOCK(4K) : ALIGN(4K)
	{
		*(COMMON)
		*(.bss)
	}
 
	/* 编译器可能会生成其他段,默认情况下会将它们放在同名的段中
	   只需根据需要在此处添加内容 */
}

使用这些组件,你现在可以构建最终内核。我们使用编译器作为链接器,因为它可以更好地控制链接过程。请注意,如果你的内核是用C++编写的,则应该改用C++编译器

然后,你可以执行一下命令链接你的内核:

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

注:一些教程建议使用i686-elf-ld而不是编译器进行链接,但这会阻止编译器在链接期间执行各种任务

myos.bin文件现在是你的内核(不再需要所有其他文件)。请注意,我们链接了libgcc,它实现了你的交叉编译器所依赖的各种运行时例程。留下它会在未来给你带来问题。如果你没有构建和安装libgcc作为交叉编译器的一部分,那么你现在应该返回并使用libgcc构建一个交叉编译器。编译器依赖于这个库并且不管你是否提供它都会使用它

验证multiboot

如果你安装了GRUB,你可以检查文件是否具有有效的multiboot version 1 header,你的内核就是这种情况。重要的是multiboot header在4字节对齐的实际程序文件的前8KiB内。如果你在引导汇编程序、链接描述文件或其他任何可能出错的地方出错,可能导致header错误。如果header无效,GRUB将在你尝试引导时给出一个错误,即找不到multiboot header。此代码片段将帮助你诊断此类情况:

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

grub-file没有输出,如果它是一个有效的multiboot内核,它将退出0(成功),否则退出 1(不成功)。之后你可以输入echo $?立即在你的shell中查看退出状态。你可以将此grub文件检查添加到你的构建脚本中,作为健全性测试,以在编译时发现问题。可以使用--is-x86-multiboot2选项检查multiboot version 2。如果你在shell中手动调用grub-file命令,可以方便地将其包装在条件中以便轻松查看状态。这个命令现在应该可以工作了:

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

启动内核

不久,你就能看到自己的内核运行

构建可引导的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变体

警告grub-mkrescue使用的引导加载程序GNU GRUB是根据GNU通用公共许可证(GPL)获得许可的。你的iso文件包含该许可下的受版权保护的材料,并且违反GPL重新分发它构成侵犯版权。GPL要求你发布与引导加载程序对应的源代码。你需要在调用grub-mkrescue时从你的发行版中获取与你已安装的GRUB软件包相对应的确切源软件包(因为发行版软件包偶尔会更新)。然后,你需要将该源代码与你的ISO一起发布以满足GPL。或者,你可以自己从源代码构建GRUB。从savannah克隆最新的GRUB git(不要使用他们2012年的最后一个版本,它已经严重过时了)。运行autogen.sh、./configure 和make dist。这就是一个GRUB压缩包。将其提取到某个地方,然后从中构建GRUB,并将其安装在一个独立的前缀中。将其添加到你的PATH并确保其grub-mkrescue程序用于生成你的iso。然后将你自己制作的GRUB压缩包与你的操作系统版本一起发布。你根本不需要发布操作系统的源代码,只需发布​​iso中引导加载程序的代码即可

测试你的操作系统(QEMU)

虚拟机对于开发操作系统非常有用,因为它们允许你快速测试代码并在执行期间访问源代码。否则,你将陷入无休止的重启循环,这只会让你烦恼。它们启动速度非常快,尤其是与你的小型操作系统结合使用时

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

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

qemu-system-i386 -cdrom myos.iso

这应该会启动一个新的虚拟机,该虚拟机仅包含你的ISO作为cdrom。如果一切顺利,你将看到引导加载程序提供的菜单。只需选择myos,如果一切顺利,你应该会看到“Hello, Kernel World!”这句话。紧随其后的是一些奇怪的字符

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

qemu-system-i386 -kernel myos.bin

测试你的操作系统(真机)

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

或者,你可以将其刻录到U盘(擦除其中的所有数据!)。为此,只需找出U盘设备的名称,在我的例子中是/dev/sdb但这可能会有所不同,并且使用错误的块设备(你的硬盘,喘气!)可能是灾难性的。如果你使用的是Linux并且/dev/sdx是你的块名称,只需:

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

然后你的操作系统将安装在你的U盘上。如果你将BIOS配置为首先从USB启动,你可以插入U盘,你的计算机应该会启动你的操作系统

或者,.iso是普通的cdrom映像。如果你愿意为了几KB的大内核上浪费一个光盘,只需将其刻录到CD或DVD

进阶

现在你可以运行新的闪亮操作系统了,恭喜!当然,根据你对此的兴趣程度,这可能只是一个开始。这里有一些事情要做

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

当前的终端驱动程序不处理换行符。VGA文本模式字体在该位置存储另一个字符,因为换行符从不打算实际呈现:它们是逻辑实体。相反,在terminal_putchar中检查c == '\n’并自增terminal_row并重置terminal_column

实现终端滚动

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

渲染彩色ASCII字符

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

调用全局构造函数

主条目:调用全局构造函数

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

Meaty Skeleton

主条目:Meaty Skeleton

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

深入了解

主条目:深入了解x86

本指南旨在概述要做什么,因此你可以为更多功能准备好内核,而无需在添加它们时从根本上重新设计它

Bare Bones II

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

常见问题

为什么是multiboot header?GRUB不能加载纯ELF文件吗?

GRUB能够加载多种格式。但是,在本教程中,我们将创建一个兼容multiboot的内核,该内核可以由任何其他兼容的引导加载程序加载。为此,multiboot header是强制性的

我的内核需要AOUT kludge吗

AOUT kludge对于ELF格式的内核不是必需的:兼容multiboot的加载程序将识别ELF可执行文件,并使用程序头将内容加载到适当的位置。你可以在ELF内核中提供AOUT kludge,在这种情况下,ELF文件的标头将被忽略。但是,对于任何其他格式,例如AOUT、COFF或PE内核,都需要AOUT组件

multiboot header可以位于内核文件中的任何位置,还是必须位于特定的偏移量中

multiboot header必须位于内核文件的前8kb中,并且必须32位(4字节)对齐,以便GRUB找到它。你可以通过将标头放在其自己的源代码文件中并将其作为第一个目标文件传递给LD来确保是这种情况

GRUB会在加载内核之前擦除BSS部分吗

是的。对于ELF内核,.bss部分会被自动识别和清除(尽管Multiboot规范对此有点模糊)。对于其他格式,如果你礼貌地要求它这样做,那就是如果你使用Multiboot标头(标志#16)中的“地址覆盖”信息,并为bss_end_addr字段提供非零值。请注意,在ELF内核中使用“地址覆盖”将禁用默认行为,并改为执行“地址覆盖”标头所描述的操作

寄存器/内存/等的状态是什么?当GRUB调用我的内核时?

GRUB是multiboot规范的实现。任何未指定的行为都是“未定义的行为”,它应该(不仅)对C/C++程序员敲响警钟……最好检查Multiboot文档的机器状态部分,不要假设其他任何内容

我仍然收到Error 13: Invalid or unsupported executable format from GRUB...

可能是最终可执行文件中缺少multiboot header,或者它不在正确的位置

如果你使用ELF以外的其他格式(例如PE),则应在multiboot header中指定AOUT kludge。上面描述的grub-file程序和“objdump -h”应该会给你更多关于错误的提示

如果你使用ELF目标文件而不是可执行文件,也可能发生这种情况(例如,你有一个带有未解析符号或不可修复重定位的ELF文件)。尝试将你的ELF文件链接到二进制可执行文件以获得更准确的错误消息

当你的内核大小增加时,一个常见问题是multiboot header不再出现在输出二进制文件的开头。常见的解决方案是将multiboot header放在单独的节中,并确保该部分在输出二进制文件中的第一个部分,或者将multiboot header本身包含在链接描述文件中

尝试在QEMU中启动iso映像时出现Boot failed: Could not read from CD-ROM (code 0009)

如果你的开发系统是从EFI引导的,则可能是你没有安装grub二进制文件的PC-BIOS版本。如果你安装了它们,那么grub-mkrescue将默认生成一个可以在QEMU中工作的混合ISO。在Ubuntu上,这可以通过以下方式实现:apt-get install grub-pc-bin

相关内容

文章

  • 操作系统开发相关书籍
  • Stivale
  • BOOTBOOT

外部链接

 类似资料:

相关阅读

相关文章

相关问答