操作系統(tǒng) ucore Lab1

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)分析代碼來了解:

  1. 操作系統(tǒng)鏡像文件ucore.img是如何一步一步生成的?(需要比較詳細(xì)地>解釋Makefile中每一條相關(guān)命令和命令參數(shù)的含義,以及說明命令導(dǎo)致>的結(jié)果)
  1. 一個被系統(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í):

  1. 從CPU加電后執(zhí)行的第一條指令開始,單步跟蹤BIOS的執(zhí)行。
  2. 在初始化位置0x7c00設(shè)置實(shí)地址斷點(diǎn),測試斷點(diǎn)正常。
  3. 從0x7c00開始跟蹤代碼運(yùn)行,將單步跟蹤反匯編得到的代碼與bootasm.S和 bootblock.asm進(jìn)行比較。
  4. 自己找一個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>
圖2.11

? 此時系統(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>文件:

<div align=center>
圖2.3

查看<font color="#dd00ee">bootasm.S</font>文件:<div align=center>
圖2.38

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

<div align=center>
圖2.34

從上面的結(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é)果:

<div align=center>
圖2.4

斷點(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

<div align=center>
Alt text

? 首先將<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端口:

<div align=center>
Alt text
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定義:

<div align=center>
Alt text

在這里我們只需要關(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>

Alt text

在這里我們需要了解一些參數(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):

<div align=center>
Alt text

? 通過一個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>得到:

<div align=center>
Alt text

最后一行的解釋:
? 其對應(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>

Alt text

6.2

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

<div align=center>
Alt text

? 第一步,聲明<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é)果:

<div align=center>
Alt text

收獲:

本次實(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容