关于bootloader的介绍不必细讲,我直接说我的设计,我没有自己写一个bootlader而是使用grub,我用汇编写好汇编文件后在text段第一行写multiboot2的header,后续我用grub启动
multiboot2可以看成一个协议,当我们的grub启动一个.bin(二进制可执行)的kernel的时候首先会扫描kernel.bin的头部,发现是multiboot2的header就按照multiboot2 header中设置的去启动,multiboot2的header layout如下
Offset Type Field Name Note 0 u32 magic required 4 u32 architecture required 8 u32 header_length required 12 u32 checksum required 16-XX tags required
我们上面说了grub启动我们的kernel的时候会先扫描头部的multiboot2 header,所以我们的内核文件一定有2部分
boos.s
,其中包含multiboot2的header,和进入kernel main程序的入口(由call
汇编指令进入kernel代码)我们怎么将汇编和C++代码编译在一起生成kernel.bin
文件呢,我们用makefile实现,并且自己指定linked脚本
kernel的引导过程如下
- 我们按下开机键后bios会去检查自己的ROM,因为BIOS的一些设置都会存在这个ROM中,比如从那个device中启动
- 假设我们从硬盘启动,bios会去第一个硬盘的第一个扇区(512字节)中检查boot signature,假设偏移量为510字节为
0x55
,511字节为0xAA
那么说明signature检查通过,说明可以从这个device启动
当BIOS的boot signature检查通过后会将其boot sector(启动扇区一般就是第一块硬盘的第一个扇区,AKA primary bootloader)load到内存的0x0000:0x7c00
中(segment0,偏移量0x7c00
),然后凭借者这个primary load跳掉我们的secondary bootload中(grub选择菜单就是secondary bootloader提供的)此时我们可以看到选择菜单问我们从那个内核(/boot/kernel.bin)启动- 假设我们选择了自己的内核
kernel.bin
,那么将会从我们的kernel.bin启动,然后kernel.bin就会被load进内存,当然前提是kernel.bin的开头要是multiboots的header,grub才能识别然后load,然后kernel.bin
中有一个入口函数(call XXX)这个函数是我们kernel代码的入口,至此进入kernel,所以最终的kernel.bin可以看成是一个loader.s的汇编文件加上内核源码的一个链接后的可执行文件multiboot2可以看成一个协议,只要我们的kernel,bin的头实现了multiboot2的header,他都可以被grub load,注意linux没有使用multiboot,而是自己的linux boot protocol
首先这个文件由汇编写成,因为我们的可执行文件是ELF格式(unix下),除去ELF头之外就是text section,所以我们的multiboot务必写在text section中,ELF的layout如下
然后请看我们的loader.s源码(GAS风格)
/* .开头的都是指示汇编器要干什么 */
/* XXX:表示label,注意他在内存中占位 */
.section .multiboot /* 设置multiboot头 */
header_start:
.long 0xE85250D6 /*magic number*/
.long 0 /*protect mode启动*/
.long header_end - header_start
.long 0x100000000 - (0xE85250D6 + 0 + (header_end - header_start)) /*checksum*/
/*end tag*/
.short 0 /*type*/
.short 0 /*flags*/
.long 8 /*size*/
header_end:
.section .text /* 写text段的指令 */
.extern KernelMain /* extern指定外部函数,在使用的时候遍历所有文件找到他执行 */
.global loader /*将其使用global暴露给链接器,连接器的链接脚本中可以指定这个loader为程序的入口(enterpoint)*/
loader:
mov $kernel_stack, %esp
push %eax /* 因为我们设置了multiboot,所以寄存器eax一般存储multiboot的指针,寄存器ebx存储magic number的指针 */
push %ebx
call KernelMain
_stop: /* 死循环,防止相面call完KernelMain后就退出 */
cli /* 禁止中断 */
hlt /* 暂停处理器*/
jmp _stop /* jump */
.section .bss
.space 2*1024*1024 /* 2Mib空间,因为当我们存东西进stack的时候是往前移动,所以我们要预留空间,不然容易发生覆写grub区域和firewall区域 */
kernel_stack:
如果不设置header 头那么grub不认为kernel.bin是一个可执行文件
因为目前只是实现通过grub启动kernel,所以kernel.cpp只是一个简单的打印,但是我们打印的时候一般用printf()
函数,但是我们的内核时自己写的,当内核启动除了内核其他的什么io函数库都没有,所以我们要自己实现将kernel打印到屏幕上这个功能
怎么实现打印呢?首先有个东西叫做videomemory,他是VGA映射到我们内存中的一个区域,当我们在这个区域中写入字符,那么屏幕就会输出字符,这个地址是从0xB8000
开始,但是我们打印的时候可以自己选择每个字符打印的颜色,所以每一个字符后面的一个字符位子用于插入颜色标记,颜色标记如下
| Value | Color |
|-------|----------------|
| 0x0 | black |
| 0x1 | blue |
| 0x2 | green |
| 0x3 | cyan |
| 0x4 | red |
| 0x5 | magenta |
| 0x6 | brown |
| 0x7 | gray |
| 0x8 | dark gray |
| 0x9 | bright blue |
| 0xA | bright green |
| 0xB | bright cyan |
| 0xC | bright red |
| 0xD | bright magenta |
| 0xE | yellow |
| 0xF | white |
举个例子,我们想打印hello到屏幕上,h打印成白色,e打印绿色,l打印成蓝色,o打印成红色,对应的区域如下
0xB8000 <- 'h'
0xB8001 <- 0xF
0xB8002 <- 'e'
0xB8003 <- 0x2
0xB8004 <- 'l'
0xB8005 <- 0x1
0xB8006 <- 'l'
0xB8007 <- 0x1
0xB8008 <- 'o'
0xB8009 <- 0x4
最后我们的kernel.cpp
代码如下(字符都按照白色打印)
void printf(const char* str){
volatile char * videomemory = (volatile char*) 0xB8000;
while( *str != '\0' ){
*videomemory++ = *str++;
*videomemory++ = 0xf0;
}
}
extern "C" void KernelMain(){ //因为Cpp会把函数名改成XXX::KernelMain,所以我们特别指定按照C格式编译
//*((int*)0xb8000)=0x07690748;
printf("hello world!");
while(1);
}
链接脚本linker.ld
如下
ENTRY(loader)
OUTPUT_FORMAT(elf32-i386)
OUTPUT_ARCH(i386:i386)
SECTIONS{
. = 1M;
.text :
{
*(.multiboot)
*(.text)
*(.rodata)
}
.data :
{
start_ctors = .;
KEEP(*(.init_array));
KEEP(*(SORT_BY_INIT_PRIORITY(.init_array.*)));
end_ctors = .;
*(.data)
}
.bss :
{
*(.bss)
}
/DISCARD/ :
{
*(.fini_array*)
*(.comment)
}
首先定义这个链接后(汇编和C++程序链接后)生成可执行文件的入口ENTRY()
,入口是汇编中的label loader
,因为loader用global指定外部的编译器和链接器可以访问他,后面2行就是平台架构之类的
再看SECTIONS
部分,这里定义了我们链接后可执行文件(ELF类型文件)各个区域的格式(那个前那个后),首先是ELF头(默认的),然后接着的是text区域,text区域首先就要是multiboot的headler放在前面(汇编中.section .multiboot
定义),这个是最重要的,关系着我们grub能不能找到multiboot的header并且启动
这里主要定义的是将汇编文件编译成对象文件(.o),将内核文件(kernel.cpp)编译成对象文件(.o),然后是链接,最后定义install(将文件cp到/boot下如果用操作系统自带的grub的话)和clean(clean 对象文件)
makefile文件如下
#指定编译32位程序
GPPPARAMS = -m32 -fno-use-cxa-atexit -nostdlib -fno-builtin -fno-rtti -fno-exceptions
# as是汇编器,指定汇编器生成可执行文件的位数
ASPARAMS = --32
#在链接的时候指定生成i386平台上的elf类型的可执行文件(elf也是unix下主要的可执行文件 macos貌似不支持)
LDPARAMS = -melf_i386
objects = loader.o kernel.o
%.o: %.cpp
# $@指定目标文件也就是.o结尾的文件 $<指定第一个依赖文件(第一个依赖的文件指的是冒号右边第一个文件) -c只做编译不做链接
g++ $(GPPPARAMS) -o $@ -c $<
%.o: %.s
as $(ASPARAMS) -o $@ $<
#这个是由grub拿到内存中由cpu直接执行的可执行文件
kernel.bin: linker.ld $(objects)
#链接 -T指定linke的script
ld $(LDPARAMS) -T $< -o $@ $(objects)
install: kernel.bin
sudo cp $< /boot/kernel.bin
clean:
rm -rf *.o kernel.bin
此时我们目录中的文件如下
root@zhr-workstation:~/os# tree
├── kernel.cpp
├── linker.ld
├── loader.s
├── makefile
此时我们选择用qemu去启动我们的内核,首先make kernel.bin
生成kernel.bin
root@zhr-workstation:~/os# make kernel.bin
as --32 -o loader.o loader.s
g++ -m32 -fno-use-cxa-atexit -nostdlib -fno-builtin -fno-rtti -fno-exceptions -o kernel.o -c kernel.cpp
ld -melf_i386 -T linker.ld -o kernel.bin loader.o kernel.o
root@zhr-workstation:~/os# ls
kernel.bin kernel.cpp kernel.o linker.ld loader.o loader.s makefile
然后我们创建一个文件夹专门用于iso镜像(我们要自己制作ISO镜像)
首先创建isofiles
,然后在isofiles
中创建boot
目录,再在boot
目录中创建grub
目录,grub
目录中创建grub.cfg
也就是grub配置文件(操作系统开机先现实选择内核的界面),然后将kernel.bin
放在isofiles/boot/
下,最后isofiles
文件结构如下
root@zhr-workstation:~/os# tree isofiles/
isofiles/
└── boot
├── grub
│ └── grub.cfg
└── kernel.bin
然后配置grub.cfg文件指定os启动的时候grub选择kernel的名字和如何启动(用multiboot2启动)他
root@zhr-workstation:~/os/isofiles/boot/grub# cat grub.cfg
set timeout = 0
set default = 0
menuentry "zhr os"{
multiboot2 /boot/kernel.bin
boot
}
然后我们开始制作iso镜像
root@zhr-workstation:~/os# grub-mkrescue -o zhros.iso isofiles
xorriso 1.5.4 : RockRidge filesystem manipulator, libburnia project.
Drive current: -outdev 'stdio:zhros.iso'
Media current: stdio file, overwriteable
Media status : is blank
Media summary: 0 sessions, 0 data blocks, 0 data, 49.9g free
Added to ISO image: directory '/'='/tmp/grub.gXB4Oq'
xorriso : UPDATE : 587 files added in 1 seconds
Added to ISO image: directory '/'='/root/os/isofiles'
xorriso : UPDATE : 591 files added in 1 seconds
xorriso : NOTE : Copying to System Area: 512 bytes from file '/usr/lib/grub/i386-pc/boot_hybrid.img'
ISO image produced: 5737 sectors
Written to medium : 5737 sectors at LBA 0
Writing to 'stdio:zhros.iso' completed successfully.
此时我们目录下面有一个zhros.iso
镜像,最后我们用qemu
启动他(-cdrom
指定我们的iso镜像启动它,可能会报错比如gtk initialization failed
,报错大概率是远程terminal执行的这个命令,只需要加上-nographic
选项即可)
root@zhr-workstation:~/os# qemu-system-x86_64 -cdrom zhros.iso
启动后如下图所使
GNU GRUB version 2.06
┌────────────────────────────────────────────��────────���───��───��─────��────────┐
│*zhr os │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└��────────────────────────��─────────────────��───��──────────────────��─────────┘
Use the ↑ and ↓ keys to select which entry is highlighted.
Press enter to boot the selected OS, `e' to edit the commands
before booting or `c' for a command-line.