Axure只是做網(wǎng)站嗎怎么注冊一個自己的網(wǎng)址
實驗一:系統(tǒng)軟件啟動過程
參考
重要文件
調(diào)用順序
1. boot/bootasm.S | bootasm.asm(修改了名字,以便于彩色顯示)a. 開啟A20 16位地址線 實現(xiàn) 20位地址訪問 芯片版本兼容通過寫 鍵盤控制器8042 的 64h端口 與 60h端口。b. 加載GDT全局描述符 lgdt gdtdescc. 使能和進入保護模式 置位 cr0寄存器的 PE位 (內(nèi)存分段訪問) PE+PG(分頁機制)movl %cr0, %eax orl $CR0_PE_ON, %eax 或操作,置位 PE位 movl %eax, %cr0d. 調(diào)用載入系統(tǒng)的函數(shù) call bootmain # 轉(zhuǎn)而調(diào)用 bootmain.c 2. boot/bootmain.c -> bootmain 函數(shù)a. 調(diào)用readseg函數(shù)從ELFHDR處讀取8個扇區(qū)大小的 os 數(shù)據(jù)。b. 將輸入讀入 到 內(nèi)存中以 進程(程序)塊 proghdr 的方式存儲c. 跳到ucore操作系統(tǒng)在內(nèi)存中的入口位置(kern/init.c中的kern_init函數(shù)的起始地址)3. kern/init.ca. 初始化終端 cons_init(); init the console kernel/driver/consore.c顯示器初始化 cga_init(); 串口初始化 serial_init(); keyboard鍵盤初始化 kbd_init();b. 打印內(nèi)核信息 & 歡迎信息 print_kerninfo(); // 內(nèi)核信息 kernel/debug/kdebug.ccprintf("%s\n\n", message);// 歡迎信息 const char *message = “qwert”c. 顯示堆棧中的多層函數(shù)調(diào)用關系 切換到保護模式,啟用分段機制grade_backtrace();d. 初始化物理內(nèi)存管理pmm_init(); // init physical memory management kernel/mm/ppm.c--->gdt_init(); // 初始化默認的全局描述符表e. 初始化中斷控制器,pic_init(); // 初始化 8259A 中斷控制器 kernel/driver/picirq.cf. 設置中斷描述符表idt_init(); // kernel/trap/trap.c // __vectors[] 來對應中斷描述符表中的256個中斷符 tools/vector.c中g. 初始化時鐘中斷,使能整個系統(tǒng)的中斷機制 8253定時器 clock_init(); // 10ms 時鐘中斷(1s中斷100次) kernel/driver/clock.c----> pic_enable(IRQ_TIMER);// 使能定時器中斷 h. 使能整個系統(tǒng)的中斷機制 enable irq interruptintr_enable(); // kernel/driver/intr.c// sti(); // set interrupt // x86.hi. lab1_switch_test();// 用戶切換函數(shù) 會 觸發(fā)中斷用戶切換中斷4. kernel/trap/trap.c trap中斷(陷阱)處理函數(shù)trap() ---> trap_dispatch() // kernel/trap/trap.c a. 10ms 時鐘中斷處理 case IRQ_TIMER:if((ticks++)%100==0) print_ticks();//向終端打印時間信息(1s打印一次)b. 串口1 中斷 case IRQ_COM1: 獲取串口字符后打印c. 鍵盤中斷 case IRQ_KBD: 獲取鍵盤字符后打印d. 用戶切換中斷
實驗目的:
操作系統(tǒng)是一個軟件,也需要通過某種機制加載并運行它。
在這里我們將通過另外一個更加簡單的軟件-bootloader來完成這些工作。
為此,我們需要完成一個能夠切換到x86的保護模式并顯示字符的bootloader,為啟動操作系統(tǒng)ucore做準備。
lab1提供了一個非常小的bootloader和ucore OS,
整個bootloader執(zhí)行代碼小于512個字節(jié),這樣才能放到硬盤的主引導扇區(qū)中。
通過分析和實現(xiàn)這個bootloader和ucore OS,讀者可以了解到:
1. 計算機原理
CPU的編址與尋址: 基于分段機制的內(nèi)存管理CPU的中斷機制外設:串口/并口/CGA,時鐘,硬盤
2. Bootloader軟件
編譯運行bootloader的過程調(diào)試bootloader的方法PC啟動bootloader的過程ELF執(zhí)行文件的格式和加載外設訪問:讀硬盤,在CGA上顯示字符串
3. ucore OS軟件
編譯運行ucore OS的過程ucore OS的啟動過程調(diào)試ucore OS的方法函數(shù)調(diào)用關系:在匯編級了解函數(shù)調(diào)用棧的結(jié)構(gòu)和處理過程中斷管理:與軟件相關的中斷處理外設管理:時鐘
實驗內(nèi)容:
lab1中包含一個bootloader和一個OS。
這個bootloader可以切換到X86保護模式,能夠讀磁盤并加載ELF執(zhí)行文件格式,并顯示字符。
而這lab1中的OS只是一個可以處理時鐘中斷和顯示字符的幼兒園級別OS。
項目組成
lab1的整體目錄結(jié)構(gòu)如下所示:
>>> tree.├── bin // =======編譯后生成======================================│ ├── bootblock // 是引導區(qū)│ ├── kernel // 是操作系統(tǒng)內(nèi)核│ ├── sign // 用于生成一個符合規(guī)范的硬盤主引導扇區(qū)│ └── ucore.img // ucore.img 通過dd指令,將上面我們生成的 bootblock 和 kernel 的ELF文件拷貝到ucore.img├── boot // =======bootloader 代碼=================================│ ├── asm.h // 是bootasm.S匯編文件所需要的頭文件, 是一些與X86保護模式的段訪問方式相關的宏定義.│ ├── bootasm.S // 0. 定義了最先執(zhí)行的函數(shù)start,部分初始化,從實模式切換到保護模式,調(diào)用bootmain.c中的bootmain函數(shù)│ └── bootmain.c // 1. 實現(xiàn)了bootmain函數(shù), 通過屏幕、串口和并口顯示字符串,加載ucore操作系統(tǒng)到內(nèi)存,然后跳轉(zhuǎn)到ucore的入口處執(zhí)行.| // 生成 bootblock.out | // 由 sign.c 在最后添加 0x55AA之后生成 規(guī)范的 512字節(jié)的├── kern // =======ucore系統(tǒng)部分===================================│ ├── debug// 內(nèi)核調(diào)試部分 ==================================================│ │ ├── assert.h // 保證宏 assert宏,在發(fā)現(xiàn)錯誤后調(diào)用 內(nèi)核監(jiān)視器kernel monitor│ │ ├── kdebug.c // 提供源碼和二進制對應關系的查詢功能,用于顯示調(diào)用棧關系。│ │ ├── kdebug.h // 其中補全print_stackframe函數(shù)是需要完成的練習。其他實現(xiàn)部分不必深究。│ │ ├── kmonitor.c // 實現(xiàn)提供動態(tài)分析命令的kernel monitor,便于在ucore出現(xiàn)bug或問題后,│ │ ├── kmonitor.h // 能夠進入kernel monitor中,查看當前調(diào)用關系。│ │ ├── panic.c // 內(nèi)核錯誤(Kernel panic)是指操作系統(tǒng)在監(jiān)測到內(nèi)部的致命錯誤,│ │ └── stab.h│ ├── driver //驅(qū)動==========================================================│ │ ├── clock.c // 實現(xiàn)了對時鐘控制器8253的初始化操作 系統(tǒng)時鐘 │ │ ├── clock.h │ │ ├── console.c // 實現(xiàn)了對串口和鍵盤的中斷方式的處理操作 串口命令行終端│ │ ├── console.h│ │ ├── intr.c // 實現(xiàn)了通過設置CPU的eflags來屏蔽和使能中斷的函數(shù)│ │ ├── intr.h│ │ ├── kbdreg.h // │ │ ├── picirq.c // 實現(xiàn)了對中斷控制器8259A的初始化和使能操作 │ │ └── picirq.h│ ├── init // 系統(tǒng)初始化======================================================│ │ └── init.c // ucore操作系統(tǒng)的初始化啟動代碼│ ├── libs│ │ ├── readline.c│ │ └── stdio.c│ ├── mm // 內(nèi)存管理 Memory management========================================│ │ ├── memlayout.h // 操作系統(tǒng)有關段管理(段描述符編號、段號等)的一些宏定義│ │ ├── mmu.h // 內(nèi)存管理單元硬件 Memory Management Unit 將線性地址映射為物理地址,包括EFLAGS寄存器等段定義│ │ ├── pmm.c // 設定了ucore操作系統(tǒng)在段機制中要用到的全局變量│ │ └── pmm.h // 任務狀態(tài)段ts,全局描述符表 gdt[],加載gdt的函數(shù)lgdt, 初始化函數(shù)gdt_init│ └── trap // 陷阱trap 異常exception 中斷interrupt 中斷處理部分=================│ ├── trap.c // 緊接著第二步初步處理后,繼續(xù)完成具體的各種中斷處理操作;│ ├── trapentry.S // 緊接著第一步初步處理后,進一步完成第二步初步處理;| | // 并且有恢復中斷上下文的處理,即中斷處理完畢后的返回準備工作;│ ├── trap.h // 緊接著第二步初步處理后,繼續(xù)完成具體的各種中斷處理操作;│ └── vectors.S // 包括256個中斷服務例程的入口地址和第一步初步處理實現(xiàn)。| // 此文件是由tools/vector.c在編譯ucore期間動態(tài)生成的├── libs // 公共庫部分===========================================================│ ├── defs.h // 包含一些無符號整型的縮寫定義│ ├── elf.h│ ├── error.h│ ├── printfmt.c│ ├── stdarg.h // argument 參數(shù)│ ├── stdio.h // 標志輸入輸出 io│ ├── string.c│ ├── string.h│ └── x86.h // 一些用GNU C嵌入式匯編實現(xiàn)的C函數(shù)├── Makefile // 指導make完成整個軟件項目的編譯,清除等工作。└── tools // 工具部分============================================================├── function.mk // mk模塊 指導make完成整個軟件項目的編譯,清除等工作。├── gdbinit // gnu debugger 調(diào)試├── grade.sh├── kernel.ld├── sign.c // 一個C語言小程序,是輔助工具,用于生成一個符合規(guī)范的硬盤主引導扇區(qū)。| // 規(guī)范的硬盤主引導扇區(qū)大小為512字節(jié),結(jié)束符為0x55AA| // obj/bootblock.out( <= 500 ) + 0x55AA -> bootblock(512字節(jié))└── vector.c // 生成vectors.S 中斷服務例程的入口地址和第一步初步處理實現(xiàn)
cpu mmu 內(nèi)存 關系
頁地址映射
中斷異常陷阱
編譯
cd lab1/
make # 變量定義 ( = or := )
# 其中 = 和 := 的區(qū)別在于,
# := 只能使用前面定義好的變量, = 可以使用后面定義的變量
# +=變量追加值 SRCS += programD.c
# +=變量追加值
# 命令前綴
# 前綴 @ :: 只輸出命令執(zhí)行的結(jié)果, 出錯的話停止執(zhí)行
# 前綴 - :: 命令執(zhí)行有錯的話, 忽略錯誤, 繼續(xù)執(zhí)行
makefile文件見
直接在硬件模擬器上運行
硬件模擬器qemu的安裝
make qemu
1. 從CPU加電后執(zhí)行的第一條指令開始,單步跟蹤BIOS的執(zhí)行
單步調(diào)試和查看BIOS代碼如果你是想看BIOS的匯編,可試試如下方法: 練習2可以單步跟蹤,方法如下:1. 修改 lab1/tools/gdbinit,set architecture i8086target remote :12342. 在 lab1目錄下,執(zhí)行make debug這時gdb停在BIOS的第一條指令處:
0xffff0: ljmp $0xf000,$0xe05b3 在看到gdb的調(diào)試界面(gdb)后,執(zhí)行如下命令,就可以看到BIOS在執(zhí)行了
si
si
...
4 此時的CS=0xf000, EIP=0xfff0,如果想看BIOS的代碼x/2i 0xffff0應該可以看到0xffff0: ljmp $0xf000,$0xe05b0xffff5: xor %dh,0x322f
進一步可以執(zhí)行x/10i 0xfe05b
可以看到后續(xù)的BIOS代碼。首先在CPU加電之后,CPU里面的ROM存儲器會將其里面保存的初始值傳給各個寄存器,
其中CS:IP = 0Xf000 : fff0(CS:代碼段寄存器;IP:指令寄存器),
這個值決定了我們從內(nèi)存中讀數(shù)據(jù)的位置,PC = 16*CS + IP。
此時系統(tǒng)處于實模式,并且截止到目前為止系統(tǒng)的總線還不是我們平常的32位,
這時的地址總線只有20位,所以地址空間的總大小只有1M,
而我們的BIOS啟動固件就在這個1M的空間里面。BIOS啟動固件需要提供以下的一些功能:☆基本輸入輸出的程序☆系統(tǒng)設置信息☆開機后自檢程序☆系統(tǒng)自啟動程序在此我們需要找到CPU加電之后的第一條指令的位置,然后在這里break,單步跟蹤BIOS的執(zhí)行,
根據(jù)PC = 16*CS + IP,我們可以得到PC = 0xffff0,
所以BIOS的第一條指令的位置為0xffff0(在這里因為此時我們的地址空間只有20位,所以是0xffff0)。在這里我們利用 make debug來觀察BIOS的單步執(zhí)行,
所以我們首先通過Makefile文件來查看make debug的相關操作:# Makefile
# 利用make debug來觀察BIOS的單步執(zhí)行
# 首先是對qemu進行的操作
# sleep 2 等待一段時間
# 針對gdbinit文件進行的調(diào)試
debug: $(UCOREIMG)$(V)$(QEMU) -S -s -parallel stdio -hda $< -serial null &$(V)sleep 2$(V)$(TERMINAL) -e "gdb -q -tui -x tools/gdbinit"# tools/gdbinit
file bin/kernel
target remote :1234
break kern_init
continue可以看到,電腦在運行到kern_init是會觸發(fā)break,然后又緊接著在下一步continue,
執(zhí)行調(diào)試
這里需要安裝 cgdb:
sudo apt-get install cgdb
開始調(diào)試:
make debug會出現(xiàn)一個新的終端,分為上下兩個窗口,上面的窗口顯示運行到的源碼,下面的窗口是gdb調(diào)試界面。
由上面的分析可知:BIOS的第一條指令的位置為0xffff0
**查看 pc指針地址 對應的指令 前強制反匯編當前的指令 **
x/i $pc 顯示1行
x/10i $pc 顯示10行
我們查看 0xffff0地址內(nèi)的信息:
x/i 0xffff0>>> 0xffff0: ljmp $0x3630,$0xf000e05b可以看到,BIOS的第一條指令是一條跳轉(zhuǎn)指令 ljmp,然后程序會跳轉(zhuǎn)到0xf000e05b,開始進行一系列的操作。在截圖中我們看到pc:0xfff0,這是因為在x86的機器里面并沒有pc這個寄存器,所謂的pc值是通過CS:IP而得到的,因此這里的PC所代表的是eip寄存器里面的值,低 16位的值。
設置斷點:
在gdb命令行中,使用b *[地址]便可以在指定內(nèi)存地址設置斷點,當qemu中的cpu執(zhí)行到指定地址時,便會將控制權(quán)交給gdb。n/s都是C語言級的斷點定位。 s會進入C函數(shù)內(nèi)部,但是不會進入沒有定位信息的函數(shù)(比如沒有加-g編譯的代碼,因為其沒有C代碼的行數(shù)標記,沒辦法定位),n不會。
ni/si都是匯編級別的斷點定位。si會進入?yún)R編和C函數(shù)內(nèi)部,ni不會
歸納:當要進入沒有調(diào)試信息的庫函數(shù)調(diào)試的時候,用si是唯一的方法。 當進入有調(diào)試信息的函數(shù),用si和s都可以,但是他們不同,si是定位到匯編級別的第一個語句,但是s是進入到C級別的第一個語句.
gdb的單步命令:
next 單步到程序源代碼的下一行,不進入函數(shù)。
nexti 單步一條機器指令,不進入函數(shù)。
step 單步到下一個不同的源代碼行(包括進入函數(shù))。
stepi 單步一條機器指令。
2. bootloader進入保護模式的過程
我們最重要的是要理解三個問題:
1、為何要開啟A20,以及如何開啟A20;
2、如何初始化GDT表;
3、如何使能和進入保護模式。
1、為何要開啟A20,以及如何開啟A20
首先關于A20,我們通過查詢資料以及說明文檔可以知道早期的8086CPU所提供的地址線只有20位,
所以可尋址空間為0~2^20(1MB),但是8086的數(shù)據(jù)處理位寬16位,無法直接訪問1M的地址空間,
所以8086提供了段地址加偏移地址的轉(zhuǎn)換機制。
PC的尋址結(jié)構(gòu)是segment:offset,segment和offset都是16位寄存器,
最大值是0ffffh,所以換算成物理地址的計算方法是把segment左移4位,再加上offset,
所以segment:offset所能表示的最大為10ffefh,而這個地址超過了1M,
但是超過1M會發(fā)生“回卷”的現(xiàn)象不會報錯,
但是從下一代的80286開始,地址線成為了24位,所能訪問的地址空間超過了1M,
此時尋址超過1M時會報錯,出現(xiàn)了向下不兼容,所以為了解決這個問題采用了A20機制。A20 Gate,將A20地址線控制器 和 鍵盤控制器的一個輸出進行AND操作,
這樣來控制A20地址線的打開與關閉,所以在實模式下,需要確保A20開關處于關閉狀態(tài),這樣可以防止訪問大于1M的地址空間,但
是在保護模式下,我們需要訪問更大的內(nèi)存空間,所以需要將A20的開關打開,如果在保護模式下,A20的開關未打開的話,此時我們只能訪問奇數(shù)兆的內(nèi)存,
即只能訪問0—1M,2—3M,4—5M……,所以如果我們要進入保護模式,首先就需要把A20開關給打開。
2、如何初始化GDT表
接下來我們需要了解下GDT表(全局描述符表),在整個操作系統(tǒng)中我們只有一張GDT表,
GDT可以放在內(nèi)存的任意位置,但是CPU必須知道GDT的入口,
在Intel里面有一個專門的寄存器GDTR用來存放GDT的入口地址,
程序員將GDT設定在內(nèi)存的某個位置之后,
可以通過LGDT指令將GDT的入口地址加載到該寄存器里面,
以后CPU就可以通過GDTR來訪問GDT了。
3、如何使能和進入保護模式
最后我們需要了解如何 使能 和 進入 保護模式,
關于這一點我們需要了解一個寄存器CR0,首先我們來看下CR0寄存器的各個位代表什么:
在這里由于我們需要進入保護模式,所以暫時可以先不用管其他的位,只需關注最低位的PE即可,
PE是啟用保護位(protection enable),當設置該位的時候即開啟了保護模式,
系統(tǒng)上電復位的時候該位默認為0,于是便是實模式;
當PE置1的時候,進入保護模式,實質(zhì)上是開啟了段級保護,只是進行了分段,沒有開啟分頁機制,
如果要開啟分頁機制的話我們需要同時置位PE和PG。有了初步了解之后我們便知道的開啟保護模式的相關操作,
首先開啟A20 Gate,其次加載全局描述符表GDT,最后只需要將CR0寄存器的最低位置為1即可。接下來我們通過觀察代碼來查看UCore具體是如何實現(xiàn)相應的操作的:
# 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 # 等待鍵盤控制器8042 0x64 端口 空閑,64h端口中的狀態(tài)寄存器的值為0x2jnz seta20.1 # 忙的話一直等待movb $0xd1, %al # 等到64h空閑之后我們會寫入0xd1 0xd1 -> port 0x64outb %al, $0x64 # 表明我們要向60h里面寫入數(shù)據(jù), 0xd1 means: write data to 8042's P2 portseta20.2:inb $0x64, %al # 等待64h端口空閑 Wait for not busy(8042 input buffer empty).testb $0x2, %al # 64h端口中的狀態(tài)寄存器的值為0x2jnz seta20.2 # 忙的話一直等待movb $0xdf, %al # 0xdf -> port 0x60 等到空閑之后,我們將0xdf寫入60h端口,至此來打開A20開關。outb %al, $0x60 # 0xdf = 11011111, means set P2's A20 bit(the 1 bit) to 1 (第20位)
首先是開啟A20,根據(jù)上文我們知道需要將第20位為1即可,
但是我們需要知道在UCore里是如何將A20置為1的。
根據(jù)說明書我們可以知道,A20地址線由鍵盤控制器8042進行控制,
我們的A20所對應的是8042里面的P21引腳,所以問題就變成了我們需要將P21引腳置1。對于8042芯片來說,有兩個端口地址60h和64h。
對于這兩個端口來說,
0x64用來發(fā)送一個鍵盤控制命令,
0x60用來傳遞參數(shù),所以將P21引腳置1的操作就變成了,
我們首先利用0x64端口傳遞一個寫入的指令,然后由0x60端口讀進去相應的參數(shù)來將P21置1。
由以下的資料我們可以知道,
我們首先要先向 64h 發(fā)送 0xd1 的指令,寫之前需要等待鍵盤控制器8042空閑,
可以通過判斷 64h端口中的狀態(tài)寄存器的值0x2,來判斷是否空閑。
然后向 60h 發(fā)送 0xdf 的指令。
在這里可能有人會有疑問,既然我們只需要將P21置為1就可以了,
那么我們是不是可以傳入多種不同的參數(shù),只需要對應的位為1就好了,答案是不行的。我們傳入的0xdf參數(shù)在這里也相當于一條指令,通過這條指令我們可以將A20的開關打開。
在這里我們還需要注意一個問題就是當前端口(60h或者64h)是否空閑,
只有當這兩個端口空閑的時候我們才可以向其傳入數(shù)據(jù)。
boot/bootasm.S
#include <asm.h># Start the CPU: switch to 32-bit protected mode, jump into C.
# The BIOS loads this code from the first sector of the hard disk into
# memory at physical address 0x7c00 and starts executing in real mode
# with %cs=0 %ip=7c00..set PROT_MODE_CSEG, 0x8 # kernel code segment selector
.set PROT_MODE_DSEG, 0x10 # kernel data segment selector
.set CR0_PE_ON, 0x1 # protected mode enable flag# start address should be 0:7c00, in real mode, the beginning address of the running bootloader
.globl start
start:
.code16 # Assemble for 16-bit modecli # Disable interruptscld # String operations increment# 初始化 Set up the important data segment registers (DS, ES, SS).xorw %ax, %ax # Segment number zeromovw %ax, %ds # -> Data Segmentmovw %ax, %es # -> Extra Segmentmovw %ax, %ss # -> Stack Segment# 1 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 # 等待鍵盤控制器8042 0x64 端口 空閑,64h端口中的狀態(tài)寄存器的值為0x2jnz seta20.1 # 忙的話一直等待movb $0xd1, %al # 等到64h空閑之后我們會寫入0xd1, 0xd1 -> port 0x64outb %al, $0x64 # 表明我們要向60h里面寫入數(shù)據(jù),0xd1 means: write data to 8042's P2 portseta20.2:inb $0x64, %al # 等待64h端口空閑, Wait for not busy(8042 input buffer empty).testb $0x2, %al # 64h端口中的狀態(tài)寄存器的值為0x2jnz seta20.2 # 忙的話一直等待movb $0xdf, %al # 等到空閑之后,我們將0xdf寫入60h端口,至此來打開A20開關,0xdf -> port 0x60outb %al, $0x60 # 0xdf = 11011111, means set P2's A20 bit(the 1 bit(第20位)) to 1# 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.# 2 load gdt 加載GDT全局描述符 在后面可以看到lgdt gdtdesc# 使能和進入保護模式 movl %cr0, %eax # 首先將cr0寄存器里面的內(nèi)容取出來orl $CR0_PE_ON, %eax # 進行一個或操作, PE是啟用保護位(protection enable),當設置該位的時候即開啟了保護模式movl %eax, %cr0 # 最后將得到的結(jié)果再寫回 cr0中 # 只是進行了分段,沒有開啟分頁機制,如果要開啟分頁機制的話我們需要同時置位PE和PG。# Jump to next instruction, but in 32-bit code segment.# Switches processor into 32-bit mode.# 3 最后通過一個長跳轉(zhuǎn)指令正式進入保護模式。ljmp $PROT_MODE_CSEG, $protcseg.code32 # Assemble for 32-bit mode
protcseg:# Set up the protected-mode data segment registers 初始化保護模式的數(shù)據(jù)段寄存器movw $PROT_MODE_DSEG, %ax # Our data segment selectormovw %ax, %ds # -> DS: Data Segmentmovw %ax, %es # -> ES: Extra Segmentmovw %ax, %fs # -> FSmovw %ax, %gs # -> GSmovw %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 # 調(diào)用bootmain函數(shù)在bootmain.c中,進行 加載ELF格式的操作系統(tǒng)OS# If bootmain returns (it shouldn't), loop.
spin:jmp spin# Bootstrap GDT
.p2align 2 # force 4 byte alignment
gdt: # GDT表的入口地址 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 第三個為數(shù)據(jù)段描述符# gdt全局描述符表 它里面有兩個參數(shù)
gdtdesc:.word 0x17 # 表示的是GDT表的大小 # sizeof(gdt) - 1.long gdt # 表示的是GDT表的入口地址 # address gdt
3. 分析bootloader加載ELF格式的OS的過程。
上面 boot/bootasm.S 的末尾,切換到保護模式,初始化一些段寄存器后,
會調(diào)用bootmain函數(shù)在bootmain.c中 ,進行 加載ELF格式的操作系統(tǒng)OS。
ELF文件格式
讀取elf
背景知識
對于硬盤來說,我們知道是分成許多扇區(qū)的其中每個扇區(qū)的大小為512字節(jié)。
讀取扇區(qū)的流程我們通過查詢指導書可以看到:1、等待磁盤準備好;2、發(fā)出讀取扇區(qū)的命令;3、等待磁盤準備好;4、把磁盤扇區(qū)數(shù)據(jù)讀到指定內(nèi)存。 接下來我們需要了解下如何具體的從硬盤讀取數(shù)據(jù),
因為我們所要讀取的操作系統(tǒng)文件是存在0號硬盤上的,
所以,我們來看一下關于0號硬盤的I/O端口:
在這里我們可以看到,對于0號硬盤的讀取操作是通過一系列的寄存器完成的,
所以在讀取硬盤時我們也是通過對這些硬盤進行操作從而得到相應的數(shù)據(jù)。
通過上面對硬盤知識的一些了解之后,
我們開始觀察具體的實現(xiàn)過程:
#include <defs.h>
#include <x86.h>
#include <elf.h>// elf文件格式定義
/*
通過bootmain函數(shù)從硬盤中 加載elf格式的 操作系統(tǒng)os 到內(nèi)存中使用程序塊方式存儲1. 將一些OS的ELF文件從硬盤中讀到內(nèi)存的ELFHDR里面 格式在elf.h中定義2. 在加載操作開始之前我們需要對ELFHDR進行判斷,觀察是否是一個合法的ELF頭3. 通過循環(huán)讀取每個段,并且將每個段讀入相應的虛存p_va 程序塊中4. 最后調(diào)用ELF header表頭中的內(nèi)核入口地址, 實現(xiàn) 內(nèi)核鏈接地址 轉(zhuǎn)化為 加載地址,無返回值。*/
#define SECTSIZE 512 // 一個扇區(qū)的大小
#define ELFHDR ((struct elfhdr *)0x10000)// scratch space 虛擬地址va(virtual address)/* 等待磁盤準備好
waitdisk - wait for disk ready */
static void
waitdisk(void) {while ((inb(0x1F7) & 0xC0) != 0x40)//判斷磁盤寄存器的狀態(tài)為標志
// 0x1F7 0號硬盤 讀時狀態(tài)寄存器
// 0xC0 = 0x11000000 最高兩位為1
// 0x40 = 0x01000000 01表示空閑
// 檢查0x1F7的最高兩位,如果是01,那么證明磁盤準備就緒,跳出循環(huán),否則繼續(xù)等待。/* do nothing */;
}/* 讀取一整塊扇區(qū)
readsect - read a single sector at @secno into @dst */
static void
readsect(void *dst, uint32_t secno) {// wait for disk to be readywaitdisk();// 1、等待磁盤準備好outb(0x1F2, 1); // 設置需要讀取得參數(shù) 扇區(qū)計數(shù) count = 1outb(0x1F3, secno & 0xFF);// 即讀取相應的內(nèi)容到寄存器里面,outb(0x1F4, (secno >> 8) & 0xFF);outb(0x1F5, (secno >> 16) & 0xFF);outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0);outb(0x1F7, 0x20); // 2、發(fā)出讀取磁盤命令 cmd 0x20 - read sectors// wait for disk to be readywaitdisk();// 3、等待磁盤準備好; // read a sectorinsl(0x1F0, dst, SECTSIZE / 4);// 4、把磁盤扇區(qū)數(shù)據(jù)讀到指定內(nèi)存。
}/* ** readseg - read @count bytes at @offset from kernel into virtual address @va,* might copy more than asked.* 從0號硬盤上讀入os文件* 第一個參數(shù)是一個虛擬地址va (virtual address),起始地址* 第二個是count(我們所要讀取的數(shù)據(jù)的大小 512*8),* SECTSIZE的定義我們通過追蹤可以看到是512,即一個扇區(qū)的大小* 第三個是offset(偏移量)* */
static void
readseg(uintptr_t va, uint32_t count, uint32_t offset) {uintptr_t end_va = va + count;//結(jié)束地址// round down to sector boundaryva -= offset % SECTSIZE;// SECTSIZE=512扇區(qū)單位長度 起始地址減去偏移 塊首地址// translate from bytes to sectors; kernel starts at sector 1uint32_t secno = (offset / SECTSIZE) + 1;// 存儲我們需要讀取的磁盤的位置// 通過一個for循環(huán)一次從磁盤中讀取一個整塊for (; va < end_va; va += SECTSIZE, secno ++) {// 繼續(xù)對虛存va和secno進行自加操作,直到讀完所需讀的東西為止。readsect((void *)va, secno);// 磁盤中讀取一個整塊 存到相應的虛存va中}
}/* bootmain - the entry of bootloader
bootmain 函數(shù) 加載 elf格式的os
操作系統(tǒng)文件是存在0號硬盤上的
讀取扇區(qū)的流程我們通過查詢指導書可以看到:
1、等待磁盤準備好;
2、發(fā)出讀取扇區(qū)的命令;
3、等待磁盤準備好;
4、把磁盤扇區(qū)數(shù)據(jù)讀到指定內(nèi)存。
*/
void
bootmain(void) {// read the 1st page off disk// 從0號硬盤上讀入os文件// 第一個參數(shù)是一個虛擬地址va(virtual address),// 第二個是count(我們所要讀取的數(shù)據(jù)的大小 512*8),// 第三個是offset(偏移量)// SECTSIZE的定義我們通過追蹤可以看到是512,即一個扇區(qū)的大小
// 1. 將一些OS的ELF文件從硬盤中讀到內(nèi)存的ELFHDR里面 格式在elf.h中定義readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);// 調(diào)用readseg函數(shù)從ELFHDR處讀取8個扇區(qū)的大小。// is this a valid ELF?
// 2. 在加載操作開始之前我們需要對ELFHDR進行判斷,觀察是否是一個合法的ELF頭if (ELFHDR->e_magic != ELF_MAGIC) {goto bad;//加載到錯誤得操作系統(tǒng), 跳轉(zhuǎn)到}struct proghdr *ph, *eph;//內(nèi)存中 進程(程序)塊 的存儲方式, 在elf.h中定義// ph表示ELF段表首地址 , eph表示ELF段表末地址// load each program segment (ignores ph flags)ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);//首地址=基地址+偏移量eph = ph + ELFHDR->e_phnum;// 末地址 = 首地址+elf大小// 3. 通過循環(huán)讀取每個段,并且將每個段讀入相應的虛存p_va 程序塊中for (; ph < eph; ph ++) {readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);}// call the entry point from the ELF header
// 4. 最后調(diào)用ELF header表頭中的內(nèi)核入口地址, 實現(xiàn) 內(nèi)核鏈接地址 轉(zhuǎn)化為 加載地址,無返回值。((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();bad:outw(0x8A00, 0x8A00);outw(0x8A00, 0x8E00);/* do nothing */while (1);
}
4. 實現(xiàn)函數(shù)調(diào)用堆棧跟蹤函數(shù)
我們需要實現(xiàn)函數(shù)調(diào)用堆棧,因此我們需要首先針對函數(shù)堆棧的操作做一些相關的了解,對于函數(shù)堆棧來說可以分為以下三部分操作:1、首先保存原相關寄存器的狀態(tài),即將相關參數(shù)以及寄存器的當前狀態(tài)壓入棧;2、其次在棧中進行函數(shù)操作,即完成函數(shù)的相關功能;3、最后釋放??臻g,回復原寄存器狀態(tài)。要實現(xiàn)以上的相關操作我們就需要對函數(shù)棧的結(jié)構(gòu)有相關的了解:
堆棧是地址逆生長.
在每次進行函數(shù)調(diào)用的時候,
首先會將函數(shù)的參數(shù) 自右向左 壓入棧中,
所以從圖中我們可以看到參數(shù)的順序是從參數(shù)3到參數(shù)1,
然后將返回地址壓入棧,即下條指令的地址壓入棧,
接著把原ebp的值壓入棧,便于稍后的恢復。
對于上一層ebp的壓入 和 返回地址 和是通過兩句匯編實現(xiàn)的:
pushl %ebp # 上一層的ebp的壓入movl %esp, %ebp # %ebp 指向%esp棧頂(存儲的是上一層的ebp,也就是返回地址)
當我們傳完參數(shù)時,我們進行push操作,將原ebp的值壓入棧,
此時我們的ebp寄存器所指的位置是上一層ebp的位置,
然后通過一個movl操作將返回地址壓入對應的棧,便實現(xiàn)了對函數(shù)棧的搭建。所以一般而言,
ss:[ebp+4]處為返回地址,
ss:[ebp+8]處為第一個參數(shù)值(最后一個入棧的參數(shù)值,此處假設其占用4字節(jié)內(nèi)存),
ss:[ebp-4]處為第一個局部變量,
ss:[ebp]處為上一層ebp值。
由于ebp中的地址處總是“上一層函數(shù)調(diào)用時的ebp值”,
而在每一層函數(shù)調(diào)用中,都能通過當時的ebp值“向上(棧底方向)”能獲取返回地址、參數(shù)值,
“向下(棧頂方向)”能獲取函數(shù)局部變量值。最后在函數(shù)調(diào)用結(jié)束后我們只需要將ebp還原,并且跳轉(zhuǎn)到返回地址即可。
接下來我們來觀察具體實現(xiàn)的代碼:我們需要在lab1中完成kernel/kdebug.c中函數(shù)print_stackframe的實現(xiàn),
可以通過函數(shù)print_stackframe來跟蹤函數(shù)調(diào)用堆棧中記錄的返回地址。
在如果能夠正確實現(xiàn)此函數(shù),
可在lab1中執(zhí)行 “make qemu”后,
在qemu模擬器中得到類似如下的輸出:
……
ebp:0x00007b08 eip:0x001009a6 args:0x00010094 0x00000000 0x00007b38 0x00100092 kern/debug/kdebug.c:308: print_stackframe+21
kernel/kdebug.c 最后的 print_stackframe() 函數(shù) 打印函數(shù)堆棧調(diào)用信息
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]*/
// 1.首先通過兩個函數(shù)得到寄存器ebp和eip的值,并存到變量里。
uint32_t ebp = read_ebp();
// 2. eip的值 存儲返回地址
uint32_t eip = read_eip();
// 3. 通過一個for循環(huán)來循環(huán)輸出棧內(nèi)的相關參數(shù)
//for(int i = 0; ebp !=0 && i < STACKFRAME_DEPTH; i++)
int i,j;
for(i = 0; ebp !=0 && i < STACKFRAME_DEPTH; i++)
// 這里 變量定義需要在最上面, 這里在中間定義,是 c99 才支持的{// 3.1打印ebp的值cprintf("ebp:0x%08x eip:0x%08x args:", ebp, eip);// cprintf()打印 格式%08x// 3.2 打印函數(shù)參數(shù) 第一個參數(shù)存在ebp+8的位置uint32_t* args = (uint32_t*)ebp + 2;//這里的2代表兩個整形數(shù)地址范圍也就是2*4=8// uint32_t* args =(uint32_t*)(ebp + 8);for( j =0; j<4; j++){cprintf("0x%08x ",args[j]);//打印函數(shù)的調(diào)用參數(shù) 參數(shù)1 參數(shù)2 參數(shù)3 參數(shù)4}// 3.3 打印換行符號cprintf("\n");// 打印換行符號// (3.4) print_debuginfo(eip-1);// 3.5 原ebp的值就存在ebp的位置,eip的值存在ebp+4的位置,所以在這里通過數(shù)組的操作實現(xiàn)具體功能。ebp = ((uint32_t*)ebp)[0];eip = ((uint32_t*)ebp)[1];}
}
5. 完善中斷初始化和處理
a. 中斷描述符表
中斷描述符表(也可簡稱為保護模式下的中斷向量表)中一個表項占多少字節(jié)?其中哪幾位代表中斷處理代碼的入口?
中斷描述符表一個表項占8字節(jié)。
其中0~15位和48~63位分別為offset的低16位和高16位。
16~31位為段選擇子。
通過段選擇子獲得段基址,加上段內(nèi)偏移量即可得到中斷處理代碼的入口。
b. 完善kern/trap/trap.c中對中斷向量表進行初始化的函數(shù)idt_init。
在idt_init函數(shù)中,依次對所有中斷入口進行初始化。
使用mmu.h中的SETGATE宏,填充中斷描述符表(idt, interrupt description table)數(shù)組內(nèi)容。
每個中斷的入口由tools/vectors.c生成,
使用trap.c中聲明的 vectors數(shù)組即可。我們需要對所有的中斷入口進行初始化,在這里我們首先需要對中斷有一個大概的了解.
在操作系統(tǒng)中,有三種特殊的中斷事件, 中斷 (interrupt) , 異常(exception), 陷入中斷(trap interrupt)。1. 由CPU外部設備引起的外部事件如I/O中斷、時鐘中斷、控制臺中斷等是異步產(chǎn)生的(即產(chǎn)生的時刻不確定),與CPU的執(zhí)行無關,我們稱之為異步中斷(asynchronous interrupt)也稱外部中斷,簡稱中斷 (interrupt)。
2. 把在CPU執(zhí)行指令期間檢測到不正常的或非法的條件(如除零錯、地址訪問越界)所引起的內(nèi)部事件,稱作同步中斷(synchronous interrupt),也稱內(nèi)部中斷,簡稱異常(exception)。
3. 把在程序中使用請求系統(tǒng)服務的系統(tǒng)調(diào)用而引發(fā)的事件, 稱作陷入中斷(trap interrupt),也稱軟中斷(soft interrupt),系統(tǒng)調(diào)用(system call)簡稱trap。對于中斷描述符表idt來說把每個 中斷或異常的編號 和 一個指向中斷服務例程 的 描述符聯(lián)系起來。同GDT(全局描述符表,地址映射)一樣,IDT(中斷描述符表)是一個8字節(jié)的描述符數(shù)組,
但IDT的第一項可以包含一個描述符。
CPU把中斷(異常)號乘以8做為 IDT的索引。
IDT可以位于內(nèi)存的任意位置,CPU通過IDT寄存器(IDTR)的內(nèi)容來尋址IDT的起始地址。
指令LIDT和SIDT用來操作IDTR。
兩條指令都有一個顯示的操作數(shù):一個6字節(jié)表示的內(nèi)存地址。 根據(jù)中斷的分類我們可以了解到,我們在進行初始化時是需要對 中斷進行分類處理的,
針對不同的權(quán)限進行不同的初始化,因此我們在編寫代碼時需要注意 內(nèi)核權(quán)限 和 用戶權(quán)限的區(qū)分,
通過指導書我們可以了解到,對于我們的UCore來說只有從 用戶態(tài) 轉(zhuǎn)化為 內(nèi)核態(tài) 時權(quán)限是 用戶權(quán)限,
所以我們在進行初始化時只需要將這一點拿出來單獨初始化即可。
kern/trap/trap.c中對中斷向量表進行初始化的函數(shù)idt_init
void
idt_init(void) {
// 1. 聲明__vectors[] 來對應中斷描述符表中的256個中斷符 tools/vector.c中extern uintptr_t __vectors[];// 代碼段偏移量
// 2. 通過for循環(huán)運用SETGATE宏定義函數(shù)(類似c++ inline內(nèi)連函數(shù)) 進行 中斷門idt[i] 的初始化// 在kernel/mm/mmu.h中 #define SETGATE(gate, istrap, sel, off, dpl) {}int i;for(i = 0; i<sizeof(idt)/sizeof(struct gatedesc); i++){// 0 中斷門 1 陷阱門 G// D_KTEXT 內(nèi)核 代碼段起始地址 在kernel/mm/memlayout.h中// __vectors[i] 偏移地址// DPL_KERNEL 內(nèi)核權(quán)限 DPL_USER 用戶權(quán)限 在kernel/mm/memlayout.h中SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL);}
// 需要對 用戶態(tài) 轉(zhuǎn) 內(nèi)核態(tài) 的中斷表進行初始化了,
// 和上面的不同之處只是在于特權(quán)值的不同,所以我們的操作如下:
// T_SWITCH_TOK 121 trap.h 中SETGATE(idt[T_SWITCH_TOK], 0, GD_KTEXT, __vectors[T_SWITCH_TOK], DPL_USER);// 3. 最后加載idt中斷描述符表 libs/x86.h
// 將 &idt_pd 首地址 加載到 中斷描述符表寄存器 (IDTR)lidt(&idt_pd);
}
c. 完善trap.c中的中斷處理函數(shù)trap_dispatch 中關于時鐘中斷得處理
使操作系統(tǒng)每遇到100次時鐘中斷后,調(diào)用print_ticks子程序,向屏幕上打印一行文字”100 ticks”。
在上面我們已經(jīng)將idt中斷向量符表完成了初始化的操作,所以我們在這里可以直接對其進行調(diào)用即可,
在這里我們需要了解下調(diào)用中斷的一個大體流程:
中斷描述符表 idt 是一個表項占用8字節(jié),
其中2-3字節(jié)是段選擇子,
0-1字節(jié)和6-7字節(jié)拼成位移,
兩者聯(lián)合便是中斷處理程序的入口地址。
我們可以看到當出發(fā)了中斷之后,
我們可以通過IDTR寄存器來查找到相應的中斷號,
我們可以
通過 IDT.base + 8*n 可以找到相應中斷的地址,然
后跳轉(zhuǎn)到具體中斷的執(zhí)行程序中就可以完成中斷處理。
所以在第三問我們需要調(diào)用時鐘中斷,并且完成對于時鐘中斷的相關操作。
trap_dispatch()
#define TICK_NUM 100 // 每隔多長時間打印時間信息// 打印 時鐘信息
static void print_ticks() {cprintf("hello linux, times %d ticks\n",TICK_NUM);
#ifdef DEBUG_GRADEcprintf("End of Test.\n");panic("EOT: kernel seems ok.");
#endif
}// 各種中斷響應處理
static void
trap_dispatch(struct trapframe *tf) {char c;switch (tf->tf_trapno) {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!*/// 1.記錄時鐘中斷ticks++; // 定義在 kern/driver/clock.c 中// 2. 判斷 ticks的狀態(tài), 執(zhí)行相應的操作if(ticks% TICK_NUM == 0)// TICK_NUM 本文件最上面 為100 {// 每當ticks計數(shù)達到100時,即出發(fā)了100次時鐘中斷后,時鐘中斷會print“100 ticks”。print_ticks();//向終端打印時間信息 }break;case IRQ_OFFSET + IRQ_COM1:// 串口1 中斷c = cons_getc();cprintf("serial [%03d] %c\n", c, c);break;case IRQ_OFFSET + IRQ_KBD://鍵盤中斷c = cons_getc();cprintf("kbd [%03d] %c\n", c, c);cprintf("keyboard interrupt\n");break;//LAB1 CHALLENGE 1 : YOUR CODE you should modify below codes.case T_SWITCH_TOU://用戶切換 用戶到內(nèi)核panic("T_SWITCH_** ??\n");break;case T_SWITCH_TOK:panic("T_SWITCH_** ??\n");break;case IRQ_OFFSET + IRQ_IDE1:case IRQ_OFFSET + IRQ_IDE2:/* do nothing */break;default:// in kernel, it must be a mistakeif ((tf->tf_cs & 3) == 0) {print_trapframe(tf);panic("unexpected trap in kernel.\n");}}
}