用 GRUB 引导自己的操作系统
在 PC 机上捣鼓自己的操作系统遇到的第一个难题就是如何将内核加载到内存中执行。如果读过于渊写的《自己动手写操作系统》就会知道这部分的工作还是蛮繁琐的。而且实际上这部分工作和操作系统没太大的关系。好在随着 linux 等开源操作系统的发展,开源的引导加载程序也已经发展的很成熟了。我们可以利用前人的成果,将自己的操作系统改造成可以用现有引导加载程序引导的内核。
引导加载程序(BootLoader)是系统加电后运行的第一段软件代码。x86 系统中的引导加载程序由 BIOS(Basic Input Output System,基本输入输出系统)和位于硬盘主引导记录(MBR,Master Boot Record)中的操作系统引导加载程序(比如,LILO 和 GRUB 等)一起组成。BIOS 在完成硬件检测和资源分配后,将硬盘 MBR 中的引导加载程序读到系统的 RAM 中,然后将控制权交给操作系统引导加载程序。引导加载程序的主要运行任务就是将内核映象从硬盘上读到 RAM 中,然后跳转到内核的入口点去运行,也即开始启动操作系统。
引导加载程序并非操作系统的一部分,但是没有引导加载程序加载操作系统,操作系统是无法自动运行起来的。可以在 x86 系统中运行的操作系统超过 100 种,其中较为有名的如微软公司出品的 DOS、Windows 系列,开放源代码的 Linux、FreeBS D等也不下 10 余种。这些各具特色的操作系统几乎都有其专用的引导加载程序,并且互相之间并不兼容。
经过对比各种常见的引导加载程序的功能和可靠性,我选择了多重操作系统启动管理器 GRUB 作为引导加载程序。GRUB 是一个来自自由软件基金会项目的多操作系统启动程序,它允许用户可以在计算机内同时拥有多个操作系统,并在计算机启动时选择希望运行的操作系统。GRUB 可用于选择操作系统分区上的不同内核,也可用于向这些内核传递启动参数。
GRUB 引导加载程序广泛应用于 Linux、各种 BSD 系统的引导,具有极高的可靠性。满足多重引导规范(The Multiboot Specification),可以引导各种满足多重引导规范的操作系统内核。并且可以通过配置文件配置为多引导模式,当加载的系统出现故障无法工作时可以自动启用备用系统,极大的提高了系统的可靠性。
多重引导规范
多重引导规范并不强制要求内核的格式,但是如果采用 ELF 格式,将会带来许多方便。本文下面的介绍都是基于内核采用 ELF 格式。如果您的内核碰巧不能采用 ELF 格式,请您参考多重引导规范的官方文本中 3.1 节关于 Multiboot Header 的介绍。
能够被 GRUB 引导的内核有两个条件:
(1) 需要有一个 Multiboot Header ,这个 Multiboot Header 必须在内核镜像的前 8192 个字节内,并且是首地址是 4 字节对其的。
(2) 内核的加载地址在 1MB 以上的内存中,这个要求是 GRUB 附加的,并非多重引导规范的规定。
Multiboot Header
Multiboot Header的分布必须如下所示:
偏移量 类型 域名 备注
0 u32 magic 必需
4 u32 flags 必需
8 u32 checksum 必需
12 u32 header_addr 如果flags[16]被置位
16 u32 load_addr 如果flags[16]被置位
20 u32 load_end_addr 如果flags[16]被置位
24 u32 bss_end_addr 如果flags[16]被置位
28 u32 entry_addr 如果flags[16]被置位
32 u32 mode_type 如果flags[2]被置位
36 u32 width 如果flags[2]被置位
40 u32 height 如果flags[2]被置位
44 u32 depth 如果flags[2]被置位
magic
域是标志头的魔数,它必须等于十六进制值 0x1BADB002。
flags
flags域指出OS映像需要引导程序提供或支持的特性。0-15 位指出需求:如果引导程序发现某些值被设置但出于某种原因不理解或不能不能满足相应的需求,它必须告知用户并宣告引导失败。16-31位指出可选的特性:如果引导程序不能支持某些位,它可以简单的忽略它们并正常引导。自然,所有 flags 字中尚未定义的位必须被置为 0。这样,flags 域既可以用于版本控制也可以用于简单的特性选择。
如果设置了 flags 字中的 0 位,所有的引导模块将按页(4KB)边界对齐。有些操作系统能够在启动时将包含引导模块的页直接映射到一个分页的地址空间,因此需要引导模块是页对齐的。
如果设置了 flags 字中的 1 位,则必须通过 Multiboot 信息结构(参见引导信息格式)的 mem_* 域包括可用内存的信息。如果引导程序能够传递内存分布(mmap_*域)并且它确实存在,则也包括它。
如果设置了 flags 字中的 2 位,有关视频模式表(参见引导信息格式)的信息必须对内核有效。
如果设置了 flags 字中的 16 位,则 Multiboot 头中偏移量 8-24 的域有效,引导程序应该使用它们而不是实际可执行头中的域来计算将 OS 映象载入到那里。如果内核映象为 ELF 格式则不必提供这样的信息,但是如果映象是 a.out 格式或者其他什么格式的话就必须提供这些信息。
checksum
域 checksum 是一个 32 位的无符号值,当与其他的 magic 域(也就是 magic 和 flags)相加时,结果必须是 32 位的无符号值 0(即magic + flags + checksum = 0)
header_addr
这里往后的 32 个字节不是必须的,并且对于内核为 ELF 格式时是不需要的,因此就不介绍了。
当引导程序调用32位操作系统时,机器状态必须如下:
EAX
必须包含魔数 0x2BADB002;这个值指出操作系统是被一个符合 Multiboot 规范的引导程序载入的。
EBX
必须包含由引导程序提供的 Multiboot 信息结构的物理地址(参见引导信息格式)。
CS
必须是一个偏移量位于 0 到 0xFFFFFFFF 之间的 32 位可读/可执行代码段。这里的精确值未定义。
DS
ES
FS
GS
SS
必须是一个偏移量位于 0 到 0xFFFFFFFF 之间的 32 位可读/可执行代码段。这里的精确值未定义。
A20 gate
必须已经开启。
CR0
第31位(PG)必须为 0。第 0 位(PE)必须为 1。其他位未定义。
EFLAGS
第17位(VM)必须为 0。第 9 位(IF)必须为 1 。其他位未定义。
所有其他的处理器寄存器和标志位未定义。这包括:
ESP
当需要使用堆栈时,OS 映象必须自己创建一个。
GDTR
尽管段寄存器像上面那样定义了,GDTR 也可能是无效的,所以 OS 映象决不能载入任何段寄存器(即使是载入相同的值也不行!)直到它设定了自己的 GDT。
IDTR
OS 映象必须在设置完它的 IDT 之后才能开中断。
Multiboot 信息结构(就目前为止定义的)的格式如下:
+-------------------+
0 | flags | (必需)
+-------------------+
4 | mem_lower | (如果flags[0]被置位则出现)
8 | mem_upper | (如果flags[0]被置位则出现)
+-------------------+
12 | boot_device | (如果flags[1]被置位则出现)
+-------------------+
16 | cmdline | (如果flags[2]被置位则出现)
+-------------------+
20 | mods_count | (如果flags[3]被置位则出现)
24 | mods_addr | (如果flags[3]被置位则出现)
+-------------------+
28 - 40 | syms | (如果flags[4]或flags[5]被置位则出现)
| |
+-------------------+
44 | mmap_length | (如果flags[6]被置位则出现)
48 | mmap_addr | (如果flags[6]被置位则出现)
+-------------------+
52 | drives_length | (如果flags[7]被置位则出现)
56 | drives_addr | (如果flags[7]被置位则出现)
+-------------------+
60 | config_table | (如果flags[8]被置位则出现)
+-------------------+
64 | boot_loader_name | (如果flags[9]被置位则出现)
+-------------------+
68 | apm_table | (如果flags[10]被置位则出现)
+-------------------+
72 | vbe_control_info | (如果flags[11]被置位则出现)
76 | vbe_mode_info |
80 | vbe_mode |
82 | vbe_interface_seg |
84 | vbe_interface_off |
86 | vbe_interface_len |
+-------------------+
第一个 longword 指出 Multiboot 信息结构中的其它域是否有效。所有目前未定义的位必须被引导程序设为 0。操作系统应该忽略任何它不理解的位。因此,flags 域也可以视作一个版本标志符,这样可以无破坏的扩展Multiboot信息结构。
如果设置了 flags 中的第 0 位,则 mem_* 域有效。mem_lower 和 mem_upper 分别指出了低端和高端内存的大小,单位是 K。低端内存的首地址是 0,高端内存的首地址是 1M。低端内存的最大可能值是 640K。返回的高端内存的最大可能值是最大值减去 1M。但并不保证是这个值。
flags 的其他位我没有用到,这里就不介绍了。需要了解的请自己阅读相关文档。
下面是一个最简单的例子:
- /**
- * boot.S
- */
- #define MULTIBOOT_HEADER_MAGIC 0x1BADB002
- #define MULTIBOOT_HEADER_FLAGS 0x00000003
- #define STACK_SIZE 0x4000
- .text
- .globl start, _start
- start:
- _start:
- jmp multiboot_entry
- .align 4
- multiboot_header:
- .long MULTIBOOT_HEADER_MAGIC
- .long MULTIBOOT_HEADER_FLAGS
- .long -(MULTIBOOT_HEADER_MAGIC + MULTIBOOT_HEADER_FLAGS)
- multiboot_entry:
- /* 初始化堆栈指针。 */
- movl $(stack + STACK_SIZE), %esp
- /* 重置 EFLAGS。 */
- pushl $0
- popf
- pushl %ebx
- pushl %eax
- /* 现在进入 C main 函数... */
- call cmain
- loop: hlt
- jmp loop
- .comm stack, STACK_SIZE
- /**
- * kernel.c
- */
- /* a.out 符号表。 */
- typedef struct aout_symbol_table
- {
- unsigned long tabsize;
- unsigned long strsize;
- unsigned long addr;
- unsigned long reserved;
- } aout_symbol_table_t;
- /* ELF 的 section header table。 */
- typedef struct elf_section_header_table
- {
- unsigned long num;
- unsigned long size;
- unsigned long addr;
- unsigned long shndx;
- } elf_section_header_table_t;
- /* Multiboot 信息。 */
- typedef struct multiboot_info
- {
- unsigned long flags;
- unsigned long mem_lower;
- unsigned long mem_upper;
- unsigned long boot_device;
- unsigned long cmdline;
- unsigned long mods_count;
- unsigned long mods_addr;
- union
- {
- aout_symbol_table_t aout_sym;
- elf_section_header_table_t elf_sec;
- } u;
- unsigned long mmap_length;
- unsigned long mmap_addr;
- } multiboot_info_t;
- /* 检测 FLAGS 中的位 BIT 是否被置位。 */
- #define CHECK_FLAG(flags,bit) ((flags) & (1 << (bit)))
- /* 与显示相关的设置。 */
- #define COLUMNS 80
- #define LINES 24
- #define ATTRIBUTE 7
- #define VIDEO 0xB8000
- static int xpos; /* X 坐标。 */
- static int ypos; /* Y 坐标。 */
- static volatile unsigned char *video; /* 指向显存。 */
- static void cls (void);
- static void itoa (char *buf, int base, int d);
- static void putchar (int c);
- void printf (const char *format, ...);
- void cmain (unsigned long magic, unsigned long addr)
- {
- multiboot_info_t *mbi;
- /* 清屏。 */
- cls ();
- /* 将 MBI 指向 Multiboot 信息结构。 */
- mbi = (multiboot_info_t *) addr;
- /* mem_* 是否有效? */
- if (CHECK_FLAG (mbi->flags, 0))
- printf ("mem_lower = %uKB, mem_upper = %uKB\n", (unsigned) mbi->mem_lower, (unsigned) mbi->mem_upper);
- /* your code here. */
- }
- /* 清屏并初始化 VIDEO,XPOS 和 YPOS。 */
- static void cls (void)
- {
- int i;
- video = (unsigned char *) VIDEO;
- for (i = 0; i < COLUMNS * LINES * 2; i++)
- *(video + i) = 0;
- xpos = 0;
- ypos = 0;
- }
- /* 将整数 D 转换为字符串并保存在 BUF 中。如果 BASE 为 'd',则 D 为十进制,如果 BASE 为 'x',则 D 为十六进制。 */
- static void itoa (char *buf, int base, int d)
- {
- char *p = buf;
- char *p1, *p2;
- unsigned long ud = d;
- int divisor = 10;
- /* 如果指定了 %d 并且 D 是负数,在开始添上负号。 */
- if (base == 'd' && d < 0)
- {
- *p++ = '-';
- buf++;
- ud = -d;
- }
- else if (base == 'x')
- divisor = 16;
- /* 用 DIVISOR 去除 UD 直到 UD == 0。 */
- do
- {
- int remainder = ud % divisor;
- *p++ = (remainder < 10) ? remainder + '0' : remainder + 'a' - 10;
- }
- while (ud /= divisor);
- /* 在字符串尾添上终结符。 */
- *p = 0;
- /* 反转 BUF。 */
- p1 = buf;
- p2 = p - 1;
- while (p1 < p2)
- {
- char tmp = *p1;
- *p1 = *p2;
- *p2 = tmp;
- p1++;
- p2--;
- }
- }
- /* 在屏幕上输出字符 C 。 */
- static void putchar (int c)
- {
- if (c == '\n' || c == '\r')
- {
- newline:
- xpos = 0;
- ypos++;
- if (ypos >= LINES)
- ypos = 0;
- return;
- }
- *(video + (xpos + ypos * COLUMNS) * 2) = c & 0xFF;
- *(video + (xpos + ypos * COLUMNS) * 2 + 1) = ATTRIBUTE;
- xpos++;
- if (xpos >= COLUMNS)
- goto newline;
- }
- /* 格式化字符串并在屏幕上输出,就像 libc 函数 printf 一样。 */
- void printf (const char *format, ...)
- {
- char **arg = (char **) &format;
- int c;
- char buf[20];
- arg++;
- while ((c = *format++) != 0)
- {
- if (c != '%')
- putchar (c);
- else
- {
- char *p;
- c = *format++;
- switch (c)
- {
- case 'd':
- case 'u':
- case 'x':
- itoa (buf, c, *((int *) arg++));
- p = buf;
- goto string;
- break;
- case 's':
- p = *arg++;
- if (! p)
- p = "(null)";
- string:
- while (*p)
- putchar (*p++);
- break;
- default:
- putchar (*((int *) arg++));
- break;
- }
- }
- }
- }
下面是编译命令:
gcc kernel.c -c -fno-builtin
gcc boot.S -c
ld kernel.o boot.o -o kernel -s -Ttext 0x100000 --entry=start
最后是用 bochs 运行的结果:
如何运行自己的内核,可以参考我的文章:制作带有 GRUB 引导功能的软盘镜像文件
用 GRUB 引导自己的操作系统相关推荐
- 红帽企业linux4参考指南读书笔记-GRUB引导器
一个引导加载器通常存在于系统的主硬盘驱动器中,通过它来加载linux内核跟一些需要的文件或其它操作系统到系统内存中. 在各种系统架构上运行RedHat企业Linux系统会使用不同的引导加载器,一般 ...
- linux系统引导设置,Linux操作系统GRUB引导程序配置方法大全 - 技术文档 - 新手入门 Linux时代......
1. GRUB 介绍 计算机在启动的时候,首先由BIOS中的程序执行自检,自检通过后,就根据CMOS 的配置找到第一个可启动磁盘的MBR中的Boot Loader程序(一般在启动盘的第一个物理扇区,占 ...
- linux grup进入操作系统,Linux操作系统下GRUB引导过程及原理
Linux操作系统下GRUB引导过程及原理 作者: 硅谷动力 CNETNews.com.cn 2008-09-09 19:53:52 GRUB是一个多重启动管理器.GRUB是GRand Unified ...
- windows和linux添加引导文件,Linux与Windows 10用grub引导教程-Go语言中文社区
前言 去年暑假的时候,写了一篇如何装 Linux 和 Windows 10 双系统的文章发在了简书上,我写这篇文章的原因是当初装双系统确实是折腾了许久,网上也找不到一篇详尽的教程.由于去年对于写教程还 ...
- linux添加磁盘后进入grub,从硬盘安装 Linux 和从 GRUB 命令行启动操作系统
标题 +=================================================+ | 从硬盘安装 Linux 和从 GRUB 命令行启动操作系统 | +======== ...
- grub 引导 多linux系统,GRUB 多系统引导
GRUB 多系统引导 [日期:2007-12-02] 来源:Linux公社 作者:Linux [字体:大 中 小] 六.通过GRUB引导Windows操作系统: 1.通过编辑 menu.lst 来引导 ...
- Linux—系统启动类故障之 GRUB引导故障
Linux GRUB引导故障: 故障原因: MBR中的GRUB引导程序遭到破坏 grub.conf文件丢失.引导配置有误 故障现象: 系统引导停滞,显示"grub>"提示符 ...
- 设置windows引导linux分区,windows下安装grub引导Linux
在安装Linux和windows双系统时通常是先安装windows再安装Linux,因为windows会对主引扇区录进行充0,因而破坏主引导记录.当安装完windows再安装Linux,Linux会将 ...
- linux6.5修复引导,CentOS 6.5 修复grub引导
在使用Linux的过程中,难免会出现开机提示grub >而无法启动,可能是系统中/boot/grub文件丢失等原因造成的,当出现此问题的时候只要系统分区没有格式化一般是可以修复的,下面就以虚拟 ...
最新文章
- 深入理解按位异或运算符
- 基于引擎的matlab+vc混合编程的配置
- delete []实现机制
- VMX虚拟机环境下CentOS/Linux扩展磁盘空间,并且增加HOME目录的大小!
- Symbian学习笔记(4)——在GUI应用中使用图像
- 面向对象开发的五大基本原则
- struts2与struts1整合,java.lang.NoClassDefFoundError: org/apache/commons/logging/LogFactory
- [C#] 動的にアセンブリをロードする
- 勘误:EOS资源抵押退还
- QT不让windows休眠的方法
- Linux 编译 python3.7
- 重装系统数据恢复工具
- Elasticsearch 父子关系
- 西瓜书之误差逆传播公式推导、源码解读及各种易混淆概念
- 火狐浏览器Firefox 提示错误代码:SSL_ERROR_UNSUPPORTED_VERSION的正确解决方法
- 剑指Offer_入门_JZZ_斐波那契数列
- 【已解决】python pymysql报错DataError (1265, Data truncated for column ‘num‘ at row 1)
- 使用photoshop2021对有阴影的背光摄影照片进行调色修改
- Js 根据经纬度坐标计算方位角
- 我,大专毕业2年,从前端转型大数据开发,薪资涨了10K!
热门文章
- hdu4982 暴搜+剪枝(k个数和是n,k-1个数的和是平方数)
- 【Groovy】map 集合 ( map 集合操作符重载 | 使用 << 操作符添加一个元素 | 代码示例 )
- 【计算理论】计算复杂性 ( NP 完全问题 | 顶点覆盖问题 | 哈密顿路径问题 | 旅行商问题 | 子集和问题 )
- 【Kotlin】变量简介 ( 可空类型 | lateinit | 初始化判定 | 非空类型 | !! 与 ? 修饰符 | ?= ?. ?: 运算符 | 抽象属性变量)
- 【代数结构】群 ( 群的定义 | 群的基本性质 | 群的证明方法 | 交换群 )
- UOJ42/BZOJ3817 清华集训2014 Sum 类欧几里得
- Java动态代理代码快速上手
- 数据结构与算法 —— 二叉树
- 进击的新版NavMesh系统:看我飞檐走壁
- Python进阶03 模块