uCore lab1 操作系統實驗

uCore lab1 —— 幼兒園

lab1有什麼?實現功能後能做啥?

  • 一個bootloader
    • 可以切換到X86保護模式
    • 能夠讀磁盤並加載ELF執行文件格式
    • 顯示字符
  • 一個OS
    • 可以處理時鐘中斷
    • 顯示字符

項目組成

lab1的整體目錄結構如下所示:

.

├── boot 

│ ├── asm.h 

│ ├── bootasm.S 

│ └── bootmain.c 

├── kern 

│ ├── debug 

│ │ ├── assert.h 

│ │ ├── kdebug.c 

│ │ ├── kdebug.h 

│ │ ├── kmonitor.c 

│ │ ├── kmonitor.h 

│ │ ├── panic.c 

│ │ └── stab.h 

│ ├── driver 

│ │ ├── clock.c 

│ │ ├── clock.h 

│ │ ├── console.c 

│ │ ├── console.h 

│ │ ├── intr.c 

│ │ ├── intr.h 

│ │ ├── kbdreg.h 

│ │ ├── picirq.c 

│ │ └── picirq.h 

│ ├── init 

│ │ └── init.c 

│ ├── libs 

│ │ ├── readline.c 

│ │ └── stdio.c 

│ ├── mm 

│ │ ├── memlayout.h 

│ │ ├── mmu.h 

│ │ ├── pmm.c 

│ │ └── pmm.h 

│ └── trap 

│ ├── trap.c 

│ ├── trapentry.S 

│ ├── trap.h 

│ └── vectors.S 

├── libs 

│ ├── defs.h 

│ ├── elf.h 

│ ├── error.h 

│ ├── printfmt.c 

│ ├── stdarg.h 

│ ├── stdio.h 

│ ├── string.c 

│ ├── string.h 

│ └── x86.h 

├── Makefile 

└── tools 

├── function.mk 

├── gdbinit 

├── grade.sh 

├── kernel.ld 

├── sign.c 

└── vector.c 

10 directories, 48 files 

其中一些比較重要的文件說明如下:

bootloader部分

  • boot/bootasm.S :定義並實現了bootloader最先執行的函數start,此函數進行了一定的初始化,完成了從實模式到保護模式的轉換,並調用bootmain.c中的bootmain函數。

  • boot/bootmain.c:定義並實現了bootmain函數實現了通過屏幕、串口和並口顯示字符串。bootmain函數加載ucore操作系統到內存,然後跳轉到ucore的入口處執行。

  • boot/asm.h:是bootasm.S彙編文件所需要的頭文件,主要是一些與X86保護模式的段訪問方式相關的宏定義。

ucore操作系統部分

系統初始化部分:

  • kern/init/init.c:ucore操作系統的初始化啓動代碼

內存管理部分:

  • kern/mm/memlayout.h:ucore操作系統有關段管理(段描述符編號、段號等)的一些宏定義

  • kern/mm/mmu.h:ucore操作系統有關X86 MMU等硬件相關的定義,包括EFLAGS寄存器中各位的含義,應用/系統段類型,中斷門描述符定義,段描述符定義,任務狀態段定義,NULL段聲明的宏SEG_NULL, 特定段聲明的宏SEG,設置中 斷門描述符的宏SETGATE(在練習6中會用到)

  • kern/mm/pmm.[ch]:設定了ucore操作系統在段機制中要用到的全局變量:任務狀態段ts,全局描述符表 gdt[],加載全局描述符表寄存器的函數lgdt,臨時的內核棧stack0;以及對全局描述符表和任務狀態段的初始化函數gdt_init

外設驅動部分:

  • kern/driver/intr.[ch]:實現了通過設置CPU的eflags來屏蔽和使能中斷的函數;

  • kern/driver/picirq.[ch]:實現了對中斷控制器8259A的初始化和使能操作;

  • kern/driver/clock.[ch]:實現了對時鐘控制器8253的初始化操作;- kern/driver/console. [ch]:實現了對串口和鍵盤的中斷方式的處理操作;

中斷處理部分:

  • kern/trap/vectors.S:包括256箇中斷服務例程的入口地址和第一步初步處理實現。注意,此文件是由tools/vector.c在編譯ucore期間動態生成的;

  • kern/trap/trapentry.S:緊接着第一步初步處理後,進一步完成第二步初步處理;並且有恢復中斷上下文的處理,即中斷處理完畢後的返回準備工作;

  • kern/trap/trap.[ch]:緊接着第二步初步處理後,繼續完成具體的各種中斷處理操作;

內核調試部分:

  • kern/debug/kdebug.[ch]:提供源碼和二進制對應關係的查詢功能,用於顯示調用棧關係。其中補全print_stackframe函數是需要完成的練習。其他實現部分不必深究。

  • kern/debug/kmonitor.[ch]:實現提供動態分析命令的kernel monitor,便於在ucore出現bug或問題後,能夠進入kernel monitor中,查看當前調用關係。實現部分不必深究。

  • kern/debug/panic.c | assert.h:提供了panic函數和assert宏,便於在發現錯誤後,調用kernel monitor。大家可在編程實驗中充分利用assert宏和panic函數,提高查找錯誤的效率。

公共庫部分

  • libs/defs.h:包含一些無符號整型的縮寫定義。

  • Libs/x86.h:一些用GNU C嵌入式彙編實現的C函數(由於使用了inline關鍵字,所以可以理解爲宏)。

工具部分

  • Makefile和function.mk:指導make完成整個軟件項目的編譯,清除等工作。

  • sign.c:一個C語言小程序,是輔助工具,用於生成一個符合規範的硬盤主引導扇區。

  • tools/vector.c:生成vectors.S,此文件包含了中斷向量處理的統一實現。

編譯方法

首先下載lab1.tar.bz2,然後解壓lab1.tar.bz2。在lab1目錄下執行make,可以生成ucore.img(生成於bin目錄下)。ucore.img是一個包含了bootloader或OS的硬盤鏡像,通過執行如下命令可在硬件虛擬環境 qemu中運行bootloader或OS:

$ make qemu 

則可以得到如下顯示界面*(僅供參考)*

(THU.CST) os is loading ... 
Special kernel symbols:
  entry 0x00100000 (phys) 
  etext 0x00103468 (phys) 
  edata 0x0010ea18 (phys) 
  end   0x0010fd80 (phys)
Kernel executable memory footprint: 64KB 
ebp:0x00007b38 eip:0x00100a55 args:0x00010094 0x00010094 0x00007b68 0x00100084
kern/debug/kdebug.c:305: print_stackframe+21 
ebp:0x00007b48 eip:0x00100d3a args:0x00000000 0x00000000 0x00000000 0x00007bb8 kern/debug/kmonitor.c:125: mon_backtrace+10 
ebp:0x00007b68 eip:0x00100084 args:0x00000000 0x00007b90 0xffff0000 0x00007b94 kern/init/init.c:48: grade_backtrace2+19 
ebp:0x00007b88 eip:0x001000a5 args:0x00000000 0xffff0000 0x00007bb4 0x00000029 kern/init/init.c:53: grade_backtrace1+27 
ebp:0x00007ba8 eip:0x001000c1 args:0x00000000 0x00100000 0xffff0000 0x00100043 kern/init/init.c:58: grade_backtrace0+19 
ebp:0x00007bc8 eip:0x001000e1 args:0x00000000 0x00000000 0x00000000 0x00103480 kern/init/init.c:63: grade_backtrace+26 
ebp:0x00007be8 eip:0x00100050 args:0x00000000 0x00000000 0x00000000 0x00007c4f kern/init/init.c:28: kern_init+79
ebp:0x00007bf8 eip:0x00007d61 args:0xc031fcfa 0xc08ed88e 0x64e4d08e 0xfa7502a8 
<unknow>: -- 0x00007d60 -- 
++ setup timer interrupts 
0: @ring 0 
0: cs = 8 
0: ds = 10 
0: es = 10 
0: ss = 10 
+++ switch to user mode +++ 
1: @ring 3 
1: cs = 1b 
1: ds = 23 
1: es = 23 
1: ss = 23 
+++ switch to kernel mode +++ 
2: @ring 0 
2: cs = 8 
2: ds = 10 
2: es = 10 
2: ss = 10 
100 ticks 
100 ticks 
100 ticks 
100 ticks

練習1

理解通過make生成執行文件的過程。

問題

  1. 操作系統鏡像文件ucore.img是如何一步一步生成的?(需要比較詳細地解釋Makefile中每一條相關命令和命令參數的含義,以及說明命令導致的結果)

  2. 一個被系統認爲是符合規範的硬盤主引導扇區的特徵是什麼?

補充材料

如何調試Makefile

當執行make時,一般只會顯示輸出,不會顯示make到底執行了哪些命令。

如想了解make執行了哪些命令,可以執行:(記住,V是大寫)

$ make "V=" 

要獲取更多有關make的信息,可上網查詢,並請執行

$ man make 

實驗過程

問題1

操作系統鏡像文件ucore.img是如何一步一步生成的?(需要比較詳細地解釋Makefile中每一條相關命令和命令參數的含義,以及說明命令導致的結果)

  • 首先,進入~/ucore_os_lab-master/labcodes_answer/lab1_result目錄下
  • 執行make,並查看詳情
$ make "V=" 
  • 觀察輸出結果

(1)通過GCC編譯器將Kernel目錄下的.c文件編譯成OBJ目錄下的.o文件。

+ cc kern/init/init.c # 編譯init.c
gcc -Ikern/init/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/init/init.c -o obj/kern/init/init.o
+ cc kern/libs/readline.c # 編譯readline.c
gcc -Ikern/libs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/libs/readline.c -o obj/kern/libs/readline.o
+ cc kern/libs/stdio.c # 編譯stdlio.c
gcc -Ikern/libs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/libs/stdio.c -o obj/kern/libs/stdio.o
+ cc kern/debug/kdebug.c # 編譯kdebug.c
gcc -Ikern/debug/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/debug/kdebug.c -o obj/kern/debug/kdebug.o
+ cc kern/debug/kmonitor.c # 編譯komnitor.c
gcc -Ikern/debug/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/debug/kmonitor.c -o obj/kern/debug/kmonitor.o
+ cc kern/debug/panic.c # 編譯panic.c
gcc -Ikern/debug/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/debug/panic.c -o obj/kern/debug/panic.o
+ cc kern/driver/clock.c # 編譯clock.c
gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/clock.c -o obj/kern/driver/clock.o
+ cc kern/driver/console.c # 編譯console.c
gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/console.c -o obj/kern/driver/console.o
+ cc kern/driver/intr.c # 編譯intr.c
gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/intr.c -o obj/kern/driver/intr.o
+ cc kern/driver/picirq.c # 編譯prcirq.c
gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/picirq.c -o obj/kern/driver/picirq.o
+ cc kern/trap/trap.c # 編譯trap.c
gcc -Ikern/trap/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/trap/trap.c -o obj/kern/trap/trap.o
+ cc kern/trap/trapentry.S # 編譯trapentry.S
gcc -Ikern/trap/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/trap/trapentry.S -o obj/kern/trap/trapentry.o
+ cc kern/trap/vectors.S # 編譯vectors.S
gcc -Ikern/trap/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/trap/vectors.S -o obj/kern/trap/vectors.o
+ cc kern/mm/pmm.c # 編譯pmm.c
gcc -Ikern/mm/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/mm/pmm.c -o obj/kern/mm/pmm.o
+ cc libs/printfmt.c # 編譯printfmt.c
gcc -Ilibs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/  -c libs/printfmt.c -o obj/libs/printfmt.o
+ cc libs/string.c # 編譯string.c
gcc -Ilibs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/  -c libs/string.c -o obj/libs/string.o

(2)ld命令根據鏈接腳本文件kernel.ld將生成的*.o文件,鏈接成BIN目錄下的kernel文件。

+ ld bin/kernel # 鏈接成kernel
ld -m    elf_i386 -nostdlib -T tools/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

(3)通過GCC編譯器將boot目錄下的.c,.S文件以及tools目錄下的sign.c文件編譯成OBJ目錄下的*.o文件。

+ cc boot/bootasm.S # 編譯bootasm.S
gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootasm.S -o obj/boot/bootasm.o
+ cc boot/bootmain.c # 編譯bootmain.c
gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootmain.c -o obj/boot/bootmain.o
+ cc tools/sign.c # 編譯sign.c
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

(4)ld命令將生成的*.o文件,鏈接成BIN目錄下的bootblock文件。

+ ld bin/bootblock  # 根據sign規範生成bootblock
ld -m    elf_i386 -nostdlib -N -e start -Ttext 0x7C00 obj/boot/bootasm.o obj/boot/bootmain.o -o obj/bootblock.o
'obj/bootblock.out' size: 488 bytes
build 512 bytes boot sector: 'bin/bootblock' success!

(5)dd命令將dev/zero, bin/bootblock,bin/kernel 寫入到bin/ucore.img

# 創建大小爲10000個塊的ucore.img,每個塊默認爲512字節,用0填充
dd if=/dev/zero of=bin/ucore.img count=10000
10000+0 records in
10000+0 records out
5120000 bytes (5.1 MB) copied, 0.042 seconds, 122 MB/s
# 把bootblock中的內容寫到第一個塊
dd if=bin/bootblock of=bin/ucore.img conv=notrunc
1+0 records in
1+0 records out
512 bytes (512 B) copied, 0 seconds, Infinity B/s
# 從第二個塊開始寫kernel中的內容
dd if=bin/kernel of=bin/ucore.img seek=1 conv=notrunc
146+1 records in
146+1 records out
74903 bytes (75 kB) copied, 0.001 seconds, 75 MB/s

總結:

# 生成ucore.img的相關代碼爲
$(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
  • 爲了生成ucore.img,首先需要生成bootblock、kernel
    • 爲了生成bootblock,首先需要生成bootasm.o、bootmain.o、sign
      • 遞歸下去
    • 爲了生成kernel,首先需要 kernel.ld init.o readline.o stdio.o kdebug.o
      • 遞歸下去

問題2

一個被系統認爲是符合規範的硬盤主引導扇區的特徵是什麼?

我們可以查看tools下的sign.c文件

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>

int main(int argc, char *argv[]) {
    struct stat st;
    if (argc != 3) {
        fprintf(stderr, "Usage: <input filename> <output filename>\n");
        return -1;
    }
    if (stat(argv[1], &st) != 0) {
        fprintf(stderr, "Error opening file '%s': %s\n", argv[1], strerror(errno));
        return -1;
    }
    printf("'%s' size: %lld bytes\n", argv[1], (long long)st.st_size);
    if (st.st_size > 510) {
        fprintf(stderr, "%lld >> 510!!\n", (long long)st.st_size);
        return -1;
    }
    char buf[512];
    memset(buf, 0, sizeof(buf));
    FILE *ifp = fopen(argv[1], "rb");
    int size = fread(buf, 1, st.st_size, ifp);
    if (size != st.st_size) {
        fprintf(stderr, "read '%s' error, size is %d.\n", argv[1], size);
        return -1;
    }
    fclose(ifp);
    buf[510] = 0x55;
    buf[511] = 0xAA;
    FILE *ofp = fopen(argv[2], "wb+");
    size = fwrite(buf, 1, 512, ofp);
    if (size != 512) {
        fprintf(stderr, "write '%s' error, size is %d.\n", argv[2], size);
        return -1;
    }
    fclose(ofp);
    printf("build 512 bytes boot sector: '%s' success!\n", argv[2]);
    return 0;
}

從sign.c的代碼來看,一個磁盤主引導扇區只有512字節。且第510個(倒數第二個)字節是0x55,第511個(倒數第一個)字節是0xAA。

練習2

使用qemu執行並調試lab1中的軟件。

實驗要求

  1. 從CPU加電後執行的第一條指令開始,單步跟蹤BIOS的執行。

  2. 在初始化位置0x7c00設置實地址斷點,測試斷點正常。

  3. 從0x7c00開始跟蹤代碼運行,將單步跟蹤反彙編得到的代碼與bootasm.S和bootblock.asm進行比較。

  4. 自己找一個bootloader或內核中的代碼位置,設置斷點並進行測試。

提示:參考附錄“啓動後第一條執行的指令”,可瞭解更詳細的解釋,以及如何單步調試和

查看BIOS代碼。

提示:查看 labcodes_answer/lab1_result/tools/lab1init 文件,用如下命令試試如何調試

bootloader第一條指令:

$ cd labcodes_answer/lab1_result/ 
$ make lab1-mon 

補充材料

我們主要通過硬件模擬器qemu來進行各種實驗。在實驗的過程中我們可能會遇上各種各樣的問題,調試是必要的。qemu支持使用gdb進行的強大而方便的調試。所以用好qemu和gdb是完成各種實驗的基本要素。

默認的gdb需要進行一些額外的配置才進行qemu的調試任務。qemu和gdb之間使用網絡端口1234進行通訊。在打開qemu進行模擬之後,執行gdb並輸入

target remote localhost:1234 

即可連接qemu,此時qemu會進入停止狀態,聽從gdb的命令。

另外,我們可能需要qemu在一開始便進入等待模式,則我們不再使用make qemu開始系統的運行,而使用make debug來完成這項工作。這樣qemu便不會在gdb尚未連接的時候擅自運行了。

gdb的地址斷點

在gdb命令行中,使用b *[地址]便可以在指定內存地址設置斷點,當qemu中的cpu執行到指定地址時,便會將控制權交給gdb。

關於代碼的反彙編

有可能gdb無法正確獲取當前qemu執行的彙編指令,通過如下配置可以在每次gdb命令行前強制反彙編當前的指令,在gdb命令行或配置文件中添加:

define hook-stop 
x/i $pc 
end 

即可

gdb的單步命令

在gdb中,有next, nexti, step, stepi等指令來單步調試程序,他們功能各不相同,區別在於單步的“跨度”上。

next 單步到程序源代碼的下一行,不進入函數。 
nexti 單步一條機器指令,不進入函數。 
step 單步到下一個不同的源代碼行(包括進入函數)。 
stepi 單步一條機器指令。 

實驗過程

問題1

從CPU加電後執行的第一條指令開始,單步跟蹤BIOS的執行。

直接看截圖吧,就是玩玩gdb那幾個命令而已

在這裏插入圖片描述

這是直接拿答案的代碼了,拿題目的是這樣:

(1)進入~/moocos/ucore_lab/labcodes/lab1/bin,即ucore.img所在文件夾。
(2)輸入指令:

qemu -S -s -hda ucore.img -monitor stdio

(3)打開gdb。

(4)輸入命令,使gdb與qemu通過1234端口進行通信,qemu會停止狀態聽從gbd命令。

target remote 127.0.0.1:1234

(5)輸入命令,單步跟蹤。

si

問題2

在初始化位置0x7c00設置實地址斷點,測試斷點正常。

(1)輸入命令,設置斷點。

b *0x7c00

(2)continue後,測試斷點。

c
x/2i $pc

運行結果:

(gdb) target remote 127.0.0.1:1234
Remote debugging using 127.0.0.1:1234
0x0000fff0 in ?? ()
(gdb) b *0x7c00
Breakpoint 1 at 0x7c00
(gdb) c
Continuing.

Breakpoint 1, 0x00007c00 in ?? ()
(gdb) x /2i $pc
=> 0x7c00:	cli    
   0x7c01:	cld    

問題3

從0x7c00開始跟蹤代碼運行,將單步跟蹤反彙編得到的代碼與bootasm.S和bootblock.asm進行比較。

  • 使用meld對比bootasm.S和bootlock.asm的代碼

meld /home/moocos/moocos/ucore_lab/labcodes/lab1/boot/bootasm.S /home/moocos/moocos/ucore_lab/labcodes/lab1/obj/bootblock.asm

  • 看就完了,是一樣的

在這裏插入圖片描述

問題4

自己找一個bootloader或內核中的代碼位置,設置斷點並進行測試。

(略)

補充

在進入練習3前,再複習一下bootloader做了什麼:

  • 切換到保護模式,啓用分段機制 (修改A20地址線)

  • 讀磁盤中ELF執行文件格式的ucore操作系統到內存

  • 顯示字符串信息

  • 把控制權交給ucore操作系統

保護模式下,有兩個段表:GDT(Global Descriptor Table)和LDT(Local Descriptor Table),每一張段表可以包含8192 (2^13)個描述符,因而最多可以同時存在2 * 2^13 = 2^14 個段。雖然保護模式下可以有這麼多段,邏輯地址空間看起來很大,但實際上段並不能擴展物理地址空間,很大程度上各個段的地址空間是相互重疊的。所謂的64TB(2^(14+32) =246)邏輯地址空間是一個理論值,沒有實際意義。在32位保護模式下,真正的物理空間仍然只有232字節那麼大。(注:在ucore lab中只用到了GDT,沒有用LDT。)

練習3

分析bootloader進入保護模式的過程。

實驗要求

BIOS將通過讀取硬盤主引導扇區到內存,並轉跳到對應內存中的位置執行bootloader。請分析bootloader是如何完成從實模式進入保護模式的。

提示:需要閱讀小節**“保護模式和分段機制”**和lab1/boot/bootasm.S源碼,瞭解如何從實模式切換到保護模式,需要了解:

  • 爲何開啓A20,以及如何開啓A20

  • 如何初始化GDT表

  • 如何使能和進入保護模式

實驗過程

打開lab1/boot/bootasm.S

  • 清理環境:關中斷,重要的段寄存器清零
# start address should be 0:7c00, in real mode, the beginning address of the running bootloader
.globl start
start:
.code16                                             # Assemble for 16-bit mode
    cli                                             # Disable interrupts
    cld                                             # String operations increment

    # Set up the important data segment registers (DS, ES, SS).
    xorw %ax, %ax                                   # Segment number zero
    movw %ax, %ds                                   # -> Data Segment
    movw %ax, %es                                   # -> Extra Segment
    movw %ax, %ss                                   # -> Stack Segment
  • 開啓A20(爲了與最早的PC兼容,物理地址線綁定在了低20位,因此高於1MB的地址默認情況下會回零。 此代碼撤消了此操作),通過將鍵盤控制器上的A20線置於高電位,全部32條地址線可用,可以訪問4G的內存空間。
    # Enable A20:
    #  For backwards compatibility with the earliest PCs, physical
    #  address line 20 is tied low, so that addresses higher than
    #  1MB wrap around to zero by default. This code undoes this.
seta20.1:
    inb $0x64, %al                                  # Wait for not busy(8042 input buffer empty).
    testb $0x2, %al
    jnz seta20.1

    movb $0xd1, %al                                 # 0xd1 -> port 0x64
    outb %al, $0x64                                 # 0xd1 means: write data to 8042's P2 port

seta20.2:
    inb $0x64, %al                                  # Wait for not busy(8042 input buffer empty).
    testb $0x2, %al
    jnz seta20.2

    movb $0xdf, %al                                 # 0xdf -> port 0x60
    outb %al, $0x60                                 # 0xdf = 11011111, means set P2's A20 bit(the 1 bit) to 1

設備和芯片的I/O端口操作實現,其實沒有複雜的東西在裏邊 ,I/O端口操作主要是看一堆文檔,把整個X86架構的PC機所有I/O端口記住,並記住它們每一個數據寄存器、命令寄存器等操作訪問標準(也可以稱之協議) ; 記住之後,整個過程中就是按標準使用I/O指令。
in, out(只能與DX,AX,AL寄存器結合使用) ;
下面的實現是提供給C使用,語法太晦澀,所以直接使用匯編實現。
inb 從I/O端口讀取一個字節(BYTE, HALF-WORD) ;
outb 向I/O端口寫入一個字節(BYTE, HALF-WORD);
inw 從I/O端口讀取一個字(WORD,即兩個字節);
outw 向I/O端口寫入一個字(WORD,即兩個字節) ;

  • 初始化GDT(全局描述表,Global Descriptor Table),一個簡單的GDT表和其描述符已經靜態儲存在引導區中,載入即可
    # Switch from real to protected mode, using a bootstrap GDT
    # and segment translation that makes virtual addresses
    # identical to physical addresses, so that the
    # effective memory map does not change during the switch.
    lgdt gdtdesc
    
    # Bootstrap GDT(這是文件的最後)
.p2align 2                                          # force 4 byte alignment
gdt:
    SEG_NULLASM                                     # null seg
    SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff)           # code seg for bootloader and kernel
    SEG_ASM(STA_W, 0x0, 0xffffffff)                 # data seg for bootloader and kernel

gdtdesc:
    .word 0x17                                      # sizeof(gdt) - 1
    .long gdt                                       # address gdt
  • 進入保護模式:通過將cr0寄存器PE位(第0位)置1便開啓了保護模式;通過長跳轉更新cs的基地址;設置段寄存器,並建立堆棧(BIOS數據區:0x0000-0x7c00)
    movl %cr0, %eax
    orl $CR0_PE_ON, %eax
    movl %eax, %cr0

    # Jump to next instruction, but in 32-bit code segment.
    # Switches processor into 32-bit mode.
    ljmp $PROT_MODE_CSEG, $protcseg
    # 這一跳就是真的跳到保護模式了
    
.code32                                             # Assemble for 32-bit mode
protcseg:
    # Set up the protected-mode data segment registers
    movw $PROT_MODE_DSEG, %ax                       # Our data segment selector
    movw %ax, %ds                                   # -> DS: Data Segment
    movw %ax, %es                                   # -> ES: Extra Segment
    movw %ax, %fs                                   # -> FS
    movw %ax, %gs                                   # -> GS
    movw %ax, %ss                                   # -> SS: Stack Segment

    # Set up the stack pointer and call into C. The stack region is from 0--start(0x7c00)
    movl $0x0, %ebp
    movl $start, %esp
    call bootmain

使用引導程序GDT和段轉換,使虛擬地址與物理地址相同,從而從實模式切換到保護模式,從而使有效內存映射在切換期間不會改變。

練習4

分析bootloader加載ELF格式的OS的過程。

實驗要求

通過閱讀bootmain.c,瞭解bootloader如何加載ELF文件。通過分析源代碼和通過qemu來運行並調試bootloader&OS,回答以下問題:

  • bootloader如何讀取硬盤扇區的?

  • bootloader是如何加載ELF格式的OS?

提示:可閱讀“硬盤訪問概述”,“ELF執行文件格式概述”這兩小節。

實驗過程

首先,我們可以先大致瞭解一下,什麼是ELF

接着,查看一下bin下的kernel文件

[~/moocos/ucore_lab/labcodes/lab1/bin]
moocos-> file kernel 
kernel: ELF 32-bit LSB  executable, Intel 80386, version 1 (SYSV), statically linked, not stripped

最後,再來看看bootloader的整個流程,到底他在做什麼

/* bootmain - the entry of bootloader */
void
bootmain(void) {
    // 首先讀取ELF的頭部:從磁盤讀取第一頁(即第一個扇區)
    readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);

    // 通過儲存在頭部的幻數判斷是否是合法的ELF文件
    if (ELFHDR->e_magic != ELF_MAGIC) {
        goto bad;
    }

    struct proghdr *ph, *eph;

    // 加載每個程序段
    // ELF頭部有描述ELF文件應加載到內存什麼位置的描述表,
	// 先將描述表的頭地址存在ph
    ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
    eph = ph + ELFHDR->e_phnum;
    for (; ph < eph; ph ++) {
        readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
    }

    // 從ELF頭調用entry point,bootloader把控制權轉交給uCore
    // note: does not return
    ((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
    // ELF文件0x1000位置後面的0xd1ec比特被載入內存0x00100000
	// ELF文件0xf000位置後面的0x1d20比特被載入內存0x0010e000

bad:
    outw(0x8A00, 0x8A00);
    outw(0x8A00, 0x8E00);

    /* do nothing */
    while (1);
}

問題1

bootloader如何讀取硬盤扇區?

  • 首先看readsect函數
#define SECTSIZE        512
#define ELFHDR          ((struct elfhdr *)0x10000) 
/* readsect - 將@secno的單個扇區讀入@dst */
static void
readsect(void *dst, uint32_t secno) {
    // 等待磁盤準備就緒
    waitdisk();
	// 發出讀取扇區的命令:用LBA模式的PIO(Program IO)方式來訪問硬盤
    outb(0x1F2, 1);                         // 設置讀取扇區的數目爲1
    outb(0x1F3, secno & 0xFF);
    outb(0x1F4, (secno >> 8) & 0xFF);
    outb(0x1F5, (secno >> 16) & 0xFF);
    outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0);
    outb(0x1F7, 0x20);                      // cmd 0x20 - 讀取扇區

    // 等待磁盤準備就緒
    waitdisk();

    // 讀一個扇區
    insl(0x1F0, dst, SECTSIZE / 4);         // 讀取到dst位置,除以4是因爲這裏以DW爲單位
}
  • 發現有個等待磁盤準備的函數
/* waitdisk - 等待磁盤準備就緒 */
static void
waitdisk(void) {
    while ((inb(0x1F7) & 0xC0) != 0x40)
        /* do nothing */;
}
  • 與0號硬盤有關的I/O端口

    • IF0H:0號硬盤數據寄存器
    • IF1H:0號硬盤錯誤寄存器(讀時)、0號硬盤Features 寄存器(寫時)
    • IF2H:0號硬盤數據扇區計數
    • IF3H:0號硬盤扇區數
    • IF4H:0號硬盤柱面(低字節)
    • IF5H:0號硬盤柱面(高字節)
    • IF6H:0號硬盤驅動器/磁頭寄存器
    • IF7H:0號硬盤狀態寄存器(讀時)、0 號硬盤命令寄存器(寫時)
  • 要到加載程序段了,readseg簡單包裝了readsect,可以從設備讀取任意長度的內容。

/* *
 * readseg - 從內核將@offset處的@count字節讀取到虛擬地址@va中,
 * 複製的數量可能會超出要求。
 * */
static void
readseg(uintptr_t va, uint32_t count, uint32_t offset) {
    uintptr_t end_va = va + count;

    // 取模,保證在一個扇區的空間範圍(512B)內
    va -= offset % SECTSIZE;

    // 從字節轉換爲扇區;內核從扇區1開始
    uint32_t secno = (offset / SECTSIZE) + 1;

    //如果速度太慢,我們可以一次讀取很多扇區。
    //我們向內存中寫入的內容超出了要求,但這沒關係-
    //我們以遞增順序加載。
    for (; va < end_va; va += SECTSIZE, seccno ++) {
        readsect((void *)va, secno);
    }
}

問題2

bootloader是如何加載ELF格式的OS?

  • 從硬盤讀了8個扇區數據到內存0x10000處,並把這裏強制轉換成elfhdr使用。
  • 校驗e_magic字段。
  • 根據偏移量分別把程序段的數據讀取到內存中。

我們看看lib下的elf.h文件(hdr其實就是header的縮寫)

struct elfhdr {
    uint32_t e_magic;     // 判斷讀出來的ELF格式的文件是否爲正確的格式
    uint8_t e_elf[12];
    uint16_t e_type;      // 1=可重定位,2=可執行,3=共享對象,4=核心映像
    uint16_t e_machine;   // 3=x86,4=68K等.
    uint32_t e_version;   // 文件版本,總是1
    uint32_t e_entry;     // 程序入口所對應的虛擬地址。
    uint32_t e_phoff;     // 程序頭表的位置偏移
    uint32_t e_shoff;     // 區段標題或0的文件位置
    uint32_t e_flags;     // 特定於體系結構的標誌,通常爲0
    uint16_t e_ehsize;    // 這個elf頭的大小
    uint16_t e_phentsize; // 程序頭中條目的大小
    uint16_t e_phnum;     // 程序頭表中的入口數目
    uint16_t e_shentsize; // 節標題中條目的大小
    uint16_t e_shnum;     // 節標題中的條目數或0
    uint16_t e_shstrndx;  // 包含節名稱字符串的節號。
};

總結

我們可以看到boot下的bootmain.c文件,其實前面是有很長一段文檔的,大致翻譯一下:

  • 這是一個簡單的bootloader,唯一的工作就是啓動;來自第一個IDE硬盤的ELF內核鏡像

  • 磁盤佈局

    • 此程序(bootasm.S和bootmain.c)是bootloader;存儲在磁盤的第一個扇區中
    • 第二個扇區開始保存內核映像;內核映像必須爲ELF格式。
  • 啓動步驟

    • 當CPU啓動時,它將BIOS加載到內存中並執行

      • BIOS初始化設備,中斷例程集
      • 讀取引導設備的第一個扇區(硬盤驅動器),進入內存並跳轉到它
    • 假設bootloader存儲在硬盤的第一個扇區中,此代碼將會接管…

    • 控制從bootasm.S開始

    • 設置保護模式
    • 創建堆棧
    • 然後運行C代碼,調用bootmain()
    • 該文件中的bootmain()會接管,讀取內核並跳轉到該內核。

練習5

實現函數調用堆棧跟蹤函數(需要編程)。

實驗要求

我們需要在lab1中完成kern/debug/kdebug.c中函數print_stackframe的實現,可以通過函數print_stackframe來跟蹤函數調用堆棧中記錄的返回地址。在如果能夠正確實現此函數,可在lab1中執行 “make qemu”後,在qemu模擬器中得到類似如下的輸出:

……
ebp:0x00007b28 eip:0x00100992 args:0x00010094 0x00010094 0x00007b58 0x00100096 kern/debug/kdebug.c:305: print_stackframe+22 
ebp:0x00007b38 eip:0x00100c79 args:0x00000000 0x00000000 0x00000000 0x00007ba8 kern/debug/kmonitor.c:125: mon_backtrace+10 
ebp:0x00007b58 eip:0x00100096 args:0x00000000 0x00007b80 0xffff0000 0x00007b84 kern/init/init.c:48: grade_backtrace2+33 
ebp:0x00007b78 eip:0x001000bf args:0x00000000 0xffff0000 0x00007ba4 0x00000029 kern/init/init.c:53: grade_backtrace1+38 
ebp:0x00007b98 eip:0x001000dd args:0x00000000 0x00100000 0xffff0000 0x0000001d kern/init/init.c:58: grade_backtrace0+23 
ebp:0x00007bb8 eip:0x00100102 args:0x0010353c 0x00103520 0x00001308 0x00000000 kern/init/init.c:63: grade_backtrace+34 
ebp:0x00007be8 eip:0x00100059 args:0x00000000 0x00000000 0x00000000 0x00007c53 kern/init/init.c:28: kern_init+88 
ebp:0x00007bf8 eip:0x00007d73 args:0xc031fcfa 0xc08ed88e 0x64e4d08e 0xfa7502a8 
<unknow>: -- 0x00007d72 –
……

請完成實驗,看看輸出是否與上述顯示大致一致,並解釋最後一行各個數值的含義。

提示:可閱讀小節“函數堆棧”,瞭解編譯器如何建立函數調用關係的。在完成lab1編譯後,查看lab1/obj/bootblock.asm,瞭解bootloader源碼與機器碼的語句和地址等的對應關係;查看lab1/obj/kernel.asm,瞭解 ucore OS源碼與機器碼的語句和地址等的對應關係。

要求完成函數kern/debug/kdebug.c::print_stackframe的實現,提交改進後源代碼包(可以編譯執行),並在實驗報告中簡要說明實現過程,並寫出對上述問題的回答。

補充材料

由於顯示完整的棧結構需要解析內核文件中的調試符號,較爲複雜和繁瑣。代碼中有一些輔助函數可以使用。例如可以通過調用print_debuginfo函數完成查找對應函數名並打印至屏幕的 功能。具體可以參見kdebug.c代碼中的註釋。

實驗過程

先看看文件開頭寫了啥

#define STACKFRAME_DEPTH 20

extern const struct stab __STAB_BEGIN__[];  // beginning of stabs table
extern const struct stab __STAB_END__[];    // end of stabs table
extern const char __STABSTR_BEGIN__[];      // beginning of string table
extern const char __STABSTR_END__[];        // end of string table

/* debug information about a particular instruction pointer */
struct eipdebuginfo {
    const char *eip_file;                   // source code filename for eip
    int eip_line;                           // source code line number for eip
    const char *eip_fn_name;                // name of function containing eip
    int eip_fn_namelen;                     // length of function's name
    uintptr_t eip_fn_addr;                  // start address of function
    int eip_fn_narg;                        // number of function arguments
};

注:stabs table(Symbol Table String)也就是一種向調試器描述程序的信息格式

/* STABS表中的條目的格式如下 */
struct stab {
    uint32_t n_strx;        // 到名稱字符串表的索引
    uint8_t n_type;         // 符號類型
    uint8_t n_other;        // 雜項信息(通常爲空)
    uint16_t n_desc;        // 說明字段
    uintptr_t n_value;      // 符號的值
};

有關特定指令指針的調試信息,嗯,啥也沒get到,繼續看吧

接下來是一大段的註釋,我直接翻成中文好了:

stab_binsearch:根據輸入的初始值範圍[ @ region_left, @ region_right],查找一個包含地址@addr並匹配類型@type,然後將其邊界保存到@region_left和@region_right所指向的位置的stab條目。

某些stab類型按指令地址升序排列。例如,標記了函數和N_SO stab,以及源文件的N_FUN stabs (stab entries with n_type == N_FUN)。

給定指令地址,此函數將查找包含該地址的類型爲@type的單個stab條目。

搜索在[@ region_left,@ region_right]範圍內進行。因此,要搜索整個N stabs,您可以執行以下操作:

left = 0;
right = N - 1;    
stab_binsearch(stabs, &left, &right, type, addr);

搜索會修改* region_left和* region_right,以將@addr括起來。

@region_left指向包含@addr且匹配的stab,而@region_right指向下一個stab之前。
如果 @ region_left> @ region_right,則@addr不包含在任何匹配的stab中。

比如,給定這些 N_SO stabs:

Index Type Address
0 SO f0100000
13 SO f0100040
117 SO f0100176
118 SO f0100178
555 SO f0100652
556 SO f0100654
657 SO f0100849

假定代碼:

left = 0;
right = 657;
stab_binsearch(stabs, &left, &right, N_SO, 0xf0100184);
// stab.h中這麼定義的:
// #define N_SO        0x64    // main source file name

函數完成任務後,將會設定left = 118, right = 554

// 函數原型
static void 
stab_binsearch(const struct stab *stabs, int *region_left, int *region_right,int type, uintptr_t addr)
// 具體實現太長了,略

只要知道這是個經典的二分搜索就好啦!

debuginfo_eip:在@info結構中填寫關於指定的指令地址@addr的信息。如果找到信息了就返回0,否則爲負。 但是即使它返回負數,也會將一些信息存儲到info中。

// 函數原型
int debuginfo_eip(uintptr_t addr, struct eipdebuginfo *info)
    
/* 調試有關特定指令指針的信息 */
struct eipdebuginfo {
    const char *eip_file;                   // source code filename for eip
    int eip_line;                           // source code line number for eip
    const char *eip_fn_name;                // name of function containing eip
    int eip_fn_namelen;                     // length of function's name
    uintptr_t eip_fn_addr;                  // start address of function
    int eip_fn_narg;                        // number of function arguments
};

print_kerninfo:打印有關內核的信息,包括內核條目的位置,數據和文本段的起始地址,可用內存的起始地址以及內核已使用了多少內存。

// 函數原型
void print_kerninfo(void)

print_debuginfo:讀取並打印地址@eip的統計信息,而info.eip_fn_addr應該是相關函數的第一個地址。

// 函數原型
void print_debuginfo(uintptr_t eip)

然後還有一個內聯彙編函數

static __noinline uint32_t read_eip(void) {
    uint32_t eip;
    asm volatile("movl 4(%%ebp), %0" : "=r" (eip));
    return eip;
}

給爺看困了,終於到我們要寫的東西了

我的天,又是長篇大論啊啊啊啊,頂住!!!!

先看函數吧:

void print_stackframe(void) {
     /* LAB1 YOUR CODE : STEP 1 */
     /* (1) call read_ebp() to get the value of ebp. the type is (uint32_t);
      * (2) call read_eip() to get the value of eip. the type is (uint32_t);
      * (3) from 0 .. STACKFRAME_DEPTH
      *    (3.1) printf value of ebp, eip
      *    (3.2) (uint32_t)calling arguments [0..4] = the contents in address (uint32_t)ebp +2 [0..4]
      *    (3.3) cprintf("\n");
      *    (3.4) call print_debuginfo(eip-1) to print the C calling function name and line number, etc.
      *    (3.5) popup a calling stackframe
      *           NOTICE: the calling funciton's return addr eip  = ss:[ebp+4]
      *                   the calling funciton's ebp = ss:[ebp]
      */
}

明晰任務很重要!

  • 調用read_ebp()來拿到ebp的值,類型是uint32_t

  • 調用read_eip()來拿到eip的值,類型是uint32_t

  • 從0到棧幀深度

    • 打印ebp,eip

    • 把在(uint32_t)ebp +2 [0…4]中的內容,賦給調用參數的[0…4]

    • 直接cprintf("\n");

    • 調用print_debuginfo(eip-1),打印C調用的函數名,行號啥的

    • 彈出調用的棧幀

(注意:調用函數返回的addr的eip = ss:[ebp+4]ebp = ss:[ebp]

print_stackframe:從當前執行點的嵌套調用指令中打印已保存的eip值的列表

說明:

x86堆棧指針(即esp)指向當前正在使用的堆棧上的最低位置。 堆棧中該位置以下的所有內容都是可用的。 將值壓入堆棧將涉及減小堆棧指針,然後將值寫入堆棧指針指向的位置。 而彈出一個值則相反。

相反,ebp(基址指針)寄存器主要通過軟件約定與堆棧相關聯。 進入C函數時,該函數的序言代碼通常通過將前一個函數的基址指針壓入堆棧來保存該基址指針,然後在函數持續時間內將當前esp值複製到ebp中。 如果程序中的所有函數都遵守該約定,則在執行程序期間的任何給定時刻,都可以通過遵循已保存的ebp指針鏈並確定是什麼嵌套函數調用序列導致此特殊事件來追溯堆棧 點在程序中。 例如,當某個特定函數由於向錯誤函數傳遞了錯誤參數而導致斷言失敗或出現緊急情況時,此功能特別有用,但是您不確定誰傳遞了錯誤參數。 堆棧回溯使您可以找到有問題的功能。

是不是有點抽象?上圖!

棧幀,多說一點

在這裏插入圖片描述

假設執行一個有兩個入口參數的函數,步驟是這樣的:

  • 調用函數前,依次壓入兩個入口參數(push xxx)

  • 壓入函數返回地址,即call指令的下一條指令(call func)

  • 壓入ebp,再將esp賦給ebp,讓它指向自己這個單元,以此爲基準(mov ebp,esp)

  • esp開闢局部變量空間(sub esp,72;不一定是72啦)

  • 壓入可能要用到的寄存器,保護起來(push ebx/esi/edi/…)

  • 執行程序

  • 保護寄存器出棧(pop edi/esi/ebx/…)

  • 釋放局部變量,esp回到基址處(mov ebp,esp)

  • ebp出棧(pop ebp)

  • 返回(ret),eip出棧,返回值放在eax

如果我們在函數中,要操作局部變量或者使用入口參數,就用ebp加上偏移,ebp就是負責棧數據的訪問;而esp是用來操作棧頂(低地址處)和開闢棧空間的

而我們經常說的棧幀是啥呢?就是圖上畫的那塊空間

明白一點了吧?繼續看說明吧!

內聯函數read_ebp()可以告訴我們當前ebp的值。 非內聯函數read_eip()很有用,它可以讀取當前eip的值,因爲在調用此函數時,read_eip()可以輕鬆地從堆棧讀取調用者的eip。
在print_debuginfo()中,函數debuginfo_eip()可以獲得有關調用鏈的足夠信息。 最後,print_stackframe()將跟蹤並打印它們以進行調試。
注意,ebp鏈的長度是有限的。 在boot / bootasm.S中,在跳轉到內核條目之前,ebp的值已設置爲零,即邊界。

說了這麼一大串,該明白了,看着圖整,準沒錯!

代碼:

void print_stackframe(void) {
    // read ebp,eip
    uint32_t ebp = read_ebp();
    uint32_t eip = read_eip();
    int i,j;    // for loop
    for(i=0; i<STACKFRAME_DEPTH&&ebp!=0;i++) {
        cprintf("ebp:0x%08x ",ebp);
        cprintf("eip:0x%08x ",eip);
        // entry args
        uint32_t* args = (uint32_t*)ebp + 2;
        cprintf("args:");
        for(j=0;j<4;j++){
            cprintf("0x%08x ",args[j]);
        }     
        cprintf("\n");
        print_debuginfo(eip-1);
        // unstack
        eip = ((uint32_t*)ebp)[1];
        ebp = ((uint32_t*)ebp)[0];      
    }
}

注意點:

  • ebp不能爲0啊,一定要考慮,爲0就說明esp還沒整活,沒整活要你ebp幹啥呢對吧
  • 在確認入口參數args時,記得把ebp轉成指針,不然會出事
  • 先出eip,再出ebp,就是這麼不科學,畢竟只是模擬;先出ebp,ebp被改變,eip就出不去了

輸出結果:

(THU.CST) os is loading ...

Special kernel symbols:
  entry  0x00100000 (phys)
  etext  0x001032db (phys)
  edata  0x0010ea16 (phys)
  end    0x0010fd20 (phys)
Kernel executable memory footprint: 64KB
ebp:0x00007b08 eip:0x001009a6 args:0x00010094 0x00000000 0x00007b38 0x00100092 
    kern/debug/kdebug.c:307: print_stackframe+21
ebp:0x00007b18 eip:0x00100cad args:0x00000000 0x00000000 0x00000000 0x00007b88 
    kern/debug/kmonitor.c:125: mon_backtrace+10
ebp:0x00007b38 eip:0x00100092 args:0x00000000 0x00007b60 0xffff0000 0x00007b64 
    kern/init/init.c:48: grade_backtrace2+33
ebp:0x00007b58 eip:0x001000bb args:0x00000000 0xffff0000 0x00007b84 0x00000029 
    kern/init/init.c:53: grade_backtrace1+38
ebp:0x00007b78 eip:0x001000d9 args:0x00000000 0x00100000 0xffff0000 0x0000001d 
    kern/init/init.c:58: grade_backtrace0+23
ebp:0x00007b98 eip:0x001000fe args:0x001032fc 0x001032e0 0x0000130a 0x00000000 
    kern/init/init.c:63: grade_backtrace+34
ebp:0x00007bc8 eip:0x00100055 args:0x00000000 0x00000000 0x00000000 0x00010094 
    kern/init/init.c:28: kern_init+84
ebp:0x00007bf8 eip:0x00007d68 args:0xc031fcfa 0xc08ed88e 0x64e4d08e 0xfa7502a8 
    <unknow>: -- 0x00007d67 --
++ setup timer interrupts

成了!NICE!!!

練習6

完善中斷初始化和處理 (需要編程)。

實驗要求

請完成編碼工作和回答如下問題:

  1. 中斷描述符表(也可簡稱爲保護模式下的中斷向量表)中一個表項佔多少字節?其中哪幾位代表中斷處理代碼的入口?

  2. 請編程完善kern/trap/trap.c中對中斷向量表進行初始化的函數idt_init。在idt_init函數中,依次對所有中斷入口進行初始化。使用mmu.h中的SETGATE宏,填充idt數組內容。每個中斷的入口由tools/vectors.c生成,使用trap.c中聲明的vectors數組即可。

  3. 請編程完善trap.c中的中斷處理函數trap,在對時鐘中斷進行處理的部分填寫trap函數中處理時鐘中斷的部分,使操作系統每遇到100次時鐘中斷後,調用print_ticks子程序,向屏幕上打印一行文字”100 ticks”。

【注意】除了系統調用中斷(T_SYSCALL)使用陷阱門描述符且權限爲用戶態權限以外,其它中斷均使用特權級(DPL)爲0的中斷門描述符,權限爲內核態權限;而ucore的應用 程序處於特權級3,需要採用`int 0x80`指令操作(這種方式稱爲軟中斷,軟件中斷,Tra中斷,在lab5會碰到)來發出系統調用請求,並要能實現從特權級3到特權級0的轉換,所以系統調用中斷(T_SYSCALL)所對應的中斷門描述符中的特權級(DPL)需要設置爲3。

要求完成問題2和問題3 提出的相關函數實現,提交改進後的源代碼包(可以編譯執行),並在實驗報告中簡要說明實現過程,並寫出對問題1的回答。完成這問題2和3要求的部分代碼後,運行整個系統,可以看到大約每1秒會輸出一次”100 ticks”,而按下的鍵也會在屏幕上顯示。

提示:可閱讀小節“中斷與異常”。

補充材料

分段存儲管理機制

只有在保護模式下才能使用分段存儲管理機制。分段機制將內存劃分成以起始地址和長度限制這兩個二維參數表示的內存塊,這些內存塊就稱之爲段(Segment)。編譯器把源程序編譯成執行程序時用到的代碼段、數據段、堆和棧等概念在這裏可以與段聯繫起來,二者在含義上是一致的。

分段機涉及4個關鍵內容:邏輯地址、段描述符(描述段的屬性)、段描述符表(包含多個段描述符的“數組”)、段選擇子(段寄存器,用於定位段描述符表中表項的索引)。轉換邏輯地址(Logical Address,應用程序員看到的地址)到物理地址(Physical Address, 實際的物理內存地址)分以下兩步:

  • 分段地址轉換:CPU把邏輯地址(由段選擇子selector和段偏移offset組成)中的段選擇子的內容作爲段描述符表的索引,找到表中對應的段描述符,然後把段描述符中保存的段基址加上段偏移值,形成線性地址(Linear Address)。如果不啓動分頁存儲管理機制,則線性地址等於物理地址。

  • 分頁地址轉換,這一步中把線性地址轉換爲物理地址。(注意:這一步是可選的,由操作系統決定是否需要。)

上述轉換過程對於應用程序員來說是不可見的。線性地址空間由一維的線性地址構成,線性地址空間和物理地址空間對等。線性地址32位長,線性地址空間容量爲4G字節。爲了使得分段存儲管理機制正常運行,需要建立好段描述符和段描述符表(參看bootasm.S,mmu.h,pmm.c)。

段描述符

在分段存儲管理機制的保護模式下,每個段由如下三個參數進行定義:段基地址(Base Address)、段界限(Limit)和段屬性(Attributes)。在ucore中的kern/mm/mmu.h中的struct segdesc 數據結構中有具體的定義。

/* segment descriptors */
struct segdesc {
    unsigned sd_lim_15_0 : 16;        // low bits of segment limit
    unsigned sd_base_15_0 : 16;        // low bits of segment base address
    unsigned sd_base_23_16 : 8;        // middle bits of segment base address
    unsigned sd_type : 4;            // segment type (see STS_ constants)
    unsigned sd_s : 1;                // 0 = system, 1 = application
    unsigned sd_dpl : 2;            // descriptor Privilege Level
    unsigned sd_p : 1;                // present
    unsigned sd_lim_19_16 : 4;        // high bits of segment limit
    unsigned sd_avl : 1;            // unused (available for software use)
    unsigned sd_rsv1 : 1;            // reserved
    unsigned sd_db : 1;                // 0 = 16-bit segment, 1 = 32-bit segment
    unsigned sd_g : 1;                // granularity: limit scaled by 4K when set
    unsigned sd_base_31_24 : 8;        // high bits of segment base address
};
  • 段基地址:規定線性地址空間中段的起始地址。在80386保護模式下,段基地址長32位。因爲基地址長度與尋址地址的長度相同,所以任何一個段都可以從32位線性地址空間中的任何一個字節開始,而不象實方式下規定的邊界必須被16整除。

  • 段界限:規定段的大小。在80386保護模式下,段界限用20位表示,而且段界限可以是以字節爲單位或以4K字節爲單位。

  • 段屬性:確定段的各種性質。

    • 段屬性中的粒度位(Granularity),用符號G標記。G=0表示段界限以字節位位單位,20位的界限可表示的範圍是1字節至1M字節,增量爲1字節;G=1表示段界限以4K字節爲單位,於是20位的界限可表示的範圍是4K字節至4G字節,增量爲4K字節。

    • 類型(TYPE):用於區別不同類型的描述符。可表示所描述的段是代碼段還是數據段,所描述的段是否可讀/寫/執行,段的擴展方向等。

    • 描述符特權級(Descriptor Privilege Level)(DPL):用來實現保護機制。

    • 段存在位(Segment-Present bit):如果這一位爲0,則此描述符爲非法的,不能被用來實現地址轉換。如果一個非法描述符被加載進一個段寄存器,處理器會立即產生異常。圖5-4顯示了當存在位爲0時,描述符的格式。操作系統可以任意的使用被標識爲可用(AVAILABLE)的位。

    • 已訪問位(Accessed bit):當處理器訪問該段(當一個指向該段描述符的選擇子被加載進一個段寄存器)時,將自動設置訪問位。操作系統可清除該位。

全局描述符表

全局描述符表是一個保存多個段描述符的“數組”,其起始地址保存在全局描述符表寄存器GDTR中。GDTR長48位,其中高32位爲基地址,低16位爲段界限。由於GDT不能有GDT本身之內的描述符進行描述定義,所以處理器採用GDTR爲GDT這一特殊的系統段。(注意,全局描述符表中第一個段描述符設定爲空段描述符。)GDTR中的段界限以字節爲單位。對於含有N個描述符的描述符表的段界限通常可設爲8*N-1。在ucore中的boot/bootasm.S中的gdt地址處和kern/mm/pmm.c中的全局變量數組gdt[]分別有基於彙編語言和C語言的全局描述符表的具體實現。

/* *
 * Global Descriptor Table:
 *
 * The kernel and user segments are identical (except for the DPL). To load
 * the %ss register, the CPL must equal the DPL. Thus, we must duplicate the
 * segments for the user and the kernel. Defined as follows:
 *   - 0x0 :  unused (always faults -- for trapping NULL far pointers)
 *   - 0x8 :  kernel code segment
 *   - 0x10:  kernel data segment
 *   - 0x18:  user code segment
 *   - 0x20:  user data segment
 *   - 0x28:  defined for tss, initialized in gdt_init
 * */
static struct segdesc gdt[] = {
    SEG_NULL,
    [SEG_KTEXT] = SEG(STA_X | STA_R, 0x0, 0xFFFFFFFF, DPL_KERNEL),
    [SEG_KDATA] = SEG(STA_W, 0x0, 0xFFFFFFFF, DPL_KERNEL),
    [SEG_UTEXT] = SEG(STA_X | STA_R, 0x0, 0xFFFFFFFF, DPL_USER),
    [SEG_UDATA] = SEG(STA_W, 0x0, 0xFFFFFFFF, DPL_USER),
    [SEG_TSS]    = SEG_NULL,
};

#define SEG_NULL                                            \
    (struct segdesc){0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
#define SEG(type, base, lim, dpl)                        \
    (struct segdesc){                                    \
        ((lim) >> 12) & 0xffff, (base) & 0xffff,        \
        ((base) >> 16) & 0xff, type, 1, dpl, 1,            \
        (unsigned)(lim) >> 28, 0, 0, 1, 1,                \
        (unsigned) (base) >> 24                            \
    }

選擇子

線性地址部分的選擇子是用來選擇哪個描述符表和在該表中索引一個描述符的。選擇子可以做爲指針變量的一部分,從而對應用程序員是可見的,但是一般是由連接加載器來設置的。

  • 索引(Index):在描述符表中從8192個描述符中選擇一個描述符。處理器自動將這個索引值乘以8(描述符的長度),再加上描述符表的基址來索引描述符表,從而選出一個合適的描述符。

  • 表指示位(Table Indicator,TI):選擇應該訪問哪一個描述符表。0代表應該訪問全局

  • 描述符表(GDT),1代表應該訪問局部描述符表(LDT)。 請求特權級(Requested Privilege Level,RPL):保護機制,在後續試驗中會進一步講解。

全局描述符表的第一項是不能被CPU使用,所以當一個段選擇子的索引(Index)部分和表指示位(Table Indicator)都爲0的時(即段選擇子指向全局描述符表的第一項時),可以當做一個空的選擇子(見mmu.h中的SEG_NULL)。當一個段寄存器被加載一個空選擇子時,處理器並不會產生一個異常。但是,當用一個空選擇子去訪問內存時,則會產生異常

實驗過程

先不急着回答問題,我們先弄明白我們的程序是怎麼跑的,不然做了半天不白搭嘛

查看kern/init下的init.c文件代碼:

int kern_init(void) {
    extern char edata[], end[];
    memset(edata, 0, end - edata);

    cons_init();                // 初始化控制檯

    const char *message = "(THU.CST) os is loading ...";
    cprintf("%s\n\n", message);

    print_kerninfo();

    grade_backtrace();

    /* 我們上次的實驗完成到這裏 */
    
    pmm_init();                 // 初始化物理內存管理

    pic_init();                 // 初始化中斷控制器(外設:8259A)
    idt_init();                 // 初始化中斷描述符表 

    clock_init();               // 初始化時鐘中斷
    intr_enable();              // 使能irq中斷

    // 下面是挑戰內容,先不管
    //LAB1: CAHLLENGE 1 If you try to do it, uncomment lab1_switch_test()
    // user/kernel mode switch test
    //lab1_switch_test();

    /* do nothing */
    while (1);
}

好,現在來看問題!

問題1

中斷描述符表(也可簡稱爲保護模式下的中斷向量表)中一個表項佔多少字節?其中哪幾位代表中斷處理代碼的入口?

打開kern下mm的mmu.c文件

/* 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
};

首先確認總字節:16+16+5+3+4+1+2+1+16 = 64 bit = 8byte

所以,中斷向量表的一個表項佔用8字節,其中2-3字節是段選擇子,0-1字節和6-7字節拼成位移,兩者聯合便是中斷處理程序的入口地址。

  • 使用段選擇符中的偏移值在GDT(全局描述符表) 或 LDT(局部描述符表)中定位相應的段描述符。
  • 利用段描述符校驗段的訪問權限和範圍,以確保該段是可以訪問的並且偏移量位於段界限內。
  • 利用段描述符中取得的段基地址加上偏移量,形成一個線性地址。

問題2

請編程完善kern/trap/trap.c中對中斷向量表進行初始化的函數idt_init。在idt_init函數中,依次對所有中斷入口進行初始化。使用mmu.h中的SETGATE宏,填充idt數組內容。每個中斷的入口由tools/vectors.c生成,使用trap.c中聲明的vectors數組即可。

一樣不急,先看函數給的說明:

  • 每個中斷服務程序(ISR,Interrupt Service Routine)的入口地址在哪?

    • 所有ISR的入口地址都存儲在_ _vector中。 uintptr_t __vectors []在哪?
    • _ _vectors []位於kern / trap / vector.S中,由tools / vector.c產生
    • lab1中嘗試“ make”命令,然後您將在kern / trap 中找到vector.S
    • 您可以使用“ extern uintptr_t __vectors [];” 定義此extern變量,稍後將使用它。
  • 現在,您應該在中斷描述表(IDT)中設置ISR的入口地址。您可以在此文件中看到idt [256]嗎? 是的,它是IDT! 您可以使用SETGATE宏來設置IDT的每個項目

// 是的,我看到idt了!
static struct gatedesc idt[256] = {{0}};

// -------------------------------------------------------------------

/* *
 * Set up a normal interrupt/trap gate descriptor
 *   - istrap: 1 for a trap (= exception) gate, 0 for an interrupt gate
 *   - sel: Code segment selector for interrupt/trap handler
 *   - off: Offset in code segment for interrupt/trap handler
 *   - dpl: Descriptor Privilege Level - the privilege level required
 *          for software to invoke this interrupt/trap gate explicitly
 *          using an int instruction.
 * */
#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;        \
}
  • 設置完IDT的內容後,您將使用“ lidt”指令讓CPU知道IDT在哪。
    • 您不知道這個指令的含義嗎? 上Google啊! 查看libs / x86.h瞭解更多信息。
    • 注意:lidt的參數是idt_pd。 嘗試找到它!

答:lidt指令用於把內存中的限長值和基地址操作數加載到IDTR寄存器中。

完成以下代碼:

void idt_init(void) {
    extern uintptr_t __vectors[]; 
    int i;
    for(i=0; i<sizeof(idt)/sizeof(struct gatedesc);i++){
        SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL);
    } 
    SETGATE(idt[T_SWITCH_TOK], 0, GD_KTEXT, __vectors[T_SWITCH_TOK], DPL_USER);
    lidt(&idt_pd);
}

爲什麼要SETGATE(idt[T_SWITCH_TOK], 0, GD_KTEXT, __vectors[T_SWITCH_TOK], DPL_USER);呢?

因爲在發生中斷時候,我們需要從用戶態切換到內核態,所以得留個口讓用戶態進來啊。

問題3

請編程完善trap.c中的中斷處理函數trap,在對時鐘中斷進行處理的部分填寫trap函數中處理時鐘中斷的部分,使操作系統每遇到100次時鐘中斷後,調用print_ticks子程序,向屏幕上打印一行文字”100 ticks”。

不急,看說明:

trap:處理或調度異常/中斷。

只有當trap()返回時,kern / trap / trapentry.S中的代碼將還原保存在trapframe中的舊CPU狀態,然後使用iret指令從異常中返回。

代碼:

void trap(struct trapframe *tf) {
    // 根據發生的陷阱類型進行調度
    trap_dispatch(tf);
}

所以我們要完成的是trap_dispatch函數中的其中一小段,看起來很simple啊(我猜的)!

case IRQ_OFFSET + IRQ_TIMER:
        /* LAB1 YOUR CODE : STEP 3 */
        /* handle the timer interrupt */
        /* (1) After a timer interrupt, you should record this event using a global variable (increase it), such as ticks in kern/driver/clock.c
         * (2) Every TICK_NUM cycle, you can print some info using a funciton, such as print_ticks().
         * (3) Too Simple? Yes, I think so!
         */
        break;

什麼意思呢?

  • 計時器中斷後,您應該使用全局變量記錄該事件(增加),例如,kern / driver / clock.c中的滴答聲
  • 在每個TICK_NUM週期中,您都可以使用諸如print_ticks()之類的函數來打印一些信息。
  • 太簡單了?我想也是!

上代碼:

case IRQ_OFFSET + IRQ_TIMER:
        ticks++;
        if(ticks%TICK_NUM==0){
            print_ticks();
        }
        break;

然後我們再偷偷改一下print_ticks,嘿嘿嘿

運行結果:

(THU.CST) os is loading ...

Special kernel symbols:
  entry  0x00100000 (phys)
  etext  0x00103481 (phys)
  edata  0x0010ea16 (phys)
  end    0x0010fd20 (phys)
Kernel executable memory footprint: 64KB
ebp:0x00007b08 eip:0x001009a6 args:0x00010094 0x00000000 0x00007b38 0x00100092 
    kern/debug/kdebug.c:307: print_stackframe+21
ebp:0x00007b18 eip:0x00100cad args:0x00000000 0x00000000 0x00000000 0x00007b88 
    kern/debug/kmonitor.c:125: mon_backtrace+10
ebp:0x00007b38 eip:0x00100092 args:0x00000000 0x00007b60 0xffff0000 0x00007b64 
    kern/init/init.c:48: grade_backtrace2+33
ebp:0x00007b58 eip:0x001000bb args:0x00000000 0xffff0000 0x00007b84 0x00000029 
    kern/init/init.c:53: grade_backtrace1+38
ebp:0x00007b78 eip:0x001000d9 args:0x00000000 0x00100000 0xffff0000 0x0000001d 
    kern/init/init.c:58: grade_backtrace0+23
ebp:0x00007b98 eip:0x001000fe args:0x001034bc 0x001034a0 0x0000130a 0x00000000 
    kern/init/init.c:63: grade_backtrace+34
ebp:0x00007bc8 eip:0x00100055 args:0x00000000 0x00000000 0x00000000 0x00010094 
    kern/init/init.c:28: kern_init+84
ebp:0x00007bf8 eip:0x00007d68 args:0xc031fcfa 0xc08ed88e 0x64e4d08e 0xfa7502a8 
    <unknow>: -- 0x00007d67 --
++ setup timer interrupts
New Rap New Star, MDSB!
New Rap New Star, MDSB!
New Rap New Star, MDSB!
New Rap New Star, MDSB!
New Rap New Star, MDSB!
New Rap New Star, MDSB!
New Rap New Star, MDSB!

New Rap New Star, MDSB!

好了好了不開玩笑,看看分數

moocos-> make grade
Check Output:            (2.3s)
  -check ring 0:                             WRONG
   !! error: missing '0: @ring 0'
   !! error: missing '0:  cs = 8'
   !! error: missing '0:  ds = 10'
   !! error: missing '0:  es = 10'
   !! error: missing '0:  ss = 10'

  -check switch to ring 3:                   WRONG
   !! error: missing '+++ switch to  user  mode +++'
   !! error: missing '1: @ring 3'
   !! error: missing '1:  cs = 1b'
   !! error: missing '1:  ds = 23'
   !! error: missing '1:  es = 23'
   !! error: missing '1:  ss = 23'

  -check switch to ring 0:                   WRONG
   !! error: missing '+++ switch to kernel mode +++'
   !! error: missing '2: @ring 0'
   !! error: missing '2:  cs = 8'
   !! error: missing '2:  ds = 10'
   !! error: missing '2:  es = 10'
   !! error: missing '2:  ss = 10'

  -check ticks:                              OK
Total Score: 10/40
make: *** [grade] Error 1

10/40…夠了夠了!知足了!後面切換用戶態內核態的,確實有點難啊我擦,戰術性放棄!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章