Axure只是做網(wǎng)站嗎怎么注冊一個(gè)自己的網(wǎng)址
實(shí)驗(yàn)一:系統(tǒng)軟件啟動(dòng)過程
參考
重要文件
調(diào)用順序
1. boot/bootasm.S | bootasm.asm(修改了名字,以便于彩色顯示)a. 開啟A20 16位地址線 實(shí)現(xiàn) 20位地址訪問 芯片版本兼容通過寫 鍵盤控制器8042 的 64h端口 與 60h端口。b. 加載GDT全局描述符 lgdt gdtdescc. 使能和進(jìn)入保護(hù)模式 置位 cr0寄存器的 PE位 (內(nèi)存分段訪問) PE+PG(分頁機(jī)制)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個(gè)扇區(qū)大小的 os 數(shù)據(jù)。b. 將輸入讀入 到 內(nèi)存中以 進(jìn)程(程序)塊 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)用關(guān)系 切換到保護(hù)模式,啟用分段機(jī)制grade_backtrace();d. 初始化物理內(nèi)存管理pmm_init(); // init physical memory management kernel/mm/ppm.c--->gdt_init(); // 初始化默認(rèn)的全局描述符表e. 初始化中斷控制器,pic_init(); // 初始化 8259A 中斷控制器 kernel/driver/picirq.cf. 設(shè)置中斷描述符表idt_init(); // kernel/trap/trap.c // __vectors[] 來對應(yīng)中斷描述符表中的256個(gè)中斷符 tools/vector.c中g. 初始化時(shí)鐘中斷,使能整個(gè)系統(tǒng)的中斷機(jī)制 8253定時(shí)器 clock_init(); // 10ms 時(shí)鐘中斷(1s中斷100次) kernel/driver/clock.c----> pic_enable(IRQ_TIMER);// 使能定時(shí)器中斷 h. 使能整個(gè)系統(tǒng)的中斷機(jī)制 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 時(shí)鐘中斷處理 case IRQ_TIMER:if((ticks++)%100==0) print_ticks();//向終端打印時(shí)間信息(1s打印一次)b. 串口1 中斷 case IRQ_COM1: 獲取串口字符后打印c. 鍵盤中斷 case IRQ_KBD: 獲取鍵盤字符后打印d. 用戶切換中斷
實(shí)驗(yàn)?zāi)康?#xff1a;
操作系統(tǒng)是一個(gè)軟件,也需要通過某種機(jī)制加載并運(yùn)行它。
在這里我們將通過另外一個(gè)更加簡單的軟件-bootloader來完成這些工作。
為此,我們需要完成一個(gè)能夠切換到x86的保護(hù)模式并顯示字符的bootloader,為啟動(dòng)操作系統(tǒng)ucore做準(zhǔn)備。
lab1提供了一個(gè)非常小的bootloader和ucore OS,
整個(gè)bootloader執(zhí)行代碼小于512個(gè)字節(jié),這樣才能放到硬盤的主引導(dǎo)扇區(qū)中。
通過分析和實(shí)現(xiàn)這個(gè)bootloader和ucore OS,讀者可以了解到:
1. 計(jì)算機(jī)原理
CPU的編址與尋址: 基于分段機(jī)制的內(nèi)存管理CPU的中斷機(jī)制外設(shè):串口/并口/CGA,時(shí)鐘,硬盤
2. Bootloader軟件
編譯運(yùn)行bootloader的過程調(diào)試bootloader的方法PC啟動(dòng)bootloader的過程ELF執(zhí)行文件的格式和加載外設(shè)訪問:讀硬盤,在CGA上顯示字符串
3. ucore OS軟件
編譯運(yùn)行ucore OS的過程ucore OS的啟動(dòng)過程調(diào)試ucore OS的方法函數(shù)調(diào)用關(guān)系:在匯編級了解函數(shù)調(diào)用棧的結(jié)構(gòu)和處理過程中斷管理:與軟件相關(guān)的中斷處理外設(shè)管理:時(shí)鐘
實(shí)驗(yàn)內(nèi)容:
lab1中包含一個(gè)bootloader和一個(gè)OS。
這個(gè)bootloader可以切換到X86保護(hù)模式,能夠讀磁盤并加載ELF執(zhí)行文件格式,并顯示字符。
而這lab1中的OS只是一個(gè)可以處理時(shí)鐘中斷和顯示字符的幼兒園級別OS。
項(xiàng)目組成
lab1的整體目錄結(jié)構(gòu)如下所示:
>>> tree.├── bin // =======編譯后生成======================================│ ├── bootblock // 是引導(dǎo)區(qū)│ ├── kernel // 是操作系統(tǒng)內(nèi)核│ ├── sign // 用于生成一個(gè)符合規(guī)范的硬盤主引導(dǎo)扇區(qū)│ └── ucore.img // ucore.img 通過dd指令,將上面我們生成的 bootblock 和 kernel 的ELF文件拷貝到ucore.img├── boot // =======bootloader 代碼=================================│ ├── asm.h // 是bootasm.S匯編文件所需要的頭文件, 是一些與X86保護(hù)模式的段訪問方式相關(guān)的宏定義.│ ├── bootasm.S // 0. 定義了最先執(zhí)行的函數(shù)start,部分初始化,從實(shí)模式切換到保護(hù)模式,調(diào)用bootmain.c中的bootmain函數(shù)│ └── bootmain.c // 1. 實(shí)現(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)錯(cuò)誤后調(diào)用 內(nèi)核監(jiān)視器kernel monitor│ │ ├── kdebug.c // 提供源碼和二進(jìn)制對應(yīng)關(guān)系的查詢功能,用于顯示調(diào)用棧關(guān)系。│ │ ├── kdebug.h // 其中補(bǔ)全print_stackframe函數(shù)是需要完成的練習(xí)。其他實(shí)現(xiàn)部分不必深究。│ │ ├── kmonitor.c // 實(shí)現(xiàn)提供動(dòng)態(tài)分析命令的kernel monitor,便于在ucore出現(xiàn)bug或問題后,│ │ ├── kmonitor.h // 能夠進(jìn)入kernel monitor中,查看當(dāng)前調(diào)用關(guān)系。│ │ ├── panic.c // 內(nèi)核錯(cuò)誤(Kernel panic)是指操作系統(tǒng)在監(jiān)測到內(nèi)部的致命錯(cuò)誤,│ │ └── stab.h│ ├── driver //驅(qū)動(dòng)==========================================================│ │ ├── clock.c // 實(shí)現(xiàn)了對時(shí)鐘控制器8253的初始化操作 系統(tǒng)時(shí)鐘 │ │ ├── clock.h │ │ ├── console.c // 實(shí)現(xiàn)了對串口和鍵盤的中斷方式的處理操作 串口命令行終端│ │ ├── console.h│ │ ├── intr.c // 實(shí)現(xiàn)了通過設(shè)置CPU的eflags來屏蔽和使能中斷的函數(shù)│ │ ├── intr.h│ │ ├── kbdreg.h // │ │ ├── picirq.c // 實(shí)現(xiàn)了對中斷控制器8259A的初始化和使能操作 │ │ └── picirq.h│ ├── init // 系統(tǒng)初始化======================================================│ │ └── init.c // ucore操作系統(tǒng)的初始化啟動(dòng)代碼│ ├── libs│ │ ├── readline.c│ │ └── stdio.c│ ├── mm // 內(nèi)存管理 Memory management========================================│ │ ├── memlayout.h // 操作系統(tǒng)有關(guān)段管理(段描述符編號、段號等)的一些宏定義│ │ ├── mmu.h // 內(nèi)存管理單元硬件 Memory Management Unit 將線性地址映射為物理地址,包括EFLAGS寄存器等段定義│ │ ├── pmm.c // 設(shè)定了ucore操作系統(tǒng)在段機(jī)制中要用到的全局變量│ │ └── pmm.h // 任務(wù)狀態(tài)段ts,全局描述符表 gdt[],加載gdt的函數(shù)lgdt, 初始化函數(shù)gdt_init│ └── trap // 陷阱trap 異常exception 中斷interrupt 中斷處理部分=================│ ├── trap.c // 緊接著第二步初步處理后,繼續(xù)完成具體的各種中斷處理操作;│ ├── trapentry.S // 緊接著第一步初步處理后,進(jìn)一步完成第二步初步處理;| | // 并且有恢復(fù)中斷上下文的處理,即中斷處理完畢后的返回準(zhǔn)備工作;│ ├── trap.h // 緊接著第二步初步處理后,繼續(xù)完成具體的各種中斷處理操作;│ └── vectors.S // 包括256個(gè)中斷服務(wù)例程的入口地址和第一步初步處理實(shí)現(xiàn)。| // 此文件是由tools/vector.c在編譯ucore期間動(dòng)態(tài)生成的├── libs // 公共庫部分===========================================================│ ├── defs.h // 包含一些無符號整型的縮寫定義│ ├── elf.h│ ├── error.h│ ├── printfmt.c│ ├── stdarg.h // argument 參數(shù)│ ├── stdio.h // 標(biāo)志輸入輸出 io│ ├── string.c│ ├── string.h│ └── x86.h // 一些用GNU C嵌入式匯編實(shí)現(xiàn)的C函數(shù)├── Makefile // 指導(dǎo)make完成整個(gè)軟件項(xiàng)目的編譯,清除等工作。└── tools // 工具部分============================================================├── function.mk // mk模塊 指導(dǎo)make完成整個(gè)軟件項(xiàng)目的編譯,清除等工作。├── gdbinit // gnu debugger 調(diào)試├── grade.sh├── kernel.ld├── sign.c // 一個(gè)C語言小程序,是輔助工具,用于生成一個(gè)符合規(guī)范的硬盤主引導(dǎo)扇區(qū)。| // 規(guī)范的硬盤主引導(dǎo)扇區(qū)大小為512字節(jié),結(jié)束符為0x55AA| // obj/bootblock.out( <= 500 ) + 0x55AA -> bootblock(512字節(jié))└── vector.c // 生成vectors.S 中斷服務(wù)例程的入口地址和第一步初步處理實(shí)現(xiàn)
cpu mmu 內(nèi)存 關(guān)系
頁地址映射
中斷異常陷阱
編譯
cd lab1/
make # 變量定義 ( = or := )
# 其中 = 和 := 的區(qū)別在于,
# := 只能使用前面定義好的變量, = 可以使用后面定義的變量
# +=變量追加值 SRCS += programD.c
# +=變量追加值
# 命令前綴
# 前綴 @ :: 只輸出命令執(zhí)行的結(jié)果, 出錯(cuò)的話停止執(zhí)行
# 前綴 - :: 命令執(zhí)行有錯(cuò)的話, 忽略錯(cuò)誤, 繼續(xù)執(zhí)行
makefile文件見
直接在硬件模擬器上運(yùn)行
硬件模擬器qemu的安裝
make qemu
1. 從CPU加電后執(zhí)行的第一條指令開始,單步跟蹤BIOS的執(zhí)行
單步調(diào)試和查看BIOS代碼如果你是想看BIOS的匯編,可試試如下方法: 練習(xí)2可以單步跟蹤,方法如下:1. 修改 lab1/tools/gdbinit,set architecture i8086target remote :12342. 在 lab1目錄下,執(zhí)行make debug這時(shí)gdb停在BIOS的第一條指令處:
0xffff0: ljmp $0xf000,$0xe05b3 在看到gdb的調(diào)試界面(gdb)后,執(zhí)行如下命令,就可以看到BIOS在執(zhí)行了
si
si
...
4 此時(shí)的CS=0xf000, EIP=0xfff0,如果想看BIOS的代碼x/2i 0xffff0應(yīng)該可以看到0xffff0: ljmp $0xf000,$0xe05b0xffff5: xor %dh,0x322f
進(jìn)一步可以執(zhí)行x/10i 0xfe05b
可以看到后續(xù)的BIOS代碼。首先在CPU加電之后,CPU里面的ROM存儲器會將其里面保存的初始值傳給各個(gè)寄存器,
其中CS:IP = 0Xf000 : fff0(CS:代碼段寄存器;IP:指令寄存器),
這個(gè)值決定了我們從內(nèi)存中讀數(shù)據(jù)的位置,PC = 16*CS + IP。
此時(shí)系統(tǒng)處于實(shí)模式,并且截止到目前為止系統(tǒng)的總線還不是我們平常的32位,
這時(shí)的地址總線只有20位,所以地址空間的總大小只有1M,
而我們的BIOS啟動(dòng)固件就在這個(gè)1M的空間里面。BIOS啟動(dòng)固件需要提供以下的一些功能:☆基本輸入輸出的程序☆系統(tǒng)設(shè)置信息☆開機(jī)后自檢程序☆系統(tǒng)自啟動(dòng)程序在此我們需要找到CPU加電之后的第一條指令的位置,然后在這里break,單步跟蹤BIOS的執(zhí)行,
根據(jù)PC = 16*CS + IP,我們可以得到PC = 0xffff0,
所以BIOS的第一條指令的位置為0xffff0(在這里因?yàn)榇藭r(shí)我們的地址空間只有20位,所以是0xffff0)。在這里我們利用 make debug來觀察BIOS的單步執(zhí)行,
所以我們首先通過Makefile文件來查看make debug的相關(guān)操作:# Makefile
# 利用make debug來觀察BIOS的單步執(zhí)行
# 首先是對qemu進(jìn)行的操作
# sleep 2 等待一段時(shí)間
# 針對gdbinit文件進(jìn)行的調(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可以看到,電腦在運(yùn)行到kern_init是會觸發(fā)break,然后又緊接著在下一步continue,
執(zhí)行調(diào)試
這里需要安裝 cgdb:
sudo apt-get install cgdb
開始調(diào)試:
make debug會出現(xiàn)一個(gè)新的終端,分為上下兩個(gè)窗口,上面的窗口顯示運(yùn)行到的源碼,下面的窗口是gdb調(diào)試界面。
由上面的分析可知:BIOS的第一條指令的位置為0xffff0
**查看 pc指針地址 對應(yīng)的指令 前強(qiáng)制反匯編當(dāng)前的指令 **
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,開始進(jìn)行一系列的操作。在截圖中我們看到pc:0xfff0,這是因?yàn)樵趚86的機(jī)器里面并沒有pc這個(gè)寄存器,所謂的pc值是通過CS:IP而得到的,因此這里的PC所代表的是eip寄存器里面的值,低 16位的值。
設(shè)置斷點(diǎn):
在gdb命令行中,使用b *[地址]便可以在指定內(nèi)存地址設(shè)置斷點(diǎn),當(dāng)qemu中的cpu執(zhí)行到指定地址時(shí),便會將控制權(quán)交給gdb。n/s都是C語言級的斷點(diǎn)定位。 s會進(jìn)入C函數(shù)內(nèi)部,但是不會進(jìn)入沒有定位信息的函數(shù)(比如沒有加-g編譯的代碼,因?yàn)槠錄]有C代碼的行數(shù)標(biāo)記,沒辦法定位),n不會。
ni/si都是匯編級別的斷點(diǎn)定位。si會進(jìn)入?yún)R編和C函數(shù)內(nèi)部,ni不會
歸納:當(dāng)要進(jìn)入沒有調(diào)試信息的庫函數(shù)調(diào)試的時(shí)候,用si是唯一的方法。 當(dāng)進(jìn)入有調(diào)試信息的函數(shù),用si和s都可以,但是他們不同,si是定位到匯編級別的第一個(gè)語句,但是s是進(jìn)入到C級別的第一個(gè)語句.
gdb的單步命令:
next 單步到程序源代碼的下一行,不進(jìn)入函數(shù)。
nexti 單步一條機(jī)器指令,不進(jìn)入函數(shù)。
step 單步到下一個(gè)不同的源代碼行(包括進(jìn)入函數(shù))。
stepi 單步一條機(jī)器指令。
2. bootloader進(jìn)入保護(hù)模式的過程
我們最重要的是要理解三個(gè)問題:
1、為何要開啟A20,以及如何開啟A20;
2、如何初始化GDT表;
3、如何使能和進(jìn)入保護(hù)模式。
1、為何要開啟A20,以及如何開啟A20
首先關(guān)于A20,我們通過查詢資料以及說明文檔可以知道早期的8086CPU所提供的地址線只有20位,
所以可尋址空間為0~2^20(1MB),但是8086的數(shù)據(jù)處理位寬16位,無法直接訪問1M的地址空間,
所以8086提供了段地址加偏移地址的轉(zhuǎn)換機(jī)制。
PC的尋址結(jié)構(gòu)是segment:offset,segment和offset都是16位寄存器,
最大值是0ffffh,所以換算成物理地址的計(jì)算方法是把segment左移4位,再加上offset,
所以segment:offset所能表示的最大為10ffefh,而這個(gè)地址超過了1M,
但是超過1M會發(fā)生“回卷”的現(xiàn)象不會報(bào)錯(cuò),
但是從下一代的80286開始,地址線成為了24位,所能訪問的地址空間超過了1M,
此時(shí)尋址超過1M時(shí)會報(bào)錯(cuò),出現(xiàn)了向下不兼容,所以為了解決這個(gè)問題采用了A20機(jī)制。A20 Gate,將A20地址線控制器 和 鍵盤控制器的一個(gè)輸出進(jìn)行AND操作,
這樣來控制A20地址線的打開與關(guān)閉,所以在實(shí)模式下,需要確保A20開關(guān)處于關(guān)閉狀態(tài),這樣可以防止訪問大于1M的地址空間,但
是在保護(hù)模式下,我們需要訪問更大的內(nèi)存空間,所以需要將A20的開關(guān)打開,如果在保護(hù)模式下,A20的開關(guān)未打開的話,此時(shí)我們只能訪問奇數(shù)兆的內(nèi)存,
即只能訪問0—1M,2—3M,4—5M……,所以如果我們要進(jìn)入保護(hù)模式,首先就需要把A20開關(guān)給打開。
2、如何初始化GDT表
接下來我們需要了解下GDT表(全局描述符表),在整個(gè)操作系統(tǒng)中我們只有一張GDT表,
GDT可以放在內(nèi)存的任意位置,但是CPU必須知道GDT的入口,
在Intel里面有一個(gè)專門的寄存器GDTR用來存放GDT的入口地址,
程序員將GDT設(shè)定在內(nèi)存的某個(gè)位置之后,
可以通過LGDT指令將GDT的入口地址加載到該寄存器里面,
以后CPU就可以通過GDTR來訪問GDT了。
3、如何使能和進(jìn)入保護(hù)模式
最后我們需要了解如何 使能 和 進(jìn)入 保護(hù)模式,
關(guān)于這一點(diǎn)我們需要了解一個(gè)寄存器CR0,首先我們來看下CR0寄存器的各個(gè)位代表什么:
在這里由于我們需要進(jìn)入保護(hù)模式,所以暫時(shí)可以先不用管其他的位,只需關(guān)注最低位的PE即可,
PE是啟用保護(hù)位(protection enable),當(dāng)設(shè)置該位的時(shí)候即開啟了保護(hù)模式,
系統(tǒng)上電復(fù)位的時(shí)候該位默認(rèn)為0,于是便是實(shí)模式;
當(dāng)PE置1的時(shí)候,進(jìn)入保護(hù)模式,實(shí)質(zhì)上是開啟了段級保護(hù),只是進(jìn)行了分段,沒有開啟分頁機(jī)制,
如果要開啟分頁機(jī)制的話我們需要同時(shí)置位PE和PG。有了初步了解之后我們便知道的開啟保護(hù)模式的相關(guān)操作,
首先開啟A20 Gate,其次加載全局描述符表GDT,最后只需要將CR0寄存器的最低位置為1即可。接下來我們通過觀察代碼來查看UCore具體是如何實(shí)現(xiàn)相應(yīng)的操作的:
# 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開關(guān)。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進(jìn)行控制,
我們的A20所對應(yīng)的是8042里面的P21引腳,所以問題就變成了我們需要將P21引腳置1。對于8042芯片來說,有兩個(gè)端口地址60h和64h。
對于這兩個(gè)端口來說,
0x64用來發(fā)送一個(gè)鍵盤控制命令,
0x60用來傳遞參數(shù),所以將P21引腳置1的操作就變成了,
我們首先利用0x64端口傳遞一個(gè)寫入的指令,然后由0x60端口讀進(jìn)去相應(yīng)的參數(shù)來將P21置1。
由以下的資料我們可以知道,
我們首先要先向 64h 發(fā)送 0xd1 的指令,寫之前需要等待鍵盤控制器8042空閑,
可以通過判斷 64h端口中的狀態(tài)寄存器的值0x2,來判斷是否空閑。
然后向 60h 發(fā)送 0xdf 的指令。
在這里可能有人會有疑問,既然我們只需要將P21置為1就可以了,
那么我們是不是可以傳入多種不同的參數(shù),只需要對應(yīng)的位為1就好了,答案是不行的。我們傳入的0xdf參數(shù)在這里也相當(dāng)于一條指令,通過這條指令我們可以將A20的開關(guān)打開。
在這里我們還需要注意一個(gè)問題就是當(dāng)前端口(60h或者64h)是否空閑,
只有當(dāng)這兩個(gè)端口空閑的時(shí)候我們才可以向其傳入數(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開關(guān),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# 使能和進(jìn)入保護(hù)模式 movl %cr0, %eax # 首先將cr0寄存器里面的內(nèi)容取出來orl $CR0_PE_ON, %eax # 進(jìn)行一個(gè)或操作, PE是啟用保護(hù)位(protection enable),當(dāng)設(shè)置該位的時(shí)候即開啟了保護(hù)模式movl %eax, %cr0 # 最后將得到的結(jié)果再寫回 cr0中 # 只是進(jìn)行了分段,沒有開啟分頁機(jī)制,如果要開啟分頁機(jī)制的話我們需要同時(shí)置位PE和PG。# Jump to next instruction, but in 32-bit code segment.# Switches processor into 32-bit mode.# 3 最后通過一個(gè)長跳轉(zhuǎn)指令正式進(jìn)入保護(hù)模式。ljmp $PROT_MODE_CSEG, $protcseg.code32 # Assemble for 32-bit mode
protcseg:# Set up the protected-mode data segment registers 初始化保護(hù)模式的數(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中,進(jìn)行 加載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全局描述符表由三個(gè)全局描述符組成SEG_NULLASM # null seg 第一個(gè)均為空描述符SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) # code seg for bootloader and kernel 第二個(gè)為代碼段描述符SEG_ASM(STA_W, 0x0, 0xffffffff) # data seg for bootloader and kernel 第三個(gè)為數(shù)據(jù)段描述符# gdt全局描述符表 它里面有兩個(gè)參數(shù)
gdtdesc:.word 0x17 # 表示的是GDT表的大小 # sizeof(gdt) - 1.long gdt # 表示的是GDT表的入口地址 # address gdt
3. 分析bootloader加載ELF格式的OS的過程。
上面 boot/bootasm.S 的末尾,切換到保護(hù)模式,初始化一些段寄存器后,
會調(diào)用bootmain函數(shù)在bootmain.c中 ,進(jìn)行 加載ELF格式的操作系統(tǒng)OS。
ELF文件格式
讀取elf
背景知識
對于硬盤來說,我們知道是分成許多扇區(qū)的其中每個(gè)扇區(qū)的大小為512字節(jié)。
讀取扇區(qū)的流程我們通過查詢指導(dǎo)書可以看到:1、等待磁盤準(zhǔn)備好;2、發(fā)出讀取扇區(qū)的命令;3、等待磁盤準(zhǔn)備好;4、把磁盤扇區(qū)數(shù)據(jù)讀到指定內(nèi)存。 接下來我們需要了解下如何具體的從硬盤讀取數(shù)據(jù),
因?yàn)槲覀兯x取的操作系統(tǒng)文件是存在0號硬盤上的,
所以,我們來看一下關(guān)于0號硬盤的I/O端口:
在這里我們可以看到,對于0號硬盤的讀取操作是通過一系列的寄存器完成的,
所以在讀取硬盤時(shí)我們也是通過對這些硬盤進(jìn)行操作從而得到相應(yīng)的數(shù)據(jù)。
通過上面對硬盤知識的一些了解之后,
我們開始觀察具體的實(shí)現(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進(jìn)行判斷,觀察是否是一個(gè)合法的ELF頭3. 通過循環(huán)讀取每個(gè)段,并且將每個(gè)段讀入相應(yīng)的虛存p_va 程序塊中4. 最后調(diào)用ELF header表頭中的內(nèi)核入口地址, 實(shí)現(xiàn) 內(nèi)核鏈接地址 轉(zhuǎn)化為 加載地址,無返回值。*/
#define SECTSIZE 512 // 一個(gè)扇區(qū)的大小
#define ELFHDR ((struct elfhdr *)0x10000)// scratch space 虛擬地址va(virtual address)/* 等待磁盤準(zhǔn)備好
waitdisk - wait for disk ready */
static void
waitdisk(void) {while ((inb(0x1F7) & 0xC0) != 0x40)//判斷磁盤寄存器的狀態(tài)為標(biāo)志
// 0x1F7 0號硬盤 讀時(shí)狀態(tài)寄存器
// 0xC0 = 0x11000000 最高兩位為1
// 0x40 = 0x01000000 01表示空閑
// 檢查0x1F7的最高兩位,如果是01,那么證明磁盤準(zhǔn)備就緒,跳出循環(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、等待磁盤準(zhǔn)備好outb(0x1F2, 1); // 設(shè)置需要讀取得參數(shù) 扇區(qū)計(jì)數(shù) count = 1outb(0x1F3, secno & 0xFF);// 即讀取相應(yīng)的內(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、等待磁盤準(zhǔn)備好; // 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文件* 第一個(gè)參數(shù)是一個(gè)虛擬地址va (virtual address),起始地址* 第二個(gè)是count(我們所要讀取的數(shù)據(jù)的大小 512*8),* SECTSIZE的定義我們通過追蹤可以看到是512,即一個(gè)扇區(qū)的大小* 第三個(gè)是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;// 存儲我們需要讀取的磁盤的位置// 通過一個(gè)for循環(huán)一次從磁盤中讀取一個(gè)整塊for (; va < end_va; va += SECTSIZE, secno ++) {// 繼續(xù)對虛存va和secno進(jìn)行自加操作,直到讀完所需讀的東西為止。readsect((void *)va, secno);// 磁盤中讀取一個(gè)整塊 存到相應(yīng)的虛存va中}
}/* bootmain - the entry of bootloader
bootmain 函數(shù) 加載 elf格式的os
操作系統(tǒng)文件是存在0號硬盤上的
讀取扇區(qū)的流程我們通過查詢指導(dǎo)書可以看到:
1、等待磁盤準(zhǔn)備好;
2、發(fā)出讀取扇區(qū)的命令;
3、等待磁盤準(zhǔn)備好;
4、把磁盤扇區(qū)數(shù)據(jù)讀到指定內(nèi)存。
*/
void
bootmain(void) {// read the 1st page off disk// 從0號硬盤上讀入os文件// 第一個(gè)參數(shù)是一個(gè)虛擬地址va(virtual address),// 第二個(gè)是count(我們所要讀取的數(shù)據(jù)的大小 512*8),// 第三個(gè)是offset(偏移量)// SECTSIZE的定義我們通過追蹤可以看到是512,即一個(gè)扇區(qū)的大小
// 1. 將一些OS的ELF文件從硬盤中讀到內(nèi)存的ELFHDR里面 格式在elf.h中定義readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);// 調(diào)用readseg函數(shù)從ELFHDR處讀取8個(gè)扇區(qū)的大小。// is this a valid ELF?
// 2. 在加載操作開始之前我們需要對ELFHDR進(jìn)行判斷,觀察是否是一個(gè)合法的ELF頭if (ELFHDR->e_magic != ELF_MAGIC) {goto bad;//加載到錯(cuò)誤得操作系統(tǒng), 跳轉(zhuǎn)到}struct proghdr *ph, *eph;//內(nèi)存中 進(jìn)程(程序)塊 的存儲方式, 在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)讀取每個(gè)段,并且將每個(gè)段讀入相應(yīng)的虛存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)核入口地址, 實(shí)現(xiàn) 內(nèi)核鏈接地址 轉(zhuǎn)化為 加載地址,無返回值。((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();bad:outw(0x8A00, 0x8A00);outw(0x8A00, 0x8E00);/* do nothing */while (1);
}
4. 實(shí)現(xiàn)函數(shù)調(diào)用堆棧跟蹤函數(shù)
我們需要實(shí)現(xiàn)函數(shù)調(diào)用堆棧,因此我們需要首先針對函數(shù)堆棧的操作做一些相關(guān)的了解,對于函數(shù)堆棧來說可以分為以下三部分操作:1、首先保存原相關(guān)寄存器的狀態(tài),即將相關(guān)參數(shù)以及寄存器的當(dāng)前狀態(tài)壓入棧;2、其次在棧中進(jìn)行函數(shù)操作,即完成函數(shù)的相關(guān)功能;3、最后釋放??臻g,回復(fù)原寄存器狀態(tài)。要實(shí)現(xiàn)以上的相關(guān)操作我們就需要對函數(shù)棧的結(jié)構(gòu)有相關(guān)的了解:
堆棧是地址逆生長.
在每次進(jìn)行函數(shù)調(diào)用的時(shí)候,
首先會將函數(shù)的參數(shù) 自右向左 壓入棧中,
所以從圖中我們可以看到參數(shù)的順序是從參數(shù)3到參數(shù)1,
然后將返回地址壓入棧,即下條指令的地址壓入棧,
接著把原ebp的值壓入棧,便于稍后的恢復(fù)。
對于上一層ebp的壓入 和 返回地址 和是通過兩句匯編實(shí)現(xiàn)的:
pushl %ebp # 上一層的ebp的壓入movl %esp, %ebp # %ebp 指向%esp棧頂(存儲的是上一層的ebp,也就是返回地址)
當(dāng)我們傳完參數(shù)時(shí),我們進(jìn)行push操作,將原ebp的值壓入棧,
此時(shí)我們的ebp寄存器所指的位置是上一層ebp的位置,
然后通過一個(gè)movl操作將返回地址壓入對應(yīng)的棧,便實(shí)現(xiàn)了對函數(shù)棧的搭建。所以一般而言,
ss:[ebp+4]處為返回地址,
ss:[ebp+8]處為第一個(gè)參數(shù)值(最后一個(gè)入棧的參數(shù)值,此處假設(shè)其占用4字節(jié)內(nèi)存),
ss:[ebp-4]處為第一個(gè)局部變量,
ss:[ebp]處為上一層ebp值。
由于ebp中的地址處總是“上一層函數(shù)調(diào)用時(shí)的ebp值”,
而在每一層函數(shù)調(diào)用中,都能通過當(dāng)時(shí)的ebp值“向上(棧底方向)”能獲取返回地址、參數(shù)值,
“向下(棧頂方向)”能獲取函數(shù)局部變量值。最后在函數(shù)調(diào)用結(jié)束后我們只需要將ebp還原,并且跳轉(zhuǎn)到返回地址即可。
接下來我們來觀察具體實(shí)現(xiàn)的代碼:我們需要在lab1中完成kernel/kdebug.c中函數(shù)print_stackframe的實(shí)現(xiàn),
可以通過函數(shù)print_stackframe來跟蹤函數(shù)調(diào)用堆棧中記錄的返回地址。
在如果能夠正確實(shí)現(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.首先通過兩個(gè)函數(shù)得到寄存器ebp和eip的值,并存到變量里。
uint32_t ebp = read_ebp();
// 2. eip的值 存儲返回地址
uint32_t eip = read_eip();
// 3. 通過一個(gè)for循環(huán)來循環(huán)輸出棧內(nèi)的相關(guān)參數(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ù) 第一個(gè)參數(shù)存在ebp+8的位置uint32_t* args = (uint32_t*)ebp + 2;//這里的2代表兩個(gè)整形數(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ù)組的操作實(shí)現(xiàn)具體功能。ebp = ((uint32_t*)ebp)[0];eip = ((uint32_t*)ebp)[1];}
}
5. 完善中斷初始化和處理
a. 中斷描述符表
中斷描述符表(也可簡稱為保護(hù)模式下的中斷向量表)中一個(gè)表項(xiàng)占多少字節(jié)?其中哪幾位代表中斷處理代碼的入口?
中斷描述符表一個(gè)表項(xiàng)占8字節(jié)。
其中0~15位和48~63位分別為offset的低16位和高16位。
16~31位為段選擇子。
通過段選擇子獲得段基址,加上段內(nèi)偏移量即可得到中斷處理代碼的入口。
b. 完善kern/trap/trap.c中對中斷向量表進(jìn)行初始化的函數(shù)idt_init。
在idt_init函數(shù)中,依次對所有中斷入口進(jìn)行初始化。
使用mmu.h中的SETGATE宏,填充中斷描述符表(idt, interrupt description table)數(shù)組內(nèi)容。
每個(gè)中斷的入口由tools/vectors.c生成,
使用trap.c中聲明的 vectors數(shù)組即可。我們需要對所有的中斷入口進(jìn)行初始化,在這里我們首先需要對中斷有一個(gè)大概的了解.
在操作系統(tǒng)中,有三種特殊的中斷事件, 中斷 (interrupt) , 異常(exception), 陷入中斷(trap interrupt)。1. 由CPU外部設(shè)備引起的外部事件如I/O中斷、時(shí)鐘中斷、控制臺中斷等是異步產(chǎn)生的(即產(chǎn)生的時(shí)刻不確定),與CPU的執(zhí)行無關(guān),我們稱之為異步中斷(asynchronous interrupt)也稱外部中斷,簡稱中斷 (interrupt)。
2. 把在CPU執(zhí)行指令期間檢測到不正常的或非法的條件(如除零錯(cuò)、地址訪問越界)所引起的內(nèi)部事件,稱作同步中斷(synchronous interrupt),也稱內(nèi)部中斷,簡稱異常(exception)。
3. 把在程序中使用請求系統(tǒng)服務(wù)的系統(tǒng)調(diào)用而引發(fā)的事件, 稱作陷入中斷(trap interrupt),也稱軟中斷(soft interrupt),系統(tǒng)調(diào)用(system call)簡稱trap。對于中斷描述符表idt來說把每個(gè) 中斷或異常的編號 和 一個(gè)指向中斷服務(wù)例程 的 描述符聯(lián)系起來。同GDT(全局描述符表,地址映射)一樣,IDT(中斷描述符表)是一個(gè)8字節(jié)的描述符數(shù)組,
但I(xiàn)DT的第一項(xiàng)可以包含一個(gè)描述符。
CPU把中斷(異常)號乘以8做為 IDT的索引。
IDT可以位于內(nèi)存的任意位置,CPU通過IDT寄存器(IDTR)的內(nèi)容來尋址IDT的起始地址。
指令LIDT和SIDT用來操作IDTR。
兩條指令都有一個(gè)顯示的操作數(shù):一個(gè)6字節(jié)表示的內(nèi)存地址。 根據(jù)中斷的分類我們可以了解到,我們在進(jìn)行初始化時(shí)是需要對 中斷進(jìn)行分類處理的,
針對不同的權(quán)限進(jìn)行不同的初始化,因此我們在編寫代碼時(shí)需要注意 內(nèi)核權(quán)限 和 用戶權(quán)限的區(qū)分,
通過指導(dǎo)書我們可以了解到,對于我們的UCore來說只有從 用戶態(tài) 轉(zhuǎn)化為 內(nèi)核態(tài) 時(shí)權(quán)限是 用戶權(quán)限,
所以我們在進(jìn)行初始化時(shí)只需要將這一點(diǎn)拿出來單獨(dú)初始化即可。
kern/trap/trap.c中對中斷向量表進(jìn)行初始化的函數(shù)idt_init
void
idt_init(void) {
// 1. 聲明__vectors[] 來對應(yīng)中斷描述符表中的256個(gè)中斷符 tools/vector.c中extern uintptr_t __vectors[];// 代碼段偏移量
// 2. 通過for循環(huán)運(yùn)用SETGATE宏定義函數(shù)(類似c++ inline內(nèi)連函數(shù)) 進(jìn)行 中斷門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) 的中斷表進(jìn)行初始化了,
// 和上面的不同之處只是在于特權(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 中關(guān)于時(shí)鐘中斷得處理
使操作系統(tǒng)每遇到100次時(shí)鐘中斷后,調(diào)用print_ticks子程序,向屏幕上打印一行文字”100 ticks”。
在上面我們已經(jīng)將idt中斷向量符表完成了初始化的操作,所以我們在這里可以直接對其進(jìn)行調(diào)用即可,
在這里我們需要了解下調(diào)用中斷的一個(gè)大體流程:
中斷描述符表 idt 是一個(gè)表項(xiàng)占用8字節(jié),
其中2-3字節(jié)是段選擇子,
0-1字節(jié)和6-7字節(jié)拼成位移,
兩者聯(lián)合便是中斷處理程序的入口地址。
我們可以看到當(dāng)出發(fā)了中斷之后,
我們可以通過IDTR寄存器來查找到相應(yīng)的中斷號,
我們可以
通過 IDT.base + 8*n 可以找到相應(yīng)中斷的地址,然
后跳轉(zhuǎn)到具體中斷的執(zhí)行程序中就可以完成中斷處理。
所以在第三問我們需要調(diào)用時(shí)鐘中斷,并且完成對于時(shí)鐘中斷的相關(guān)操作。
trap_dispatch()
#define TICK_NUM 100 // 每隔多長時(shí)間打印時(shí)間信息// 打印 時(shí)鐘信息
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
}// 各種中斷響應(yīng)處理
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.記錄時(shí)鐘中斷ticks++; // 定義在 kern/driver/clock.c 中// 2. 判斷 ticks的狀態(tài), 執(zhí)行相應(yīng)的操作if(ticks% TICK_NUM == 0)// TICK_NUM 本文件最上面 為100 {// 每當(dāng)ticks計(jì)數(shù)達(dá)到100時(shí),即出發(fā)了100次時(shí)鐘中斷后,時(shí)鐘中斷會print“100 ticks”。print_ticks();//向終端打印時(shí)間信息 }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");}}
}