靜態分析代碼。編程
實驗的目錄結構以下:數組
.
├── boot
├── kern
│ ├── debug
│ ├── driver
│ ├── init
│ ├── libs
│ ├── mm
│ └── trap
├── libs
└── tools函數
其中./boot
裏面是bootloader的相關代碼;
./kern
裏面是操做系統的相關代碼;
./toos/sign.c
描述了怎樣把bootloader變成一個規範的主引導扇區。工具
操做系統鏡像文件ucore.img是如何一步一步生成的?(須要比較詳細地解釋Makefile中每一條相關命令和命令參數的含義,以及說明命令致使的結果)測試
輸入 make V=
命令,make工具便把目錄下的文件進行了編譯。經過設置V=
參數,把編譯過程打印了下來。大體以下:ui
gcc
命令,把./kern
目錄下的代碼都編譯成obj/kern/*/*.o
文件;ld
命令經過/tools/kern.ls
文件配置,把obj/kern/*/*.o
文件鏈接成bin/kern
;gcc
命令,把boot
目錄下的文件編譯成obj/boot/*.o
文件;gcc
把tools/sign.c
編譯成obj/sign/tools/sign.o
;ld
把obj/boot/*.o
鏈接成obj/bootblock.o
;obj/bootblock.o
文件規範化爲,符合規範的硬盤住引導扇區的文件bin/bootblock
dd
命令建立了一個bin/ucore.img
文件;dd
命令把bin/bootblock
寫入bin/ucore.img
文件;dd
命令創bin/kernel
寫入bin/ucore.img
文件。命令及參數解釋:編碼
gcc
: Linux下的C語言編譯器。操作系統
ld
:把必定量的目標文件跟檔案文件鏈接起來,並重定位它們的數據,鏈接符號引用。通常,在編譯一個程序時,最後一步就是運行'ld'。debug
用法:指針
ld [option] [objs...]
參數:
-o:指定輸出文件名; -e:指定程序的入口符號。 -m: 指定鏈接器 -N: 指定 可讀寫 的 正文 和 數據 節(section). 若是 輸出格式 支持 Unix 風格的 幻數(magic number), 則 輸出文件 標記爲 OMAGIC.當 使用 `-N' 選項 時, linker 不作數據段 的 頁對齊(page-align). -e: 設置程序開端 -T: 等同於 -c 告訴 ld 從指定文件中讀取鏈接命令.
dd
:用指定大小的塊拷貝一個文件,並在拷貝的同時進行指定的轉換。
參數註釋:
/dev/zero
: 是一個輸入設備,你可你用它來初始化文件。該設備無窮盡地提供0(是ASCII 0 就是NULL),
一個被系統認爲是符合規範的硬盤主引導扇區的特徵是什麼?
問題一種提到,bootloader.o
文件通過sign.o的操做後,變成符合規範的引導文件。因此,咱們先來看看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); // 若是文件長度大於510,則報錯退出 if (st.st_size > 510) { fprintf(stderr, "%lld >> 510!!\n", (long long)st.st_size); return -1; } // 申請一個512長度的buf數組,並初始化爲0 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數組的最後兩位置爲 0x55, 0xAA 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; }
上面這段代碼作的事情除了參數校驗之外,就是把源文件讀到長度512字節的buf
數組裏,而後給最後兩字節賦值爲了0x55和0xAA。
因此,咱們能夠猜想主引導扇區的規則以下:
網上搜了下資料,說
結束標誌(佔2個字節)其值爲AA55,存儲時低位在前,高位在後,即看上去是55AA(十六進制)。
從CPU加電後執行的第一條指令開始,單步跟蹤BIOS的執行。
執行make debug
命令,啓動qemu和gdb開始debug。
而後在gdb中輸入b *0x7c00
,在內存0x7c00處設置斷點。
continue
讓程序繼續執行,程序會在前面設置的0x7c00的斷點處停下來。
輸入x /10i $pc
查看接下來的10條指令,獲得以下輸出:
=> 0x7c00: cli
0x7c01: cld
0x7c02: xor %eax,%eax
0x7c04: mov %eax,%ds
0x7c06: mov %eax,%es
0x7c08: mov %eax,%ss
0x7c0a: in $0x64,%al
0x7c0c: test $0x2,%al
0x7c0e: jne 0x7c0a
0x7c10: mov $0xd1,%al
能夠發現,這和boot/bootasm.S
文件中的內容一致。經過單步跟蹤,發現執行指令確實是bootasm.S
中的指令,大體過程以下:
cli
)cld
)上面最後一步跳轉到bootmain
中執行,接下來咱們來看下bootmain
中的執行過程:
ELFHDR->e_entry
的入口函數能夠看出上面最後調用調用ELFHDR->e_entry
的入口函數,即切換到kernel
處了。
在初始化位置0x7c00設置實地址斷點,測試斷點正常。
在gdb中執行如下命令
b *0x7c00 continue
發現程序執行到0x7c00處確實停下來了,說明斷點正常。
從0x7c00開始跟蹤代碼運行,將單步跟蹤反彙編獲得的代碼與bootasm.S和 bootblock.asm進行比較。
bootblock.asm
把bootasm.S
和bootmain.c
都內容都整合到一塊兒了。
而且bootblock.asm
中每行代碼下面都帶有地址信息,和用gdb單步調試的時候基本一致。
本身找一個bootloader或內核中的代碼位置,設置斷點並進行測試。
break kern_init
分析過程詳見練習2問題一,進入保護模式的過程以下:
爲什麼開啓A20,以及如何開啓A20
爲什麼開啓A20:若不開啓A20,cpu在訪問地址空間時第20位始終會是0,這時只能訪問奇數段不能訪問偶數段;開啓A20後,cpu可訪問連續地址空間。
如何開啓A20:
如何初始化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寄存器置1
bootloader如何讀取硬盤扇區的?
讀硬盤扇區的代碼以下:
// bootmain.c /* 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); // count = 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 - read sectors // wait for disk to be ready waitdisk(); // read a sector insl(0x1F0, dst, SECTSIZE / 4); }
從outb()
能夠看出這裏是用LBA模式的PIO(Program IO)方式來訪問硬盤的。從磁盤IO地址和對應功能表
能夠看出,該函數一次只讀取一個扇區。
IO地址 | 功能 |
---|---|
0x1f0 | 讀數據,當0x1f7不爲忙狀態時,能夠讀。 |
0x1f2 | 要讀寫的扇區數,每次讀寫前,你須要代表你要讀寫幾個扇區。最小是1個扇區 |
0x1f3 | 若是是LBA模式,就是LBA參數的0-7位 |
0x1f4 | 若是是LBA模式,就是LBA參數的8-15位 |
0x1f5 | 若是是LBA模式,就是LBA參數的16-23位 |
0x1f6 | 第0~3位:若是是LBA模式就是24-27位 第4位:爲0主盤;爲1從盤 |
0x1f7 | 狀態和命令寄存器。操做時先給命令,再讀取,若是不是忙狀態就從0x1f0端口讀數據 |
其中insl
的實現以下:
// x86.h static inline void insl(uint32_t port, void *addr, int cnt) { asm volatile ( "cld;" "repne; insl;" : "=D" (addr), "=c" (cnt) : "d" (port), "0" (addr), "1" (cnt) : "memory", "cc"); }
bootloader是如何加載ELF格式的OS?
0x10000
處,並把這裏強制轉換成elfhdr
使用;e_magic
字段;咱們須要在lab1中完成kdebug.c中函數print_stackframe的實現,能夠經過函數print_stackframe來跟蹤函數調用堆棧中記錄的返回地址。
首先,能夠經過read_ebp()
和read_eip()
函數來獲取當前ebp寄存器和eip 寄存器的信息。
咱們知道在push ebp以前會先把調用參數入棧,而ebp長16位(也就是2Byte),因此(ebp+2)[0...3]
就是傳入參數。
因爲函數調用的過程會把上一層的ebp壓入棧中,因此當前ebp中存的值正是上一層的ebp。而上一層的eip 事實上已經不是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 (unit32_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] */ uint32_t ebp = read_ebp(), eip = read_eip(); for (int i = 0; i < STACKFRAME_DEPTH && ebp != 0; i++) { cprintf("ebp: 0x%08x eip: 0x%08x args:", ebp, eip); for (int ij= 0; j < 4; j++) { cprintf(" 0x%08x", ((uint32_t*)(ebp + 2))[j]); } cprintf("\n"); print_debuginfo(eip - 1); eip = *((uint32_t*) ebp + 1); ebp = *((uint32_t*) ebp); } }
執行 make qemu
獲得以下結果:
(THU.CST) os is loading ... Special kernel symbols: entry 0x00100000 (phys) etext 0x0010325f (phys) edata 0x0010ea16 (phys) end 0x0010fd20 (phys) Kernel executable memory footprint: 64KB ebp: 0x00007b38 eip: 0x00100a27 args: 0x0d210000 0x00940010 0x00940001 0x7b680001 kern/debug/kdebug.c:305: print_stackframe+21 ebp: 0x00007b48 eip: 0x00100d21 args: 0x007f0000 0x00000010 0x00000000 0x00000000 kern/debug/kmonitor.c:125: mon_backtrace+10 ebp: 0x00007b68 eip: 0x0010007f args: 0x00a10000 0x00000010 0x7b900000 0x00000000 kern/init/init.c:48: grade_backtrace2+19 ebp: 0x00007b88 eip: 0x001000a1 args: 0x00be0000 0x00000010 0x00000000 0x7bb4ffff kern/init/init.c:53: grade_backtrace1+27 ebp: 0x00007ba8 eip: 0x001000be args: 0x00df0000 0x00000010 0x00000000 0x00000010 kern/init/init.c:58: grade_backtrace0+19 ebp: 0x00007bc8 eip: 0x001000df args: 0x00500000 0x00000010 0x00000000 0x00000000 kern/init/init.c:63: grade_backtrace+26 ebp: 0x00007be8 eip: 0x00100050 args: 0x7d6e0000 0x00000000 0x00000000 0x00000000 kern/init/init.c:28: kern_init+79 ebp: 0x00007bf8 eip: 0x00007d6e args: 0x7c4f0000 0xfcfa0000 0xd88ec031 0xd08ec08e <unknow>: -- 0x00007d6d --
請完成編碼工做和回答以下問題:
- 中斷描述符表(也可簡稱爲保護模式下的中斷向量表)中一個表項佔多少字節?其中哪幾位表明中斷處理代碼的入口?
中斷描述符表的一個表項佔8字節。根據中斷類型的不一樣,其中每一個字節表明的意義也不一樣。
一個表項的結構以下:
能夠看到,其中第16到31位爲中斷例程的段選擇子,第0到15位 和 第48到63位分別爲偏移量的地位和高位。這幾個數據一塊兒決定了中斷處理代碼的入口地址。
- 請編程完善kern/trap/trap.c中對中斷向量表進行初始化的函數idt_init。在idt_init函數中,依次對全部中斷入口進行初始化。使用mmu.h中的SETGATE宏,填充idt數組內容。每一箇中斷的入口由tools/vectors.c生成,使用trap.c中聲明的vectors數組便可。
/* idt_init - initialize IDT to each of the entry points in kern/trap/vectors.S */ void idt_init(void) { // (1) 拿到外部變量 __vector extern uintptr_t __vectors[]; // (2) 使用SETGATE宏,對中斷描述符表中的每個表項進行設置 for (int i = 0; i < 256; i++) { uint16_t istrap = 0, off = 0, dpl = 3; SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL); } // set for switch from user to kernel SETGATE(idt[T_SWITCH_TOU], 0, GD_KTEXT, __vectors[T_SWITCH_TOU], DPL_USER); SETGATE(idt[T_SWITCH_TOK], 0, GD_KTEXT, __vectors[T_SWITCH_TOK], DPL_USER); // (3) 調用lidt函數,設置中斷描述符表 lidt(&idt_pd); }
- 請編程完善trap.c中的中斷處理函數trap,在對時鐘中斷進行處理的部分填寫trap函數中處理時鐘中斷的部分,使操做系統每遇到100次時鐘中斷後,調用print_ticks子程序,向屏幕上打印一行文字」100 ticks」。
在函數體頭部聲明一個靜態變量用於計數
static int32_t tick_count = 0;
而後,在時間中斷 IRQ_OFFSET + IRQ_TIMER
的case中添加判斷打印的條件:
tick_count++; if (0 == (tick_count % TICK_NUM)) { print_ticks(); }