OS課程 ucore_lab1實(shí)驗(yàn)報(bào)告
練習(xí)一:理解通過make生成執(zhí)行文件的過程。
? ? 列出本實(shí)驗(yàn)各練習(xí)中對應(yīng)的OS原理的知識點(diǎn),并說明本實(shí)驗(yàn)中的實(shí)現(xiàn)部分如何對應(yīng)和體現(xiàn)了原理中的基本概念和關(guān)鍵知識點(diǎn)。
在此練習(xí)中,大家需要通過靜態(tài)分析代碼來了解:
- 操作系統(tǒng)鏡像文件ucore.img是如何一步一步生成的?(需要比較詳細(xì)地>解釋Makefile中每一條相關(guān)命令和命令參數(shù)的含義,以及說明命令導(dǎo)致>的結(jié)果)
- 一個被系統(tǒng)認(rèn)為是符合規(guī)范的硬盤主引導(dǎo)扇區(qū)的特征是什么?
1.1
? 1. 生成<font color="#dd00dd">ucore.img</font>需要<font color="#dd00dd">kernel</font>和<font color="#dd00dd">bootblock</font>
生成<font color="#dd00dd">ucore.img</font>的代碼如下:
$(UCOREIMG): $(kernel) $(bootblock)
$(V)dd if=/dev/zero of=$@ count=10000
$(V)dd if=$(bootblock) of=$@ conv=notrunc
$(V)dd if=$(kernel) of=$@ seek=1 conv=notrunc
$(call create_target,ucore.img)
首先先創(chuàng)建一個大小為1000字節(jié)的塊,然后再將bootblock 復(fù)制過去。
生成<font color="#dd00dd">ucore.img</font>需要先生成<font color="#dd00dd">kernel</font>和<font color="#dd00dd">bootblock</font>
? 2. 生成<font color="#dd00dd">kernel</font>的代碼如下:
$(kernel): tools/kernel.ld
$(kernel): $(KOBJS)
@echo "bbbbbbbbbbbbbbbbbbbbbb$(KOBJS)"
@echo + ld $@
$(V)$(LD) $(LDFLAGS) -T tools/kernel.ld -o $@ $(KOBJS)
@$(OBJDUMP) -S $@ > $(call asmfile,kernel)
@$(OBJDUMP) -t $@ | $(SED) '1,/SYMBOL TABLE/d; s/ .* / /; /^$$/d' > $(call symfile,kernel)
通過<font color="#df00df">make V=</font>指令得到執(zhí)行的具體命令如下:
+ cc kern/init/init.c //編譯init.c
gcc -c kern/init/init.c -o obj/kern/init/init.o
+ cc kern/libs/readline.c //編譯readline.c
gcc -c kern/libs/readline.c -o
obj/kern/libs/readline.o
+ cc kern/libs/stdio.c //編譯stdlio.c
gcc -c kern/libs/stdio.c -o obj/kern/libs/stdio.o
+ cc kern/debug/kdebug.c //編譯kdebug.c
gcc -c kern/debug/kdebug.c -o obj/kern/debug/kdebug.o
+ cc kern/debug/kmonitor.c //編譯komnitor.c
gcc -c kern/debug/kmonitor.c -o
obj/kern/debug/kmonitor.o
+ cc kern/debug/panic.c //編譯panic.c
gcc -c kern/debug/panic.c -o obj/kern/debug/panic.o
+ cc kern/driver/clock.c //編譯clock.c
gcc -c kern/driver/clock.c -o obj/kern/driver/clock.o
+ cc kern/driver/console.c //編譯console.c
gcc -c kern/driver/console.c -o
obj/kern/driver/console.o
+ cc kern/driver/intr.c //編譯intr.c
gcc -c kern/driver/intr.c -o obj/kern/driver/intr.o
+ cc kern/driver/picirq.c //編譯prcirq.c
gcc -c kern/driver/picirq.c -o
obj/kern/driver/picirq.o
+ cc kern/trap/trap.c //編譯trap.c
gcc -c kern/trap/trap.c -o obj/kern/trap/trap.o
+ cc kern/trap/trapentry.S //編譯trapentry.S
gcc -c kern/trap/trapentry.S -o
obj/kern/trap/trapentry.o
+ cc kern/trap/vectors.S //編譯vectors.S
gcc -c kern/trap/vectors.S -o obj/kern/trap/vectors.o
+ cc kern/mm/pmm.c //編譯pmm.c
gcc -c kern/mm/pmm.c -o obj/kern/mm/pmm.o
+ cc libs/printfmt.c //編譯printfmt.c
gcc -c libs/printfmt.c -o obj/libs/printfmt.o
+ cc libs/string.c //編譯string.c
gcc -c libs/string.c -o obj/libs/string.o
+ ld bin/kernel //鏈接成kernel
ld -o bin/kernel
obj/kern/init/init.o obj/kern/libs/readline.o
obj/kern/libs/stdio.o obj/kern/debug/kdebug.o
obj/kern/debug/kmonitor.o obj/kern/debug/panic.o
obj/kern/driver/clock.o obj/kern/driver/console.o
obj/kern/driver/intr.o obj/kern/driver/picirq.o
obj/kern/trap/trap.o obj/kern/trap/trapentry.o
obj/kern/trap/vectors.o obj/kern/mm/pmm.o
obj/libs/printfmt.o obj/libs/string.o
+ cc boot/bootasm.S //編譯bootasm.c
gcc -c boot/bootasm.S -o obj/boot/bootasm.o
+ cc boot/bootmain.c //編譯bootmain.c
gcc -c boot/bootmain.c -o obj/boot/bootmain.o
+ cc tools/sign.c //編譯sign.c
gcc -c tools/sign.c -o obj/sign/tools/sign.o
gcc -O2 obj/sign/tools/sign.o -o bin/sign
+ ld bin/bootblock //根據(jù)sign規(guī)范生成bootblock
ld -m elf_i386 -nostdlib -N -e start -Ttext 0x7C00
obj/boot/bootasm.o obj/boot/bootmain.o
-o obj/bootblock.o
//創(chuàng)建大小為10000個塊的ucore.img,初始化為0,每個塊為512字節(jié)
dd if=/dev/zero of=bin/ucore.img count=10000
//把bootblock中的內(nèi)容寫到第一個塊
dd if=bin/bootblock of=bin/ucore.img conv=notrunc
//從第二個塊開始寫kernel中的內(nèi)容
dd if=bin/kernel of=bin/ucore.img seek=1 conv=notrunc
根據(jù)其中可以看到,要生成<font color="#dd00dd"> kernel</font>,需要GCC編譯器將<font color="#dd00dd"> kern</font>目錄下的.c文件全部編譯生成層的.o文件的支持。具體聲明:
obj/kern/init/init.o
obj/kern/libs/readline.o
obj/kern/libs/stdio.o
obj/kern/debug/kdebug.o
obj/kern/debug/kmonitor.o
obj/kern/debug/panic.o
obj/kern/driver/clock.o
obj/kern/driver/console.o
obj/kern/driver/intr.o
obj/kern/driver/picirq.o
obj/kern/trap/trap.o
obj/kern/trap/trapentry.o
obj/kern/trap/vectors.o
obj/kern/mm/pmm.o
obj/libs/printfmt.o
obj/libs/string.o
? 3.生成<font color="#dd00dd"> bootblock</font>:
代碼如下:
$(bootblock): $(call toobj,$(bootfiles)) | $(call totarget,sign)
@echo "========================$(call toobj,$(bootfiles))"
@echo + ld $@
$(V)$(LD) $(LDFLAGS) -N -e start -Ttext 0x7C00 $^ -o $(call toobj,bootblock)
@$(OBJDUMP) -S $(call objfile,bootblock) > $(call asmfile,bootblock)
@$(OBJCOPY) -S -O binary $(call objfile,bootblock) $(call outfile,bootblock)
@$(call totarget,sign) $(call outfile,bootblock) $(bootblock)
同樣根據(jù)<font color="#0000df">make V=</font>指令打印的結(jié)果,得到要生成的<font color="#df00df">bootblock</font>,首先要生成<font color="#df00df">bootasm.o、bootmain.o、sign</font>,
代碼如下:
bootfiles = $(call listf_cc,boot)
$(foreach f,$(bootfiles),$(call cc_compile,$(f),$(CC),$(CFLAGS) -Os -nostdinc))
由宏定義批量實(shí)現(xiàn)了。
而實(shí)際的命令在<font color="#df00df">make V=</font>的指令結(jié)果里可以看到。
下面是<font color="#df00df">bootasm.S</font>生成<font color="#df00df">bootasm.o</font>的具體命令:
gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootasm.S -o obj/boot/bootasm.o
下面是<font color="#df00df">bootmain.c</font>生成<font color="#df00df">bootmain.o</font>的具體命令:
gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootmain.c -o obj/boot/bootmain.o
查閱資料:
? --ggdb 生成可供fdb使用的調(diào)試信息
? --m32 生成適用于32位環(huán)境的代碼
? --gstabs 生成stabs格式的調(diào)試信息
? -- nostdinc 不是有標(biāo)準(zhǔn)庫
? --fno-stack-protector 不生成用于檢測緩沖區(qū)溢出的代碼
? --Os 為減少代碼大小而進(jìn)行優(yōu)化
添加搜索頭文件的路徑
? --fno-builtin 不進(jìn)行builtin函數(shù)的優(yōu)化
下列代碼為<font color="#df00df">sign</font>生成的代碼:
$(call add_files_host,tools/sign.c,sign,sign)
$(call create_target_host,sign,sign)
下列是生成<font color="#df00df">sign</font>的具體的命令:
gcc -Itools/ -g -Wall -O2 -c tools/sign.c -o obj/sign/tools/sign.o
gcc -g -Wall -O2 obj/sign/tools/sign.o -o bin/sign
有了上述的<font color="#df00df">bootasm.o、bootmain.o、sign</font>。接下來就可以生成<font color="#df00df">block</font>了,實(shí)際命令如下:
ld -m elf_i386 -nostdlib -N -e start -Ttext 0x7C00 obj/boot/bootasm.o obj/boot/bootmain.o -o obj/bootblock.o
參數(shù)解釋:
? --m 模擬為i386上的連接器
? --N 設(shè)置代碼段和數(shù)據(jù)段均為可讀寫
? --e 指定入口
? --Ttext 制定代碼段開始位置
總結(jié):
編譯所有生成bin/kernel所需的文件 鏈接生成bin/kernel 編譯bootasm.S bootmain.c sign.c 根據(jù)sign規(guī)范生成obj/bootblock.o 生成ucore.img
1.2
截取sign.c文件中的部分源碼:
char buf[512]; //定義buf數(shù)組
memset(buf, 0, sizeof(buf));
// 把buf數(shù)組的最后兩位置為 0x55, 0xAA
buf[510] = 0x55;
buf[511] = 0xAA;
FILE *ofp = fopen(argv[2], "wb+");
size = fwrite(buf, 1, 512, ofp);
if (size != 512) { //大小為512字節(jié)
fprintf(stderr, "write '%s' error,
size is %d.\n", argv[2], size);
return -1;
}
可知一個被系統(tǒng)認(rèn)為是符合規(guī)范的硬盤主引導(dǎo)扇區(qū)的特征有以下幾點(diǎn):
? --磁盤主引導(dǎo)扇區(qū)只有512字節(jié)
? --磁盤最后兩個字節(jié)為0x55AA
? --由不超過466字節(jié)的啟動代碼和不超過64字節(jié)的硬盤分區(qū)表加上兩個字節(jié)的結(jié)束符構(gòu)成。
練習(xí)二 使用qemu執(zhí)行并調(diào)試lab1中的軟件
為了熟悉使用qemu和gdb進(jìn)行的調(diào)試工作,我們進(jìn)行如下的小練習(xí):
- 從CPU加電后執(zhí)行的第一條指令開始,單步跟蹤BIOS的執(zhí)行。
- 在初始化位置0x7c00設(shè)置實(shí)地址斷點(diǎn),測試斷點(diǎn)正常。
- 從0x7c00開始跟蹤代碼運(yùn)行,將單步跟蹤反匯編得到的代碼與bootasm.S和 bootblock.asm進(jìn)行比較。
- 自己找一個bootloader或內(nèi)核中的代碼位置,設(shè)置斷點(diǎn)并進(jìn)行測試。
從CPU加電后執(zhí)行的第一條指令開始,單步跟蹤BIOS的執(zhí)行。
? 首先在CPU加電之后,CPU里面的ROM存儲器會將其里面保存的初始值傳給各個寄存器,其中CS:IP = 0Xf000 : fff0(CS:代碼段寄存器;IP:指令寄存器),這個值決定了我們從內(nèi)存中讀數(shù)據(jù)的位置,PC = 16*CS + IP。
<div align=center>
? 此時系統(tǒng)處于實(shí)模式,并且截止到目前為止系統(tǒng)的總線還不是我們平常的32位,這時的地址總線只有20位,所以地址空間的總大小只有1M,而我們的BIOS啟動固件就在這個1M的空間里面。
BIOS啟動固件需要提供以下的一些功能:
? ☆基本輸入輸出的程序
? ☆系統(tǒng)設(shè)置信息
? ☆開機(jī)后自檢程序
? ☆系統(tǒng)自啟動程序
? 在此我們需要找到CPU加電之后的第一條指令的位置,然后在這里break,單步跟蹤BIOS的執(zhí)行,根據(jù)PC = 16*CS + IP,我們可以得到PC = 0xffff0,所以BIOS的第一條指令的位置為0xffff0(在這里因?yàn)榇藭r我們的地址空間只有20位,所以是0xffff0)。
? 在這里我們利用make debug來觀察BIOS的單步執(zhí)行:
2.1
修改<font color="#df00df">lab1/tools/gdbinit</font>,內(nèi)容為:
set architecture i8086
target remote :1234
然后在lab1執(zhí)行:
make debug
在gdb的調(diào)試界面,執(zhí)行如下命令:
si
來單步跟蹤,在gdb的調(diào)試界面,執(zhí)行如下命令來查看BIOS代碼:
x /2i$pc
得到如下截圖:
<div align=center>[圖片上傳失敗...(image-b3508b-1571536103241)]
2.2
修改gdbinit文件:
set architecture i8086
target remote :1234
b *0x7c00
c
x/2i $pc
得到如下結(jié)果,正常:
<div align=center>[圖片上傳失敗...(image-dde4bf-1571536103241)]
2.3
改寫makefile文件:
debug: $(UCOREIMG)
$(V)$(TERMINAL) -e "$(QEMU) -S -s -d in_asm -D $(BINDIR)/q.log -parallel stdio -hda $< -serial null"
$(V)sleep 2
$(V)$(TERMINAL) -e "gdb -q -tui -x tools/gdbinit"
然后執(zhí)行 make debug:
得到<font color="#dd00ee">q.log</font>文件:


并與<font color="#dd00ee">bootlock.asm</font>文件對比:

從上面的結(jié)果可以看到:
<font color="#dd00ee">bootasm.S</font>文件中的代碼和<font color="#dd00ee">bootlock.asm</font>是一樣的,對于q.log文件,斷點(diǎn)之后的代碼和<font color="#dd00ee">bootasm.S、bootlock.asm</font>是一樣的。
2.4
修改gdbinit文件,在0x7c4a處設(shè)置斷點(diǎn) (調(diào)用bootmain函數(shù)處):
set architecture i8086
target remote :1234
break *0x7c4a
輸入<font color="#dd00">make debug</font> 得到結(jié)果:

斷點(diǎn)設(shè)置正常!
練習(xí)三 分析bootloader進(jìn)入保護(hù)模式的過程。
? BIOS將通過讀取硬盤主引導(dǎo)扇區(qū)到內(nèi)存,并轉(zhuǎn)跳到對應(yīng)內(nèi)存中的位置執(zhí)行bootloader。請分析bootloader是如何完成從實(shí)模式進(jìn)入保護(hù)模式的。
? 1.何開啟A20,以及如何開啟A20
? 2.如何初始化GDT表
? 3.如何使能和進(jìn)入保護(hù)模式
關(guān)中斷和清除段寄存器
.globl start
start:
.code16
cli //關(guān)中斷
cld //清除方向標(biāo)志
xorw %ax, %ax //ax清0
movw %ax, %ds //ds清0
movw %ax, %es //es清0
movw %ax, %ss //ss清0
3.1
? 初始時<font color="#dd00">A20</font>為0,訪問超過1MB的空間時就會從.循環(huán)計(jì)數(shù),將<font color="#dd00">A20</font>的地址線置為1后才可以訪問4G內(nèi)存。<font color="#dd00">A20</font>地址由8042控制,8042由2個I/O端口:0x60和0x64
打開<font color="#dd00">A20</font>流程:
? 1. 等待8042 Input buffer為空
? 2. 發(fā)送Write 8042 Output Port(P2)命令到Input buffer;
? 3. 等待8042 Input buffer為空
? 4. .將8042Output Port(P2)得到字節(jié)的第2位置1,然后哦寫入8042 Input buffer;
seta20.1: //等待8042鍵盤控制器不忙
inb $0x64, %al //從0x64端口中讀入一個字節(jié)到al中
testb $0x2, %al //測試al的第2位
jnz seta20.1 //al的第2位為0,則跳出循環(huán)
movb $0xd1, %al //將0xd1寫入al中
outb %al, $0x64 //將0xd1寫入到0x64端口中
seta20.2: //等待8042鍵盤控制器不忙
inb $0x64, %al //從0x64端口中讀入一個字節(jié)到al中
testb $0x2, %al //測試al的第2位
jnz seta20.2 //al的第2位為0,則跳出循環(huán)
movb $0xdf, %al //將0xdf入al中
outb %al, $0x60 //將0xdf入到0x64端口中,打開A20
3.2
1. 載入GDT表
lgdt gdtdesc //載入GDT表
2:進(jìn)入保護(hù)模式
? 通過將<font color="#ff11ff">cr0</font>寄存器PE位置1便開啟了保護(hù)模式
<font color="#ff00ff">cr0</font>的第0位為1表示處于保護(hù)模式。
movl %cr0, %eax //加載cro到eax
orl $CR0_PE_ON, %eax //將eax的第0位置為1
movl %eax, %cr0 //將cr0的第0位置為1
3 通過長跳轉(zhuǎn)更新cs的基地址:
? 以上已經(jīng)打開了保護(hù)模式,所以這里需要用到邏輯地址。<font color="#dd00dd">$PROT_MODE_CSEG</font>的值為0x80。
ljmp $PROT_MODE_CSEG, $protcseg//長跳轉(zhuǎn)進(jìn)入保護(hù)模式
.code32
protcseg:
4: 設(shè)置段寄存器 并建立堆棧。
movw $PROT_MODE_DSEG, %ax //
movw %ax, %ds
movw %ax, %es
movw %ax, %fs
movw %ax, %gs
movw %ax, %ss
movl $0x0, %ebp //設(shè)置幀指針
movl $start, %esp //設(shè)置棧指針
####### 5:轉(zhuǎn)到保護(hù)模式完成,進(jìn)入boot主方法。
call bootmain //調(diào)用bootmain函數(shù)
3.3
? 將<font color="#ff11ff">cr0</font>寄存器置1

? 首先將<font color="#ff11ff">cr0</font>寄存器里面的內(nèi)容取出來,然后進(jìn)行一個或操作,最后將得到的結(jié)果再寫入<font color="#ff11ff">cr0</font>中,由上文我們知道,在這里需要將<font color="#ff11ff">cr0</font>的最低位設(shè)置為1,所以我們的或操作是用來使得<font color="#ff11ff" >cr0</font>的最低位為1的操作,也就是說我們的<font color="#dddd">CR0_PE_ON</font>的值必須為1,這樣才可以達(dá)成目的,然后通過查詢<font color="#dddd">CR0_PE_ON</font>的定義我們發(fā)現(xiàn)的確為1,所以順利開啟PE位。
練習(xí)四 分析bootloader加載ELF格式的OS的過程。
通過閱讀bootmain.c,了解bootloader如何加載ELF文件。通過分析源代碼和通過qemu來運(yùn)行并調(diào)試bootloader&OS,
? 1. bootloader如何讀取硬盤扇區(qū)的?
? 2. bootloader是如何加載ELF格式的OS?
4.1
? 分析bootloader讀取硬盤扇區(qū)的代碼:
BootLoader讓CPU進(jìn)入保護(hù)模式后,下一步的工作就是從硬盤上加載并運(yùn)行OS??紤]到實(shí)現(xiàn)的簡單性,BootLoader的訪問硬盤都是LBA模式的PIO(Program IO)方式,即所有的IO操作是通過CPU訪問硬盤的IO地址寄存器完成的。
? 上一個練習(xí)中BootLoader已經(jīng)成功進(jìn)入了保護(hù)模式,接下來我們要做的是從硬盤讀取并運(yùn)行OS。對于硬盤來說,我們知道是分成許多扇區(qū)的,其中每個扇區(qū)大小為512字節(jié)。讀取扇區(qū)的流程可從指導(dǎo)書查閱得到:
? 1. 等待磁盤準(zhǔn)備好
利用waitdisk()函數(shù)進(jìn)行檢查
? 2. 發(fā)出讀取扇區(qū)的命令
寫地址0x1f2~0x1f7,第一條設(shè)置讀取扇區(qū)的數(shù)目為1,然后四條是設(shè)置LBA的參數(shù),最后一條是發(fā)出讀取磁盤的命令.
<div align=center>
以下是地址查詢功能:
Alt text
? 3. 等待磁盤準(zhǔn)備好
利用waitdisk()函數(shù)進(jìn)行檢查
? 4. 把磁盤扇區(qū)數(shù)據(jù)讀到指定內(nèi)存
接下來我們了解一下如何具體的從硬盤讀取數(shù)據(jù)。
因?yàn)槲覀兯x取的操作系統(tǒng)文件是存在0號硬盤上的,所以我們來看一下觀念與0號硬盤的I/O端口:

static void
waitdisk(void) { //如果0x1F7的最高2位是01,跳出循環(huán)
while ((inb(0x1F7) & 0xC0) != 0x40)
/* do nothing */;
}
/* readsect - read a single sector at @secno into @dst */
static void
readsect(void *dst, uint32_t secno) {
// wait for disk to be ready
waitdisk();
outb(0x1F2, 1); //讀取一個扇區(qū)
outb(0x1F3, secno & 0xFF); //要讀取的扇區(qū)編號
outb(0x1F4, (secno >> 8)&0xFF);//用來存放讀寫柱面的低8位字節(jié)
outb(0x1F5, (secno >> 16)&0xFF);//用來存放讀寫柱面的高2位字節(jié)
// 用來存放要讀/寫的磁盤號及磁頭號
outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0);
outb(0x1F7, 0x20); // cmd 0x20 - read sectors
// wait for disk to be ready
waitdisk();
// read a sector
insl(0x1F0, dst, SECTSIZE / 4); //獲取數(shù)據(jù)
}
? 一般主板有2個IDE通道,每個通道可以接2個IDE硬盤。訪問第一個硬盤的扇區(qū)可設(shè)置IO地址寄存器0x1f0-0x1f7實(shí)現(xiàn)的,具體參數(shù)見上表,一般第一個IDE通道通過訪問IO地址0x1f0-0x1f7來實(shí)現(xiàn),第二個IDE通道通過訪問0x170-0x17f實(shí)現(xiàn)。每個每個通道的主從盤的選擇通過第6個IO偏移地址寄存器來設(shè)置。從outb()可以看出這里是用LBA模式的PIO(Program IO)方式來訪問硬盤的。從磁盤IO地址和對應(yīng)功能表可以看出,該函數(shù)一次只讀取一個扇區(qū)。
? readseg簡單包裝了readsect,可以從設(shè)備讀取任意長度的內(nèi)容。
static void
readseg(uintptr_t va, uint32_t count, uint32_t offset) {
uintptr_t end_va = va + count;
va -= offset % SECTSIZE;
uint32_t secno = (offset / SECTSIZE) + 1;
// 加1因?yàn)?扇區(qū)被引導(dǎo)占用
// ELF文件從1扇區(qū)開始
for (; va < end_va; va += SECTSIZE, secno ++) {
readsect((void *)va, secno);
}
}
4.2
? 接下來我們需要讀取ELF格式的OS,在讀取ELF格式的OS之前我們需要了解ELF格式的文件在UCore里面是如何進(jìn)行存儲的,首先我們來觀察一下用來讀取ELF的結(jié)構(gòu)體elfhdr。
ELF定義:

在這里我們只需要關(guān)注其中的幾個參數(shù):
e_magic:是用來判斷讀出來的ELF格式的文件是否為正確的格式;
e_phoff:是program header表的位置偏移;
e_phnum:是program header表中的入口數(shù)目;
e_entry:是程序入口所對應(yīng)的虛擬地址。
? 由于我們需要把ELF格式的OS加載到內(nèi)存中的程序塊中,所以我們需要了解下在內(nèi)存中進(jìn)程塊是如何存儲的:
<div align=center>

在這里我們需要了解一些參數(shù):
p_va:一個對應(yīng)當(dāng)前段的虛擬地址;
p_memsz:當(dāng)前段的內(nèi)存大??;
p_offset:段相對于文件頭的偏移。
? 了解了程序在磁盤和內(nèi)存中分別的存儲方式之后我們就需要開始從內(nèi)存中讀取數(shù)據(jù)加載到內(nèi)存中來。由于上問的操作,我們將一些OS的ELF文件讀到<font color="#55ff55">ELFHDR</font>里面,所以在加載操作開始之前我們需要對<font color="#55ff55">ELFHDR</font>進(jìn)行判斷,觀察是否是一個合法的ELF頭。
以下是bootmain函數(shù)代碼:
void
bootmain(void) {
// 首先讀取ELF的頭部
readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);
// 通過儲存在頭部的幻數(shù)判斷是否是合法的ELF文件
if (ELFHDR->e_magic != ELF_MAGIC) {
goto bad;
}
struct proghdr *ph, *eph;
// ELF頭部有描述ELF文件應(yīng)加載到內(nèi)存什么位置的描述表,
// 先將描述表的頭地址存在ph
//ph表示ELF段表首地址,eph表示ELF段表末地址
ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
eph = ph + ELFHDR->e_phnum;
//接下來通過循環(huán)讀取每個段,并且將每個段讀入相應(yīng)的虛存p_va中。
// 按照描述表將ELF文件中數(shù)據(jù)載入內(nèi)存
for (; ph < eph; ph ++) {
readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
}
// ELF文件0x1000位置后面的0xd1ec比特被載入內(nèi)存0x00100000
// ELF文件0xf000位置后面的0x1d20比特被載入內(nèi)存0x0010e000
// 根據(jù)ELF頭部儲存的入口信息,找到內(nèi)核的入口,調(diào)用頭表中的內(nèi)核入口地址實(shí)現(xiàn)內(nèi)核鏈接地址轉(zhuǎn)化為加載地址,無返回值。
((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
bad:
outw(0x8A00, 0x8A00);
outw(0x8A00, 0x8E00);
while (1);
}
總結(jié):
? 1. 從硬盤讀了8個扇區(qū)數(shù)據(jù)到內(nèi)存<font color="#ff0055">0x10000</font>處,并把這里強(qiáng)制轉(zhuǎn)換成<font color="#440055">elfhdr</font>使用。
? 2. 校驗(yàn)<font color="#rr0055">e_magic </font>子段
? 3. 根據(jù)偏移量分別把程序短的數(shù)據(jù)讀取到內(nèi)存中
練習(xí)五 實(shí)現(xiàn)函數(shù)調(diào)用堆棧跟蹤函數(shù)
? 我們需要在lab1中完成kdebug.c中函數(shù)print_stackframe的實(shí)現(xiàn),可以通過函數(shù)print_stackframe來跟蹤函數(shù)調(diào)用堆棧中記錄的返回地址。在如果能夠正確實(shí)現(xiàn)此函數(shù),可在lab1中執(zhí)行 “make qemu”后,在qemu模擬器中得到類似如下的輸出:
Alt text
請完成實(shí)驗(yàn),看看輸出是否與上述顯示大致一致,并解釋最后一行各個數(shù)值的含義。
函數(shù)堆棧:
? 理解函數(shù)堆棧最重要的兩點(diǎn)是棧的結(jié)構(gòu)和EBP寄存器的作用。一個函數(shù)調(diào)用可分解為零到多個push指令(用于參數(shù)入棧)和一個CALL指令。CALL指令內(nèi)部還暗含了一個將返回地址壓棧的動作,這是由硬件完成的。幾乎所有本地編譯器都會在每個函數(shù)體之前插入類似如下的匯編指令:
pushl %ebp
movl %esp,%ebp
? 這兩條匯編指令的含義是:首先將ebp寄存器入棧,然后將棧頂指針esp賦值給ebp。<font color="#aaaa">movl %esp,%ebp</font>這條指令表面上看是用esp覆蓋ebp原來的值,其實(shí)不然。因?yàn)榻oebp賦值之前,原ebp值已經(jīng)被壓棧(位于棧頂),而新的ebp又恰恰指向棧頂。此時ebp寄存器就已經(jīng)處于一個非常重要的地位,該寄存器中存儲著占中的一個地址(原ebp入棧后的棧頂),從改地址為基準(zhǔn),向上(棧底方向)能獲取返回地址、參數(shù)值,向下(棧頂方向)能獲取函數(shù)局部變量值,而改地址出又存儲著上一層函數(shù)調(diào)用時的ebp值。
? 一般而言,<font color="#bb00bb">ss:[ebp+4]</font>處為返回地址(即調(diào)用時的 eip),<font color="#bb00bb">ss:[ebp+8]</font>處為第一個參數(shù)值(最后一個入棧的參數(shù)值,此處假設(shè)其占用4字節(jié)內(nèi)存),<font color="#bb00bb">ss:[ebp-4]</font>處為第一個局部變量,<font color="#bb00bb">ss:[ebp]</font>處為上一層ebp值。由于ebp中的地址處總是“上一層函數(shù)調(diào)用時的ebp值”,而在每一層函數(shù)調(diào)用中,都能通過當(dāng)時的ebp值“向上(棧底方向)”能獲取返回地址、參數(shù)值,“向下(棧頂方向)”能獲取函數(shù)局部變量值。如此形成遞歸,直至到達(dá)棧底。這就是函數(shù)調(diào)用棧。
? 打開 <font color="#dd00ee">labcodes/lab1/kern/debug/kdebug.c</font>,找到 <font color="#dd00dd">print_stackframe</font>函數(shù):
<div align=center>[圖片上傳失敗...(image-7e3d15-1571536103241)]
實(shí)現(xiàn):

? 通過一個for循環(huán)來循環(huán)輸出棧內(nèi)的相關(guān)參數(shù),首先獲取棧傳入的參數(shù),根據(jù)上面的分析我們可以知道第一個參數(shù)存在<font color="#bb00bb">ebp+8</font>的位置,在這里是通過<font color="#bb00bb">ebp+2</font>來實(shí)現(xiàn)的,因?yàn)樵谶@里2是int型,所以可以得到第一個參數(shù),然后我們需要得到原ebp以及返回地址的值,根據(jù)分析我們知道原ebp的值就存在ebp的位置,eip的值存在<font color="#bb00bb">ebp+4</font>的位置,所以在這里通過數(shù)組的操作實(shí)現(xiàn)具體功能。
執(zhí)行<font color="#dd00dd">make qemu</font>得到:

最后一行的解釋:
? 其對應(yīng)的是第一個使用堆棧的函數(shù),<font color="#aa00aa">bootmain.c</font>中的<font color="#bb00bb">bootmain</font>。(因?yàn)榇藭rebp對應(yīng)地址的值為0)
<font color="#cc00cc">bootloader</font>設(shè)置的堆棧從0x7c00開始,使用<font color="#dd00dd">”call bootmain”</font>轉(zhuǎn)入<font color="#dd00dd">bootmain</font>函數(shù)。
call指令壓棧,所以<font color="#ff00ff">bootmain</font>中ebp為<font color="#eeaadd">0x7bf8</font>。
練習(xí)六 完善中斷初始化和處理
請完成編碼工作和回答如下問題:
? 1. 中斷描述符表(也可簡稱為保護(hù)模式下的中斷向量表)中一個表項(xiàng)占多少字節(jié)?其中哪幾位代表中斷處理代碼的入口?
? 2. 請編程完善kern/trap/trap.c中對中斷向量表進(jìn)行初始化的函數(shù)idt_init。在idt_init函數(shù)中,依次對所有中斷入口進(jìn)行初始化。使用mmu.h中的SETGATE宏,填充idt數(shù)組內(nèi)容。每個中斷的入口由tools/vectors.c生成,使用trap.c中聲明的vectors數(shù)組即可。
? 3. 請編程完善trap.c中的中斷處理函數(shù)trap,在對時鐘中斷進(jìn)行處理的部分填寫trap函數(shù)中處理時鐘中斷的部分,使操作系統(tǒng)每遇到100次時鐘中斷后,調(diào)用print_ticks子程序,向屏幕上打印一行文字”100 ticks”。【注意】除了系統(tǒng)調(diào)用中斷(T_SYSCALL)使用陷阱門描述符且權(quán)限為用戶態(tài)權(quán)限以外,其它中斷均使用特權(quán)級(DPL)為0的中斷門描述符,權(quán)限為內(nèi)核態(tài)權(quán)限;而ucore的應(yīng)用程序處于特權(quán)級3,需要采用`int 0x80`指令操作(這種方式稱為軟中斷,軟件中斷,Tra中斷,在lab5會碰到)來發(fā)出系統(tǒng)調(diào)用請求,并要能實(shí)現(xiàn)從特權(quán)級3到特權(quán)級0的轉(zhuǎn)換,所以系統(tǒng)調(diào)用中斷(T_SYSCALL)所對應(yīng)的中斷門描述符中的特權(quán)級(DPL)需要設(shè)置為3。
?要求完成問題2和問題3 提出的相關(guān)函數(shù)實(shí)現(xiàn),提交改進(jìn)后的源代碼包(可以編譯執(zhí)行),并在實(shí)驗(yàn)報(bào)告中簡要說明實(shí)現(xiàn)過程,并寫出對問題1的回答。完成這問題2和3要求的部分代碼后,運(yùn)行整個系統(tǒng),可以看到大約每1秒會輸出一次”100 ticks”,而按下的鍵也會在屏幕上顯示。
6.1
一個表項(xiàng)的結(jié)構(gòu)如下:
/*lab1/kern/mm/mmu.h*/
/* Gate descriptors for interrupts and traps */
struct gatedesc {
unsigned gd_off_15_0 : 16; // low 16 bits of offset in segment
unsigned gd_ss : 16; // segment selector
unsigned gd_args : 5; // # args, 0 for interrupt/trap gates
unsigned gd_rsv1 : 3; // reserved(should be zero I guess)
unsigned gd_type : 4; // type(STS_{TG,IG32,TG32})
unsigned gd_s : 1; // must be 0 (system)
unsigned gd_dpl : 2; // descriptor(meaning new) privilege level
unsigned gd_p : 1; // Present
unsigned gd_off_31_16 : 16; // high bits of offset in segment
};
? 中斷描述符表一個表項(xiàng)占8字節(jié)。其中015位和4863位分別為offset偏移量的低16位和高6位,16~31位為段選擇子。通過段選擇子獲得段基址,加上偏移量即可得到中斷處理代碼的入口。如下圖:
<div align=center>

6.2
打開kern/trap/trap.c找到idt_init函數(shù),完成代碼:

? 第一步,聲明<font color="#ff00ff">__vertors[]</font>,其中存放著中斷服務(wù)程序的入口地址。這個數(shù)組生成于vertor.S中。
? 第二步,填充中斷描述符表IDT。
? 第三步,加載中斷描述符表。
其中SETGATE在mmu.h中有定義:
#define SETGATE(gate, istrap, sel, off, dpl){ \
(gate).gd_off_15_0 = (uint32_t)(off) & 0xffff; \
(gate).gd_ss = (sel); \
(gate).gd_args = 0; \
(gate).gd_rsv1 = 0; \
(gate).gd_type = (istrap) ? STS_TG32 : STS_IG32; \
(gate).gd_s = 0; \
(gate).gd_dpl = (dpl); \
(gate).gd_p = 1; \
(gate).gd_off_31_16 = (uint32_t)(off) >> 16; \
}
- gate:為相應(yīng)的idt[]數(shù)組內(nèi)容,處理函數(shù)的入口地址
- istrap:系統(tǒng)段設(shè)置為1,中斷門設(shè)置為0
- sel:段選擇子
- off:為<font color="#ff00ff">__vectors[]</font>數(shù)組內(nèi)容
-
dpl:設(shè)置特權(quán)級。這里中斷都設(shè)置為內(nèi)核級,即第0級
<div align=center>[圖片上傳失敗...(image-ad46d7-1571536103241)]
6.3
? 根據(jù)指導(dǎo)書查看函數(shù)<font color="#ff00ff">trap_dispatch</font>,發(fā)現(xiàn)<font color="#ff00ff">print_ticks()</font>子程序已經(jīng)被實(shí)現(xiàn)了,所以我們直接進(jìn)行判斷輸出即可,如下(見注釋):
........
........
case IRQ_OFFSET + IRQ_TIMER:
ticks ++; //每一次時鐘信號會使變量ticks加1
if (ticks==TICK_NUM) {//TICK_NUM已經(jīng)被預(yù)定義成了100,每到100便調(diào)用print_ticks()函數(shù)打印
ticks-=TICK_NUM;
print_ticks();
}
break;
.........
.........
根據(jù)提示補(bǔ)充:
<div align=center>[圖片上傳失敗...(image-22f305-1571536103241)]
運(yùn)行結(jié)果:

收獲:
本次實(shí)驗(yàn)花費(fèi)了大量的時間與精力,但收獲也同樣不少:學(xué)習(xí)了如何基本的運(yùn)行qemu,如何單步調(diào)試動態(tài)調(diào)試,了解到bootloader啟動過程,分段機(jī)制,ELF文件格式等等相關(guān)知識,懂得如何中斷,堆棧的利用,學(xué)會一些基本的編程知識。
參考鏈接:
[1].https://blog.csdn.net/Ni9htMar3/article/details/62422984
[2].https://blog.csdn.net/tiu2014/article/details/53998595
[3].https://blog.csdn.net/tangyuanzong/article/details/78595854
[4].https://blog.csdn.net/qq_19876131/article/details/51706973
[5].http://qiaoin.github.io/ucore-ex1-notes.html

