自制os 1:bootloader

文章目录

  • 前言
  • loader.s
  • kernel.cpp
    • 打印
  • 链接
    • 链接脚本
    • makefile
  • qemu启动kernel

前言

关于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部分

  1. 用汇编(我用的是GAS aka GNU Assambly Syntax)写的boos.s,其中包含multiboot2的header,和进入kernel main程序的入口(由call汇编指令进入kernel代码)
  2. 第二个就是kernel代码,我们这里目前没有实现一个完整的kernel,而只是打印,就为了印证从grub启动kernel

我们怎么将汇编和C++代码编译在一起生成kernel.bin文件呢,我们用makefile实现,并且自己指定linked脚本

kernel的引导过程如下

  1. 我们按下开机键后bios会去检查自己的ROM,因为BIOS的一些设置都会存在这个ROM中,比如从那个device中启动
  2. 假设我们从硬盘启动,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)启动
  3. 假设我们选择了自己的内核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

loader.s

首先这个文件由汇编写成,因为我们的可执行文件是ELF格式(unix下),除去ELF头之外就是text section,所以我们的multiboot务必写在text section中,ELF的layout如下
自制os 1:bootloader_第1张图片
然后请看我们的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是一个可执行文件

kernel.cpp

因为目前只是实现通过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并且启动

makefile

这里主要定义的是将汇编文件编译成对象文件(.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

qemu启动kernel

此时我们目录中的文件如下

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.                           
                                                   

选择我们的内核zhros,如下打印出了我们kernel的hello world
自制os 1:bootloader_第2张图片

你可能感兴趣的:(os,c++,开发语言,系统架构)