关于bootloader的介绍不必细讲,我直接说我的设计,我没有自己写一个bootlader而是使用multiboot2,我用汇编写好汇编文件后在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(启动扇区一般就是第一块硬盘的第一个扇区)load到内存的0x0000:0x7c00中(segment0,偏移量0x7c00),加入我们的bootloader是grub那么我们可以看到选择菜单问我们从那个内核(/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
首先这个文件由汇编写成,因为我们的可执行文件是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, %esppush %eax /* 因为我们设置了multiboot,所以寄存器eax一般存储multiboot的指针,寄存器ebx存储magic number的指针 */push %ebxcall 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_i386objects = loader.o kernel.o%.o: %.cpp
# $@指定目标文件也就是.o结尾的文件 $<指定第一个依赖文件(第一个依赖的文件指的是冒号右边第一个文件) -c只做编译不做链接g++ $(GPPPARAMS) -o $@ -c $< %.o: %.sas $(ASPARAMS) -o $@ $<#这个是由grub拿到内存中由cpu直接执行的可执行文件
kernel.bin: linker.ld $(objects)
#链接 -T指定linke的scriptld $(LDPARAMS) -T $< -o $@ $(objects)install: kernel.binsudo cp $< /boot/kernel.binclean: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 = 0menuentry "zhr os"{multiboot2 /boot/kernel.binboot
}
然后我们开始制作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.
选择我们的内核zhros,如下打印出了我们kernel的hello world
