一、前言

  本文档记录的内容既适用于x86也适用于x64,只是对于后者有一些环境要求。
  之前开发使用的方法是:自己的引导代码+虚拟软盘。优点是搭建简单,所有代码都是自己编写,可控性强。最近想试试使用grub的引导功能,于是花了些时间琢磨。搜出来的相​关资料有不少,但是要么是grub1的,要么太过零散,要么描述太过简略。总之,没有一篇文章详细的讲述整个配置过程。所以我就在搭建的过程中顺手整理了这么一篇完整的、​完全从零开始的方法,其中每一步都有较丰富的说明。
  另外,本文档介绍的方法适用于osx和linux,实际上整个过程中大部分必须使用到linux。也就是说如果要按照本文档来搭建开发环境,linuxer只需要使用自己​的linux系统就行,而osxer还得备一套linux系统(比如虚拟机)。使用linux的主要原因是我选择了ext2作为文件系统,而osx上貌似只有读写ext2​的fuse-ext2,没有用于创建ext2分区的fdisk等工具(如果同好有osx的ext2创建工具推荐,劳烦分享给我(ns.boxcounter[at]gmail.com)吧,不胜感激)。如果改用fat32就没有这个烦恼,整个过程都可以在osx下完成,因为osx的fdisk就可以创建fat32分区。

  我使用的系统、软件情况:

  • x86 ubuntu 12.04.2/x64 ubuntu 13.10、osx 10.8.4/osx 10.9
  • nasm 2.10.09
  • bochs 2.6.1
  • grub2 2.0.0

  如果同好使用的环境不一样,可能需要根据情况自行调整一些细节。
  另外,本文提供的命令在显示时候可能会自动折行,所以复制到剪贴板中之后(在折行处)可能会有多余的空格,请同好自行删减。

二、创建虚拟磁盘,并分区

  首先说明:
  这里的目标磁盘的属性是:16 headers, 63 sectors per track, 512 bytes per sector。意味着每一个cylinder的大小是516096bytes(16 * 63 * 512)。
  “#cylinders”表示柱面数,主要关系到磁盘大小。如果是10MB的磁盘,#cylinders=20。
  需要在linux系统中进行,使用的工具是kpartx,系统默认没有自带,需要下载。

  好了,开始了。

  1. dd if=/dev/zero of=antos.img bs=516096 count=#cylinders
    创建虚拟磁盘。也可以使用bochs附带的bximage工具来完成。

  2. ps aux | grep loop
    默认是搜索不到名为“[loopX]”进程的。如果有发现,那记住输出中的“[loopX]”进程。

  3. kpartx -av ./antos.img
    挂载虚拟磁盘,可能没有输出。

  4. ps aux | grep loop
    正常情况下,这里会发现一个名为“[loop0]”的进程。说明antos.img被挂载到了“/dev/loop0”设备上。如果前面搜索结果中已经有了“[loopX]”进程,那新增加的那个进程就是挂载的设备名。

  5. fdisk -u -C#cylinders -S63 -H16 /dev/loop0
    为磁盘分区。以#cylinders=20、单个分区为例:

     root@ubuntu:~# fdisk -u -C20 -S63 -H16 /dev/loop0
     Device contains neither a valid DOS partition table, nor Sun, SGI or OSF disklabel
     Building a new DOS disklabel with disk identifier 0x136d49ee.
     Changes will remain in memory only, until you decide to write them.
     After that, of course, the previous content won't be recoverable.
    
     Warning: invalid flag 0x0000 of partition table 4 will be corrected by w(rite)
    
     Command (m for help): o <<<=== Create a new empty DOS partition table
     Building a new DOS disklabel with disk identifier 0x5bd665d5.
     Changes will remain in memory only, until you decide to write them.
     After that, of course, the previous content won't be recoverable.
    
     Warning: invalid flag 0x0000 of partition table 4 will be corrected by w(rite)
    
     Command (m for help): n <<<=== Create a new partition
     Partition type:
     p   primary (0 primary, 0 extended, 4 free)
     e   extended
     Select (default p): <<<=== 回车
     Partition number (1-4, default 1): <<<=== 回车
     First sector (2048-20159, default 2048): <<<=== 回车
     Using default value 2048
     Last sector, +sectors or +size{K,M,G} (2048-20159, default 20159): <<<=== 回车
     Using default value 20159
    
     Command (m for help): a <<<=== Toggle the bootable flag (Optional)
     Partition number (1-4): 1 <<<=== 分区1
    
     Command (m for help): p <<<=== Print the partition table.
    
     Disk /dev/loop0: 10 MB, 10321920 bytes
     16 heads, 63 sectors/track, 20 cylinders, total 20160 sectors
     Units = sectors of 1 * 512 = 512 bytes
     Sector size (logical/physical): 512 bytes / 512 bytes
     I/O size (minimum/optimal): 512 bytes / 512 bytes
     Disk identifier: 0x5bd665d5
    
     Device Boot      Start         End      Blocks   Id  System
     /dev/loop0p1   *        2048       20159        9056   83  Linux
     <<<=== 如果使用附录2记录的方法,需要记录Start和Blocks的值,本例子中分别是2048和9056。
    
     Command (m for help): w <<<=== Write partition table to our 'disk' and exit
     The partition table has been altered!
    
     Calling ioctl() to re-read partition table.
    
     WARNING: Re-reading the partition table failed with error 22: Invalid argument.
     The kernel still uses the old table. The new table will be used at
     the next reboot or after you run partprobe(8) or kpartx(8)
     Syncing disks.
     <<<=== Ignore any errors about rereading the partition table. Since it's not a physical device we really don't care.
    
  6. kpartx -dv ./antos.img
    卸载磁盘,应该输出“loop deleted : /dev/loop0”。

  7. kpartx -av ./antos.img
    挂载分区磁盘,这次新创建的分区也会自动挂载。
    正常会输出“add map loop0p1 (252:0): 0 18112 linear /dev/loop0 2048”,表示分区挂载到了“/dev/mapper/loop0p1”设备上。

  8. mke2fs -b1024 /dev/mapper/loop0p1
    格式化分区,"-b1024"表示使用1KB的block。

  9. mkdir /tmp/antos
    创建挂载目录。

  10. mount -text2 /dev/mapper/loop0p1 /tmp/antos
    挂载分区到目录。

  11. ls /tmp/antos/
    如果前面的步骤都成功,会看到名为“lost+found”的目录,说明磁盘和分区都正确的创建了。

三、安装grub2到虚拟磁盘

  假设磁盘挂载到设备“/dev/loop0”上,分区挂载到“/tmp/antos”目录下。

grub-install --no-floppy --modules="biosdisk part_msdos ext2 configfile normal multiboot" --root-directory=/tmp/antos /dev/loop0

  安装过程中可能会报警告,只要最后输出“Installation finished. No error reported.”就表示安装成功了。
  如果使用的系统是x64架构的,需要使用新一些的系统,比如ubuntu 13.10。具体原因请参考附录一。
  (在fedora等redhat系中使用的名称是“grub2-install”)

四、在bochs中使用虚拟磁盘

  1. 确认虚拟磁盘的属性。
    先挂载虚拟磁盘,然后执行“disk -u -l /dev/loop0”,正常会有如下输出

     1 Disk /dev/loop0: 10 MB, 10321920 bytes
     2 16 heads, 63 sectors/track, 20 cylinders, total 20160 sectors
     3 Units = sectors of 1 * 512 = 512 bytes
     4 Sector size (logical/physical): 512 bytes / 512 bytes
     5 I/O size (minimum/optimal): 512 bytes / 512 bytes
     6 Disk identifier: 0x6418cb2e
     7
     8       Device Boot      Start         End      Blocks   Id  System
     9 /dev/loop0p1   *        2048       20159        9056   83  Linux
    

    第2行说明了虚拟磁盘的属性。(前面使用fdisk为磁盘分区的时候也有输出同样的内容,如果记下来了,就可以不需要这一步)

  2. 创建bochs虚拟机配置文件
    不带参数运行bochs,应该会有这样的输出:

     1. Restore factory default configuration
     2. Read options from...
     3. Edit options
     4. Save options to...
     5. Restore the Bochs state from...
     6. Begin simulation
     7. Quit now
    
     Please choose one: [2]
    

    选择4,然后输入配置文件名,比如“antos.bxrc”,提示保存成功后退出bochs。这样就有了一份默认配置的bochs虚拟机配置文件。

  3. 修改bochs虚拟机配置文件,以适应我们的需要

    1. 添加虚拟磁盘。
      将这一行

        ata0-master: type=none
      

      改为

        ata0-master: type=disk, path="./antos.img", cylinders=#cylinders,heads=#heads,spt=#sec-per-track
      

      对应之前获取到的磁盘属性,这一行应该是:

        ata0-master: type=disk, path="./antos.img", cylinders=20,heads=16,spt=63
      
    2. 修改启动项为磁盘。
      将这一行

        boot: floppy
      

      改为

        boot: disk
      
    3. 开启bochs的magic breakpoint。
      将这一行

        magic_break: enabled=0
      

      改为

        magic_break: enabled=1
      

  到这里,就可以在bochs中运行了(命令是“bochs -f antos.bxrc”),并且看到grub2的命令提示符,如图:

  

五、编写最简单的系统内核

// kernel.asm源码
[section .kernel]
[bits 32]

load_base equ 0x100000

;
; multiboot header
;
align 8
multiboot_header:
MBH_magic            equ 0xE85250D6
MBH_architecture     equ 0            ; 32-bit protected mode
MBH_header_length    equ multiboot_header_end - multiboot_header
MBH_checksum         equ -(MBH_header_length + MBH_magic + MBH_architecture)

dd MBH_magic
dd MBH_architecture
dd MBH_header_length
dd MBH_checksum

; tags
info_request_tag:
dw 1
dw 0
dd info_request_tag_end - info_request_tag
dd 5
dd 6
info_request_tag_end:
align 8
address_tag:
dw 2
dw 0
dd address_tag_end - address_tag
dd load_base                 ; header_addr
dd load_base                 ; load_addr
dd 0                         ; load_end_addr
dd 0                         ; bss_end_addr
address_tag_end:
align 8
entry_address_tag:
dw 3
dw 0
dd entry_address_tag_end - entry_address_tag
dd load_base + kernel_entry
entry_address_tag_end:
align 8
; end tag
dw 0
dw 0
dd 8
multiboot_header_end:

kernel_entry:
xchg bx, bx                 ; magic breakpoint
jmp $

  编译

nasm kernel.asm -o kernel.bin

  编译过程中可能会报警告,无视它。
  将虚拟磁盘挂载到某个目录,然后将kernel.bin拷贝到分区的根目录,即和/boot目录同一层目录。

六、使用grub2启动自行编写的操作系统内核

  假设分区挂载到“/tmp/antos”目录下,那么创建grub需要的配置文件“/tmp/antos/boot/grub/grub.cfg”,将以下几行文本贴进去:

set default=0
insmod ext2
set root=(hd0,1)
set timeout=10
menuentry "antos 0.0.1" {
   insmod ext2
   set root=(hd0,1)
   multiboot2 (hd0,1)/kernel.bin
   boot
}

  其中(hd0,1)表示咱之前创建的虚拟磁盘的第一个分区,kernel.bin就是前面编译的系统内核文件。

  现在可以启动咱的bochs虚拟机了,执行“bochs -f antos.bxrc”。
  再输入c继续执行后,应该就能看到bochs从咱的虚拟磁盘引导,然后可以看见grub的选择界面,最后会中断到咱系统内核的“xchg bx, bx”指令,这是bochs内置的主动中断指令,即magic breakpoint机制。如下:

o 00449811185i[CPU0 ] [449811185] Stopped on MAGIC BREAKPOINT
(0) Magic breakpoint
Next at t=449811185
(0) [0x000000100053] 0010:0000000000100053 (unk. ctxt): jmp .-2 (0x00100053) ; ebfe
00449811185i[XGUI ] Mouse capture off
<bochs:2>

  ok,整个配置过程就完毕了,整个过程都是在linux中完成的。使用fat32的osx同好可以使用类似的方法来完成。整个过程每一步的功能都写的很清楚了,看到这里理​理思路应该就明白整个流程了。

七、osx中读写ext2文件系统的虚拟磁盘

  最后说说osx相关的内容,因为我不想每次做开发的时候都需要开个linux虚拟机。以下是在osx下读写虚拟磁盘的方法,比如更新的kernel.bin等等。linu​xer可以无视这一步。

  1. 安装osxfuse和fuse-ext2
    fuse-ext2默认只能以只读方式挂载设备,所以需要进行以下修改使其默认以可读可写方式挂载设备:

     sudo vi /System/Library/Filesystems/fuse-ext2.fs/fuse-ext2.util
    

    搜索定位到Mount函数,为其名为“OPTIONS”的变量增加额外的“rw+”选项。
    比如:原内容为:

     function Mount ()
     {
           LogDebug "[Mount] Entering function Mount..."
           # Setting both defer_auth and defer_permissions. The option was renamed
           # starting with MacFUSE 1.0.0, and there seems to be no backward
           # compatibility on the options.
           OPTIONS="auto_xattr,defer_permissions"
           ...
     }
    

    改为:

     function Mount ()
     {
           LogDebug "[Mount] Entering function Mount..."
           # Setting both defer_auth and defer_permissions. The option was renamed
           # starting with MacFUSE 1.0.0, and there seems to be no backward
           # compatibility on the options.
           OPTIONS="auto_xattr,defer_permissions,rw+"
           ...
     }
    
  2. 挂载磁盘到设备

     hdiutil attach -nomount antos.img
    

    这是会输出如下信息:

     /dev/disk1              FDisk_partition_scheme
     /dev/disk1s1            Linux
    

    说明虚拟磁盘已经挂载到/dev/disk1设备上了,分区已经挂载到/dev/disk1s1。(之所以加上-nomount参数,是因为hdiutil没法正确地挂载ext2分区到目录)

  3. 挂载分区到目录

     /sbin/mount_fuse-ext2 /dev/disk1s1 ./mnt
    

    /dev/disk1s1是步骤3中得到的设备(分区)名。
    到这里,就可以对分区内容进行修改了,比如更新kernel.bin等等。

  4. 从目录卸载分区

     umount ./mnt
    
  5. 卸载虚拟磁盘

     hdiutil detach /dev/disk1
    

    (注意是磁盘设备,不是分区设备disk1s1)。

八、附录一:x64 ubuntu 12.04.2中执行grub-install遇到的问题

  我在x64 ubuntu 12.04.2中执行“安装grub2到虚拟磁盘”操作时总是失败,如下:

root@x64:~# grub-install --no-floppy --modules="biosdisk part_msdos ext2 configfile normal multiboot" --root-directory=/tmp/antos /dev/loop0
Path `/tmp/antos/boot/grub' is not readable by GRUB on boot. Installation is impossible. Aborting.

  加上–deubg选项后,发现grub-install输出了这么几行:

+ /usr/local/sbin/grub-probe -t fs /tmp/antos/boot/grub
+ return 1
+ gettext_printf Path `%s' is not readable by GRUB on boot. Installation is impossible. Aborting.\n /tmp/antos/boot/grub

  手动执行grub-probe,输出如下:

root@ubuntu:~# /usr/local/sbin/grub-probe -t fs /tmp/antos/boot/grub/usr/local/sbin/grub-probe: error: disk `lvm/loop0p1' not found.

  我查阅了很多资料,都没有明确的解决方案。经过了约莫20个小时的摸索,最终发现只需要使用新版本的系统自带的grub 2.0.0即可。(注:x64 ubuntu 12.04.2中的grub2是我源码编译的2.0.0版,也尝试过使用trunk源码编译或者使用系统自带的1.99,都会报错。)

九、附录二:创建虚拟磁盘分区的另外一种方法(losetup)

  需要说明,这种方法较前面介绍的使用kpartx的方法要繁琐,所以并不推荐(特别是如果要使用多分区)。补充在这里的原因是我最开始搜索到的资料使用的就是losetu​p工具,摸索成功之后才发现kpartx。
  另外,操作过程中有部分步骤和前面讲述的步骤一样,所以省略了那些步骤的说明。

  1. dd if=/dev/zero of=antos.img bs=516096 count=#cylinders
  2. losetup /dev/loop0 ./antos.img
    这个时候执行“ps aux | grep loop”,会看到一个名为[loop0]的进程。(如果loop0被占用,可以换一个设备)
  3. fdisk -u -C#cylinders -S63 -H16 /dev/loop0
  4. losetup /dev/loop0 ./antos.img
    这个时候执行“ps aux | grep loop”,会看到一个名为[loop0]的进程。(如果loop0被占用,可以换一个设备)
  5. losetup -d /dev/loop0
    到这里,虚拟磁盘已经创建完毕了,从设备(“loop0”)上卸载虚拟磁盘,准备格式化。
  6. losetup -o1048576 /dev/loop0 ./antos.img
    再次挂载,与前面挂载不同的是,这次使用了“-o1048576”参数,目的是跳过前1048576字节,来到分区的开始。前面提到要记住Start的值,即分区开始扇区号,这里就需要使用它了,1048576=20​48*512。
  7. mke2fs -b1024 /dev/loop0 9056
    对加载到“loop0”设备上的分区(注意是分区,不是整个磁盘了,前面咱跳到了分区开始处)进行格式化,使用的是ext2文件系统。
    ”-b1024“表示使用1KB的block,9056就是之前的Blocks的值,即整个分区的blocks数。
  8. mkdir /tmp/antos
  9. mount -text2 /dev/loop0 /tmp/antos
  10. umount /dev/loop0
  11. losetup -d /dev/loop0
    卸载目录和设备。

十、主要参考资料

  • The Multiboot Specification
  • grub2源码
  • Mac OS X下读写ext2/ext3文件系统

十一、版本记录

  • v1.0 - 2013-09-02,初始发布。
  • v1.1 - 2013-11-14,增加x64系统下的说明。

本文的pdf版:2013-11-14-linux、osx下搭建操作系统开发环境_v1.1