项目地址:https://github.com/lucasysfeng/lucasOShtml
上一讲咱们介绍了计算机的启动流程,并给出了一份简单的主引导记录代码,此份代码仅仅是显示几个字符,并无作它本应该作的事--启动内核。本讲咱们首先看下内核是如何被启动的,而后写一个简单的内核,用已经实现的主引导记录配合GRUB启动它。ios
前一讲咱们说到,计算机读取"主引导记录"前面446字节的机器码以后,会运行事先安装的“启动管理器”bootloader,由用户选择启动哪一个内核,以后就会载入内核,将控制权交给内核。GNU GRUB(GRand Unified Bootloader)就是一种bootloader,知足多重引导规范(The Multiboot Specification),GRUB可选择操做系统分区上的不一样内核,下图就是GRUB的图形界面:git
图 GRUB界面github
可以被GRUB启动的内核须要知足两个的条件:shell
(1) 内核的前8K字节内必需要包含多重引导规范的头信息(Multiboot Header);
(2) 内核要加载在内存地址的1MB以上。sass
那么Multiboot Header是什么样子的呢?它必须包含4字节对齐的3个域(还有其余非必须域,咱们不讨论),以下:app
魔数域(magic):标志头的魔数,必须等于 0x1BADB002。。
标志域(flag):是否须要引导程序支持某些特性,咱们不关心这些特性,这个标志置为0。
校验域(checksum):校验等式是否成立(magic + flags + checksum = 0)函数
本文不讨论GRUB的实现,咱们会用前人已经写好的GRUB(笔者会给出),咱们要作的是完成符合GRUB启动规范的内核。为了完成这个内核,咱们须要写少许的汇编用来在内核中加入Multiboot Header,而后用C语言写内核入口,最后将汇编目标代码和C语言目标代码连接起来生成真正的内核。下面就让咱们一步步地完成这些吧!ui
第一步 汇编入口google
MBOOT_MAGIC equ 0x1BADB002 ; multiboot magic域,必须为此值 MBOOT_FLAGS equ 0x00 ; multiboot flag域, GRUB启动时是否要作一些特殊操做 MBOOT_CHECKSUM equ -(MBOOT_MAGIC + MBOOT_FLAGS) ; multiboot checksum域,校验上面两个域是否正确 [BITS 32] ; 以32位编译 section .text dd MBOOT_MAGIC dd MBOOT_FLAGS dd MBOOT_CHECKSUM dd start [GLOBAL start] [EXTERN kernel_main] ; 内核入口函数, EXTERN代表此符号在外部定义 start: cli ; 禁用中断 call kernel_main ; 调用内核入口函数 jmp $ ; 无限循环
在上面汇编中,咱们定义了GRUB启动须要的域MBOOT_MAGIC、MBOOT_FLAGS和MBOOT_CHECKSUM,并调用了内核入口函数kernel_main, kernel_main下一节实现。
$nasm -f elf boot.asm -o boot.o
运行上面命令后会生成目标文件boot.o,-f elf的意思是生成ELF格式的目标代码。
/**************************************************** # File : kernel.c # Blog : www.cnblogs.com/lucasysfeng # Author : lucasysfeng # Description : 内核入口函数 ****************************************************/ int kernel_main() { // 显存开始地址 char *display_buf = (char*)0xb8000; // 清屏 unsigned int i = 0; const unsigned int total = 80 * 25 * 2; // 一屏25行,每行80个字符,每一个字符2个字节 while(i < total) { display_buf[i++] = ' '; display_buf[i++] = 0x04; // 颜色 } // 显示字符 const char *str = "Hello World, welcome to mykernel!"; for (i = 0; '\0' != *str;) { display_buf[i++] = *(str++); display_buf[i++] = 0x04; } return 0; }
0xb8000h是显存开始的地址,读者能够看第一讲(http://www.cnblogs.com/lucasysfeng/p/4846119.html)“实模式内存地址空间分布”那张图,找到0xb8000h这个地址。从0xb8000h这个地址开始,每2个字节表示一个字符,前一个字节是字符的ASCII码,后一个字节是这个字符的颜色和属性,颜色和属性此处先不用关心。这段C代码的其他部分相信读者都能看得懂,我就不过多解释了。
$gcc -m32 -c -o kernel.o kernel.c
运行上面命令后,目标文件kernel.o就生成了。
上面讲到了能被GRUB启动的内核须要知足的条件:
(1) 内核的前8K字节内必需要包含多重引导规范的头信息(Multiboot Header);
(2) 内核要加载在内存地址的1MB以上。
咱们将头信息放在了汇编生成的目标文件boot.o中,所以咱们须要将boot.o和kernel.o连接到一块儿生成真正的kernel,而且这个真正的内核要加载到1MB内存上,为此,咱们须要下面的连接脚本和命令(关于连接脚本的使用自行google,笔者的另外一篇文章《连接到底干了什么》能够参考):
/*************************** * 文件名: link.ld ***************************/ ENTRY(start) SECTIONS { . = 0x100000; .text : { *(.text) . = ALIGN(4096); } .data : { *(.data) *(.rodata) . = ALIGN(4096); } }
咱们用ld命令连接目标文件boot.o和kernel.o,指明使用连接脚本link.ld:
$ ld -T link.ld -m elf_i386 -nostdlib boot.o kernel.o -o kernel
运行上面命令后,会生成咱们要启动的真正的内核kernel,那么这个kernel是否知足GRUB启动规范呢?咱们能够经过反汇编来看一下:
$ objdump -d kernel | head -n30
结果以下图所示,咱们看到100000了吗,这个就是起始的地址即1M,看到02 b0 ad 1b 00 00了吗,这个就是GRUB魔数域1b ad b0 02(大小端问题,反向存储)
$ objdump -d kernel | head -n30 kernel: 文件格式 elf32-i386 Disassembly of section .text: 00100000 <start-0x10>: 100000: 02 b0 ad 1b 00 00 add 0x1bad(%eax),%dh 100006: 00 00 add %al,(%eax) 100008: fe 4f 52 decb 0x52(%edi) 10000b: e4 10 in $0x10,%al 10000d: 00 10 add %dl,(%eax) ... 00100010 <start>: 100010: fa cli 100011: bc 03 80 00 00 mov $0x8003,%esp 100016: bd 00 00 00 00 mov $0x0,%ebp 10001b: 83 e4 f0 and $0xfffffff0,%esp 10001e: 89 1d 00 a0 10 00 mov %ebx,0x10a000 100024: e8 03 00 00 00 call 10002c <kernel_main> 00100029 <stop>: 100029: f4 hlt 10002a: eb fd jmp 100029 <stop> 0010002c <kernel_main>: 10002c: 55 push %ebp 10002d: 89 e5 mov %esp,%ebp 10002f: 83 ec 08 sub $0x8,%esp
咱们这里不制做软盘镜像,而是使用已经制做好的软盘镜像,镜像名称lucasOS.img,已经放在github上了。咱们也无需制做GRUB,这个软盘镜像已经包含了GRUB.咱们要作的是把内核文件kernel拷贝到软盘镜像lucasOS.img中。
1. 获取lucasOS.img软盘镜像。lucasOS目录下的lucasOS.img就是咱们要的软盘镜像。
$ git clone https://github.com/lucasysfeng/lucasOS.git $cd lucasOS/code/chapter2
2. 建立挂载点。
mkdir lucasOS
3. 挂载软盘镜像。注意把lucasOS.img改成你的lucasOS.img所在路径。
sudo mount lucasOS.img lucasOS
4. 把内核文件拷贝到软盘镜像中。注意把kernel改成你的kernel所在路径。
sudo cp kernel lucasOS/kernel
5. 卸载软盘镜像。
sudo umount mnt/lucasOS
上面步骤可写成Makefile(your-rootpasswd改成你的sudo密码)以下:
CC = gcc ASM = nasm LD = ld # 这里挂载点采用相对路径,即当前目录下的lucasOS MOUNT_POINT = lucasOS CC_FLAGS = -c -Wall -m32 -ggdb -gstabs+ -nostdinc -fno-builtin -fno-stack-protector LD_FLAGS = -T link.ld -m elf_i386 -nostdlib all: boot.o kernel.o link update_kernel boot.o: boot.asm @echo '编译boot, 生成GRUB须要的信息..' $(ASM) -f elf boot.asm kernel.o: kernel.c @echo '编译kernel..' $(CC) $(CC_FLAGS) kernel.c -o kernel.o .PHONY: kernel link: @echo '连接boot和kernel, 生成最终kernel..' $(LD) $(LD_FLAGS) boot.o kernel.o -o kernel .PHONY: update_kernel update_kernel: @echo '将kernel拷贝到软盘镜像..' @if [ ! -d $(MOUNT_POINT) ]; then mkdir $(MOUNT_POINT); fi # 挂载点不存在则建立 echo "your-rootpasswd" | sudo -S mount lucasOS.img $(MOUNT_POINT) sudo cp kernel $(MOUNT_POINT)/kernel sleep 1 sudo umount $(MOUNT_POINT) .PHONY: clean clean: rm *.o kernel
用bochs,bochsrc配置以下(须要根据bochs安装路径改变):
############################################################### # Configuration file for Bochs ############################################################### # how much memory the emulated machine will have megs: 32 # filename of ROM images romimage: file=/opt/local/share/bochs/BIOS-bochs-latest #/opt/share/bochs/BIOS-bochs-latest vgaromimage: file=/opt/local/share/bochs/VGABIOS-lgpl-latest #/usr/share/vgabios/vgabios.bin # what disk images will be used floppya: 1_44=lucasOS.img, status=inserted # choose the boot disk. boot: floppy # where do we send log messages? # log: bochsout.txt # disable the mouse mouse: enabled=0 # enable key mapping, using US layout as default. # keyboard_mapping: enabled=1, map=/opt/local/share/bochs/keymaps/x11-pc-us.map keyboard: keymap=/opt/local/share/bochs/keymaps/x11-pc-us.map
也可用vbox来启动:
建立的虚拟机lucasOS须要添加软驱控制器以下图:
而后添加虚拟软盘选中lucasOS.img,这样就能够启动了,而且有GRUB效果:
本系列GitHub地址 https://github.com/lucasysfeng/lucasOS,用下面命令获取代码:
git clone https://github.com/lucasysfeng/lucasOS.git
本讲的代码是code/chapter2,笔者已经将上面的命令集成到Makefile中了,读者只需进入目录,按ReadMe.txt说明执行便可,有问题请留言。
相似项目hurlex
git clone https://github.com/hurley25/hurlex-doc.git
下载后Makefile修改以下,bochsrc如前面
#!Makefile # # -------------------------------------------------------- # # hurlex 这个小内核的 Makefile # 默认使用的C语言编译器是 GCC、汇编语言编译器是 nasm # # -------------------------------------------------------- # # patsubst 处理全部在 C_SOURCES 字列中的字(一列文件名),若是它的 结尾是 '.c',就用 '.o' 把 '.c' 取代 C_SOURCES = $(shell find . -name "*.c") C_OBJECTS = $(patsubst %.c, %.o, $(C_SOURCES)) S_SOURCES = $(shell find . -name "*.s") S_OBJECTS = $(patsubst %.s, %.o, $(S_SOURCES)) CC = gcc LD = ld ASM = nasm MOUNT_POINT = ../kernel C_FLAGS = -c -Wall -m32 -ggdb -gstabs+ -nostdinc -fno-builtin -fno-stack-protector -I include LD_FLAGS = -T scripts/kernel.ld -m elf_i386 -nostdlib ASM_FLAGS = -f elf -g -F stabs all: $(S_OBJECTS) $(C_OBJECTS) link update_image # The automatic variable `$<' is just the first prerequisite .c.o: @echo 编译代码文件 $< ... $(CC) $(C_FLAGS) $< -o $@ .s.o: @echo 编译汇编文件 $< ... $(ASM) $(ASM_FLAGS) $< link: @echo 连接内核文件... $(LD) $(LD_FLAGS) $(S_OBJECTS) $(C_OBJECTS) -o hx_kernel .PHONY:clean clean: $(RM) $(S_OBJECTS) $(C_OBJECTS) hx_kernel .PHONY:update_image update_image: @echo '将kernel拷贝到软盘镜像..' @if [ ! -d $(MOUNT_POINT) ]; then mkdir $(MOUNT_POINT); fi # 挂载点不存在则建立 echo "wxwpxh" | sudo -S mount floppy.img $(MOUNT_POINT) sudo cp hx_kernel $(MOUNT_POINT)/hx_kernel sleep 1 sudo umount $(MOUNT_POINT) .PHONY:mount_image mount_image: sudo mount floppy.img $(MOUNT_POINT) .PHONY:umount_image umount_image: sudo umount $(MOUNT_POINT) .PHONY:qemu qemu: qemu -fda floppy.img -boot a .PHONY:bochs bochs: bochs -f scripts/bochsrc.txt .PHONY:debug debug: qemu -S -s -fda floppy.img -boot a & sleep 1 cgdb -x scripts/gdbinit
1. http://www.jamesmolloy.co.uk/tutorial_html/2.-Genesis.html