操作系统内核Hack:(三)引导程序制作

来源:互联网 发布:linux samba服务器搭建 编辑:程序博客网 时间:2024/05/18 23:52

操作系统内核Hack:(三)引导程序制作

关于本文涉及到的完整源码请参考MiniOS的v1_bootloader分支。


1.制作方法

现在我们已经了解了关于BootLoader的一切知识,让我们开始动手做一个BootLoader吧!但真正开始之前,我们还要做出一个选择,在之前的讨论中我们曾说过,有两种学习和制作引导程序和操作系统内核的路线:1)《Orange’s:一个操作系统的实现》书中的路线;2)Linux 0.11的路线。

1.1 两种实现思路

具体来说,第一种路线就是将BootLoader和Kernel都放到预先格式化成FAT的软盘上,BootLoader通过FAT和ELF的知识定位并加载Kernel,借助FreeDOS启动或调试我们的操作系统。并非对FAT和DOS心存偏见,毕竟现在的确很多汇编语言书籍仍然以DOS作为学习平台,但我个人更喜欢第二种路线。在引导过程,我们不依赖任何文件系统,在裸扇区中加载运行第二阶段BootLoader和Kernel。进入Kernel之后,我们再挂载一个根文件系统。为何要这样做而不跟着《Orange’s》作者的思路走呢?因为我们毕竟主要学习和模仿的是*nix族的操作系统,既然如此,为何不纯粹一些?直接循着当年Linus在Linux 0.11中的思路,这才算是一次酣畅淋漓的操作系统内核的Hack之旅!

经过了本系列前两回对实验环境和底层编程基础知识的准备,加上刚才对实现思路的分析,现在我们就参考《Linux内核完全注释》中Linux 0.11的思路,借鉴《Orange’s:一个操作系统的实现》中NASM的汇编代码,“双剑合璧,威力无穷”。让我们站在前人的肩膀上,一起动手来实现一个属于我们自己的操作系统BootLoader!

1.2 Linux 0.11参考

既然要借鉴思路,我们就先看一下Linux 0.11这个成品是什么样子。因为《Linux内核完全注释》中提供的源代码链接要做很多手动修改和准备工作才能使用,而网上有很多热心的网友分享了开箱即用的0.11源码安装包。

1.2.1 编译内核

cdai@cdai ~/Source $ git clone https://github.com/yuanxinyu/Linux-0.11cdai@cdai ~/Source $ cd Linux-0.11cdai@cdai-Vostro-1450 ~/Source/Linux-0.11 $ make help<<<<This is the basic help info of linux-0.11>>>Usage:     make --generate a kernel floppy Image with a fs on hda1     make start -- start the kernel in qemu     make debug -- debug the kernel in qemu & gdb at port 1234     make disk  -- generate a kernel Image & copy it to floppy     make cscope -- genereate the cscope index databases     make tags -- generate the tag file     make cg -- generate callgraph of the system architecture     make clean -- clean the object files     make distclean -- only keep the source code filesNote!:     * You need to install the following basic tools:          ubuntu|debian, qemu|bochs, ctags, cscope, calltree, graphviz           vim-full, build-essential, hex, dd, gcc 4.3.2...     * Becarefull to change the compiling options, which will heavily     influence the compiling procedure and running result.     ...<<<Be Happy To Play With It :-)>>>cdai@cdai ~/Source/Linux-0.11 $ sudo apt-get install -y ctags cscope graphviz qemucdai@cdai ~/Source/Linux-0.11 $ make

1.2.2 下载硬盘IMG

从OldLinux下载硬盘镜像hdc-0.11.img,大小为127MB。将其拷贝到Linux0.11目录下后,就可以执行make start用qemu模拟Linux 0.11的运行环境了。

cdai@cdai ~/Source/Linux-0.11 $ tree├── boot├── fs├── hdc-0.11.img├── Image├── include├── init├── kernel├── lib├── Makefile├── Makefile.header├── mm├── README.md├── readme.old├── System.map└── tools8 directories, 10 filescdai@cdai ~/Source/Linux-0.11 $ make start

1.2.3 安装运行

启动时,系统会从软盘引导,并挂载根文件系统:

SeaBIOS (version 1.7.4-20140219_122725-roseapple)iPXE (http://ipxe.org) 00:0.3.0 C900 PCI2.10 PnP PMM+00FC1110+00F21110 C900Booting from Floppy...Loading system...Partition table ok.43012/62000 free blocks19719/20666 free inodes3446 buffers = 3528704 bytes buffer spaceFree mem: 12574720 bytes Ok.[/usr/root]# lsREADME      hello       mtools.howto        shoelace.tar.Zgcclib140   hello.c     shoe

2.文件目录组织

文件目录组织是按照打包后的模块划分,一级目录分为boot1、boot2、system三个文件夹,tools中放的则是打包工具类。对应《Orange’s》代码,boot1/bootsect.s相当于boot.asm,boot2/setup.s相当于loader.asm。

cdai@cdai ~/Workspace/syspace/1-assembly $ tree -d minios/minios/├── boot1│   └── bootsect.s├── boot2│   └── setup.s├── Makefile├── system│   ├── fs│   ├── include│   ├── init│   ├── kernel│   ├── lib│   └── mm└── tools    └── build.c10 directories, 4 files

3.实用工具

3.1 构建工具

3.1.1 Makefile

首先,Makefile份内的工作就是编译出bootsect、setup、system、build。之后,调用build处理前三者,最终产生拼接好的可引导Image镜像文件。在此过程中,Makefile还有一些小任务要完成。例如利用临时汇编文件tmp.s将system模块的大小拼接到bootsect.asm的头部,加载system的代码中会用到它。

################## Macro & Rule#################AS      = nasmASFLAGS = -f elfCC      = gccCFLAGS  = -Wall -OLD      = ld# -Ttext org -e entry -s(omit all symbol info)# -x(discard all local symbols) -M(print memory map)LDFLAGS = -Ttext 0 -e main --oformat binary -s -x -M%.o:    %.c    $(CC) $(CFLAGS) -c -o $@ $<#%.o:   %.asm#   $(AS) $(ASFLAGS) -o $@ $<##################   Default#################all:    ImageImage:  boot1/bootsect boot2/setup system/system tools/build        tools/build boot1/bootsect boot2/setup system/system > a.bin    dd if=a.bin of=Image bs=8192 conv=notrunc    rm -f a.bintools/build:    tools/build.c    $(CC) $(CFLAGS) -o $@ $<# SYSSIZE= number of clicks (16 bytes) to be loadedboot1/bootsect: boot1/bootsect.asm system/system    (echo -n "SYSSIZE equ (";ls -l system/system | grep system \        | cut -d " " -f 5 | tr '\012' ' '; echo "+ 15 ) / 16") > tmp.asm    cat $< >> tmp.asm    $(AS) -o $@ tmp.asm    rm -f tmp.asmboot2/setup:    boot2/setup.asm    $(AS) -o $@ $<system/system:  system/init/main.o system/init/myprint.o    $(LD) $(LDFLAGS) \    system/init/main.o \    system/init/myprint.o \    -o $@ > System.mapsystem/init/main.o: system/init/main.csystem/init/myprint.o: system/init/myprint.asm    $(AS) $(ASFLAGS) -o $@ $<################## Create floppy#################disk:    bximage -q -fd -size=1.44 Image.PHONY: disk##################   Start Vm#################start:  Image    bochs -q -f bochsrcqemu:   Image    qemu-system-x86_64 -m 16M -boot a -fda Image.PHONY: start##################   GitHub#################commit:    git add .    git commit -m "$(MSG)"##################   Clean#################clean:    rm -f boot1/bootsect boot2/setup system/system tools/build     rm -f system/**/*.o    rm -f a.bin tmp.asm System.map.PHONY: clean

3.1.2 build.c

build.c主要完成一些不便于或无法在Makefile中实现的操作:

  1. 文件有效性的检查:例如bootsect末尾的0x55AA签名
  2. 文件填充:例如对不足四个扇区大小的setup进行填充
  3. 文件内容解析:例如解析出a.out或ELF格式的system,将其中二进制代码部分提取出来
  4. 拼接:将bootsect、setup、system三者拼接成Image可引导镜像
#include <stdio.h>      /* fprintf */#include <string.h>#include <stdlib.h>     /* exit */#include <sys/types.h>  /* unistd.h needs this */#include <unistd.h>     /* read/write */#include <fcntl.h>#define BUFFER_SIZE 1024#define SETUP_SECTS 4   /* max number of sectors of setup */#define STRINGIFY(x) #x /* cat string */#define GCC_HEADER 1024 /* GCC header length */#define SYS_SIZE 0x2000 /* max system length (SYS_SIZE*16=128KB) */void die(const char *str){    fprintf(stderr, "%s\n", str);    exit(1);}void usage(){    die("Usage: build bootsect setup system [> image]");}void copy_bootsect(char const *filename, char *buf){    int c, fd;    if ((fd = open(filename, O_RDONLY, 0)) < 0)        die("Unable to open 'bootsect'");    for (c = 0; c < BUFFER_SIZE; c++)        buf[c] = 0;    c = read(fd, buf, BUFFER_SIZE);    close(fd);    fprintf(stderr, "'bootsect' is %d bytes.\n", c);    if (c != 512)        die("'bootsect' must be exactly 512 bytes");    if ((*(unsigned short *)(buf + 510)) != 0xAA55)        die("'bootsect' hasn't got boot flag (0xAA55)");    c = write(1, buf, 512);    if (c != 512)        die("Write call failed");}void copy_setup(char const *filename, char *buf){    int c, i, fd;    if ((fd = open(filename, O_RDONLY, 0)) < 0)        die("Unable to open 'setup'");    for (i = 0; (c = read(fd, buf, BUFFER_SIZE)) > 0; i += c)        if (write(1, buf, c) != c)            die("Write call failed");    close(fd);    fprintf(stderr, "'setup' is %d bytes\n", i);    if (i > SETUP_SECTS * 512)        die("'setup' exceeds " STRINGIFY(SETUP_SECTS) " sectors");    // Fill '\0' if smaller than max bytes    for (c = 0; c < BUFFER_SIZE; c++)        buf[c] = 0;    while(i < SETUP_SECTS * 512) {        c = SETUP_SECTS * 512 - i;        if (c > BUFFER_SIZE)            c = BUFFER_SIZE;        if (write(1, buf, c) != c)            die("Write call failed");        i += c;    }}void copy_system(char const *filename, char *buf){    int c, i, fd;    if ((fd = open(filename, O_RDONLY, 0)) < 0)        die("Unable to open 'system'");    /*if (read(fd, buf, GCC_HEADER) != GCC_HEADER)        die("Unable to read header of 'system'");    if (((long *) buf)[5] != 0)        die("Non-GCC header of 'system'");*/    for (i = 0; (c = read(fd, buf, BUFFER_SIZE)) > 0; i += c)        if (write(1, buf, c) != c)            die("Write call failed");    close(fd);    fprintf(stderr, "'system' is %d bytes\n", i);    if (i > SYS_SIZE * 16)        die("'system' is too big");}int main(int argc, char const *argv[]){    char buf[BUFFER_SIZE];    if (argc != 4)        usage();        copy_bootsect(argv[1], buf);    copy_setup(argv[2], buf);    copy_system(argv[3], buf);    return 0;}

编写build.c中可能会碰到一些C语言的小问题:例如如何字符串拼接,sizeof(array)和sizeof(pointer)陷阱等,这里就不细说了,回头总结C语言开发问题时才一并总结吧。

3.2 调试工具

3.2.1 BIN反汇编

由于BIN文件是Raw Binary,没有文件头及其他附加信息,所以直接运行objdump是会报错的。用objdump -D -b binary -m i386,其中-D表示对整个文件反汇编,-b表示二进制,-m表示指令集架构。objdump的确可以反汇编出一些指令,但不够准确,尤其是16位操作数的处理。最好的办法就是用NASM自带的反汇编工具ndisasm,-o指定起始地址,-e指定开头跳过的字节数,-k指定不反汇编的位置,例如不用-o参数时-k 0x1FE,2会跳过0x55AA签名。与objdump对比一下,看!是不是比objdump好多了!

cdai@cdai ~/Workspace/syspace/1-assembly/minios $ objdump -D -b binary -m i386 Image Image:     file format binaryDisassembly of section .data:00000000 <.data>:   0:   b8 c0 07 8e d8          mov    $0xd88e07c0,%eax   5:   b8 00 90 8e c0          mov    $0xc08e9000,%eax   a:   b9 00 01 31 f6          mov    $0xf6310100,%ecx   f:   31 ff                   xor    %edi,%edi  11:   f3 a5                   rep movsl %ds:(%esi),%es:(%edi)  13:   ea 18 00 00 90 8c c8    ljmp   $0xc88c,$0x90000018  1a:   8e d8                   mov    %eax,%ds    ...cdai@cdai ~/Workspace/syspace/1-assembly/minios $ ndisasm -o 0x7c00 Image00007C00  B8C007            mov ax,0x7c000007C03  8ED8              mov ds,ax00007C05  B80090            mov ax,0x900000007C08  8EC0              mov es,ax00007C0A  B90001            mov cx,0x10000007C0D  31F6              xor si,si00007C0F  31FF              xor di,di00007C11  F3A5              rep movsw00007C13  EA18000090        jmp word 0x9000:0x1800007C18  8CC8              mov ax,cs00007C1A  8ED8              mov ds,ax

3.2.2 Bochs调试

因为之前在《C实战:强大的程序调试工具GDB》中已经学习并总结过GDB调试器的使用,所以这里就简单说一下Bochs对应的命令:

  • 执行:b加断点,c继续执行,s和p分别是step in/step next单步调试
  • 查看寄存器:r查看通用寄存器,sreg查看段寄存器
  • 查看内存:x查看线性地址,xp查看物理地址。例如xp /40bx 0x7c00表示以十六进制(x)字节(b)的形式,查看0x7c00位置长度为40字节的数据
  • 反汇编:u反汇编一段内存中的代码。例如x 0x7c00 0x7c10
<bochs:5> xp /40bx 0x7c00[bochs]:0x00007c00 <bogus+       0>:    0xb8    0xc0    0x07    0x8e    0xd8    0xb8    0x00    0x900x00007c08 <bogus+       8>:    0x8e    0xc0    0xb9    0x00    0x01    0x31    0xf6    0x310x00007c10 <bogus+      16>:    0xff    0xf3    0xa5    0xea    0x18    0x00    0x00    0x900x00007c18 <bogus+      24>:    0x8c    0xc8    0x8e    0xd8    0x8e    0xc0    0x8e    0xd00x00007c20 <bogus+      32>:    0xbc    0x00    0xff    0xba    0x00    0x00    0xb9    0x02<bochs:6> u 0x7c00 0x7c1000007c00: (                    ): mov ax, 0x07c0            ; b8c00700007c03: (                    ): mov ds, ax                ; 8ed800007c05: (                    ): mov ax, 0x9000            ; b8009000007c08: (                    ): mov es, ax                ; 8ec000007c0a: (                    ): mov cx, 0x0100            ; b9000100007c0d: (                    ): xor si, si                ; 31f600007c0f: (                    ): xor di, di                ; 31ff

4.源码分析

4.1 第一阶段引导:bootsect.asm

因为受限于引导扇区的512字节,所以bootsect的工作很简单,首先就是将自己挪动到90000h高地址处,“明哲保身”避免被后面要加载的模块覆盖掉。然后打印提示信息”Loading system…”并加载setup和system后,就跳转到setup继续执行。为了简化,我们暂时没有根据make时拼接到bootsect.asm头部的SYSSIZE去加载system,而只是加载了第6个扇区

; ############################;           Constants; ############################SETUPLEN    equ 4BOOTSEG     equ 0x07c0INITSEG     equ 0x9000SETUPSEG    equ 0x9020SYSSEG      equ 0x1000; ############################;       Booting Process; ############################    org     07c00h; 1) Move bootsect to 0x90000    mov     ax, BOOTSEG    mov     ds, ax    mov     ax, INITSEG    mov     es, ax    mov     cx, 256         ; cx    = counter    xor     si, si          ; ds:si = source    xor     di, di          ; es:di = target    rep     movsw           ; move word by word    jmp     INITSEG:go-$$   ; far jump!go:     mov     ax, cs          ; re-init registers    mov     ds, ax    mov     es, ax    mov     ss, ax    mov     sp, 0xff00      ; arbitrary value >> 512; 2) Load setup module at 0x90200load_setup:    mov     dx, 0000h       ; dx    = driver(dh)/head(dl)    mov     cx, 0002h       ; cx    = track(ch)/sector(cl)    mov     bx, 0200h       ; es:bx = target(es=9000h,bx=90200-90000)    mov     ah, 02h         ; ah    = service id(ah=02 means read)    mov     al, SETUPLEN    ; al    = number of sectors to read(al)    int     13h    jnc     ok_load_setup    xor     dl, dl          ; reset floppy and retry if failed    xor     ah, ah    int     13h    jmp     load_setup      ; 3) Display loading messageok_load_setup:    mov     ah, 03h         ; read cursur position    xor     bh, bh    int     10h    mov     bp, msg1-$$     ; es:bp = message start address    mov     cx, 21          ; cx    = message length     mov     ax, 1301h    mov     bx, 0007h       ; bx    = page no.(bh=0 page-0)                            ;         attribute(bl=7 white fg and black bg)    mov     dl, 0h    int     10h; 4) Load system module at 0x10000load_system:    mov     ax, SYSSEG    mov     es, ax    mov     dx, 0000h       ; dx    = driver(dh)/head(dl)    mov     cx, 0006h       ; cx    = track(ch)/sector(cl)    mov     bx, 00h         ; es:bx = target(es=1000h,bx=0)    mov     ah, 02h         ; ah    = service id(ah=02 means read)    mov     al, 01h         ; al    = number of sectors to read(al)    int     13h    jnc     ok_load_system    xor     dl, dl          ; reset floppy and retry if failed    xor     ah, ah    int     13h    jmp     load_system; 5) Jump to setupok_load_system:    jmp     SETUPSEG:0h; ############################;           Message; ############################msg1:           db  13,10               ; CRLF    db  "Loading system..."    db  13,10               ; CRLFtimes   510-($-$$)      db  0    dw      0xaa55

为什么在bootsect默认保留一些字节?因为只有bootsect的长度是确定的512字节,在末尾保留一些“全局变量”的话,其他模块很容易就能访问到。

4.2 第二阶段引导:setup.asm

setup.asm看似很长,其实都是“见怪不怪”的代码了。开头部分的段描述符的宏定义和属性的常量定义,后面就是进入保护模式的常规代码了,就是这么简单。为了简化,跳转system前直接将ss置为00000h地址处。尽管如此,这段代码却耗费了我大量时间去调错,那究竟了哪里容易出问题呢?

; ############################;   Constants & Macro; ############################; 描述符类型DA_32       equ 4000h   ; 32 位段DA_LIMIT_4K equ 8000h   ; 段界限粒度为 4K 字节; 存储段描述符类型DA_DR       equ 90h ; 存在的只读数据段类型值DA_DRW      equ 92h ; 存在的可读写数据段属性值DA_C        equ 98h ; 存在的只执行代码段属性值DA_CR       equ 9Ah ; 存在的可执行可读代码段属性值; Descriptor macro%macro Descriptor 3    dw  %2 & 0FFFFh                         ; Limit 1    dw  %1 & 0FFFFh                         ; Base addr 1    db  (%1 >> 16) & 0FFh                   ; Base addr 2    dw  ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh) ; Attr 1 + Limit 2 + Attr 2    db  (%1 >> 24) & 0FFh                   ; Base addr 3%endmacro; ############################;   Booting Process; ############################[SECTION .s16][BITS   16]LABEL_BEGIN:; 1) Enter protection mode    mov     ax, cs    mov     ds, ax    mov     es, ax    mov     ss, ax    mov     sp, 0100h    ; 1.1) Init descriptor    xor     eax, eax    mov     ax, cs    shl     eax, 4    add     eax, LABEL_SEG_CODE32    mov     word [LABEL_DESC_CODE32 + 2], ax    shr     eax, 16    mov     byte [LABEL_DESC_CODE32 + 4], al    mov     byte [LABEL_DESC_CODE32 + 7], ah    ; 1.2) Load gdt to gdtr    xor     eax, eax    mov     ax, ds    shl     eax, 4    add     eax, LABEL_GDT          ; eax <- gdt base addr    mov     dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt base addr    lgdt    [GdtPtr]    ; 1.3) Disable interrupt    cli    ; 1.4) Enable A20 addr line    in      al, 92h    or      al, 00000010b    out     92h, al    ; 1.5) Set PE in cr0    mov     eax, cr0    or      eax, 1    mov     cr0, eax    ; 1.6) Jump to protective mode!    jmp     dword SelectorCode32:0  ; SelectorCode32 (LABEL_SEG_CODE32:0)[SECTION .s32]ALIGN   32[BITS   32]LABEL_SEG_CODE32:; 2) Jump to system    mov     ax, SelectorData    mov     ds, ax    mov     es, ax    mov     ss, ax    mov     esp, 1000h    jmp     SelectorSystem:0SegCode32Len equ $ - LABEL_SEG_CODE32[SECTION .gdt];                            Base Addr,        Limit,   AttributeLABEL_GDT:          Descriptor       0,            0, 0LABEL_DESC_CODE32:  Descriptor       0, SegCode32Len, DA_CR  | DA_32 | DA_LIMIT_4KLABEL_DESC_SYSTEM:  Descriptor  10000h,       0ffffh, DA_CR  | DA_32 | DA_LIMIT_4KLABEL_DESC_DATA:    Descriptor   0,       0ffffh, DA_DRW | DA_32 | DA_LIMIT_4KLABEL_DESC_VIDEO:   Descriptor 0B8000h,       0ffffh, DA_DRWGdtLen      equ $ - LABEL_GDTGdtPtr      dw  GdtLen - 1                  ; GDT limit            dd  0                           ; GDT base addrSelectorCode32      equ LABEL_DESC_CODE32 - LABEL_GDTSelectorSystem      equ LABEL_DESC_SYSTEM - LABEL_GDTSelectorData        equ LABEL_DESC_DATA - LABEL_GDTSelectorVideo       equ LABEL_DESC_VIDEO - LABEL_GDT

此外,SegCode32Len equ $ - LABEL_SEG_CODE32一定要放到32位代码段的末尾,因为这个长度会设置到32位代码段的描述符中的Limit字段上。如果执行长度超过Limit的话,Bochs同样会没有任何提示的崩溃。

4.3 内核空壳:main.c

一路磕磕绊绊,我们终于进入了32位保护模式下的内核代码。现阶段我们在内核中还做不了什么,但这一路辛苦走下来,到这里还是应该尝点甜头儿的。那我们就在内核代码中用C程序调用汇编代码,输出一段话吧。我们先看源代码,后面会详细解释一下为什么要混合使用C和汇编,以及源代码中的细节问题。

4.3.1 源代码

从C程序调用汇编代码其实非常简单,我们并不用关注myprint()到底是什么语言实现的,只要这个函数原型使我们产生的代码能够与myprint()的实现一起工作就可以了。此外,编译myprint.asm的时候要注意不能编译成默认的BIN文件,而要编译为ELF格式产生可重定位的相关信息,这样链接器才能将其与main.o正确地链接到一起。注意参数msg的类型要声明为const char *msg,避免编译时的警告。

// main.cvoid myprint(const char *msg, int len);int main(int argc, char const *argv[]){    myprint("Hello, MiniOS!\n", 15);    return 0;}

我们在.loop循环外初始化循环中要用到的变量:

  • gs:显存映射空间的选择符,值32对应我们在setup.asm中建立的GDT中的SelectorVideo
  • ah:字符串的属性,我们一会儿在Bochs中打印内存时能看到它
  • ebx:循环中要递增的列值
  • ecx:循环中要递减的循环次数计数器,对应C程序传入的参数len
  • edx:循环中要递增的字符串首地址,对应C程序传入的参数msg。esp相当于一个二级指针,[esp+4]得到的只是msg,[msg]得到才是第一个字符’H’。所以说,函数原型(char *msg)是与汇编代码([esp+4])一一对应的,从此处也能一瞥C语言的灵活和强大。
; myprint.asm[section .data][section .text]global myprintmyprint:    mov     ax, 32          ; SelectorVideo    mov     gs, ax    mov     ah, 0Ch         ; attr    mov     ebx, 0          ; col    mov     ecx, [esp+8]    ; msg len    mov     edx, [esp+4]    ; msg addr.loop:    ;(80 * row + col) * 2    mov     edi, ebx    add     edi, (80 * 20)      imul    edi, 2    mov     al, byte [edx]    mov     [gs:edi], ax    inc     ebx    dec     ecx    inc     edx    cmp     ecx, 0h    jne     .loop    jmp     $

4.3.2 反汇编

Makefile中已经有了三个反汇编的target,分别利用ndisasm和objdump工具反汇编bootsect、setup和system。这里我们反汇编一下system,看一下我们的C和汇编混合代码是如何链接的,链接后又是什么样子?不要被细节干扰,抓住几个关键点就可以了,如果相关知识忘记了的话可以参考《六星经典CSAPP-笔记(3)程序的机器级表示》:

  • a~e:被调用函数的惯例,保存ebp挪动esp分配栈空间
  • 11~19:保存两个函数参数msg(0x00010076指向的就是字符串的首地址)和len(0xf=15)到栈上
  • 20:调用汇编代码中的函数myprint(),call指令自动将25行的地址压入栈上作为返回地址后跳转,call的操作数0x1b加上IP指向的0x25等于0x40,恰好是myprint()在汇编代码中的起始地址。因为call压栈挪动了esp,所以两个函数参数的位置也就从(%esp)和0x4(%esp)变成了0x4(%esp)和0x8(%esp)了
[root@localhost minios]# make disasm-sysnasm -f elf -o system/init/myprint.o system/init/myprint.asmld -Ttext 10000 -e main --oformat binary -s -x -M \    system/init/main.o \    system/init/myprint.o \    -o system/system > System.mapobjdump -b binary -m i386 -D system/systemsystem/system:     file format binaryDisassembly of section .data:0000000000000000 <.data>:   0:   8d 4c 24 04             lea    0x4(%esp),%ecx   4:   83 e4 f0                and    $0xfffffff0,%esp   7:   ff 71 fc                pushl  0xfffffffc(%ecx)   a:   55                      push   %ebp   b:   89 e5                   mov    %esp,%ebp   d:   51                      push   %ecx   e:   83 ec 14                sub    $0x14,%esp  11:   c7 44 24 04 0f 00 00    movl   $0xf,0x4(%esp)  18:   00   19:   c7 04 24 76 00 01 00    movl   $0x10076,(%esp)  20:   e8 1b 00 00 00          call   0x40  25:   b8 00 00 00 00          mov    $0x0,%eax    ...    40:   66 b8 20 00             mov    $0x20,%ax  44:   8e e8                   movl   %eax,%gs  46:   b4 0c                   mov    $0xc,%ah  48:   bb 00 00 00 00          mov    $0x0,%ebx  4d:   8b 4c 24 08             mov    0x8(%esp),%ecx  51:   8b 54 24 04             mov    0x4(%esp),%edx  55:   89 df                   mov    %ebx,%edi  57:   81 c7 40 06 00 00       add    $0x640,%edi  5d:   69 ff 02 00 00 00       imul   $0x2,%edi,%edi  63:   8a 02                   mov    (%edx),%al  65:   65 66 89 07             mov    %ax,%gs:(%edi)  69:   43                      inc    %ebx  6a:   49                      dec    %ecx  6b:   42                      inc    %edx  6c:   81 f9 00 00 00 00       cmp    $0x0,%ecx  72:   75 e1                   jne    0x55  74:   eb fe                   jmp    0x74  76:   48                        77:   65                        78:   6c                        79:   6c                        7a:   6f                        7b:   2c 20                     7d:   4d                        7e:   69 6e 69 4f 53 21 0a        ...

4.3.3 源码解释

关于内核雏形部分的代码看起来不那么直接,直接写个C程序的main方法不就可以了吗?但这都是有原因的,解释:

  1. 为何要输出字符串?因为我们想直观地看到内核确实可以正常工作,体现出我们的劳动成果
  2. 为何要混合编程?既然要输出点什么,而这又是我们自己的操作系统内核,像printf这种系统调用我们还没有实现,所以只能靠底层的汇编代码去输出字符串。这里为了简化,我们在汇编中没有遵守正常的规则,例如保存ebp、挪动esp分配栈等
  3. **为何不直接在C中嵌入NASM代码?**C语言支持直接嵌入汇编代码,但不支持NASM语法的汇编代码,如果我们不想再学习另一种汇编语法的话,就只能单独拆分出一个asm文件
  4. 为何不直接用int 0x10中断?因为BIOS中断在保护模式下是不能用的,而我们进入保护模式后只是设置了栈的段寄存器ss,还有很多工作没做,这里为了避免出错所以用直接写显存在内存映射地址空间的方式
  5. 如何写显存映射空间?显存在内存中映射空间的起始地址是0B8000h,每行80个字符共35行。每个字符占用2个字节,第一个字节是字符的ASCII码,第二个字节是属性。例如第12行的最后一个字符在内存中的位置就是(80 * 11 + 79) * 2。为了简化我们直接将字符串输出到第21行,而没有读当前光标位置

4.3.4 测试

下面就测试一下我们的代码是否正确,说起来这还是我们写的第一个比较复杂的汇编代码,学习了如何在汇编语言中实现高级语言中的循环。一直执行到循环后的jmp $一行,通过Bochs就能看到输出。如果环境没有图形界面而安装Bochs时没有安装GUI也没有关系,打印一下0xB8B40内存位置就能跳过Bochs前面的输出,看到我们刚刚输出的内容了。每个字符后面跟着的\7和\x0C就是该字符的属性了

(0) [0x0000000000010072] 0010:00000072 (unk. ctxt): jnz .-31 (0x00010055)     ; 75e1<bochs:161> Next at t=13185655(0) [0x0000000000010074] 0010:00000074 (unk. ctxt): jmp .-2 (0x00010074)      ; ebfe<bochs:162> xp /500bc 0x0B8B40[bochs]:0x000b8b40 <bogus+       0>:  L   \7    o   \7    a   \7    d   \7  0x000b8b48 <bogus+       8>:  i   \7    n   \7    g   \7        \7  0x000b8b50 <bogus+      16>:  s   \7    y   \7    s   \7    t   \7  0x000b8b58 <bogus+      24>:  e   \7    m   \7    .   \7    .   \7  0x000b8b60 <bogus+      32>:  .   \7        \7        \7        \7  0x000b8b68 <bogus+      40>:      \7        \7        \7        \7      ...0x000b8c80 <bogus+     320>:  H   \x0C  e   \x0C  l   \x0C  l   \x0C0x000b8c88 <bogus+     328>:  o   \x0C  ,   \x0C      \x0C  M   \x0C0x000b8c90 <bogus+     336>:  i   \x0C  n   \x0C  i   \x0C  O   \x0C0x000b8c98 <bogus+     344>:  S   \x0C  !   \x0C \x0A \x0C      \7  

5.总结

现在回头想想,似乎找到了当时阅读《Orange’s》一书时,进行到第五章就没有继续下去的原因:一是第三章“保护模式”过早地交代了保护模式的方方面面。尽管搭配了很多汇编代码丰富了示例,但也有些扰乱了学习进程,其实这一章是可以分成两部分,像中断、特权级、分页等一半的知识是可以在学会如何加载Kernel后再了解的;二是FAT和FreeDOS的引入又进一步干扰了进度。如果你只是把它当作未来Kernel的管理工具那还好,要是像我容易刨根问底的话可能就卡在那里了。其实像《30天自制操作系统》一书,不出几章很早就学完了Kernel的引导。在这一点上,《Orange’s》的作者比较细致,但也略显罗嗦。

5.1 引导过程小结

这里整理一下Orange’s与Linux 0.11对内存使用上的差异,以及各自比较麻烦的地方。

5.1.1 Orange’s引导过程

Orange’s的引导工作都是在两阶段引导程序boot和loader中完成的。整个引导过程还是比较清晰的,但有几个地方很麻烦:一是解析FAT文件系统格式将内核读取到内存;二是解析内核ELF文件头将内核的代码部分加载到指定位置。关于问题一之前也提到过,Orange’s作者使用FAT软盘和FreeDOS来方便调试。

  1. boot:从FAT格式软盘加载loader,打印一些信息后跳转
  2. loader:从FAT格式软盘加载内核,进入保护模式后准备GDT/IDT和PDT、栈段寄存器ss等后,跳转到内核

所以Orange’s的内存使用情况如下图所示:boot始终在7c00h,kernel和loader的位置也固定在30000h和90000h,PDT被放置在了高地址100000h上。下面就看一下Linux 0.11的引导过程是什么样?它是如何使用内存空间的?

    ;              ┃                                    ┃    ;              ┃                 .                  ┃    ;              ┃                 .                  ┃    ;              ┃                 .                  ┃    ;              ┣━━━━━━━━━━━━━━━━━━┫    ;              ┃■■■■■■■■■■■■■■■■■■┃    ;              ┃■■■■■■Page  Tables■■■■■■┃    ;              ┃■■■■■(大小由LOADER决定)■■■■┃    ;    00101000h ┃■■■■■■■■■■■■■■■■■■┃ PageTblBase    ;              ┣━━━━━━━━━━━━━━━━━━┫    ;              ┃■■■■■■■■■■■■■■■■■■┃    ;    00100000h ┃■■■■Page Directory Table■■■■┃ PageDirBase  <- 1M    ;              ┣━━━━━━━━━━━━━━━━━━┫    ;              ┃□□□□□□□□□□□□□□□□□□┃    ;       F0000h ┃□□□□□□□System ROM□□□□□□┃    ;              ┣━━━━━━━━━━━━━━━━━━┫    ;              ┃□□□□□□□□□□□□□□□□□□┃    ;       E0000h ┃□□□□Expansion of system ROM □□┃    ;              ┣━━━━━━━━━━━━━━━━━━┫    ;              ┃□□□□□□□□□□□□□□□□□□┃    ;       C0000h ┃□□□Reserved for ROM expansion□□┃    ;              ┣━━━━━━━━━━━━━━━━━━┫    ;              ┃□□□□□□□□□□□□□□□□□□┃ B8000h ← gs    ;       A0000h ┃□□□Display adapter reserved□□□┃    ;              ┣━━━━━━━━━━━━━━━━━━┫    ;              ┃□□□□□□□□□□□□□□□□□□┃    ;       9FC00h ┃□□extended BIOS data area (EBDA)□┃    ;              ┣━━━━━━━━━━━━━━━━━━┫    ;              ┃■■■■■■■■■■■■■■■■■■┃    ;       90000h ┃■■■■■■■LOADER.BIN■■■■■■┃ somewhere in LOADER ← esp    ;              ┣━━━━━━━━━━━━━━━━━━┫    ;              ┃■■■■■■■■■■■■■■■■■■┃    ;       80000h ┃■■■■■■■KERNEL.BIN■■■■■■┃    ;              ┣━━━━━━━━━━━━━━━━━━┫    ;              ┃■■■■■■■■■■■■■■■■■■┃    ;       30000h ┃■■■■■■■■KERNEL■■■■■■■┃ 30400h ← KERNEL 入口 (KernelEntryPointPhyAddr)    ;              ┣━━━━━━━━━━━━━━━━━━┫    ;              ┃                                    ┃    ;        7E00h ┃              F  R  E  E            ┃    ;              ┣━━━━━━━━━━━━━━━━━━┫    ;              ┃■■■■■■■■■■■■■■■■■■┃    ;        7C00h ┃■■■■■■BOOT  SECTOR■■■■■■┃    ;              ┣━━━━━━━━━━━━━━━━━━┫    ;              ┃                                    ┃    ;         500h ┃              F  R  E  E            ┃    ;              ┣━━━━━━━━━━━━━━━━━━┫    ;              ┃□□□□□□□□□□□□□□□□□□┃    ;         400h ┃□□□□ROM BIOS parameter area □□┃    ;              ┣━━━━━━━━━━━━━━━━━━┫    ;              ┃◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇◇┃    ;           0h ┃◇◇◇◇◇◇Int  Vectors◇◇◇◇◇◇┃    ;              ┗━━━━━━━━━━━━━━━━━━┛ ← cs, ds, es, fs, ss    ;    ;    ;       ┏━━━┓       ┏━━━┓    ;       ┃■■■┃ 我们使用  ┃□□□┃ 不能使用的内存    ;       ┗━━━┛       ┗━━━┛    ;       ┏━━━┓       ┏━━━┓    ;       ┃      ┃ 未使用空间  ┃◇◇◇┃ 可以覆盖的内存    ;       ┗━━━┛       ┗━━━┛

5.1.2 Linux 0.11引导过程

Linux引导程序的第一阶段与Orange’s基本一致,但第二阶段要复杂一些,主要区别在于:Linux将Orange’s的loader进入保护模式后的内核准备工作拆分到一个单独的程序head中。具体来说,Linux的system的头部其实一个汇编程序head.s编译成的,system中head后面的部分才是真正的kernel。head中负责初始化PDT、重新放置GDT/IDT等工作。为了节约内存,PDT、GDT等数据会覆盖掉head代码,所以这部分代码的技巧性很高。

5.1.3 我的MiniOS

从上面的分析能够看出,我们目前的引导过程类似Orange’s的经典两阶段引导,但本质(比如命名、放置位置、软盘上程序的加载方式等)上都是借鉴Linux的。所以前面说到的Orange’s的麻烦之处我们也巧妙地避开了:

  • 关于Orange’s的第一点因为我们的bootsect和setup是连续放置的所以省了;
  • 关于第二点可以看一下我们的Makefile,因为链接时丢掉了ELF各种符号表等信息,直接得到了纯二进制文件,并且在软盘上system也紧接着setup,所以第二点也可以省了。

接下来对引导和内核程序的完善过程中,我们还是更倾向于Linux,从setup中拆分出head,这样做的好处就是:内核程序以00000h为入口地址,最终进入内核时GDT、IDT、PDT等系统数据都位于00000h后的低地址,整体结构非常清晰。相比之下,Orange’s尽管引导过程能简单一些,但内核代码和系统数据比较零散,不够清晰。

  1. bootsect
    1.1 移动自己:将自己从7c00h拷贝到90000h。因为在2.1中bootsect区域会被用来保存BIOS信息,而2.2中system会被拷贝到00000h,所以为了避免bootsect中的信息被system覆盖掉,最好现在就挪到90000h高地址
    1.2 加载setup:加载setup到90200h
    1.3 显示信息:输出”Loading system…”
    1.4 加载system:根据SYSSIZE加载system到10000h
    1.5 跳转:跳转到setup继续执行
  2. setup
    2.1 读取内存大小:从BIOS读取内存等信息覆盖到90000h~901ffh,即bootsect的位置。因为bootsect中包含SYSSIZE,所以必须在bootsect中加载system,不能等到setup中再做
    2.2 移动system:BIOS中断已经使用完毕,可以将system拷贝到00000h覆盖掉BIOS中断表了
    2.3 进入保护模式:设置GDTR、A20、CR0进入保护模式
    2.4 跳转:跳转到system继续执行
  3. system(head)
    3.1 重新放置GDT:包括GDT/IDT,setup中的GDT是用于进入保护模式的临时GDT
    3.2 初始化PDT:根据内存大小确定PDT大小
    3.3 跳转:进入到main()函数,进入真正的内核

从上面的引导过程,也让我们清晰地了解了为什么Linux 0.11会在内存中频繁的挪动代码——bootsect拷贝自己到90000h,setup挪动system到00000h——这两次挪动的原因以及为什么system不能等到setup读取完BIOS后直接加载到00000h的原因都在上面给出了解答。

2 0
原创粉丝点击