天天看點

MIT 6.828 作業系統工程 2018 fall xv6 lab1 筆記 and 中文注釋源代碼閱讀

mit 6.828 lab 代碼和筆記,以及中文注釋源代碼已放置在github中:
[https://github.com/yunwei37/xv6-labs](https://github.com/yunwei37/xv6-labs)
           

init

  1. setup

    實驗内容采用git分發:

    git clone https://pdos.csail.mit.edu/6.828/2018/jos.git lab           
    測試的話可以使用:
    make grade
               

Part 1: PC Bootstrap

  • 需要了解x86彙編以及内聯彙編的寫法,參看: http://www.delorie.com/djgpp/doc/brennan/brennan_att_inline_djgpp.html https://pdos.csail.mit.edu/6.828/2018/readings/pcasm-book.pdf
  • 運作 qemu
    cd lab
    make 
    make qemu
               
  • PC的實體位址空間:
    +------------------+  <- 0xFFFFFFFF (4GB)
    |      32-bit      |
    |  memory mapped   |
    |     devices      |
    |                  |
    /\/\/\/\/\/\/\/\/\/\
    
    /\/\/\/\/\/\/\/\/\/\
    |                  |
    |      Unused      |
    |                  |
    +------------------+  <- depends on amount of RAM
    |                  |
    |                  |
    | Extended Memory  |
    |                  |
    |                  |
    +------------------+  <- 0x00100000 (1MB)
    |     BIOS ROM     |
    +------------------+  <- 0x000F0000 (960KB)
    |  16-bit devices, |
    |  expansion ROMs  |
    +------------------+  <- 0x000C0000 (768KB)
    |   VGA Display    |
    +------------------+  <- 0x000A0000 (640KB)
    |                  |
    |    Low Memory    |
    |                  |
    +------------------+  <- 0x00000000
               
  • 使用 gdb 調試qemu:

打開新的視窗:

cd lab
make qemu-gdb
           

在另外一個終端:

make
make gdb
           

開始使用gdb調試,首先進入實模式;

  • IBM PC從實體位址0x000ffff0開始執行,該位址位于為ROM BIOS保留的64KB區域的最頂部。
  • PC從CS = 0xf000和IP = 0xfff0開始執行。
  • 要執行的第一條指令是jmp指令,它跳轉到分段位址 CS = 0xf000和IP = 0xe05b。

實體位址 = 16 *網段 + 偏移量

然後,BIOS所做的第一件事就是jmp倒退到BIOS中的較早位置;

Part 2: The Boot Loader 引導加載程式

PC的軟碟和硬碟分為512個位元組的區域,稱為扇區。

當BIOS找到可引導的軟碟或硬碟時,它将512位元組的引導扇區加載到實體位址0x7c00至0x7dff的記憶體中,然後使用jmp指令将CS:IP設定為0000:7c00,将控制權傳遞給引導程式裝載機。

引導加載程式必須執行的兩個主要功能:

  • 将處理器從實模式切換到 32位保護模式;
  • 通過x86的特殊I / O指令直接通路IDE磁盤裝置寄存器,從硬碟讀取核心;

引導加載程式的源代碼:

boot/boot.S

#include <inc/mmu.h>

# 啟動CPU:切換到32位保護模式,跳至C代碼;
# BIOS将該代碼從硬碟的第一個扇區加載到
# 實體位址為0x7c00的記憶體,并開始以實模式執行
# %cs=0 %ip=7c00.

.set PROT_MODE_CSEG, 0x8         # 核心代碼段選擇器
.set PROT_MODE_DSEG, 0x10        # 核心資料段選擇器
.set CR0_PE_ON,      0x1         # 保護模式啟用标志

.globl start
start:
  .code16                     # 彙編為16位模式
  cli                         # 禁用中斷
  cld                         # 字元串操作增量,将标志寄存器Flag的方向标志位DF清零。
                              # 在字串操作中使變址寄存器SI或DI的位址指針自動增加,字串處理由前往後。

  # 設定重要的資料段寄存器(DS,ES,SS)
  xorw    %ax,%ax             # 第零段
  movw    %ax,%ds             # ->資料段
  movw    %ax,%es             # ->額外段
  movw    %ax,%ss             # ->堆棧段

  # 啟用A20:
  #   為了與最早的PC向後相容,實體
  #   位址線20綁在低電平,是以位址高于
  #   1MB會被預設傳回從零開始。  這邊代碼撤消了此操作。
seta20.1:
  inb     $0x64,%al               # 等待其不忙狀态
  testb   $0x2,%al
  jnz     seta20.1

  movb    $0xd1,%al               # 0xd1 -> 端口 0x64
  outb    %al,$0x64

seta20.2:
  inb     $0x64,%al               # 等待其不忙狀态
  testb   $0x2,%al
  jnz     seta20.2

  movb    $0xdf,%al               # 0xdf -> 端口 0x60
  outb    %al,$0x60

  # 使用引導GDT從實模式切換到保護模式
  # 并使用段轉換以保證虛拟位址和它們的實體位址相同
  # 是以
  # 有效記憶體映射在切換期間不會更改。
  lgdt    gdtdesc
  movl    %cr0, %eax
  orl     $CR0_PE_ON, %eax
  movl    %eax, %cr0
  
  # 跳轉到下一條指令,但還是在32位代碼段中。
  # 将處理器切換為32位指令模式。
  ljmp    $PROT_MODE_CSEG, $protcseg

  .code32                     # 32位模式彙編
protcseg:
  # 設定保護模式資料段寄存器
  movw    $PROT_MODE_DSEG, %ax    # 我們的資料段選擇器
  movw    %ax, %ds                # -> DS: 資料段
  movw    %ax, %es                # -> ES:額外段
  movw    %ax, %fs                # -> FS
  movw    %ax, %gs                # -> GS
  movw    %ax, %ss                # -> SS: 堆棧段
  
  # 設定堆棧指針并調用C代碼,bootmain
  movl    $start, %esp
  call bootmain

  # 如果bootmain傳回(不應該這樣),則循環
spin:
  jmp spin

# Bootstrap GDT
.p2align 2                                # 強制4位元組對齊 
gdt:
  SEG_NULL                # 空段
  SEG(STA_X|STA_R, 0x0, 0xffffffff)    # 代碼段
  SEG(STA_W, 0x0, 0xffffffff)            # 資料部分

gdtdesc:
  .word   0x17                            # sizeof(gdt) - 1
  .long   gdt                             # address gdt
           

boot/main.c

#include <inc/x86.h>
#include <inc/elf.h>

/**********************************************************************
 * 這是一個簡單的啟動裝載程式,唯一的工作就是啟動
 * 來自第一個IDE硬碟的ELF核心映像。
 *
 * 磁盤布局
 *  * 此程式(boot.S和main.c)是引導加載程式。這應該
 *    被存儲在磁盤的第一個扇區中。
 *
 *  * 第二個扇區開始儲存核心映像。
 *
 *  * 核心映像必須為ELF格式。
 *
 * 啟動步驟
 *  * 當CPU啟動時,它将BIOS加載到記憶體中并執行
 *
 *  *  BIOS初始化裝置,中斷例程集以及
 *    讀取引導裝置的第一個扇區(例如,硬碟驅動器)
 *    進入記憶體并跳轉到它。
 *
 *  * 假設此引導加載程式存儲在硬碟的第一個扇區中
 *    此代碼接管...
 *
 *  * 控制從boot.S開始-設定保護模式,
 *    和一個堆棧,然後運作C代碼,然後調用bootmain()
 *
 *  * 該檔案中的bootmain()會接管,讀取核心并跳轉到該核心。
 **********************************************************************/

#define SECTSIZE    512
#define ELFHDR        ((struct Elf *) 0x10000) // /暫存空間

void readsect(void*, uint32_t);
void readseg(uint32_t, uint32_t, uint32_t);

void
bootmain(void)
{
    struct Proghdr *ph, *eph;

    // 從磁盤讀取第一頁
    readseg((uint32_t) ELFHDR, SECTSIZE*8, 0);

    // 這是有效的ELF嗎?
    if (ELFHDR->e_magic != ELF_MAGIC)
        goto bad;

    // 加載每個程式段(忽略ph标志)
    ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff);
    eph = ph + ELFHDR->e_phnum;
    for (; ph < eph; ph++)
        // p_pa是該段的加載位址(同樣
        // 是實體位址)
        readseg(ph->p_pa, ph->p_memsz, ph->p_offset);

    // 從ELF标頭中調用入口點
    // 注意:不傳回!
    ((void (*)(void)) (ELFHDR->e_entry))();

bad:
    outw(0x8A00, 0x8A00);
    outw(0x8A00, 0x8E00);
    while (1)
        /* do nothing */;
}

// 從核心将“偏移”處的“計數”位元組讀取到實體位址“ pa”中。
// 複制數量可能超過要求
void
readseg(uint32_t pa, uint32_t count, uint32_t offset)
{
    uint32_t end_pa;

    end_pa = pa + count;

    // 向下舍入到扇區邊界
    pa &= ~(SECTSIZE - 1);

    // 從位元組轉換為扇區,核心從扇區1開始
    offset = (offset / SECTSIZE) + 1;

    // 如果速度太慢,我們可以一次讀取很多扇區。
    // 我們向記憶體中寫入的内容超出了要求,但這沒關系 --
    // 我們以遞增順序加載.
    while (pa < end_pa) {
        // 由于尚未啟用分頁,是以我們正在使用
        // 一個特定的段映射 (參閱 boot.S), 我們可以
        // 直接使用實體位址.  一旦JOS啟用MMU
        // ,就不會這樣了
        readsect((uint8_t*) pa, offset);
        pa += SECTSIZE;
        offset++;
    }
}

void
waitdisk(void)
{
    // 等待磁盤重新運作
    while ((inb(0x1F7) & 0xC0) != 0x40)
        /* do nothing */;
}

void
readsect(void *dst, uint32_t offset)
{
    // 等待磁盤準備好
    waitdisk();

    outb(0x1F2, 1);        // count = 1
    outb(0x1F3, offset);
    outb(0x1F4, offset >> 8);
    outb(0x1F5, offset >> 16);
    outb(0x1F6, (offset >> 24) | 0xE0);
    outb(0x1F7, 0x20);    // cmd 0x20 - 讀取扇區

    // 等待磁盤準備好
    waitdisk();

    // 讀取一個扇區
    insl(0x1F0, dst, SECTSIZE/4);
}

           

加載核心

  • ELF二進制檔案:
    可以将ELF可執行檔案視為具有加載資訊的标頭,然後是幾個程式段,每個程式段都是要在指定位址加載到記憶體中的連續代碼或資料塊。ELF二進制檔案以固定長度的ELF标頭開頭,其後是可變長度的程式标頭, 列出了要加載的每個程式段。
               

執行

objdump -h obj/kern/kernel

,檢視核心可執行檔案中所有部分的名稱,大小和連結位址的完整清單:

  • .text:程式的可執行指令。
  • .rodata:隻讀資料,例如C編譯器生成的ASCII字元串常量。
  • .data:資料部分儲存程式的初始化資料,例如用int x = 5等初始化程式聲明的全局變量;
  • VMA 連結位址,該節期望從中執行的記憶體位址。
  • LMA 加載位址,
obj/kern/kernel:     file format elf32-i386

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         00001acd  f0100000  00100000  00001000  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .rodata       000006bc  f0101ae0  00101ae0  00002ae0  2**5
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .stab         00004291  f010219c  0010219c  0000319c  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  3 .stabstr      0000197f  f010642d  0010642d  0000742d  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .data         00009300  f0108000  00108000  00009000  2**12
                  CONTENTS, ALLOC, LOAD, DATA
  5 .got          00000008  f0111300  00111300  00012300  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  6 .got.plt      0000000c  f0111308  00111308  00012308  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  7 .data.rel.local 00001000  f0112000  00112000  00013000  2**12
                  CONTENTS, ALLOC, LOAD, DATA
  8 .data.rel.ro.local 00000044  f0113000  00113000  00014000  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  9 .bss          00000648  f0113060  00113060  00014060  2**5
                  CONTENTS, ALLOC, LOAD, DATA
 10 .comment      00000024  00000000  00000000  000146a8  2**0
                  CONTENTS, READONLY
           

檢視引導加載程式的.text部分:

objdump -h obj/boot/boot.out

obj/boot/boot.out:     file format elf32-i386

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         0000019c  00007c00  00007c00  00000074  2**2
                  CONTENTS, ALLOC, LOAD, CODE
  1 .eh_frame     0000009c  00007d9c  00007d9c  00000210  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .stab         00000870  00000000  00000000  000002ac  2**2
                  CONTENTS, READONLY, DEBUGGING
  3 .stabstr      00000940  00000000  00000000  00000b1c  2**0
                  CONTENTS, READONLY, DEBUGGING
  4 .comment      00000024  00000000  00000000  0000145c  2**0
                  CONTENTS, READONLY
           

引導加載程式使用ELF 程式标頭來決定如何加載這些部分,程式标頭指定要加載到記憶體中的ELF對象的哪些部分以及每個目标位址應占據的位置。

檢查程式頭:

objdump -x obj/kern/kernel

ELF對象需要加載到記憶體中的區域是标記為“ LOAD”的區域。

Program Header:
    LOAD off    0x00001000 vaddr 0xf0100000 paddr 0x00100000 align 2**12
         filesz 0x00007dac memsz 0x00007dac flags r-x
    LOAD off    0x00009000 vaddr 0xf0108000 paddr 0x00108000 align 2**12
         filesz 0x0000b6a8 memsz 0x0000b6a8 flags rw-
   STACK off    0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**4
         filesz 0x00000000 memsz 0x00000000 flags rwx
           

檢視核心程式的入口點

objdump -f obj/kern/kernel

obj/kern/kernel:     file format elf32-i386
architecture: i386, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x0010000c
           
  • 在開始時,gdb會提示:The target architecture is assumed to be i8086
  • 切換到保護模式之後(ljmpl $0x8,$0xfd18f指令後),提示: The target architecture is assumed to be i386

練習6:

重置機器(退出QEMU / GDB并再次啟動它們)。在BIOS進入引導加載程式時檢查0x00100000處的8個記憶體字,然後在引導加載程式進入核心時再次檢查。

進入引導加載程式:

(gdb) x/8x 0x00100000
0x100000:    0x00000000    0x00000000    0x00000000    0x00000000
0x100010:    0x00000000    0x00000000    0x00000000    0x00000000
           

設定斷點: b *0x7d81

引導加載程式進入核心:

(gdb) x/8x 0x00100000
0x100000:    0x1badb002    0x00000000    0xe4524ffe    0x7205c766
0x100010:    0x34000004    0x2000b812    0x220f0011    0xc0200fd8
           

Part 3: The Kernel 核心

使用虛拟記憶體解決位置依賴性

核心的連結位址(由objdump列印)與加載位址之間存在(相當大的)差異;作業系統核心通常喜歡被連結并在很高的虛拟位址(例如0xf0100000)上運作,以便将處理器虛拟位址空間的下部留給使用者程式使用。

  • 連結位址 f0100000
  • 加載位址 00100000

許多機器在位址0xf0100000上沒有任何實體記憶體,是以我們不能指望能夠在其中存儲核心;将使用處理器的記憶體管理硬體将虛拟位址0xf0100000(核心代碼期望在其上運作的連結位址)映射到實體位址0x00100000(引導加載程式将核心加載到實體記憶體中)。

這樣,盡管核心的虛拟位址足夠高,可以為使用者程序留出足夠的位址空間,但是它将被加載到PC RAM中1MB點的BIOS ROM上方的實體記憶體中。

在這個階段中,僅映射前4MB的實體記憶體;

映射:kern/entrypgdir.c 中手寫,靜态初始化的頁面目錄和頁面表。

直到kern / entry.S設定了CR0_PG标志,記憶體引用才被視為實體位址。

  • 将範圍從0xf0000000到0xf0400000的虛拟位址轉換為實體位址0x00000000到0x00400000
  • 将虛拟位址0x00000000到0x00400000轉換為實體位址0x00000000到0x00400000
  • kern/entrypgdir.c:
#include <inc/mmu.h>
#include <inc/memlayout.h>

pte_t entry_pgtable[NPTENTRIES];

// entry.S頁面目錄從虛拟位址KERNBASE開始映射前4MB的實體記憶體
// (也就是說,它映射虛拟位址
// 位址[KERNBASE,KERNBASE + 4MB)到實體位址[0,4MB)
// 我們選擇4MB,因為這就是我們可以在一頁的空間中映射的表
// 這足以使我們完成啟動的早期階段。我們也映射
// 虛拟位址[0,4MB)到實體位址[0,4MB)這個
// 區域對于entry.S中的一些指令至關重要,然後我們
// 不再使用它。
//
// 頁面目錄(和頁面表)必須從頁面邊界開始,
// 是以是“ __aligned__”屬性。 另外,由于限制
// 與連結和靜态初始化程式有關, 我們在這裡使用“ x + PTE_P”
// 而不是更标準的“ x | PTE_P”。  其他地方
// 您應該使用“ |”組合标志。
__attribute__((__aligned__(PGSIZE)))
pde_t entry_pgdir[NPDENTRIES] = {
    // 将VA的[0,4MB)映射到PA的[0,4MB)
    [0]
        = ((uintptr_t)entry_pgtable - KERNBASE) + PTE_P,
    // 将VA的[KERNBASE,KERNBASE + 4MB)映射到PA的[0,4MB)
    [KERNBASE>>PDXSHIFT]
        = ((uintptr_t)entry_pgtable - KERNBASE) + PTE_P + PTE_W
};

// 頁表的條目0映射到實體頁0,條目1映射到
// 實體頁面1,依此類推
__attribute__((__aligned__(PGSIZE)))
pte_t entry_pgtable[NPTENTRIES] = {
    0x000000 | PTE_P | PTE_W,
    0x001000 | PTE_P | PTE_W,
    0x002000 | PTE_P | PTE_W,
    0x003000 | PTE_P | PTE_W,
    0x004000 | PTE_P | PTE_W,
    0x005000 | PTE_P | PTE_W,
  ................
           
  • kern/entry.S
/* See COPYRIGHT for copyright information. */

#include <inc/mmu.h>
#include <inc/memlayout.h>

# 邏輯右移
#define SRL(val, shamt)        (((val) >> (shamt)) & ~(-1 << (32 - (shamt))))


###################################################################
# 核心(此代碼)連結到位址〜(KERNBASE + 1 Meg),
# 但引導加載程式會将其加載到位址〜1 Meg。
#    
# RELOC(x)将符号x從其連結位址映射到其在
# 實體記憶體中的實際位置(其加載位址)。     
###################################################################

#define    RELOC(x) ((x) - KERNBASE)

#define MULTIBOOT_HEADER_MAGIC (0x1BADB002)
#define MULTIBOOT_HEADER_FLAGS (0)
#define CHECKSUM (-(MULTIBOOT_HEADER_MAGIC + MULTIBOOT_HEADER_FLAGS))

###################################################################
# 進入點
###################################################################

.text

# Multiboot标頭
.align 4
.long MULTIBOOT_HEADER_MAGIC
.long MULTIBOOT_HEADER_FLAGS
.long CHECKSUM

# '_start'指定ELF入口點。  既然當引導程式進入此代碼時我們還沒設定
# 虛拟記憶體,我們需要
# bootloader跳到入口點的*實體*位址。
.globl        _start
_start = RELOC(entry)

.globl entry
entry:
    movw    $0x1234,0x472            # 熱啟動

    # 我們尚未設定虛拟記憶體, 是以我們從
    # 引導加載程式加載核心的實體位址為:1MB
    # (加上幾個位元組)處開始運作.  但是,C代碼被連結為在
    # KERNBASE+1MB 的位置運作。  我們建立了一個簡單的頁面目錄,
    # 将虛拟位址[KERNBASE,KERNBASE + 4MB)轉換為
    # 實體位址[0,4MB)。  這4MB區域
    # 直到我們在實驗2 mem_init中設定真實頁面表為止
    # 是足夠的。

    # 将entry_pgdir的實體位址加載到cr3中。   entry_pgdir
    # 在entrypgdir.c中定義。
    movl    $(RELOC(entry_pgdir)), %eax
    movl    %eax, %cr3
    # 打開分頁功能。
    movl    %cr0, %eax
    orl    $(CR0_PE|CR0_PG|CR0_WP), %eax
    movl    %eax, %cr0

    # 現在啟用了分頁,但是我們仍在低EIP上運作
    # (為什麼這樣可以?) 進入之前先跳到上方c代碼中的
    # KERNBASE
    mov    $relocated, %eax
    jmp    *%eax
relocated:

    # 清除幀指針寄存器(EBP)
    # 這樣,一旦我們調試C代碼,
    # 堆棧回溯将正确終止。
    movl    $0x0,%ebp            # 空幀指針

    # 設定堆棧指針
    movl    $(bootstacktop),%esp

    # 現在轉到C代碼
    call    i386_init

    # 代碼永遠不會到這裡,但如果到了,那就讓它循環當機吧。
spin:    jmp    spin


.data
###################################################################
# 啟動堆棧
###################################################################
    .p2align    PGSHIFT        # 頁面對齊
    .globl        bootstack
bootstack:
    .space        KSTKSIZE
    .globl        bootstacktop   
bootstacktop:
           

不在這兩個範圍之一内的任何虛拟位址都将導緻硬體異常:導緻QEMU轉儲計算機狀态并退出。

練習7:

使用QEMU和GDB跟蹤到JOS核心并在movl %eax, %cr0處停止。檢查0x00100000和0xf0100000的記憶體。現在,使用stepiGDB指令單步執行該指令。同樣,檢查記憶體為0x00100000和0xf0100000。

在movl %eax, %cr0處停止:

(gdb) x 0x00100000
   0x100000:    add    0x1bad(%eax),%dh
(gdb) x 0xf0100000
   0xf0100000 <_start-268435468>:    add    %al,(%eax)           

si:

0x00100028 in ?? ()
(gdb) x 0x00100000
   0x100000:    add    0x1bad(%eax),%dh
(gdb) x 0xf0100000
   0xf0100000 <_start-268435468>:    add    0x1bad(%eax),%dh
           

建立新映射後 的第一條指令是:

mov $relocated, %eax

這時的eax是:

(gdb) info registers

eax 0xf010002f -267386833

格式化列印到控制台:

  • kern/printf.c
    核心的cprintf控制台輸出的簡單實作,
    基于printfmt()和核心控制台的cputchar()。
               
  • lib/printfmt.c
// 精簡的基本printf樣式格式化例程,
// 被printf,sprintf,fprintf等共同使用
// 核心和使用者程式也使用此代碼。

#include <inc/types.h>
#include <inc/stdio.h>
#include <inc/string.h>
#include <inc/stdarg.h>
#include <inc/error.h>

/*
 * 數字支援空格或零填充和字段寬度格式。
 * 
 *
 * 特殊格式%e帶有整數錯誤代碼
 * 并輸出描述錯誤的字元串。
 * 整數可以是正數或負數,
 * ,使-E_NO_MEM和E_NO_MEM等效。
 */

static const char * const error_string[MAXERROR] =
{
    [E_UNSPECIFIED]    = "unspecified error",
    [E_BAD_ENV]    = "bad environment",
    [E_INVAL]    = "invalid parameter",
    [E_NO_MEM]    = "out of memory",
    [E_NO_FREE_ENV]    = "out of environments",
    [E_FAULT]    = "segmentation fault",
};

/*
 * 使用指定的putch函數和關聯的指針putdat
 * 以相反的順序列印數字(基數<= 16).
 */
static void
printnum(void (*putch)(int, void*), void *putdat,
     unsigned long long num, unsigned base, int width, int padc)
{
    // 首先遞歸地列印所有前面的(更重要的)數字
    if (num >= base) {
        printnum(putch, putdat, num / base, base, width - 1, padc);
    } else {
        // 在第一個數字前列印任何需要的填充字元
        while (--width > 0)
            putch(padc, putdat);
    }

    // 然後列印此(最低有效)數字
    putch("0123456789abcdef"[num % base], putdat);
}

// 從varargs清單中擷取各種可能大小的unsigned int,
// 取決于lflag參數。
static unsigned long long
getuint(va_list *ap, int lflag)
{
    if (lflag >= 2)
        return va_arg(*ap, unsigned long long);
    else if (lflag)
        return va_arg(*ap, unsigned long);
    else
        return va_arg(*ap, unsigned int);
}

// 與getuint相同
// 符号擴充
static long long
getint(va_list *ap, int lflag)
{
    if (lflag >= 2)
        return va_arg(*ap, long long);
    else if (lflag)
        return va_arg(*ap, long);
    else
        return va_arg(*ap, int);
}


// 用于格式化和列印字元串的主要函數
void printfmt(void (*putch)(int, void*), void *putdat, const char *fmt, ...);

void
vprintfmt(void (*putch)(int, void*), void *putdat, const char *fmt, va_list ap)
{
    register const char *p;
    register int ch, err;
    unsigned long long num;
    int base, lflag, width, precision, altflag;
    char padc;

    while (1) {
        while ((ch = *(unsigned char *) fmt++) != '%') {
            if (ch == '\0')
                return;
            putch(ch, putdat);
        }

        // 處理%轉義序列
        padc = ' ';
        width = -1;
        precision = -1;
        lflag = 0;
        altflag = 0;
    reswitch:
        switch (ch = *(unsigned char *) fmt++) {

        // 标記以在右側填充
        case '-':
            padc = '-';
            goto reswitch;

        // 标記以0代替空格
        case '0':
            padc = '0';
            goto reswitch;

        // 寬度字段
        case '1':
        case '2':
        case '3':
        case '4':
        case '5':
        case '6':
        case '7':
        case '8':
        case '9':
            for (precision = 0; ; ++fmt) {
                precision = precision * 10 + ch - '0';
                ch = *fmt;
                if (ch < '0' || ch > '9')
                    break;
            }
            goto process_precision;

        case '*':
            precision = va_arg(ap, int);
            goto process_precision;

        case '.':
            if (width < 0)
                width = 0;
            goto reswitch;

        case '#':
            altflag = 1;
            goto reswitch;

        process_precision:
            if (width < 0)
                width = precision, precision = -1;
            goto reswitch;

        // long标志(對long long加倍)
        case 'l':
            lflag++;
            goto reswitch;

        // 字元
        case 'c':
            putch(va_arg(ap, int), putdat);
            break;

        // 錯誤資訊
        case 'e':
            err = va_arg(ap, int);
            if (err < 0)
                err = -err;
            if (err >= MAXERROR || (p = error_string[err]) == NULL)
                printfmt(putch, putdat, "error %d", err);
            else
                printfmt(putch, putdat, "%s", p);
            break;

        // 字元串
        case 's':
            if ((p = va_arg(ap, char *)) == NULL)
                p = "(null)";
            if (width > 0 && padc != '-')
                for (width -= strnlen(p, precision); width > 0; width--)
                    putch(padc, putdat);
            for (; (ch = *p++) != '\0' && (precision < 0 || --precision >= 0); width--)
                if (altflag && (ch < ' ' || ch > '~'))
                    putch('?', putdat);
                else
                    putch(ch, putdat);
            for (; width > 0; width--)
                putch(' ', putdat);
            break;

        // (帶符号)十進制
        case 'd':
            num = getint(&ap, lflag);
            if ((long long) num < 0) {
                putch('-', putdat);
                num = -(long long) num;
            }
            base = 10;
            goto number;

        // 無符号十進制
        case 'u':
            num = getuint(&ap, lflag);
            base = 10;
            goto number;

        // (無符号)八進制
        case 'o':
            num = getint(&ap, lflag);
            if ((long long) num < 0) {
                putch('-', putdat);
                num = -(long long) num;
            }
            base = 8;
            goto number;

        // 指針
        case 'p':
            putch('0', putdat);
            putch('x', putdat);
            num = (unsigned long long)
                (uintptr_t) va_arg(ap, void *);
            base = 16;
            goto number;

        // (無符号)十六進制
        case 'x':
            num = getuint(&ap, lflag);
            base = 16;
        number:
            printnum(putch, putdat, num, base, width, padc);
            break;

        // 跳過 %
        case '%':
            putch(ch, putdat);
            break;

        // 遇到不符合規範的%格式,跳過
        default:
            putch('%', putdat);
            for (fmt--; fmt[-1] != '%'; fmt--)
                /* do nothing */;
            break;
        }
    }
}

           
  • kern/console.c

控制台IO相關代碼;

練習8:

我們省略了一小段代碼-使用“%o”形式的模式列印八進制數字所必需的代碼。查找并填寫此代碼片段。

case 'o':
            num = getint(&ap, lflag);
            if ((long long) num < 0) {
                putch('-', putdat);
                num = -(long long) num;
            }
            base = 8;
            goto number;           

參考:

https://blog.csdn.net/weixin_30466039/article/details/97003339?utm_medium=distribute.pc_relevant.none-task-blog-OPENSEARCH-7.compare&depth_1-utm_source=distribute.pc_relevant.none-task-blog-OPENSEARCH-7.compare
  1. 解釋printf.c和 console.c之間的接口。
    console.c 提供了輸入輸出字元的功能,大部分都在處理IO接口相關。
               
  2. 從console.c解釋以下内容:
if (crt_pos >= CRT_SIZE) {
       int i;
        memcpy(crt_buf, crt_buf + CRT_COLS, (CRT_SIZE - CRT_COLS) * sizeof(uint16_t));
       for (i = CRT_SIZE - CRT_COLS; i < CRT_SIZE; i++)
               crt_buf[i] = 0x0700 | ' ';
       crt_pos -= CRT_COLS;
}
           

當crt_pos >= CRT_SIZE,其中CRT_SIZE = 8025,由于我們知道crt_pos取值範圍是0~(8025-1),那麼這個條件如果成立則說明現在在螢幕上輸出的内容已經超過了一頁。是以此時要把頁面向上滾動一行,即把原來的1~79号行放到現在的0~78行上,然後把79号行換成一行空格(當然并非完全都是空格,0号字元上要顯示你輸入的字元int c)。是以memcpy操作就是把crt_buf字元數組中1~79号行的内容複制到0~78号行的位置上。而緊接着的for循環則是把最後一行,79号行都變成空格。最後還要修改一下crt_pos的值。

  1. 參考上述代碼
  2. “Hello World”
  3. 不确定值
  4. 在vprintfmt中倒序處理參數

堆棧

在此過程中編寫一個有用的新核心螢幕函數,該函數将顯示堆棧的回溯資訊:儲存的清單來自導緻目前執行點的嵌套調用指令的指令指針(IP)值。

練習10:

http://www.cnblogs.com/fatsheep9146/p/5079930.html

練習11:

實作上述指定的backtrace函數。(預設參數下,并沒有遇到文中的bug

先了解一下test_backtrace是做什麼的;然後列印出堆棧資訊和ebp函數調用鍊鍊資訊,觀察即可發現。

代碼:

int
mon_backtrace(int argc, char **argv, struct Trapframe *tf)
{
    cprintf("Stack backtrace:\n");
    uint32_t *ebp;
    ebp = (uint32_t *)read_ebp();
    while(ebp!=0){
        cprintf("  ebp %08x",ebp);
        cprintf(" eip %08x  args",*(ebp+1));
        for(int i=2;i<7;++i)
            cprintf(" %08x",*(ebp+i));
        cprintf("\n");
        ebp = (uint32_t *)*ebp;
    }
    return 0;
}           

列印輸出:

ebp f0110f18 eip f01000a5  args 00000000 00000000 00000000 f010004e f0112308
  ebp f0110f38 eip f010007a  args 00000000 00000001 f0110f78 f010004e f0112308
  ebp f0110f58 eip f010007a  args 00000001 00000002 f0110f98 f010004e f0112308
  ebp f0110f78 eip f010007a  args 00000002 00000003 f0110fb8 f010004e f0112308
  ebp f0110f98 eip f010007a  args 00000003 00000004 00000000 f010004e f0112308
  ebp f0110fb8 eip f010007a  args 00000004 00000005 00000000 f010004e f0112308
  ebp f0110fd8 eip f01000fc  args 00000005 00001aac 00000640 00000000 00000000
  ebp f0110ff8 eip f010003e  args 00000003 00001003 00002003 00003003 00004003
           

(為什麼回溯代碼無法檢測到實際有多少個參數?如何解決此限制?):可以利用後續的擷取調試資訊的方法;

練習12:

通過objdump列印出符号表資訊,并嘗試找到函數;

yunwei@ubuntu:~/lab$ objdump -G obj/kern/kernel | grep f01000
0      SO     0      0      f0100000 1      {standard input}
1      SOL    0      0      f010000c 18     kern/entry.S
2      SLINE  0      44     f010000c 0      
3      SLINE  0      57     f0100015 0      
4      SLINE  0      58     f010001a 0      
5      SLINE  0      60     f010001d 0      
6      SLINE  0      61     f0100020 0      
7      SLINE  0      62     f0100025 0      
8      SLINE  0      67     f0100028 0      
9      SLINE  0      68     f010002d 0      
10     SLINE  0      74     f010002f 0      
11     SLINE  0      77     f0100034 0      
12     SLINE  0      80     f0100039 0      
13     SLINE  0      83     f010003e 0      
14     SO     0      2      f0100040 31     kern/entrypgdir.c
72     SO     0      0      f0100040 0      
73     SO     0      2      f0100040 2889   kern/init.c
108    FUN    0      0      f0100040 2973   test_backtrace:F(0,25)
118    FUN    0      0      f01000aa 3014   i386_init:F(0,25)
           

看看

kdebug.h

裡面的

debuginfo_eip

函數:

#ifndef JOS_KERN_KDEBUG_H
#define JOS_KERN_KDEBUG_H

#include <inc/types.h>

// 調試有關特定指令指針的資訊
struct Eipdebuginfo {
    const char *eip_file;        // EIP的源代碼檔案名
    int eip_line;            //  EIP的源代碼行号

    const char *eip_fn_name;    // 包含EIP的函數的名稱
                    //  - 注意:不為空終止!
    int eip_fn_namelen;        // 函數名稱的長度
    uintptr_t eip_fn_addr;        // 函數開始位址
    int eip_fn_narg;        // 函數參數的數量
};

int debuginfo_eip(uintptr_t eip, struct Eipdebuginfo *info);

#endif           

由于包含EIP的函數的名稱不為空終止,是以需要使用提示:

提示:printf格式字元串為列印非空終止的字元串(如STABS表中的字元串)提供了一種簡單而又晦澀的方法。 printf("%.*s", length, string)最多可列印的length字元string。檢視printf手冊頁,以了解其工作原理。

在 mon_backtrace() 中繼續修改,使用 debuginfo_eip 擷取相關資訊并列印:

int
mon_backtrace(int argc, char **argv, struct Trapframe *tf)
{
    cprintf("Stack backtrace:\n");
    uint32_t *ebp;
    int valid;
    struct Eipdebuginfo ei;
    ebp = (uint32_t *)read_ebp();
    while(ebp!=0){
        cprintf("  ebp %08x",ebp);
        cprintf(" eip %08x  args",*(ebp+1));
        valid = debuginfo_eip(*(ebp+1),&ei);
        for(int i=2;i<7;++i)
            cprintf(" %08x",*(ebp+i));
        cprintf("\n");
        if(valid == 0)
            cprintf("         %s:%d: %.*s+%d\n",ei.eip_file,ei.eip_line,ei.eip_fn_namelen,ei.eip_fn_name,*(ebp+1) - ei.eip_fn_addr);
        ebp = (uint32_t *)*ebp;
    }
    return 0;
}           

可以參考 inc/stab.h:

//JOS uses the N_SO, N_SOL, N_FUN, and N_SLINE types.
#define    N_SLINE        0x44    // text segment line number           

知道我們需要使用N_SLINE進行搜尋;以及stab的資料結構:

// Entries in the STABS table are formatted as follows.
struct Stab {
    uint32_t n_strx;    // index into string table of name
    uint8_t n_type;         // type of symbol
    uint8_t n_other;        // misc info (usually empty)
    uint16_t n_desc;        // description field
    uintptr_t n_value;    // value of symbol
};           

參考 的注釋部分:

// stab_binsearch(stabs, region_left, region_right, type, addr)
//
//    某些stab類型按升序排列在位址中
//    例如, N_FUN stabs ( n_type ==
//    N_FUN 的 stabs 條目), 标記了函數, 和 N_SO stabs,标記源檔案。
//
//    給定指令位址,此函數查找單個 stab
//    條目, 包含該位址的'type'類型。
//
//    搜尋在[* region_left,* region_right]範圍内進行。
//    是以,要搜尋整個N個stabs,可以執行以下操作:
//
//        left = 0;
//        right = N - 1;     /* rightmost stab */
//        stab_binsearch(stabs, &left, &right, type, addr);
//           

在 kern/kdebug.c 中 debuginfo_eip 相應位置修改,添加行數搜尋:

stab_binsearch(stabs, &lline, &rline, N_SLINE, addr);
    if(lline<=rline){
        info->eip_line = stabs[rline].n_value;
    }else{
        info->eip_line = 0;
        return -1;
    }
           

pass

running JOS: (1.4s) 
  printf: OK 
  backtrace count: OK 
  backtrace arguments: OK 
  backtrace symbols: OK 
  backtrace lines: OK 
Score: 50/50

           

結果是:

Stack backtrace:
  ebp f0110f18 eip f01000a5  args 00000000 00000000 00000000 f010004e f0112308
         kern/init.c:6: test_backtrace+101
  ebp f0110f38 eip f010007a  args 00000000 00000001 f0110f78 f010004e f0112308
         kern/init.c:46: test_backtrace+58
  ebp f0110f58 eip f010007a  args 00000001 00000002 f0110f98 f010004e f0112308
         kern/init.c:46: test_backtrace+58
  ebp f0110f78 eip f010007a  args 00000002 00000003 f0110fb8 f010004e f0112308
         kern/init.c:46: test_backtrace+58
  ebp f0110f98 eip f010007a  args 00000003 00000004 00000000 f010004e f0112308
         kern/init.c:46: test_backtrace+58
  ebp f0110fb8 eip f010007a  args 00000004 00000005 00000000 f010004e f0112308
         kern/init.c:46: test_backtrace+58
  ebp f0110fd8 eip f01000fc  args 00000005 00001aac 00000640 00000000 00000000
         kern/init.c:70: i386_init+82
  ebp f0110ff8 eip f010003e  args 00000003 00001003 00002003 00003003 00004003
         kern/entry.S:-267386818: <unknown>+0
           

雖然似乎eip并不一定指向對應的行...

總結:

這兩天大緻搞清楚了boot的方式,然後浏覽了一小部分的對應源代碼(雖然也不是很多的樣子),gdb還不算很熟練,大部分情況下還是使用cprintf打log;