mit 6.828 lab 代碼和筆記,以及中文注釋源代碼已放置在github中:
[https://github.com/yunwei37/xv6-labs](https://github.com/yunwei37/xv6-labs)
init
-
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- 解釋printf.c和 console.c之間的接口。
console.c 提供了輸入輸出字元的功能,大部分都在處理IO接口相關。
- 從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的值。
- 參考上述代碼
- “Hello World”
- 不确定值
- 在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;