跟我一塊兒寫操做系統(二)——史上最簡單的內核

  項目地址: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

  1. 彙編代碼以下:

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下一節實現。

2. 編譯生成目標文件boot.o

  $nasm -f elf boot.asm -o boot.o

運行上面命令後會生成目標文件boot.o,-f  elf的意思是生成ELF格式的目標代碼。

第二步 內核入口

  1. 內核代碼以下:

/****************************************************
# 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代碼的其他部分相信讀者都能看得懂,我就不過多解釋了。 

2. 編譯生成目標文件kernel.o

  $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

相關文章
相關標籤/搜索