操作系统内核Hack:(三)BootLoader制作

操作系统内核Hack:(三)BootLoader制作

关于本文涉及到的完整源码请参考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 编译内核

[email protected] ~/Source $ git clone https://github.com/yuanxinyu/Linux-0.11
[email protected] ~/Source $ cd Linux-0.11

[email protected]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 files

Note!:
     * 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 :-)>>>

[email protected] ~/Source/Linux-0.11 $ sudo apt-get install -y ctags cscope graphviz qemu
[email protected] ~/Source/Linux-0.11 $ make

1.2.2 下载硬盘IMG

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

[email protected] ~/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
└── tools

8 directories, 10 files
[email protected] ~/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 C900

Booting from Floppy...

Loading system...

Partition table ok.
43012/62000 free blocks
19719/20666 free inodes
3446 buffers = 3528704 bytes buffer space
Free mem: 12574720 bytes
 Ok.
[/usr/root]# ls
README      hello       mtools.howto        shoelace.tar.Z
gcclib140   hello.c     shoe

2.文件目录组织

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

[email protected] ~/Workspace/syspace/1-assembly $ tree -d minios/
minios/
├── boot1
│   └── bootsect.s
├── boot2
│   └── setup.s
├── Makefile
├── system
│   ├── fs
│   ├── include
│   ├── init
│   ├── kernel
│   ├── lib
│   └── mm
└── tools
    └── build.c

10 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      = nasm
ASFLAGS = -f elf
CC      = gcc
CFLAGS  = -Wall -O
LD      = 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 [email protected] $<

#%.o:   %.asm
#   $(AS) $(ASFLAGS) -o [email protected] $<

#################
#   Default
#################

all:    Image

Image:  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.bin

tools/build:    tools/build.c
    $(CC) $(CFLAGS) -o [email protected] $<

# SYSSIZE= number of clicks (16 bytes) to be loaded
boot1/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 [email protected] tmp.asm
    rm -f tmp.asm

boot2/setup:    boot2/setup.asm
    $(AS) -o [email protected] $<

system/system:  system/init/main.o system/init/myprint.o
    $(LD) $(LDFLAGS)     system/init/main.o     system/init/myprint.o     -o [email protected] > System.map

system/init/main.o: system/init/main.c

system/init/myprint.o: system/init/myprint.asm
    $(AS) $(ASFLAGS) -o [email protected] $<

#################
# Create floppy
#################

disk:
    bximage -q -fd -size=1.44 Image

.PHONY: disk

#################
#   Start Vm
#################

start:  Image
    bochs -q -f bochsrc

qemu:   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好多了!

[email protected] ~/Workspace/syspace/1-assembly/minios $ objdump -D -b binary -m i386 Image 

Image:     file format binary

Disassembly 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
    ...

[email protected] ~/Workspace/syspace/1-assembly/minios $ ndisasm -o 0x7c00 Image
00007C00  B8C007            mov ax,0x7c0
00007C03  8ED8              mov ds,ax
00007C05  B80090            mov ax,0x9000
00007C08  8EC0              mov es,ax
00007C0A  B90001            mov cx,0x100
00007C0D  31F6              xor si,si
00007C0F  31FF              xor di,di
00007C11  F3A5              rep movsw
00007C13  EA18000090        jmp word 0x9000:0x18
00007C18  8CC8              mov ax,cs
00007C1A  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    0x90
0x00007c08 <bogus+       8>:    0x8e    0xc0    0xb9    0x00    0x01    0x31    0xf6    0x31
0x00007c10 <bogus+      16>:    0xff    0xf3    0xa5    0xea    0x18    0x00    0x00    0x90
0x00007c18 <bogus+      24>:    0x8c    0xc8    0x8e    0xd8    0x8e    0xc0    0x8e    0xd0
0x00007c20 <bogus+      32>:    0xbc    0x00    0xff    0xba    0x00    0x00    0xb9    0x02
<bochs:6> u 0x7c00 0x7c10
00007c00: (                    ): mov ax, 0x07c0            ; b8c007
00007c03: (                    ): mov ds, ax                ; 8ed8
00007c05: (                    ): mov ax, 0x9000            ; b80090
00007c08: (                    ): mov es, ax                ; 8ec0
00007c0a: (                    ): mov cx, 0x0100            ; b90001
00007c0d: (                    ): xor si, si                ; 31f6
00007c0f: (                    ): 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 4
BOOTSEG     equ 0x07c0
INITSEG     equ 0x9000
SETUPSEG    equ 0x9020
SYSSEG      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 0x90200
load_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 message
ok_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 0x10000
load_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 setup
ok_load_system:
    jmp     SETUPSEG:0h

; ############################
;           Message
; ############################

msg1:
    db  13,10               ; CRLF
    db  "Loading system..."
    db  13,10               ; CRLF

times   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:0

SegCode32Len equ $ - LABEL_SEG_CODE32

[SECTION .gdt]
;                            Base Addr,        Limit,   Attribute
LABEL_GDT:          Descriptor       0,            0, 0
LABEL_DESC_CODE32:  Descriptor       0, SegCode32Len, DA_CR  | DA_32 | DA_LIMIT_4K
LABEL_DESC_SYSTEM:  Descriptor  10000h,       0ffffh, DA_CR  | DA_32 | DA_LIMIT_4K
LABEL_DESC_DATA:    Descriptor   0,       0ffffh, DA_DRW | DA_32 | DA_LIMIT_4K
LABEL_DESC_VIDEO:   Descriptor 0B8000h,       0ffffh, DA_DRW

GdtLen      equ $ - LABEL_GDT
GdtPtr      dw  GdtLen - 1                  ; GDT limit
            dd  0                           ; GDT base addr

SelectorCode32      equ LABEL_DESC_CODE32 - LABEL_GDT
SelectorSystem      equ LABEL_DESC_SYSTEM - LABEL_GDT
SelectorData        equ LABEL_DESC_DATA - LABEL_GDT
SelectorVideo       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.c
void 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 myprint

myprint:
    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-sys
nasm -f elf -o system/init/myprint.o system/init/myprint.asm
ld -Ttext 10000 -e main --oformat binary -s -x -M     system/init/main.o     system/init/myprint.o     -o system/system > System.map
objdump -b binary -m i386 -D system/system

system/system:     file format binary

Disassembly 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   \x0C
0x000b8c88 <bogus+     328>:  o   \x0C  ,   \x0C      \x0C  M   \x0C
0x000b8c90 <bogus+     336>:  i   \x0C  n   \x0C  i   \x0C  O   \x0C
0x000b8c98 <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的原因都在上面给出了解答。

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-10-17 22:50:20

操作系统内核Hack:(三)BootLoader制作的相关文章

操作系统内核Hack:(一)实验环境搭建

操作系统内核Hack:(一)实验环境搭建 三四年前,心血来潮,入手<Orange's:一个操作系统的实现>学习操作系统内核,还配套买了王爽的<汇编语言(第二版)>和<80X86汇编语言程序设计教程>,虽然Orang's只看了不到三分之一,但当时还是很认真的,练习也做了不少.唯一遗憾的就是没有留下文字记录,导致现在忘得差不多一干二净了,后悔不已!如今想再捡起来,弥补当时的懒惰,虽然困难重重,但这么优秀的国产书怎么能看完就算了呢!而且当年还是在Windows下练习的,现在终

操作系统内核的绝佳学习材料——JOS

操作系统内核的绝佳学习材料--JOS 前言:关于JOS和一些经验之谈 这一学期的操作系统课使用的是MIT用于教学的JOS操作系统,并且StonyBrook在其基础上做了大量改动,最重要的变化就是从32位移植到了64位.因为个人之前曾系统学习过Linux 0.11内核(<操作系统内核Hack:(四)内核雏形>,实现到时钟中断部分停下了),深知自己从零开始实现内核的工作量.即便是如我个人实现的MiniOS这种简单的不能再简单的,也是需要花费很多时间和精力的.虽然这些付出非常值得(在上这门课时给我带

AACOS:基于编译器和操作系统内核的算法设计与实现

AACOS:基于编译器和操作系统内核的算法设计与实现 [计算机科学技术] 谢晓啸 湖北省沙市中学 [关键词]: 编译原理,操作系统内核实现,算法与数据结构,算法优化 0.索引 1.引论 1.1研究内容 1.2研究目的 1.3研究提要 正文 2.1研究方法 2.2编译器部分 2.2.1从计算器程序中得到的编译器制作启示 2.2.2在编译器中其它具体代码的实现 2.2.3编译器中栈的高级应用 2.2.3编译器中树的高级应用 2.2.4编译器与有限状态机 2.3操作系统内核部分 2.3.1操作系统与底

从操作系统内核看Java非阻塞IO事件检测

非阻塞服务器模型最重要的一个特点是,在调用读取或写入接口后立即返回,而不会进入阻塞状态.在探讨单线程非阻塞IO模型前必须要先了解非阻塞情况下Socket事件的检测机制,因为对于非阻塞模式最重要的事情是检测哪些连接有感兴趣的事件发生,一般会有如下三种检测方式. 应用程序遍历socket检测 如图所示,当多个客户端向服务器请求时,服务器端会保存一个socket连接列表,应用层线程对socket列表进行轮询尝试读取或写入.对于读取操作,如果成功读取到若干数据则对读取到的数据进行处理,读取失败则下个循环

RFID标签天线的三种制作方法

在RFID标签中,天线层是主要的功能层,其目标是传输最大的能量进出标签芯片.RFID天线是按照射频识别所要求的功能而设计的电子线路,将导电银浆或导电碳浆网印在PVC.PC或PET上,再与面层.保护层和底层等合成的.RFID标签天线的制印质量是RFID制造过程中需要控制的关键问题.天线的制作方法常见的有蚀刻法.烫印法和导电油墨印刷法.下面简单介绍这三种作用方法的特点和操作技术要领. 1 蚀刻法 天线在蚀刻前应先印刷上抗蚀膜,首先将PET薄膜片材两面覆上金属(如铜.铝等)箔,然后采用印刷法(网印.凹

用java做操作系统内核:软盘读写

在前两节,我们将一段代码通过软盘加载到了系统内存中,并指示cpu执行加入到内存的代码,事实上,操作系统内核加载也是这么做的.只不过我们加载的代码,最大只能512 byte, 一个操作系统内核,少说也要几百兆,由此,系统内核不可能直接从软盘读入系统内存. 通常的做法是,被加载进内存的512 Byte程序,实际上是一个内核加载器,它运行起来后,通过读取磁盘,将存储在磁盘上的内核代码加载到指定的内存空间,然后再把cpu的控制权提交给加载进来的系统内核. 这就需要我们理解软盘的物理结构,以及软盘的数据读

Office操作:Word一分钟制作表格

表格在我们的日常工作中是必不可少的,Word对表格的制作提供了很好的支持,那么如何在Word上快速建立一个合适的表格呢?表格制作又需要涉及到哪些技巧呢?这里和大家一同了解表格制作的步骤.只需要通过简单的几个步骤即可实现表格的插入操作: 表格制作的方法: 第一种方法适合制作最简单的表格: 首先打开word文档,直接点击"插入"菜单下方的"表格".第一个插入表格,鼠标放下去自然就会出现表格,按需要选择行列的数量便可以得到自己想要的表格了. Office操作:Word一分

Unix操作系统内核结构报告

Unix操作系统内核结构报告 1.有一个程序的代码如下: main() { int i ; for(i=0; i<3; i++) fork(); } 请问该程序运行时共建立了多少个进程?请用进程家族树来画出父子进程之间的关系. 解:一共建立了7个进程. 2.UNIX 系统中用“最近最少使用(LRU)” 算法来构建数据缓冲池.如果核心采用“先进先出(FIFO)”算法来构建缓冲池,则对缓冲区算法 getblk 来说,会造成功能上的区别主要有哪些? 解:getblk是把缓冲区分配给磁盘块的一个算法.用

操作系统是如何工作的————一个精简的操作系统内核(20135304 刘世鹏)

操作系统是如何工作的————一个精简的操作系统内核 作者:20135304 刘世鹏 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 实验过程 使用实验楼虚拟机打开shell,加载实验所需linux内核,执行搭建好的系统 cd LinuxKernel/linux-3.9.4 qemu -kernel arch/x86/boot/bzImage 一直在执行mystartkernel,交替执行