操作系统——环境搭建,制作Grub2可引导内核

PC启动过程

PC开机上电时将CS寄存器初始化为0xf000IP寄存器初始化为0xfff0,所以第一条指令地址在[fff0:f000] 0xffff0 处。此处代码功能为跳转到[f000:e05b] 0xfe05b。而这里的代码为ROM BIOS代码段,执行BIOS中的程序进行初始化并加载bootable的外设。[参考]

BOIS接下来会查找设置为bootable的外设(硬盘,U盘,etc),将它的第一个扇区加载到物理内存0x7c000x7dff,然后使用跳转指令跳转到这个地方[0000:7c00] 0x7c00执行加载的bootloader中的程序。

bootloader有以下特征和功能:

  • 大小应该是512bytes,因为BIOS会读取一个512bytes的扇区。
  • 将处理器从实模式切换到保护模式
  • 直接从硬盘中读取内核并加载到内存,所以bootloader需要知道内核大小,位置。
  • 将控制权限交给操作系统

实模式到保护模式的实现:重要的寄存器CR0寄存器[参考]。

bootloader可以通过ELF格式的目标文件中的Program header table来读取内核。[[参考]()]

Grub2

知道启动过程我们就可以写出bootloader了,但是这里我们使用Grub2来引导我们的简单内核。Grub2通过一个嵌入在磁盘扇区中的简单bootloader启动,用来初始化并引导其他操作系统。以下步骤与软件和操作系统有关,但思路相同。

  • 创建一个空的20M镜像
  • 使用分区工具为其建立一个bootable的分区,让BIOS能够加载它。
  • 使用losetup命令将文件虚拟化成块设备,让其成为磁盘驱动器,在我的Archlinux系统上,需要使用# modprobe loop开启回环设备,还需要使用# partx -u /dev/loop0来更新分区。
  • 为分区创建一个文件系统。(注意创建分区的设备名)
  • 挂载该分区。
  • 安装grub2到分区。嵌入bootloader程序到磁盘。[参考]
  • 我们设定镜像文件的第一个分区,偏移了2048个扇区(一个扇区512字节),所以可以直接挂载镜像文件,但是需要加上偏移。umount -o loop,offset=<偏移,这里使2048*512>
`创建空的镜像文件
$ dd if=/dev/zero of=yos.img bs=1M count=20
`为镜像文件分区
$ fdisk yos.img
`创建一份新的DOS分区表
> o
`新建一个分区
> n
`主分区
> p
`分区号默认
> 1
`第一个扇区默认
> 2048
`最后一个扇区默认
> 20479
`使改扇区bootable可以启动
> a
`保存
> w
`将文件虚拟为磁盘驱动
# losetup /dev/loop0 yos.img
`为分区创建一个文件系统
# mkfs.ext4 /dev/loop0p1
`挂载分区
# mount /dev/loop0p1/ /mnt/usb
`安装grub2
# mkdir /mnt/usb/boot
# grub-install --target=i386-pc /dev/loop0 --boot-directory=/mnt/usb/boot/
`直接挂载镜像文件
# mount -o loop,offset=1048576 yos.img /mnt/usb/

编写内核

这篇[Grub2文档(1)]告诉grub若想启动内核,其应该包含MultiBoot2 header。并且这片文档给出一个可加载内核示例代码。我们仿照手册的示例代码编写entry.S

/*mbh = multiboot_header*/
#define MBH_MAGIC 0xE85250D6
#define MBH_ARCH  0
.extern kmain

.text
.global _start
_start:
    jmp _entry

.align 8
mbh_start:
    .long    MBH_MAGIC
    .long    MBH_ARCH
    .long    mbh_end-mbh_start
    .long    -(mbh_end-mbh_start+MBH_ARCH+MBH_MAGIC)
.align 8
address_tag_start:
    .short   2
    .short   1
    .long    address_tag_end-address_tag_start
    .long    mbh_start
    .long    _start
    .long    0
    .long    0
address_tag_end:
.align 8
entry_address_tag_start:
    .short   3
    .short   1
    .long    entry_address_tag_end-entry_address_tag_start
    .long    _entry
entry_address_tag_end:
.align 8
    .short    0
    .short    0
    .long     8
mbh_end:

_entry:
    cli 
    call kmain
    hlt

使用gcc -c entry.S -o entry.o -m32 -nostdlib -fo-builtin -nodefaultlibs 编译好。

我们在写一个kmain函数用于显示字符。

void WriteChar(unsigned char ch,unsigned char forecolor,unsigned char backcolor,int x,int y);

void kmain(){
    char * str = "HellOS";
    for(int i=0;i<25;i++){
        for(int j=0;j<80;j++){
            WriteChar(0,0,0,j,i);
        }
    }
    for(unsigned i=0;str[i]!='\0';i++){
        WriteChar(str[i],0xf,0,i,0);
    }
    while(1){}
}

void WriteChar(unsigned char ch,unsigned char forecolor,unsigned char backcolor,int x,int y){
    volatile short* where;
    where = (volatile short *)0xb8000 + (y*80+x);
    *where = ch | (((backcolor<<4)|(forecolor&0x0f))<<8);
}

使用gcc -c kmain.c -o kmain.o -m32 -nostdlib -fo-builtin -nodefaultlibs 编译好。

编写我们的链接文件并链接上面两个文件。

OUTPUT_FORMAT(elf32-i386)
OUTPUT_ARCH(i386)
ENTRY(_start)

SECTIONS{
    . = 0x100000;
    .text : {*(.text)}
    .data : {*(.data)}
    .bss : {*(.bss)}
}
ld -m elf_i386 -T link.ld -o kernel entry.o kmain.o

将制作好的镜像挂载在了/mnt/usb这个目录,将编译好的kernel放在这个文件中,创建一个配置文件<挂载点>/boot/grub/grub.cfg方便启动,当然也可以手动选择加载的内核,路径可以随意。

menuentry 'kernel'{
    set root='hd0,msdos1'
    multiboot2 /kernel
    boot
}

以后每次编译好内核我们只需要覆盖<挂载点>/kernel这个文件就可以了。可以写一个Makefile来简化操作。

kernel_helo2
kernel_helo

备份好这次的镜像以供以后使用,以后只需要将编译的内核覆盖即可。[下载]

环境

  • 操作系统:Linux 4.18.16-arch1-1-ARCH #1 SMP PREEMPT Sat Oct 20 22:06:45 UTC 2018 x86_64 GNU/Linux ArchLinux
  • gcc:gcc (GCC) 8.2.1 20180831
  • make:GNU Make 4.2.1为 x86_64-unknown-linux-gnu 编译
  • grub:grub-install (GRUB) 2.02

参考

[INTEL 80386 PROGRAMMER`S REFERENCE MANUAL 1986]

[Grub2文档(1)]

[Grub2文档(2)]

[Grub2简介]

[制作镜像下载]

备份好这次的镜像以供以后使用,以后只需要将编译的内核覆盖即可。[下载]

Last modification:October 28th, 2019 at 12:34 pm
Buy Me A Coffee

Leave a Comment