操作系统实验报告1:ucore Lab 1
操作系統實驗報告1
實驗內容
-
閱讀 uCore 實驗項目開始文檔 (uCore Lab 0),準備實驗平臺,熟悉實驗工具。
-
uCore Lab 1:系統軟件啟動過程
(1) 編譯運行 uCore Lab 1 的工程代碼;
(2) 完成 uCore Lab 1 練習 1-4 的實驗報告;
(3) 嘗試實現 uCore Lab 1 練習 5-6 的編程作業;
(4) 思考如何實現 uCore Lab 1 擴展練習 1-2。
實驗環境
- 架構:Intel x86_64 (虛擬機)
- 操作系統:Ubuntu 20.04
- 匯編器:gas (GNU Assembler) in AT&T mode
- 編譯器:gcc
(1)編譯運行 uCore Lab 1 的工程代碼
在lab1的makefile文件目錄下, 輸入命令:
make即可編譯運行 uCore Lab 1 的工程代碼
執行截圖:
如果輸入make,程序報錯,提示make: Nothing to be done for 'TARGETS'.,那么說明文件沒有更新而且已經編譯過了,想要再次強制編譯,只要輸入make clean,然后再輸入make就可以編譯了:
(2) uCore Lab 1 練習 1-4 實驗報告
lab1 練習 1:理解通過 make 生成執行文件的過程
列出本實驗各練習中對應的 OS 原理的知識點,并說明本實驗中的實現部分如何對應和體現了原理中的基本概念和關鍵知識點。
在此練習中,大家需要通過靜態分析代碼來了解:
操作系統鏡像文件 ucore.img 是如何一步一步生成的?(需要比較詳細地解釋 Makefile 中每一條相關命令和命令參數的含義,以及說明命令導致的結果)
首先找到makefile文件中注釋為create ucore.img這一部分的內容:
# create ucore.img UCOREIMG := $(call totarget,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$(call create_target,ucore.img)UCOREIMG := $(call totarget,ucore.img)表示調用call函數生成UCOREIMG,其中call為調用call函數,totarget,ucore.img中的totarget可以在tools/function.mk中找到,定義為totarget = $(addprefix $(BINDIR)$(SLASH),$(1)),addprefix代表在前面加上,$(BINDIR)代表bin,$(SLASH)代表/,所以totarget,ucore.img的意思就是在ucore.img前面加上bin/,調用call函數生成的UCOREIMG即為bin/ucore.img。
$(UCOREIMG): $(kernel) $(bootblock)表示生成UCOREIMG首先要依賴kernel和bootblock這兩個文件的生成,這兩個文件具體的生成過程會在后面提到。
$(V)dd if=/dev/zero of=$@ count=10000表示創建一個每個塊默認為512字節,一共10000個塊,用0填充的文件,分配給UCOREIMG。if=/dev/zero代表讀取/dev/zero文件,/dev/zero文件是一個特殊的文件,讀取它會提供無限的空字符,of=$@的$@代表之前讀取的數據將會復制到的目標文件,這里是UCOREIMG,count=10000表示一共10000個塊。
$(V)dd if=$(bootblock) of=$@ conv=notrunc表示將bootblock中的內容寫到UCOREIMG的第一個塊里。conv=notrun代表寫入文件時防止截斷,保持數據完整性。
$(V)dd if=$(kernel) of=$@ seek=1 conv=notrunc表示從UCOREIMG的第二個塊開始寫kernel里的內容,seek=1代表跳過seek個塊之后再開始填寫,這里為跳過1個塊。
$(call create_target,ucore.img)直接返回。
生成UCOREIMG首先要依賴kernel和bootblock這兩個文件的生成,首先來看kernel文件的生成
找到makefile文件中注釋為kernel這一部分的內容:
KINCLUDE += kern/debug/ \kern/driver/ \kern/trap/ \kern/mm/KSRCDIR += kern/init \kern/libs \kern/debug \kern/driver \kern/trap \kern/mmKCFLAGS += $(addprefix -I,$(KINCLUDE))$(call add_files_cc,$(call listf_cc,$(KSRCDIR)),kernel,$(KCFLAGS))KOBJS = $(call read_packet,kernel libs)# create kernel target kernel = $(call totarget,kernel)$(kernel): tools/kernel.ld$(kernel): $(KOBJS)@echo + ld $@$(V)$(LD) $(LDFLAGS) -T tools/kernel.ld -o $@ $(KOBJS)@$(OBJDUMP) -S $@ > $(call asmfile,kernel)@$(OBJDUMP) -t $@ | $(SED) '1,/SYMBOL TABLE/d; s/ .* / /; /^$$/d' > $(call symfile,kernel)$(call create_target,kernel)一開始的KINCLUDE和KSRCDIR處的代碼將kern目錄的前綴定義為kinclude和ksrcdir
KCFLAGS += $(addprefix -I,$(KINCLUDE))表示將kinclude的目錄前綴加上-I選項,提供交互模式
$(call add_files_cc,$(call listf_cc,$(KSRCDIR)),kernel,$(KCFLAGS))生成kern目錄下的.o文件,這些.o文件生成時使用的具體命令的參數和方式都差不多,具體含義后面會提到。
KOBJS = $(call read_packet,kernel libs)表示使用call函數鏈接read_packet和kernel libs給KOBJS
kernel = $(call totarget,kernel)代表表示調用call函數生成kernel,實際為文件bin/kernel
$(kernel): tools/kernel.ld表示生成kernel文件需要依賴tools以及kernel.ld鏈接配置文件
$(kernel): $(KOBJS)表示生成kernel時還需要依賴KOBJS
@echo + ld $@中的echo表示顯示內容,ld代表鏈接,$@代表目標文件,語句代表將下面的文件和目標文件鏈接起來,同時打印kernel目標文件名
$(V)$(LD) $(LDFLAGS) -T tools/kernel.ld -o $@ $(KOBJS)代表使用kernel.ld作為連接器腳本,鏈接的文件有obj/libs/和obj/kernel/下的所有的obj文件生成kernel文件,關鍵參數為-T <scriptfile>,代表讓連接器使用指定的腳本,這里是kernel.ld
@$(OBJDUMP) -S $@ > $(call asmfile,kernel)代表使用objdump工具對kernel文件進行反匯編,便于調試,-S選項為交替顯示C源碼和匯編代碼。
@$(OBJDUMP) -t $@ | $(SED) '1,/SYMBOL TABLE/d; s/ .* / /; /^$$/d' > $(call symfile,kernel)代表使用objdump工具通過解析kernel文件從而能得到符號表。
$(call create_target,kernel)生成kernel直接返回
輸入make "V=",查看生成kernel文件的具體過程:
可以看到,生成kernel,首先要依賴
kernel.ld init.o stdio.o readline.o panic.o kdebug.o kmonitor.o clock.o console.o picirq.o intr.o trap.o vectors.o trapentry.o pmm.o string.o printfmt.o其中kernel.ld已經存在,而生成kernel時,makefile中帶@的前綴的指令都不是必需的,編譯選項中:
ld表示鏈接,-m表示模擬指定的連接器,-nostdlib表示不使用標準庫,-T表示讓連接器使用指定的腳本,tools/kernel.ld是指定連接器腳本,-o表示指定輸出文件的名稱。
依賴的.o文件生成時使用的具體命令的參數和方式都差不多,比如pmm.o,輸入make "V="查看編譯實際命令:
其中的關鍵參數選項:
-I<dir>如-Ikern/mm/、-Ikern/debug/等表示給搜索頭文件添加路徑
-march=i686表示指定CPU架構為i686
-fno-builtin表示除非使用__builtin_前綴,否則不優化builtin函數
-fno-PIC表示生成位置無關代碼
-Wall表示開啟所有警告
-ggdb表示生成gdb可以使用的調試信息,便于使用qemu和gdb來進行調試
-m32表示生成在32位環境下適用的代碼,因為ucore是32位的軟件
-gstabs表示生成stabs格式的調試信息,便于monitor顯示函數調用棧信息
-nostdinc表示不使用標準庫,因為OS內核是提供服務的,不依賴其它服務
-fno-stack-protector表示不生成檢測緩沖區溢出部分的代碼
然后來看bootblock文件的生成
找到makefile文件中注釋為create bootblock這一部分的內容:
# create bootblock bootfiles = $(call listf_cc,boot) $(foreach f,$(bootfiles),$(call cc_compile,$(f),$(CC),$(CFLAGS) -Os -nostdinc))bootblock = $(call totarget,bootblock)$(bootblock): $(call toobj,$(bootfiles)) | $(call totarget,sign)@echo + ld $@$(V)$(LD) $(LDFLAGS) -N -e start -Ttext 0x7C00 $^ -o $(call toobj,bootblock)@$(OBJDUMP) -S $(call objfile,bootblock) > $(call asmfile,bootblock)@$(OBJCOPY) -S -O binary $(call objfile,bootblock) $(call outfile,bootblock)@$(call totarget,sign) $(call outfile,bootblock) $(bootblock)$(call create_target,bootblock)bootfiles = $(call listf_cc,boot)中使用call調用listf_cc函數過濾對應目錄下的.c和.S文件,用boot替換listf_cc里面的變量,將listf_cc的返回值賦給bootfiles
$(foreach f,$(bootfiles),$(call cc_compile,$(f),$(CC),$(CFLAGS) -Os -nostdinc))編譯bootfiles生成.o文件,其中-Os參數表示為減小代碼大小而進行優化
上面兩行代碼用來生成bootasm.o,bootmain.o,實際的代碼是由宏批量生成。
bootblock = $(call totarget,bootblock)表示bootblock實際為文件bin/bootblock
$(bootblock): $(call toobj,$(bootfiles)) | $(call totarget,sign)其中的toobj表示給輸出參數加上前綴obj/,文件后綴名改為.o,語句表示bootblock依賴于obj/boot/*.o與bin/sign文件
@echo + ld $@代表將下面的文件和目標文件鏈接起來,同時打印kernel目標文件名
$(V)$(LD) $(LDFLAGS) -N -e start -Ttext 0x7C00 $^ -o $(call toobj,bootblock)表示鏈接所有.o文件生成obj/bootblock.o文件,其中-N代表設置代碼段和數據段均可讀寫,-e start代表指定入口為start,-Ttext 0x7C00代表代碼段開始位置為0x7C00
@$(OBJDUMP) -S $(call objfile,bootblock) > $(call asmfile,bootblock)表示使用objdump工具對obj/bootblock.o文件進行反匯編得到obj/bootblock.asm文件,便于調試,-S選項為交替顯示C源碼和匯編代碼。
@$(OBJCOPY) -S -O binary $(call objfile,bootblock) $(call outfile,bootblock)表示使用objcopy工具將obj/bootblock.o拷貝到obj/bootblock.out文件,其中-S選項代表移除所有符號和重定位信息,-O binary選項代表指定輸出格式為二進制
@$(call totarget,sign) $(call outfile,bootblock) $(bootblock)表示使用bin/sign工具將之前的obj/bootblock.out用來生成bin/bootblock目標文件
$(call create_target,bootblock)直接返回
輸入make "V=",查看生成bootblock文件的具體過程:
其中之前沒有提到過的關鍵參數有:
-N代表設置代碼段和數據段均可讀寫,-e <entry>代表指定入口,這里是start,-Ttext代表代碼段開始位置,這里是0x7C00
可以看到,生成bootblock,首先要依賴
bootasm.o bootmain.o sign生成bootasm.o依賴bootasm.S,輸入make "V=",查看生成bootasm.o文件的具體過程:
編譯命令中關鍵的參數選項有:
-I<dir>如-Iboot/、-Ilibs/等表示給搜索頭文件添加路徑
-fno-builtin表示除非使用__builtin_前綴,否則不優化builtin函數
-Wall表示開啟所有警告
-ggdb表示生成gdb可以使用的調試信息,便于使用qemu和gdb來進行調試
-m32表示生成在32位環境下適用的代碼,因為ucore是32位的軟件
-gstabs表示生成stabs格式的調試信息,便于monitor顯示函數調用棧信息
-nostdinc表示不使用標準庫,因為OS內核是提供服務的,不依賴其它服務
-fno-stack-protector表示不生成檢測緩沖區溢出部分的代碼
-Os參數表示為減小代碼大小而進行優化,因為主引導扇區只有512字節,其中最后兩位已被占用,最后寫出的bootloader不能大于510字節。
生成bootmain.o依賴bootmain.c,輸入make "V=",查看生成bootmain.o文件的具體過程:
編譯命令的過程和參數選項和上面生成bootasm.o差不多。
找到makefile文件中注釋為create 'sign' tools這一部分的內容,查看sign的生成過程:
# create 'sign' tools $(call add_files_host,tools/sign.c,sign,sign) $(call create_target_host,sign,sign)輸入make "V=",查看生成sign的具體過程:
其中和上面相比,之前沒有出現過的關鍵選項參數有:
-g代表在編譯的時候加入調試信息
-O2代表開啟O2編譯優化
一個被系統認為是符合規范的硬盤主引導扇區的特征是什么?
在sign.c文件中,可以找到以下核心代碼:
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;可以看到,代碼中char buf[512],buf[510] = 0x55,buf[511] = 0xAA,說明一個被系統認為是符合規范的硬盤主引導扇區的特征是:
- 一共512個字節
- 倒數第二個字節是0x55,倒數第一個字節是0xAA
lab1 練習 2:使用qemu執行并調試lab1中的軟件。(要求在報告中簡要寫出練習過程)
為了熟悉使用qemu和gdb進行的調試工作,我們進行如下的小練習:
1. 從CPU加電后執行的第一條指令開始,單步跟蹤BIOS的執行。
根據附錄的內容,進行單步調試和查看BIOS的代碼:
修改 lab1/tools/gdbinit,
set architecture i8086 //將執行模式設置為i8086 target remote :1234 //使用本地端口1234進行qmenu和gdb之間的通信然后在lab1的目錄下輸入make debug,出現gdb調試界面之后,輸入si單步跟蹤BIOS的執行,通過語句x /2i $pc可以顯示當前eip處的匯編指令,查看BIOS的代碼。
執行截圖:
可以看到,一開始gdb在BIOS的第一條指令處0xfff0停止。
輸入si后,可以看到gdb跳轉到下一地址處,即可單步跟蹤BIOS了,輸入x /2i $pc會顯示當前eip處的匯編指令,輸入x /2i 0xffff0即可查看0xffff0處及往下的一行代碼。
2. 在初始化位置0x7c00設置實地址斷點,測試斷點正常。
在lab1/tools/gdbinit文件中加入b *0x7c00或在gdb輸入框輸入b *0x7c00,就可以在0x7c00設置斷點。
可以看到,輸入c使程序繼續運行后,程序在0x7c00處停下,斷點正常。
3. 從0x7c00開始跟蹤代碼運行,將單步跟蹤反匯編得到的代碼與bootasm.S和 bootblock.asm進行比較。
通過改寫Makefile文件,將這部分代碼:
debug: $(UCOREIMG)$(V)$(QEMU) -S -s -parallel stdio -hda $< -serial null &$(V)sleep 2$(V)$(TERMINAL) -e "gdb -q -tui -x tools/gdbinit"改為:
debug: $(UCOREIMG)$(V)$(TERMINAL) -e "$(QEMU) -S -s -d in_asm -D $(BINDIR)/qemu.log -parallel stdio -hda $< -serial null"$(V)sleep 2$(V)$(TERMINAL) -e "gdb -q -tui -x tools/gdbinit"執行截圖:
在調用qemu的時候加上了-d in_asm -D qemu.log等參數,就可以在qemu.log里看到匯編指令(從0x00007c00處開始10行代碼):
---------------- IN: 0x00007c00: fa cli ---------------- IN: 0x00007c01: fc cld 0x00007c02: 31 c0 xorw %ax, %ax 0x00007c04: 8e d8 movw %ax, %ds 0x00007c06: 8e c0 movw %ax, %es 0x00007c08: 8e d0 movw %ax, %ss---------------- IN: 0x00007c0a: e4 64 inb $0x64, %al---------------- IN: 0x00007c0c: a8 02 testb $2, %al 0x00007c0e: 75 fa jne 0x7c0a---------------- IN: 0x00007c10: b0 d1 movb $0xd1, %al在bootasm.S中:
.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# 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, %aljnz seta20.1movb $0xd1, %al # 0xd1 -> port 0x64在bootblock.asm中:
.globl start start: .code16 # Assemble for 16-bit modecli # Disable interrupts7c00: fa cli cld # String operations increment7c01: fc cld # Set up the important data segment registers (DS, ES, SS).xorw %ax, %ax # Segment number zero7c02: 31 c0 xor %eax,%eaxmovw %ax, %ds # -> Data Segment7c04: 8e d8 mov %eax,%dsmovw %ax, %es # -> Extra Segment7c06: 8e c0 mov %eax,%esmovw %ax, %ss # -> Stack Segment7c08: 8e d0 mov %eax,%ss00007c0a <seta20.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).7c0a: e4 64 in $0x64,%altestb $0x2, %al7c0c: a8 02 test $0x2,%aljnz seta20.17c0e: 75 fa jne 7c0a <seta20.1>movb $0xd1, %al # 0xd1 -> port 0x647c10: b0 d1 mov $0xd1,%al可以看到,反匯編得到的代碼與bootasm.S和bootblock.asm基本相同。
4. 自己找一個bootloader或內核中的代碼位置,設置斷點并進行測試。
在0x7c08處設置斷點,進行測試。
執行截圖:
可以看到,輸入b *0x7c08在0x7c08設置斷點后,再輸入c使程序繼續運行后,程序在0x7c08處停下,斷點正常。
lab1 練習 3:分析bootloader 進入保護模式的過程。
BIOS將通過讀取硬盤主引導扇區到內存,并轉跳到對應內存中的位置執行bootloader。請分析bootloader是如何完成從實模式進入保護模式的。
提示:需要閱讀小節“保護模式和分段機制”和lab1/boot/bootasm.S源碼,了解如何從實模式切換到保護模式,需要了解:
- 為何開啟A20,以及如何開啟A20
- 如何初始化GDT表
- 如何使能和進入保護模式
在lab1/boot/bootasm.S文件中,可以看到文件開頭有一段注釋:
# 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.大概意思是,BIOS將此代碼從硬盤的第一個扇區加載到物理地址為0x7c00的內存中,并開始以實模式在cs=0 ip=7c00執行。
程序一開始先設置內核代碼段選擇子、內核數據段選擇子、保護模式使能標志置為1
.set PROT_MODE_CSEG, 0x8 # 內核代碼段選擇子 .set PROT_MODE_DSEG, 0x10 # 內核數據段選擇子 .set CR0_PE_ON, 0x1 # 保護模式使能標志然后清理環境,關閉中斷將flag置0并設置字符串操作是遞增方向,將寄存器ax、ds、es、ss置0:
.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然后啟用A20,將A20地址線置1,根據附錄“關于A20 Gate”,因為一開始時A20地址線控制是被屏蔽的(總為0),直到系統軟件通過一定的IO操作去打開它。很顯然,在實模式下要訪問高端內存區,這個開關必須打開,在保護模式下,由于使用32位地址線,如果A20恒等于0,那么系統只能訪問奇數兆的內存,即只能訪問0–1M、2-3M、4-5M…,這樣無法有效訪問所有可用內存。所以在保護模式下,為了使能所有地址位的尋址能力,這個開關也必須打開。
為了與最早的PC機向后兼容,物理地址行20被限制在低位,因此高于1MB的地址默認為零。此代碼將撤消此操作,通過打開A20,將鍵盤控制器上的A20線置于高電位,就能使全部32條地址線可用,可以訪問4G的內存空間。
因為A20的地址位是由芯片8042管理,這個芯片與鍵盤控制器有關,通過給8042芯片發命令來激活A20的地址位,8042的兩個I/O端口是0x64和0x60,通過發送0xdi命令到0x64端口、發送0xdf到0x60端口就可以激活
打開A20的具體步驟大致如下:
下面的代碼分為兩部分,兩部分代碼都要通過讀0x64端口的第2位確保8042的輸入緩沖區為空后再進行操作。
在seta20.1中,首先把數據0xd1寫入端口0x64,發送消息給CPU準備往8042芯片的P2端口寫數據;
在seta20.2中,首先把數據0xdf寫入端口0x60,從而將8042芯片的P2端口的A20地址線設置為1。
seta20.1:inb $0x64, %al # Wait for not busy(8042 input buffer empty).testb $0x2, %aljnz seta20.1movb $0xd1, %al # 0xd1 -> port 0x64outb %al, $0x64 # 0xd1 means: write data to 8042's P2 portseta20.2:inb $0x64, %al # Wait for not busy(8042 input buffer empty).testb $0x2, %aljnz seta20.2movb $0xdf, %al # 0xdf -> port 0x60outb %al, $0x60 # 0xdf = 11011111, means set P2's A20 bit(the 1 bit) to 1在kern/mm/pmm.c文件中可以找到gdt的初始化函數,通過這段代碼完成gdt的初始化:
static void gdt_init(void) {ts.ts_esp0 = (uint32_t)&stack0 + sizeof(stack0);ts.ts_ss0 = KERNEL_DS;gdt[SEG_TSS] = SEG16(STS_T32A, (uint32_t)&ts, sizeof(ts), DPL_KERNEL);gdt[SEG_TSS].sd_s = 0;lgdt(&gdt_pd);ltr(GD_TSS); }而在bootasm.S文件中,可以看到:
# Bootstrap GDT .p2align 2 # force 4 byte alignment gdt:SEG_NULLASM # null segSEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) # code seg for bootloader and kernelSEG_ASM(STA_W, 0x0, 0xffffffff) # data seg for bootloader and kernelgdtdesc:.word 0x17 # sizeof(gdt) - 1.long gdt # address gdt其中SEG_ASM可以在asm.h文件中找到:
#define SEG_ASM(type,base,lim) \.word (((lim) >> 12) & 0xffff), ((base) & 0xffff); \.byte (((base) >> 16) & 0xff), (0x90 | (type)), \(0xC0 | (((lim) >> 28) & 0xf)), (((base) >> 24) & 0xff)可以看到,SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff)和SEG_ASM(STA_W, 0x0, 0xffffffff)把數據段和代碼段的base設為0,lim即limit設置為4G,數據段可讀可執行,代碼段可寫,這樣就可以是邏輯地址對應于線性地址。
因為一個簡單的GDT表和其描述符已經靜態儲存在引導區中,所以直接使用lgdt命令初始化后,將gdt的desc段表示內容加載到gdt就行。
lgdt gdtdesc將cr0寄存器的PE位置即最低位設置為1,就可以開啟保護模式:
movl %cr0, %eax orl $CR0_PE_ON, %eax movl %eax, %cr0接著,通過長跳轉使cs的基地址得到更新,將cs修改為32位段寄存器,此時CPU進入32位模式
ljmp $PROT_MODE_CSEG, $protcseg .code32 protcseg:設置段寄存器ds、es、fs、gs、ss,并建立堆棧的幀指針和棧指針
movw $PROT_MODE_DSEG, %ax movw %ax, %ds movw %ax, %es movw %ax, %fs movw %ax, %gs movw %ax, %ss movl $0x0, %ebp movl $start, %esp調用bootmain函數,bootloader從實模式進入保護模式
call bootmainlab1 練習 4:分析bootloader加載ELF格式的OS的過程。
通過閱讀bootmain.c,了解bootloader如何加載ELF文件。通過分析源代碼和通過qemu來運行并調試bootloader&OS,
- bootloader如何讀取硬盤扇區的?
- bootloader是如何加載ELF格式的OS?
提示:可閱讀“硬盤訪問概述”,“ELF執行文件格式概述”這兩小節。
在閱讀材料“硬盤訪問概述中”,表明了磁盤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端口讀數據 |
讀取一個硬盤扇區的流程大致如下:
在閱讀材料“ELF執行文件格式概述”中,表明了bootloader是如何加載ELF格式的OS:
ELF header在文件開始處描述了整個文件的組織。ELF的文件頭包含整個執行文件的控制結構,其定義在elf.h中:
struct elfhdr {uint magic; // must equal ELF_MAGICuchar elf[12];ushort type;ushort machine;uint version;uint entry; // 程序入口的虛擬地址uint phoff; // program header 表的位置偏移uint shoff;uint flags;ushort ehsize;ushort phentsize;ushort phnum; //program header表中的入口數目ushort shentsize;ushort shnum;ushort shstrndx; };program header描述與程序執行直接相關的目標文件結構信息,用來在文件中定位各個段的映像,同時包含其他一些用來為程序創建進程映像所必需的信息。可執行文件的程序頭部是一個program header結構的數組, 每個結構描述了一個段或者系統準備程序執行所必需的其它信息。目標文件的 “段” 包含一個或者多個 “節區”(section) ,也就是“段內容(Segment Contents)” 。程序頭部僅對于可執行文件和共享目標文件有意義。可執行目標文件在ELF頭部的e_phentsize和e_phnum成員中給出其自身程序頭部的大小。程序頭部的數據結構如下表所示:
struct proghdr {uint type; // 段類型uint offset; // 段相對文件頭的偏移值uint va; // 段的第一個字節將被放到內存中的虛擬地址uint pa;uint filesz;uint memsz; // 段在內存映像中占用的字節數uint flags;uint align; };根據elfhdr和proghdr的結構描述,bootloader就可以完成對ELF格式的ucore操作系統的加載過程(參見boot/bootmain.c中的bootmain函數)。
在bootmain.c文件中,首先:
宏定義:
#define SECTSIZE 512 //表示一個扇區的大小 #define ELFHDR ((struct elfhdr *)0x10000) // 表示虛擬地址的起始地址接著是:
static void waitdisk(void) {while ((inb(0x1F7) & 0xC0) != 0x40)/* do nothing */; }waitdisk()函數用來等待硬盤準備好,不斷查詢0x1F7寄存器的最高兩位,當最高兩位為01,即磁盤空閑時,才返回。
然后在文件中找到readsect函數:
static void readsect(void *dst, uint32_t secno) {// wait for disk to be readywaitdisk();outb(0x1F2, 1); //讀取一個扇區outb(0x1F3, secno & 0xFF); //制定扇區號的0-7位outb(0x1F4, (secno >> 8) & 0xFF); //制定扇區號的8-15位outb(0x1F5, (secno >> 16) & 0xFF); //制定扇區號的16-23位outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0); //制定扇區號的24-31位// 31-29位都是1,28位為0,表示訪問"Disk 0",27-0位是偏移量outb(0x1F7, 0x20); // 使用0x20命令,讀取扇區// wait for disk to be readywaitdisk();insl(0x1F0, dst, SECTSIZE / 4);// 將扇區的數據讀取到dst位置 }可以看到,readsect函數的作用是從設備的第secno個扇區的文章讀取數據到dst內存中。
然后找到readseg函數:
// 參數va表示虛擬地址的起始地址,參數count表示讀取數據的總大小,參數offset表示偏移量 static void readseg(uintptr_t va, uint32_t count, uint32_t offset) {uintptr_t end_va = va + count; //計算讀取數據的結束地址va -= offset % SECTSIZE; //用起始地址減去偏移地址,得到塊的首地址uint32_t secno = (offset / SECTSIZE) + 1; //0扇區已經被占用,所以ELF文件從1扇區開始//將end_va和va地址之間的數據讀取到內存中for (; va < end_va; va += SECTSIZE, secno ++) {readsect((void *)va, secno);} }可以看到,readseg函數使用了readsect函數,用來從設備中讀入任意長度的內容。
接著,找到bootmain函數:
void bootmain(void) {// 首先從磁盤的第一個扇區中將ELF文件bin/kernel的內容讀取出來readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);// 檢驗ELF頭部的e_magic變量判斷是不是ELF文件if (ELFHDR->e_magic != ELF_MAGIC) {goto bad;}struct proghdr *ph, *eph;// 讀取ELF頭部的e_phoff變量得到描述表的頭地址。表示ELF文件應該加載到內存的什么位置ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);// 讀取ELF頭部的e_phnum變量,得到描述表的元素數目。eph = ph + ELFHDR->e_phnum;// 按照描述表將ELF文件中數據按照偏移、虛擬地址、長度等信息載入內存for (; ph < eph; ph ++) {readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);}// 通過ELF頭部的e_entry變量儲存的入口信息,找到內核的入口地址,并開始執行內核代碼((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();bad:outw(0x8A00, 0x8A00);outw(0x8A00, 0x8E00);while (1); }bootloader加載ELF格式的OS的大致過程是先等待磁盤準備就緒,然后先讀取ELF的頭部判斷是否合法,接著讀取ELF內存位置的描述表,然后按照描述表的內容,將ELF文件中的數據載入內存,根據ELF頭部的入口信息找到內核入口執行內核代碼。
(3) 嘗試實現 uCore Lab 1 練習 5-6 的編程作業;
lab1 練習 5:實現函數調用堆棧跟蹤函數
首先需要根據閱讀材料“函數堆棧”,了解函數堆棧的概念:
棧是一個很重要的編程概念(編譯課和程序設計課都講過相關內容),與編譯器和編程語言有緊密的聯系。理解調用棧最重要的兩點是:棧的結構,EBP寄存器的作用。一個函數調用動作可分解為:零到多個PUSH指令(用于參數入棧),一個CALL指令。CALL指令內部其實還暗含了一個將返回地址(即CALL指令下一條指令的地址)壓棧的動作(由硬件完成)。幾乎所有本地編譯器都會在每個函數體之前插入類似如下的匯編指令:
pushl %ebp movl %esp , %ebp這樣在程序執行到一個函數的實際指令前,已經有以下數據順序入棧:參數、返回地址、ebp寄存器。由此得到類似如下的棧結構(參數入棧順序跟調用方式有關,這里以C語言默認的CDECL為例):
+| 棧底方向 | 高位地址 | ... | | ... | | 參數3 | | 參數2 | | 參數1 | | 返回地址 | | 上一層[ebp] | <-------- [ebp] | 局部變量 | 低位地址這兩條匯編指令的含義是:首先將ebp寄存器入棧,然后將棧頂指針esp賦值給ebp。“mov ebp esp”這條指令表面上看是用esp覆蓋ebp原來的值,其實不然。因為給ebp賦值之前,原ebp值已經被壓棧(位于棧頂),而新的ebp又恰恰指向棧頂。此時ebp寄存器就已經處于一個非常重要的地位,該寄存器中存儲著棧中的一個地址(原ebp入棧后的棧頂),從該地址為基準,向上(棧底方向)能獲取返回地址、參數值,向下(棧頂方向)能獲取函數局部變量值,而該地址處又存儲著上一層函數調用時的ebp值。
一般而言,ss:[ebp+4]處為返回地址,ss:[ebp+8]處為第一個參數值(最后一個入棧的參數值,此處假設其占用4字節內存),ss:[ebp-4]處為第一個局部變量,ss:[ebp]處為上一層ebp值。由于ebp中的地址處總是“上一層函數調用時的ebp值”,而在每一層函數調用中,都能通過當時的ebp值“向上(棧底方向)”能獲取返回地址、參數值,“向下(棧頂方向)”能獲取函數局部變量值。如此形成遞歸,直至到達棧底。這就是函數調用棧。
我們需要在lab1中完成kdebug.c中函數print_stackframe的實現,可以通過函數print_stackframe來跟蹤函數調用堆棧中記錄的返回地址。在如果能夠正確實現此函數,可在lab1中執行 “make qemu”后,在qemu模擬器中得到類似如下的輸出:
ebp:0x00007b28 eip:0x00100992 args:0x00010094 0x00010094 0x00007b58 0x00100096kern/debug/kdebug.c:305: print_stackframe+22 ebp:0x00007b38 eip:0x00100c79 args:0x00000000 0x00000000 0x00000000 0x00007ba8kern/debug/kmonitor.c:125: mon_backtrace+10 ebp:0x00007b58 eip:0x00100096 args:0x00000000 0x00007b80 0xffff0000 0x00007b84kern/init/init.c:48: grade_backtrace2+33 ebp:0x00007b78 eip:0x001000bf args:0x00000000 0xffff0000 0x00007ba4 0x00000029kern/init/init.c:53: grade_backtrace1+38 ebp:0x00007b98 eip:0x001000dd args:0x00000000 0x00100000 0xffff0000 0x0000001dkern/init/init.c:58: grade_backtrace0+23 ebp:0x00007bb8 eip:0x00100102 args:0x0010353c 0x00103520 0x00001308 0x00000000kern/init/init.c:63: grade_backtrace+34 ebp:0x00007be8 eip:0x00100059 args:0x00000000 0x00000000 0x00000000 0x00007c53kern/init/init.c:28: kern_init+88 ebp:0x00007bf8 eip:0x00007d73 args:0xc031fcfa 0xc08ed88e 0x64e4d08e 0xfa7502a8 <unknow>: -- 0x00007d72 –按照函數print_stackframe中所給的詳細的注釋,一步一步進行編寫函數print_stackframe,首先使用read_ebp()和read_eip()獲取32位的寄存器ebp和eip中的值并分別賦給32位變量ebp_val和eip_val。
然后進入一個for循環,從0到STACKFRAME_DEPTH,即遍歷棧,打印每個棧幀的信息,每次循環,使用變量call_args指向存放參數的ss:[ebp+8]的位置,然后依次打印調用函數的四個參數,輸出換行符后,打印eip和ebp相關的信息,最后eip指向返回地址,ebp指向原ebp的地址。
void print_stackframe(void) {/* LAB1 YOUR CODE : STEP 1 *//* (1) call read_ebp() to get the value of ebp. the type is (uint32_t);*/uint32_t ebp_val = read_ebp();/* (2) call read_eip() to get the value of eip. the type is (uint32_t);*/uint32_t eip_val = read_eip();/* (3) from 0 .. STACKFRAME_DEPTH*/for (int i = 0; ebp_val != 0 && i < STACKFRAME_DEPTH; ++i) {/* (3.1) printf value of ebp, eip*/cprintf("ebp:0x%08x eip:0x%08x args:", ebp_val, eip_val);/* (3.2) (uint32_t)calling arguments [0..4] = the contents in address (uint32_t)ebp +2 [0..4]*/uint32_t *call_args = (uint32_t *)ebp_val + 2;cprintf("0x%08x 0x%08x 0x%08x 0x%08x", call_args[0], call_args[1], call_args[2], call_args[3]);/* (3.3) cprintf("\n");*/cprintf("\n");/* (3.4) call print_debuginfo(eip-1) to print the C calling function name and line number, etc.*/print_debuginfo(eip_val - 1);/* (3.5) popup a calling stackframe*//* NOTICE: the calling funciton's return addr eip = ss:[ebp+4]*/eip_val = *((uint32_t *)(ebp_val + 4));/* the calling funciton's ebp = ss:[ebp]*/ebp_val = *((uint32_t *)ebp_val);} }執行截圖:
可以看到,輸出與上述顯示大致一致,最后一行是:
ebp:0x00007bf8 eip:0x00007d74 args:0xc031fcfa 0xc08ed88e 0x64e4d08e 0xfa7502a8 <unknow>: -- 0x00007d72 –
其中ebp:0x00007bf8中ebp的值0x00007bf8代表kern_init函數的棧頂地址;
eip:0x00007d74中eip的值eip:0x00007d74代表kern_init函數的返回地址,即bootmain函數調用kern_init函數之后對應的下一條指令的地址;
args:0xc031fcfa 0xc08ed88e 0x64e4d08e 0xfa7502a8代表的是bootloader指令的前16個字節。
<unknow>: -- 0x00007d72 –代表的是bootmain函數內調用OS kernel入口函數的該指令的地址
最后一行代表的是堆棧最深的一層,對應的是第一個使用堆棧的函數,堆棧從0x7c00開始,然后使用了bootmain函數,指令壓棧,所以bootmaind中寄存器ebp的值為0x7bf8
lab1 練習 6:完善中斷初始化和處理
請完成編碼工作和回答如下問題:
中斷描述符表(也可簡稱為保護模式下的中斷向量表)中一個表項占多少字節?其中哪幾位代表中斷處理代碼的入口?
請編程完善kern/trap/trap.c中對中斷向量表進行初始化的函數idt_init。在idt_init函數中,依次對所有中斷入口進行初始化。使用mmu.h中的SETGATE宏,填充idt數組內容。每個中斷的入口由tools/vectors.c生成,使用trap.c中聲明的vectors數組即可。
請編程完善trap.c中的中斷處理函數trap,在對時鐘中斷進行處理的部分填寫trap函數中處理時鐘中斷的部分,使操作系統每遇到100次時鐘中斷后,調用print_ticks子程序,向屏幕上打印一行文字”100 ticks”。
要求完成問題2和問題3提出的相關函數實現,提交改進后的源代碼包(可以編譯執行),并在實驗報告中簡要說明實現過程,并寫出對問題1的回答。完成這問題2和3要求的部分代碼后,運行整個系統,可以看到大約每1秒會輸出一次“100 ticks”,而按下的鍵也會在屏幕上顯示。
提示:可閱讀小節“中斷與異常”。
1. 中斷向量表中一個表項占多少字節?其中哪幾位代表中斷處理代碼的入口?
答:在中斷向量表中,一個表項會占8個字節,其中第0-1和第6-7字節組合在一起表示偏移量,第2~3字節表示段選擇的編號,在選擇的段中,計算偏移量后得到的位置,就是中斷處理代碼的入口。
2. 請編程完善kern/trap/trap.c中對中斷向量表進行初始化的函數idt_init。
根據注釋完成代碼,首先根據(1)注釋中的You can use "extern uintptr_t __vectors[];" to define this extern variable which will be used later.定義一個extern uintptr_t類型變量__vectors[],用來存放256個在vectors.S定義的中斷處理例程的入口地址
然后根據(2)注釋,使用SETGATE宏,通過循環語句對中斷描述符表中的每一個表項進行設置,其中SETGATE宏可以在mmu.h中找到:
#define SETGATE(gate, istrap, sel, off, dpl)宏的參數gate代表選擇的idt數組的項,是處理函數的入口地址
參數istrap為1時代表系統段,為0時代表中斷門
參數sel是中斷處理函數的段選擇子,GD_KTEXT代表是.text段
參數off是__vectors數組內容,在vector.S中,有256個中斷處理例程
參數dpl是優先級,宏定義DPL_KERNEL是0代表內核級,宏定義DPL_USER是3代表用戶級。
宏定義T_SWITCH_TOK是用于用戶態切換到內核態的中斷號。
接著根據(3)注釋,使用lidt函數加載中斷描述符表。
代碼如下:
void idt_init(void) {/* LAB1 YOUR CODE : STEP 2 *//* (1) Where are the entry addrs of each Interrupt Service Routine (ISR)?All ISR's entry addrs are stored in __vectors. where is uintptr_t __vectors[] ?__vectors[] is in kern/trap/vector.S which is produced by tools/vector.c(try "make" command in lab1, then you will find vector.S in kern/trap DIR)You can use "extern uintptr_t __vectors[];" to define this extern variable which will be used later. */extern uintptr_t __vectors[];/* (2) Now you should setup the entries of ISR in Interrupt Description Table (IDT).Can you see idt[256] in this file? Yes, it's IDT! you can use SETGATE macro to setup each item of IDT */int idt_size = sizeof(idt) / sizeof(struct gatedesc);for (int i = 0; i < idt_size; ++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);/* (3) After setup the contents of IDT, you will let CPU know where is the IDT by using 'lidt' instruction.You don't know the meaning of this instruction? just google it! and check the libs/x86.h to know more.Notice: the argument of lidt is idt_pd. try to find it! */lidt(&idt_pd); }3. 請編程完善trap.c中的中斷處理函數trap,在對時鐘中斷進行處理的部分填寫trap函數
根據注釋完成代碼,首先(1)注釋要求讓用于記錄時鐘中斷次數的位于kern/driver/clock.c的全局變量ticks加一,代碼語句ticks++
然后(2)注釋讓每個TICK_NUM的循環完成后,都調用一次print_ticks()函數打印“100 ticks”,然后將ticks置為0,以便下一次時重新進行TICK_NUM循環。
程序實現功能是操作系統在每遇到100次時鐘中斷后,就使用一次print_ticks()打印一次“100 ticks”。
代碼如下:
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 */ticks++;/* (2) Every TICK_NUM cycle, you can print some info using a funciton, such as print_ticks(). */if (ticks % TICK_NUM == 0) {print_ticks();ticks = 0;}/* (3) Too Simple? Yes, I think so! */break;問題(2)和問題(3)都完成后,執行程序截圖:
可以看到,大概每1秒輸出一次“100 ticks”文字,而且按下的鍵也會在屏幕上顯示。
(4) 思考如何實現 uCore Lab 1 擴展練習 1-2。
擴展練習 Challenge 1
擴展proj4,增加syscall功能,即增加一用戶態函數(可執行一特定系統調用:獲得時鐘計數值),當內核初始完畢后,可從內核態返回到用戶態的函數,而用戶態的函數又通過系統調用得到內核態的服務。
提示: 規范一下 challenge 的流程。
kern_init 調用 switch_test,該函數如下:
static void switch_test(void) {print_cur_status(); // print 當前 cs/ss/ds 等寄存器狀態cprintf("+++ switch to user mode +++\n");switch_to_user(); // switch to user modeprint_cur_status();cprintf("+++ switch to kernel mode +++\n");switch_to_kernel(); // switch to kernel modeprint_cur_status(); }switchto* 函數建議通過 中斷處理的方式實現。主要要完成的代碼是在 trap 里面處理 T_SWITCH_TO* 中斷,并設置好返回的狀態。
在 lab1 里面完成代碼以后,執行 make grade 應該能夠評測結果是否正確。
首先,要在init.c文件的kern_init()函數里面,將原先被注釋掉的代碼lab1_switch_test()去掉注釋,變成可以執行的語句。
然后看到下面的static void lab1_switch_to_user(void)和static void lab1_switch_to_kernel(void)需要實現,對于static void lab1_switch_to_user(void),這個函數的功能是從內核態返回到用戶態,需要調用T_SWITCH_TOU中斷,在函數中使用內聯匯編實現:
asm volatile ("pushl %%ss \n""pushl %%esp \n""int %0 \n""movl %%ebp, %%esp": : "i"(T_SWITCH_TOU) );在調用中斷之前首先需要使用語句"pushl %%ss \n""和pushl %%esp \n"提前將ss、esp壓入棧,因為當切換優先級時,中斷返回時iret指令會額外彈出ss和esp兩位,但使用"int %0 \n"語句調用T_SWITCH_TOU中斷時并不會產生特權級的切換,因此不用壓入ss和esp,所以要先將棧壓兩位,預先留出空間,在中斷返回后使用"movl %%ebp, %%esp" : : "i"(T_SWITCH_TOU)語句恢復棧指針,修復esp。
而在函數static void lab1_switch_to_user(void)中,實現的功能是從內核態切換回用戶態,需要調用T_SWITCH_TOU中斷,在函數中使用內聯匯編實現:
asm volatile ("int %0 \n""movl %%ebp, %%esp \n": : "i"(T_SWITCH_TOK) );從用戶態切換到內核態時,由于用戶態使用"int %0 \n"語句調用T_SWITCH_TOU中斷時會自動切換到內核態,不會另外彈出ss、esp兩位,中斷返回時,esp仍在堆棧中,在中斷返回后要使用 "movl %%ebp, %%esp \n" : : "i"(T_SWITCH_TOK)語句恢復棧指針,修復esp。
然后在trap.c文件中,找到trap_dispatch()函數中等待完成的case T_SWITCH_TOU和case T_SWITCH_TOK,先定義一個struct trapframe類型的變量switchktou和一個struct trapframe *類型的指針變量switchutok。
對于case T_SWITCH_TOU情況,要實現的是內核態轉換到用戶態時寄存器的修改,代碼如下:
case T_SWITCH_TOU:// 如果原先保存在trapframe中的cs不是代表用戶態的USER_CSif (tf->tf_cs != USER_CS) {// 將保存在trapframe中的cs改成代表用戶態的USER_CStf->tf_cs = USER_CS;// 將其它的段選擇子都修改為代表用戶態的USER_DS,保證中斷返回之后可以正常訪問數據tf->tf_ds = USER_DS;tf->tf_es = USER_DS;tf->tf_ss = USER_DS;// 為了程序在CPL較低的情況下也能使用IO,需要將對應的IOPL位置改成用戶態tf->tf_eflags |= FL_IOPL_MASK;}break;指令iret認定發生中斷的時候是否發生了PL的切換,是通過判斷CPL和跳轉回的地址的cs對應的段描述符的CPL是否相等來確定的,所以將保存在trapframe中的cs改成代表用戶態的USER_CS,將其它的段選擇子都修改為代表用戶態的USER_DS,保證中斷返回之后可以正常訪問數據,中斷返回才能正常。
對于case T_SWITCH_TOK情況,要實現的是內核態轉換到用戶態時寄存器的修改,代碼如下:
case T_SWITCH_TOK:// 如果原先保存在trapframe中的cs不是代表內核態的KERNEL_CSif (tf->tf_cs != KERNEL_CS) {// 將保存在trapframe中的cs改成代表內核態的KERNEL_CStf->tf_cs = KERNEL_CS;// 將其它的段選擇子都修改為代表內核態的KERNEL_DS,保證中斷返回之后可以正常訪問數據tf->tf_ds = KERNEL_DS;tf->tf_es = KERNEL_DS;// 將調用IO所需權限降低,才能輸出文本tf->tf_eflags |= 0x3000;}break;為了能夠執行T_SWITCH_TOK的軟中斷,將trapframe中保存的cs修改為代表內核態的段選擇子KERNEL_CS,并且將其它的段選擇子都修改為代表內核態KERNEL_DS,然后進行正常的中斷返回,為了輸出文本,還需要將調用IO所需權限降低。
執行截圖:
擴展練習 Challenge 2
用鍵盤實現用戶模式內核模式切換。具體目標是:“鍵盤輸入3時切換到用戶模式,鍵盤輸入0時切換到內核模式”。 基本思路是借鑒軟中斷(syscall功能)的代碼,并且把trap.c中軟中斷處理的設置語句拿過來。
注意:
關于調試工具,不建議用lab1_print_cur_status()來顯示,要注意到寄存器的值要在中斷完成后tranentry.S里面iret結束的時候才寫回,所以在trap.c里面不好觀察,建議用print_trapframe(tf)
關于內聯匯編,最開始調試的時候,參數容易出現錯誤,可能的錯誤代碼如下
asm volatile ( “sub $0x8, %%esp \n”
“int %0 \n”
“movl %%ebp, %%esp”
: )
要去掉參數int %0 \n這一行
首先在trap.c文件中找到與鍵盤中斷返回有關的代碼,即case IRQ_OFFSET + IRQ_KBD,在其中加入一個感知鍵盤輸入數組的條件判斷語句,如果輸入是3則進入用戶模式,如果輸入是0則進入內核模式。因為在內核態進入到用戶態的過程中,iret指令中斷返回時會額外彈出兩位,所以為了保護堆棧上的信息,可以將trapframe的地址保存到一個變量中,當鍵盤輸入3準備從內核模式切換到用戶模式時,可以可以從這個變量中獲取正確的trapframe的地址,恢復棧指針,修復esp。
而因為用戶態進入到內核態的過程中,因為iret指令調用中斷時是系統默認的從權限較低的模式轉換到權限較高的模式,所以中斷時會自動切換到內核態,堆棧不會再彈出另外的兩位,所以當鍵盤輸入0準備從用戶模式切換到內核模式,實現中斷返回時,原來的esp還在堆棧中,所以需要把ebp的值傳送給esp,恢復棧指針,修復esp。
總結
以上是生活随笔為你收集整理的操作系统实验报告1:ucore Lab 1的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: windows远程下载
- 下一篇: 操作系统实验报告2:Linux 下 x8