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;
-
解釋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;