操作系统内核Hack:(四)内核雏形

操作系统内核Hack:(四)内核雏形

在本系列的前一篇文章《操作系统内核Hack:(三)BootLoader制作》中,我们制作出了一个两阶段引导BootLoader,并进入了一个内核的空壳main函数。本文我们继续完善引导程序和内核,让内核的内容一点点充实起来。本文的代码可以参考GitHub上的MiniOS分支kernel_prototype。

1.周边代码修改

1.1 常量和宏提取

像各个模块的内存这种常量,会经常被引导,所以就提取出一个单独的文件var.inc。同理,保护模式相关的常量和宏都提取到了pm.inc,这里主要是拷贝了Orange’s的代码和注释。

;   var.inc
; ############################ ; Constants ; ############################

SETUPLEN    equ 4
BOOTSEG     equ 0x07c0
INITSEG     equ 0x9000
SETUPSEG    equ 0x9020
SYSSEG      equ 0x1000
NEWSYSSEG   equ 0x0000

MEMSIZE     equ 0       ; INITSEG:MEMSIZE


;   pm.inc
; ############################ ; Macros ; ############################

; 描述符类型
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

1.2 Makefile修改

因为include/的提取和后面第二阶段引导过程拆分的影响,所以Makefile也要做相应的修改:

  • 新加编译参数ASINC,编译bootsect.asm、setup.asm和head.asm时都要用到
  • 链接参数LDFLAGS中的entry地址改为startup_32,这也是head.asm中的代码起始地址
  • SYSSIZE不再除以16,而是system的真实大小,后面bootsect.asm中会用到
ASINC   = -I include/
ASFLAGS = -f elf
LD  = ld
# -Ttext org -e entry -s(omit all symbol info)
# -x(discard all local symbols) -M(print memory map)
LDFLAGS = -Ttext 0 -e startup_32 --oformat binary -s -x -M

    ...

# SYSSIZE = system file size
boot1/bootsect: boot1/bootsect.asm include/var.inc system/system
    (echo -n "SYSSIZE equ ";ls -l system/system | grep system \
        | cut -d " " -f 5 | tr '\012' ' ') > tmp.asm
    cat $< >> tmp.asm
    $(AS) $(ASINC) -o $@ tmp.asm
    rm -f tmp.asm

boot2/setup:    boot2/setup.asm include/var.inc include/pm.inc
    $(AS) $(ASINC) -o $@ $<

system/system:  system/init/head.o system/init/main.o
    $(LD) $(LDFLAGS) \
    system/init/head.o \
    system/init/main.o \
    -o $@ > System.map

system/init/head.o:     system/init/head.asm include/var.inc include/pm.inc
    $(AS) $(ASFLAGS) $(ASINC) -o $@ $<

    ...

2.第一阶段引导完善

2.1 system加载

之前为了简化代码,避免被细枝末节干扰,所以从软盘加载system到内存的代码写的非常简单,只读取了一个扇区。随着我们的system模块越来越大,这样是肯定不行的,于是就想参考Linux源码做一下改进。

结果不看不知道,一细研究还真吓一跳每个磁道有18个扇区,每个磁头有80个磁道,根据要加载的数据大小,自己负责切换扇区、磁道、磁头号。就这样还不行,要加载到的内存位置是用[es:bx]表示的,也就是说bx偏移逐渐增加最后要溢出的时候,我们还得修改es重置bx,避免它溢出。原来想写一段通用的从软盘加载数据的代码这么费劲啊!先看看我写的,因为之前已经加载过bootsect和setup了,所以第一个磁道还需读取13个扇区,之后的每个磁道读取18个扇区,最后一个磁道根据还剩余多少扇区没读决定要读取多少,先不考虑磁头和bx溢出问题。结果已经来回调试改进了好几遍了,还是有问题。

; 4) Load system module at 0x10000
; Assume SYSSIZE < 1 head
; 1 track = 18 sectors * 512b = 9216(b)
; 1 head = 80 tracks * 9216 = 720(kb)
_Sector:        db 0
_Track:         db 0
Sector          equ _Sector-$$
Track           equ _Track-$$
SECT_PER_TRACK  equ 18
LEFT_IN_TRACK1  equ SECT_PER_TRACK - 1 - SETUPLEN

load_system:
        mov     ax, SYSSEG
        mov     es, ax
        mov     bx, 0000h               ; es:bx = target(es=1000h,bx=0)
        mov     dx, 0000h               ; dx = driver(dh)/head(dl)
        mov     cx, 0006h               ; cx = track(ch)/sector(cl)

        mov     ax, SYSSIZE
        add     ax, 511
        shr     ax, 9                   ; al = (SYSSIZE + 511) / 512, sectors to read
        mov     byte [Sector], al

        cmp     al, LEFT_IN_TRACK1
        jbe     .loop
        mov     al, LEFT_IN_TRACK1      ; al = (al <= 13) ? al : 13
.loop
        mov     ah, 02h                 ; ah = service id(ah=02 means read)
        int     13h                     ; ignore any error

        sub     byte [Sector], al       ; remainingSector -= al
        cmp     byte [Sector], 0
        je      ok_load_system

        xor     ah, ah
        shl     ax, 9
        add     bx, ax                  ; offset += (al * 512)

        add     byte [Track], 1
        mov     ch, byte [Track]        ; track++
        mov     cl, 1                   ; start at first sector

        xor     ax, ax
        mov     al, byte [Sector]
        cmp     al, SECT_PER_TRACK
        jbe     .loop
        mov     al, SECT_PER_TRACK      ; al = (al <= 18) ? al : 18
        jmp     .loop

最后发现一次读取跨磁道的扇区也没关系,Bochs的BIOS支持一次最多读取72个扇区。于是就放弃了,先读取最多72个吧,对于现阶段的system的规模是暂时够用了,到时再改吧。此外,要注意的是对要加载的扇区数的计算:这里SYSSIZE是system的实际大小,而不是Linux中所谓的click数(实际size加15后左移了4位)。并且为了避免丢失余数的差一问题,我们要先加上511。

; 4) Load system module at 0x10000
; Assume SYSSIZE < 72 sectors (36864)
; 1 track = 18 sectors * 512b = 9216(b)
; 1 head = 80 tracks * 9216 = 720(kb)
MAX_ONE_READ    equ 72

load_system:
    mov     ax, SYSSEG
    mov     es, ax
    mov     bx, 0000h       ; es:bx = target(es=1000h,bx=0)
    mov     dx, 0000h       ; dx = driver(dh)/head(dl)
    mov     cx, 0006h       ; cx = track(ch)/sector(cl)

    mov     ax, SYSSIZE
    add     ax, 511
    shr     ax, 9           ; al = (SYSSIZE + 511) / 512 sectors to read

    cmp     al, MAX_ONE_READ
    jbe     .loop
    mov     al, MAX_ONE_READ    ; al = (al <= 72) ? al : 72
.loop
    mov     ah, 02h         ; ah = service id(ah=02 means read)
    int     13h         ; ignore any error

2.2 关闭软驱马达

至此所有要加载的数据就都加载完了,所以我们可以关掉软驱的马达了。这样可以关闭软盘控制器FDC、禁止DMA和中断请求。具体细节有待深入研究。

; 5) Kill motor
ok_load_system:
    mov     dx, 0x3f2       ; floppy controller port
    mov     al, 0           ; floppy A
    outb                ; output al to dx port

3.第二阶段引导拆分

在上一篇文章中,在setup.asm中进入了保护模式,并执行了一段32位的代码。我们其实可以将进入保护模式之后的内核初始化工作继续填到setup.asm这段32位代码中,但这样做不如Linux 0.11的方式优雅,即将这部分工作放到system模块的头部去完成。缺点是可能引导过程有些零散,但优点就是因为system会被加载到0x0处,所以后续初始化的页目录表和页表、重放置后的GDT都会在低地址,安全、集中且易于管理,这在我们的上一篇文章中也提到了。

3.1 上半部:setup.asm

setup.asm首先读取BIOS中的有用信息保存到0x9000,即覆盖了bootsect的内存位置,因为它已经没有用了。然后将system拷贝到0x0低地址,进入保护模式后就跳转到system。

%include "var.inc"
%include "pm.inc"

; ############################
; Booting Process
; ############################

[SECTION .s16]
[BITS   16]

; 1) Read memory info from BIOS
    mov     ax, INITSEG
    mov     ds, ax          ; save to bootsect space
    mov     ah, 0x88
    int     0x15
    mov     [MEMSIZE], ax       ; ax=3c00h (15360kb=15mb)

; 2) Move system to 0x0000
; round-1: 10000~1ffff => 0000~ffff
; ...
; round-5: 80000~8ffff => 70000~7ffff
; 
; NOTE: 8000h word = 10000h byte
WordPerMove     equ 8000h

move_system:
    mov     ax, 0h
.loop
    mov     es, ax
    add     ax, 1000h
    mov     ds, ax
    mov     cx, WordPerMove     ; cx = counter
    xor     si, si          ; ds:si = source
    xor     di, di          ; es:di = target
    rep     movsw           ; move

    cmp     ax, INITSEG
    jne     .loop

; 3) Enter protection mode
    mov     ax, cs
    mov     ds, ax
    mov     es, ax
    mov     ss, ax
    mov     sp, 0100h

    ; 3.1) 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]

    ; 3.2) Disable interrupt
    cli

    ; 3.3) Enable A20 addr line
    in  al, 92h
    or  al, 00000010b
    out     92h, al

    ; 3.4) Set PE in cr0
    mov     eax, cr0
    or      eax, 1
    mov     cr0, eax

    ; 3.5) Jump to protective mode!
    jmp     dword SelectorSystem:0  ; 0x0000:0x0


[SECTION .gdt]
; Base Addr, Limit, Attribute
LABEL_GDT:      Descriptor      0h,           0h, 0h
LABEL_DESC_SYSTEM:  Descriptor      0h,       0ffffh, DA_CR | DA_32 | DA_LIMIT_4K
LABEL_DESC_DATA:    Descriptor  0h,       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

SelectorSystem  equ LABEL_DESC_SYSTEM - LABEL_GDT
SelectorData    equ LABEL_DESC_DATA - LABEL_GDT
SelectorVideo   equ LABEL_DESC_VIDEO - LABEL_GDT

读取BIOS中内存信息的方式有几种,目前setup.s中用的是最简单的一种,也是Linux 0.11中用的方式。代码只有三行,极其简单。获取结果保存在ax中,单位是KB,不是Byte:

 mov ah, 0x88 int 0x15 mov [MEMSIZE], ax ; ax=3c00h (15360kb=15mb)

但这种方式的缺点也同样明显,就是最大只支持64MB。因为ax只有16位,最大能表示65536。所以在Orange’s中作者用了另一种更为强大的方式,ax=0xe820的int 0x15中断,除了能获得内存大小外,还能获得到内存分布。当然缺点就是代码比前面这种方式要麻烦多了,所以这里就不细说了,知道有多种获取BIOS中内存信息的方式就可以了。

3.2 下半部:head.asm

这就是我们新拆分出来的head.asm,它将各个段寄存器置为内核的代码段Selector后,就开始内核初始化的工作了:

  1. 直接写显存的方式打印一条消息
  2. 重置GDT的地址。至于GDT的内容则与setup.asm的一样
  3. 将main函数的地址压到栈上,这样setup_paging就会将它当做调用的返回地址,执行ret时就会跳转到main.c中的main函数了
  4. 开启分页管理
%include "var.inc"
%include "pm.inc"

extern main

global startup_32

pdt:


[SECTION .text]
ALIGN   32
[BITS   32]

startup_32:
    mov     ax, 16      ; SelectorData
    mov     ds, ax
    mov     es, ax
    mov     ss, ax
    mov     esp, TopOfStack

; 1) Print welcome message
    mov     ax, 24      ; SelectorVideo
    mov     gs, ax
    mov     ah, 0Ch
    mov     ebx, 0
    mov     ecx, len
    mov     edx, Message

.loop:
    mov     edi, ebx
    add     edi, (80 * 20)  ; (80 * row + col) * 2
    imul    edi, 2
    mov     al, byte [edx]
    mov     [gs:edi], ax

    inc     ebx
    dec     ecx
    inc     edx
    cmp     ecx, 0h
    jne     .loop

; 2) Reset GDTR
    lgdt    [GdtPtr]

; 3) Prepare return address
    push    main
    jmp     setup_paging


; Temporary data and stack, will be overriden later
Message:
    db  "Welcome to MiniOS"
len     equ $ - Message


; LinearAddr[31~22] = 10 bits = 1024 entry (* 4B = 4096B)
; So PDT has 1024 entries (1024 page tables, occupy 4096b totally)
; LinearAddr[21~12] = 10 bits = 1024 entry (* 4B = 4096B)
; So PD has 1024 entries (1024 pages, occupy 4096b totally)
; LinearAddr[11~0] = 12 bits = 4096 byte 
; So offset (page size) is 4096b
PdtSize     equ     1024
PtSize      equ     1024
EntrySize   equ     4
PageSize    equ     4096

times   PdtSize*EntrySize-($-$$) db 0

pg0:
times   PtSize*EntrySize    db 0

pg1:
times   PtSize*EntrySize    db 0

pg2:
times   PtSize*EntrySize    db 0

pg3:
times   PtSize*EntrySize    db 0


; 4) Setup paging
PgRw        equ     111h

ALIGN   32
setup_paging:

    ; 4.1) Clear page space
    mov     ecx, PdtSize + PtSize       ; counter = 5*1024
    xor     eax, eax
    xor     edi, edi
    cld                     ; DF=0: edi move forward
    rep     stosd               ; move eax => [es:edi] by dword

    ; 4.2) Fill page dir
    mov     dword [pdt], pg0 + PgRw     ; 111h(7): read/write page
    mov     dword [pdt+04h], pg1 + PgRw 
    mov     dword [pdt+08h], pg2 + PgRw
    mov     dword [pdt+0ch], pg3 + PgRw

    ; 4.3) Fill page table
    ; pg0~3 can represent 0h ~ fff000h (16MB) memory space

    ; 0x3ffc: start addr of last entry of last PT
    mov     edi, (pg3 + PtSize * EntrySize) - EntrySize     

    ; 0xfff000: start addr of last page represented by last entry
    mov     eax, ((4 * PtSize * PageSize) - PageSize) + PgRw    

    std                     ; DF=1: edi move backward
.loop:
    stosd                   ; move eax => [es:edi] by dword
    sub     eax, PageSize
    jge     .loop

    ; 4.4) Set cr3 (PDBR, Page-Dir Base address Register)
    xor     eax, eax
    mov     cr3, eax

    ; 4.5) Set PG bit of cr0 to enable paging 
    mov     eax, cr0
    or  eax, 80000000h
    mov     cr0, eax

    ; 4.6) Transfer control to main()
    ret


; Temporary stack space
times   100h    db  0
TopOfStack  equ $ 


;[SECTION .gdt]
; Base Addr, Limit, Attribute
LABEL_GDT:      Descriptor      0h,           0h, 0h
LABEL_DESC_SYSTEM:  Descriptor      0h,       0ffffh, DA_CR | DA_32 | DA_LIMIT_4K
LABEL_DESC_DATA:    Descriptor  0h,       0ffffh, DA_DRW | DA_32 | DA_LIMIT_4K
times   253     dd  0x0, 0x0        ; space for LDT and TSS

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

SelectorSystem  equ LABEL_DESC_SYSTEM - LABEL_GDT
SelectorData    equ LABEL_DESC_DATA - LABEL_GDT

代码有些长,但比较清晰,真正的难点在于我们之前没有接触到的分页管理机制。其实我们此刻不是必须开启分页管理,但为了避免麻烦,我们这次就多做一点,把分页给弄好,这样以后就没有后顾之忧了。

3.2.1 内存位置

可能大家刚才看上面代码时没有注意标签,现在后看一下就会发现精心放置好的标签,包括pdt、pg0~3、以及后面的栈空间和GDT。这些标签和代码在运行时对应的内存空间非常重要,都是内核最重要的数据,所以它们的位置绝对不是随意放置的:

  • pdt放在最开头,使页目录表覆盖掉head.asm的部分代码
  • setup_paging位于pg3后,避免自己把自己覆盖掉
  • 栈放在setup_paging,作为临时的内核栈,因为main函数地址需要入栈
  • GDT放在最后
--------pdt-----------
| 0x0000 | 0000 0000 |
|  ...   |    ...    |
| 0x0FFC | 0000 0000 |
|-------pg0----------|
| 0x1000 | 0000 0000 |
|  ...   |    ...    |
| 0x1FFC | 0000 0000 |
|-------pg1----------|
| 0x2000 | 0000 0000 |
|  ...   |    ...    |
| 0x2FFC | 0000 0000 |
|-------pg2----------|
| 0x3000 | 0000 0000 |
|  ...   |    ...    |
| 0x3FFC | 0000 0000 |
|-------pg3----------|
| 0x4000 | 0000 0000 |
|  ...   |    ...    |
| 0x4FFC | 0000 0000 |
|  ...   |    ...    |
|  ...   |    ...    |
|------stack---------|
|  ...   |    ...    |
|-------gdt----------|
|  ...   |    ...    |

3.2.2 代码解释

《Linux 0.11中的页目录表及页表内容分析》作者对Linux 0.11中setup_paging处的代码进行了详细分析,清晰易懂,非常棒!

首先解释一下cld和std两个命令的用处,其实很简单:“在字符串的比较、赋值、读取等一系列和rep连用的操作中,di或si是可以自动增减的而不需要人来加减它的值,cld即告诉程序si,di向前移动,std指令为设置方向,告诉程序si,di向后移动”。

下面就重点说一下PDT和PT的初始化过程,为了使代码尽可能的清晰,很多“魔数”都提取成了常量。

    ; 4.1) Clear page space
    mov     ecx, PdtSize + PtSize       ; counter = 5*1024
    xor     eax, eax
    xor     edi, edi
    cld                     ; DF=0: edi move forward
    rep     stosd               ; move eax => [es:edi] by dword

    ; 4.2) Fill page dir
    mov     dword [pdt], pg0 + PgRw     ; 111h(7): read/write page
    mov     dword [pdt+04h], pg1 + PgRw 
    mov     dword [pdt+08h], pg2 + PgRw
    mov     dword [pdt+0ch], pg3 + PgRw

    ; 4.3) Fill page table
    ;      pg0~3 can represent 0h ~ fff000h (16MB) memory space

    ; 0x3ffc: start addr of last entry of last PT
    mov     edi, (pg3 + PtSize * EntrySize) - EntrySize     

    ; 0xfff000: start addr of last page represented by last entry
    mov     eax, ((4 * PtSize * PageSize) - PageSize) + PgRw    

    std                     ; DF=1: edi move backward
.loop:
    stosd                   ; move eax => [es:edi] by dword
    sub     eax, PageSize
    jge     .loop

3.2.3 PDT和PT的样子

PDT和PT到底是什么样子呢?要是看完前面的代码解释还是觉得很抽象的话,我们就直观的看看初始化成功后,内存从低到高的模样!

--------pdt-----------
| 0x0000 | 0000 1111 | => pg0
| 0x0004 | 0000 2111 | => pg1
| 0x0008 | 0000 3111 | => pg2
| 0x000C | 0000 4111 | => pg3
| 0x0010 | 0000 0000 |
|  ...   |    ...    |
| 0x0FFC | 0000 0000 |
|-------pg0----------|     Physical Address
| 0x1000 | 0000 0111 | => [00000000~00000FFF]
| 0x1004 | 0000 1111 | => [00001000~00001FFF]
| 0x1008 | 0000 2111 | => [00002000~00002FFF]
|  ...   |    ...    |
| 0x1FFC | 003F F111 | => [003FF000~003FFFFF]
|-------pg1----------|
| 0x2000 | 0040 0111 | => [00400000~00400FFF]
| 0x2004 | 0040 1111 | => [00401000~00401FFF]
| 0x2008 | 0040 2111 | => [00402000~00402FFF]
|  ...   |    ...    |
| 0x2FFC | 007F F111 | => [007FF000~007FFFFF]
|-------pg2----------|
| 0x3000 | 0080 0111 | => [00800000~00800FFF]
| 0x3004 | 0080 1111 | => [00801000~00801FFF]
| 0x3008 | 0080 2111 | => [00802000~00802FFF]
|  ...   |    ...    |
| 0x3FFC | 00BF F111 | => [00BFF000~00BFFFFF]
|-------pg3----------|
| 0x4000 | 00C0 0111 | => [00C00000~00C00FFF]
| 0x4004 | 00C0 1111 | => [00C01000~00C01FFF]
| 0x4008 | 00C0 2111 | => [00C02000~00C02FFF]
|  ...   |    ...    |
| 0x4FFC | 00FF F111 | => [00CFF000~00CFFFFF]

下面就运行起来Bochs,验证一下页表是否初始化成功了。我们查看几个关键位置就可以了,比如页目录表(0x0000),四个页表的开头部分(0x1000, 0x2000, 0x3000, 0x4000),以及pg4的最末尾部分(0x4ffc)。

(0) Breakpoint 1, 0x00005047 in ?? ()
Next at t=15473080
(0) [0x0000000000005047] 0008:00005047 (unk. ctxt): xor eax, eax              ; 31c0
<bochs:3> xp /32bx 0x00000
[bochs]: 0x00000000 <bogus+ 0>: 0x11 0x11 0x00 0x00 0x11 0x21 0x00 0x00 0x00000008 <bogus+ 8>: 0x11 0x31 0x00 0x00 0x11 0x41 0x00 0x00 0x00000010 <bogus+ 16>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00000018 <bogus+ 24>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00  <bochs:4> xp /32bx 0x01000 [bochs]: 0x00001000 <bogus+ 0>: 0x11 0x01 0x00 0x00 0x11 0x11 0x00 0x00 0x00001008 <bogus+ 8>: 0x11 0x21 0x00 0x00 0x11 0x31 0x00 0x00 0x00001010 <bogus+ 16>: 0x11 0x41 0x00 0x00 0x11 0x51 0x00 0x00 0x00001018 <bogus+ 24>: 0x11 0x61 0x00 0x00 0x11 0x71 0x00 0x00  <bochs:5> xp /32bx 0x02000 [bochs]: 0x00002000 <bogus+ 0>: 0x11 0x01 0x40 0x00 0x11 0x11 0x40 0x00 0x00002008 <bogus+ 8>: 0x11 0x21 0x40 0x00 0x11 0x31 0x40 0x00 0x00002010 <bogus+ 16>: 0x11 0x41 0x40 0x00 0x11 0x51 0x40 0x00 0x00002018 <bogus+ 24>: 0x11 0x61 0x40 0x00 0x11 0x71 0x40 0x00  <bochs:6> xp /32bx 0x03000 [bochs]: 0x00003000 <bogus+ 0>: 0x11 0x01 0x80 0x00 0x11 0x11 0x80 0x00 0x00003008 <bogus+ 8>: 0x11 0x21 0x80 0x00 0x11 0x31 0x80 0x00 0x00003010 <bogus+ 16>: 0x11 0x41 0x80 0x00 0x11 0x51 0x80 0x00 0x00003018 <bogus+ 24>: 0x11 0x61 0x80 0x00 0x11 0x71 0x80 0x00  <bochs:8> xp /4bx 0x04ffc [bochs]: 0x00004ffc <bogus+ 0>: 0x11 0xf1 0xff 0x00

4.宝贵的参考资料

个人感觉底层编程的学习曲线非常陡峭,要积累好多知识,爬过好多的“坑”,才能走到这一步,所以好的学习资料是非常重要的。以下就是我学习过程中常用的资料,它们的用法是:以《Linux内核完全剖析》为主,如果看不懂就去找《Orange’s:一个操作系统实现》中对应的章节对比学习。渐渐熟悉Linux的代码后,就参照Linux实现我们的操作系统。如果碰到NASM语法的相关问题,就去看一下博古以通今的博客,作者用NASM重写的代码还是靠谱的,只不过下不到全部代码了。

4.1 Linux 0.11源码

《完全剖析》中给出的代码包不是很方便,有热心的网友已经做了优化,“一键”就可直接编译运行起来Linux 0.11。

4.2 用NASM重写Linux 0.11

《用nasm语言重新实现linux-0.11 bootsect.s(博古以通今)》
《用nasm语言重新实现linux-0.11 setup.s (博古以通今)》
《nasm重写linux-0.11 head.s (博古以通今)》

4.3 NASM汇编指令

官方文档提供了一份NASM汇编指令列表,令我惊讶的是这份列表貌似并不全,有些查不到的指令如jge也是可用的。

此外,汇编语言没有高级语言那些控制结构,所以jmp对于实现逻辑就非常重要了,这是一份总结的不错的各种跳转指令的列表。

你可能感兴趣的:(操作系统,内核,bootloader)