通常編寫一個操作系統內核是一項浩大的工程。但我今天的目標是制作一個簡單的內核,用比較方便的方法在虛擬機上驗證它能夠被grub裝載和運行,并且可通過gdb進行調試,為接下去的工作創造一個基礎環境。
首先,為了方便運行和調試我們需要一個虛擬機。虛擬機有很多選擇,這里用最簡單的qemu。
先用dd創建一個文件作為虛擬盤,100MB就可以了:
$ dd if=/dev/zero of=disk.img count=204800 bs=512
然后對這個虛擬磁盤進行分區:
$ fdisk -c=dos disk.img
用命令n創建一個分區就可以了。通常情況下分區的起始扇區是2048(不帶選項-c=dos),如果用老式的msdos格式分區表(命令c),就可以選擇從63扇區開始。以前文章里提到63個扇區足夠塞下grub的核心映像,所以為了測試下grub就選擇了msdos模式。用命令w把變動寫入虛擬盤。
把這個分區虛擬成設備文件:
$ sudo losetup -o 32256 /dev/loop1 disk.img
這里指定了起始扇區的偏移量。63個扇區,每個扇區512個字節,總共是32256字節。如果你的分區起始扇區是2048,那么偏移量應該是2048 * 512字節。
格式化:
$ sudo mkfs.ext4 /dev/loop1
掛載起來,這樣就可以方便地往里面放kernel和grub需要的配置文件和模塊什么的:
$ sudo mount /dev/loop1 /mnt
安裝grub:
$ sudo grub-install --boot-directory=/mnt --modules="part_msdos" disk.img
使用qemu來啟動虛擬機(我用的是64位系統):
$ qemu-system-x86_64 -hda disk.img -m 1024 -s &
這時候應該能夠看到grub的提示符了。當然現在還沒有grub菜單也沒有kernel,我們暫時先關掉虛擬機。
接下來可以為grub建立個multiboot啟動菜單:
$ sudo vi /mnt/grub/grub.cfg
制作菜單命令:
menuentry "Hello" { multiboot (hd0,msdos1)/kernel boot}
確保數據寫回了虛擬盤:
$ sync
這時候如果你再打開虛擬機,應該就可以看到啟動菜單了,當然因為還沒有kernel,選擇菜單項后無法繼續,會提示kernel找不到。
下一步,我們用c語言從頭編寫個最簡單kernel程序。這個kernel沒有實現操作系統的基本功能。但是可以被grub裝載和運行。
kernel.c:
/* 在文件里嵌入一個簽名。Grub在multiboot時會尋找這個簽名 */#define GRUB_MAGIC 0x1BADB002#define GRUB_FLAGS 0x0#define GRUB_CHECKSUM (-1 * (GRUB_MAGIC + GRUB_FLAGS))struct grub_signature { unsigned int magic; unsigned int flags; unsigned int checksum;};struct grub_signature gs __attribute__ ((section (".grub_sig"))) = { GRUB_MAGIC, GRUB_FLAGS, GRUB_CHECKSUM};/* 顯示字符的函數。因為我們什么庫都不能用,只能直接寫屏了。0xB8000是VGA彩色字符模式的數據緩存。每個字符用兩個字節表示。前一個是Ascii碼,后一個代表顏色。 */void puts( const char *s, int color ){ volatile char *buffer = (volatile char*)0xB8000; while( *s != 0 ) { *buffer++ = *s++; *buffer++ = color; }}/* 主函數,程序入口。最后用個死循環,把代碼指針困在那里。*/void main (void) { puts("Hello World!", 0x7); while (1) {}}
有必要再寫個鏈接模板,確保編譯好的kernel裝載在內存地址0x100000,這里是grub代碼最后跳轉到的區域,從這里我們的kernel接過了接力棒。另外,雖然我用的虛擬機是64位的,但是我需要生成一個32位的kernel,因為做64位的kernel還需要做額外的設置工作,比如從32位保護模式打開long mode,比較麻煩。所以先暫時用32位的kernel。
kernel.ld:
OUTPUT_FORMAT("elf32-i386")ENTRY(main)SECTIONS{ .grub_sig 0x100000 : AT(0x100000) { *(.grub_sig) } .text : { *(.text) } .data : { *(.data) } .bss : { *(.bss) } /DISCARD/ : { *(.comment) *(.eh_frame) }}
還有一個Makefile,主要是設置一些編譯選項。
Makefile:
CC = gccLD = ldCFLAGS = -std=c99 -pedantic -Wall -nostdlib -ffreestanding -m32LDFLAGS = -T kernel.ld -nostdlib -n -melf_i386OBJS = kernel.o.PHONY: allall: kernel%.o: %.c ${CC} -c ${CFLAGS} $<kernel: $(OBJS) kernel.ld ${LD} ${LDFLAGS} -o kernel ${OBJS}clean: rm -f ${OBJS} kernel
編譯生成kernel并放入我們的虛擬盤里:
$ sudo cp kernel /mnt/$ sync
再次啟動虛擬機,在啟動菜單里選擇multiboot我們的kernel,應該就能看到Hello World!的字符顯示在虛擬機屏幕上了。
如果想要調試,可以運行gdb。因為我們在啟動qemu的時候使用了-s選項,所以qemu默認會打開tcp端口1234作為gdb調試端口。在gdb中可以使用target remote tcp::1234命令來連接。試試看連接,會發現cpu一直在執行0x100066處的指令。用objdump -D kernel看下kernel的匯編代碼:
0010004c <main>: 10004c:55 push %ebp 10004d:89 e5 mov %esp,%ebp 10004f:83 ec 08 sub $0x8,%esp 100052:c7 44 24 04 07 00 00 movl $0x7,0x4(%esp) 100059:00 10005a:c7 04 24 68 00 10 00 movl $0x100068,(%esp) 100061:e8 a6 ff ff ff call 10000c <puts> 100066:eb fe jmp 100066 <main+0x1a>
0x100066處的指令正好是死循環的那條jmp指令。
新聞熱點
疑難解答