實驗一:系統軟件啓動過程
練習1:理解通過make生成執行文件的過程
1.1 操作系統鏡像文件ucore.img是如何一步一步生成的?
Makefile
如下,解釋部分內容參考
PROJ := challenge
EMPTY :=
SPACE := $(EMPTY) $(EMPTY)
SLASH := /
##make "V="可輸出make執行的命令
V := @
#need llvm/cang-3.5+
#USELLVM := 1
##選擇交叉編譯器檢查GCCPREFIX的設置
# try to infer the correct GCCPREFX
ifndef GCCPREFIX
GCCPREFIX := $(shell if i386-elf-objdump -i 2>&1 | grep '^elf32-i386$$' >/dev/null 2>&1; \
then echo 'i386-elf-'; \
elif objdump -i 2>&1 | grep 'elf32-i386' >/dev/null 2>&1; \
then echo ''; \
else echo "***" 1>&2; \
echo "*** Error: Couldn't find an i386-elf version of GCC/binutils." 1>&2; \
echo "*** Is the directory with i386-elf-gcc in your PATH?" 1>&2; \
echo "*** If your i386-elf toolchain is installed with a command" 1>&2; \
echo "*** prefix other than 'i386-elf-', set your GCCPREFIX" 1>&2; \
echo "*** environment variable to that prefix and run 'make' again." 1>&2; \
echo "*** To turn off this error, run 'gmake GCCPREFIX= ...'." 1>&2; \
echo "***" 1>&2; exit 1; fi)
endif
##設置QEMU
# try to infer the correct QEMU
ifndef QEMU
QEMU := $(shell if which qemu-system-i386 > /dev/null; \
then echo 'qemu-system-i386'; exit; \
elif which i386-elf-qemu > /dev/null; \
then echo 'i386-elf-qemu'; exit; \
elif which qemu > /dev/null; \
then echo 'qemu'; exit; \
else \
echo "***" 1>&2; \
echo "*** Error: Couldn't find a working QEMU executable." 1>&2; \
echo "*** Is the directory containing the qemu binary in your PATH" 1>&2; \
echo "***" 1>&2; exit 1; fi)
endif
# eliminate default suffix rules
.SUFFIXES: .c .S .h
##如果遇到error或者被中斷了就刪除所有目標文件
# delete target files if there is an error (or make is interrupted)
.DELETE_ON_ERROR:
##設置編譯器選項
# define compiler and flags
ifndef USELLVM
##gcc編譯,-g爲了gdb調式,-Wall生成警告信息,-O2優化處理級別
HOSTCC := gcc
HOSTCFLAGS := -g -Wall -O2
CC := $(GCCPREFIX)gcc
##-fno-builtin不使用C語言的內建函數,-ggdb爲GDB生成更豐富的調試信息,-m32用32位編譯,-gstabs生成stabs格式調試信息但不包括GDB調試信息,-nostdinc不在系統默認頭文件目錄中尋找頭文件,$(DEFS)未定義可用來擴展信息
CFLAGS := -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc $(DEFS)
##$(shell)可以輸出shell指令,-fno-stack-protector禁用堆棧保護,-E僅預處理不進行編譯彙編鏈接可以提高速度,-x c指明c語言
##/dev/null指定目標文件,>/dev/null 2>&1標準錯誤重定向到標準輸出,&&先運行前一句若成功再運行後一句
##意爲只預處理,所有出錯全部作爲垃圾(/dev/null類似垃圾文件)測試能否開啓-fno-stack-protector,若能則CFLAGS += -fno-stack-protector
CFLAGS += $(shell $(CC) -fno-stack-protector -E -x c /dev/null >/dev/null 2>&1 && echo -fno-stack-protector)
else
##若使用clang,類似處理
HOSTCC := clang
HOSTCFLAGS := -g -Wall -O2
CC := clang
CFLAGS := -fno-builtin -Wall -g -m32 -mno-sse -nostdinc $(DEFS)
CFLAGS += $(shell $(CC) -fno-stack-protector -E -x c /dev/null >/dev/null 2>&1 && echo -fno-stack-protector)
endif
##源文件類型爲.c和.S
CTYPE := c S
LD := $(GCCPREFIX)ld
##shell中命令 ld -V可以輸出支持的版本,|管道將前者的輸出作爲後者的輸入,grep在輸入中搜索elf_i386字串,找到就輸出elf_i386
##意味如果支持elf_i386則LDFLAGS := -m elf_i386
LDFLAGS := -m $(shell $(LD) -V | grep elf_i386 2>/dev/null)
##-nostdlib不連接系統標準庫文件
LDFLAGS += -nostdlib
OBJCOPY := $(GCCPREFIX)objcopy
OBJDUMP := $(GCCPREFIX)objdump
##定義一些shell命令
COPY := cp
MKDIR := mkdir -p
MV := mv
RM := rm -f
AWK := awk
SED := sed
SH := sh
TR := tr
TOUCH := touch -c
OBJDIR := obj
BINDIR := bin
ALLOBJS :=
ALLDEPS :=
TARGETS :=
##在function.mk中定義了大量輔助函數,部分說明參考了引用中的博文
include tools/function.mk
##call:call func,變量1,變量2,...
##listf:列出某地址下某類型的文件
##listf_cc:列出變量1下的.c與.S文件
listf_cc = $(call listf,$(1),$(CTYPE))
# for cc
##將文件打包
add_files_cc = $(call add_files,$(1),$(CC),$(CFLAGS) $(3),$(2),$(4))
##創建目標文件包
create_target_cc = $(call create_target,$(1),$(2),$(3),$(CC),$(CFLAGS))
# for hostcc
add_files_host = $(call add_files,$(1),$(HOSTCC),$(HOSTCFLAGS),$(2),$(3))
create_target_host = $(call create_target,$(1),$(2),$(3),$(HOSTCC),$(HOSTCFLAGS))
##patsubst替換通配符
##cgtype(filenames,type1,type2)把文件名中type1的改爲type2,如.c改爲.o
cgtype = $(patsubst %.$(2),%.$(3),$(1))
##列出所有目標文件,並按規則改後綴名
objfile = $(call toobj,$(1))
asmfile = $(call cgtype,$(call toobj,$(1)),o,asm)
outfile = $(call cgtype,$(call toobj,$(1)),o,out)
symfile = $(call cgtype,$(call toobj,$(1)),o,sym)
# for match pattern
match = $(shell echo $(2) | $(AWK) '{for(i=1;i<=NF;i++){if(match("$(1)","^"$$(i)"$$")){exit 1;}}}'; echo $$?)
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
# include kernel/user
INCLUDE += libs/
CFLAGS += $(addprefix -I,$(INCLUDE))
LIBDIR += libs
$(call add_files_cc,$(call listf_cc,$(LIBDIR)),libs,)
# -------------------------------------------------------------------
# kernel
KINCLUDE += kern/debug/ \
kern/driver/ \
kern/trap/ \
kern/mm/
KSRCDIR += kern/init \
kern/libs \
kern/debug \
kern/driver \
kern/trap \
kern/mm
KCFLAGS += $(addprefix -I,$(KINCLUDE))
$(call add_files_cc,$(call listf_cc,$(KSRCDIR)),kernel,$(KCFLAGS))
KOBJS = $(call read_packet,kernel libs)
# create kernel target
##將所有文件鏈接生成kernel
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)
# -------------------------------------------------------------------
# create bootblock
##將所有文件鏈接生成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)
# -------------------------------------------------------------------
# create 'sign' tools
##生成sign輔助工具
$(call add_files_host,tools/sign.c,sign,sign)
$(call create_target_host,sign,sign)
# -------------------------------------------------------------------
# create ucore.img
##生成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)
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
$(call finish_all)
IGNORE_ALLDEPS = clean \
dist-clean \
grade \
touch \
print-.+ \
handin
ifeq ($(call match,$(MAKECMDGOALS),$(IGNORE_ALLDEPS)),0)
-include $(ALLDEPS)
endif
# files for grade script
TARGETS: $(TARGETS)
.DEFAULT_GOAL := TARGETS
.PHONY: qemu qemu-nox debug debug-nox
qemu-mon: $(UCOREIMG)
$(V)$(QEMU) -no-reboot -monitor stdio -hda $< -serial null
qemu: $(UCOREIMG)
$(V)$(QEMU) -no-reboot -parallel stdio -hda $< -serial null
log: $(UCOREIMG)
$(V)$(QEMU) -no-reboot -d int,cpu_reset -D q.log -parallel stdio -hda $< -serial null
qemu-nox: $(UCOREIMG)
$(V)$(QEMU) -no-reboot -serial mon:stdio -hda $< -nographic
TERMINAL :=gnome-terminal
debug: $(UCOREIMG)
$(V)$(QEMU) -S -s -parallel stdio -hda $< -serial null &
$(V)sleep 2
$(V)$(TERMINAL) -e "gdb -q -tui -x tools/gdbinit"
debug-nox: $(UCOREIMG)
$(V)$(QEMU) -S -s -serial mon:stdio -hda $< -nographic &
$(V)sleep 2
$(V)$(TERMINAL) -e "gdb -q -x tools/gdbinit"
.PHONY: grade touch
GRADE_GDB_IN := .gdb.in
GRADE_QEMU_OUT := .qemu.out
HANDIN := proj$(PROJ)-handin.tar.gz
TOUCH_FILES := kern/trap/trap.c
MAKEOPTS := --quiet --no-print-directory
grade:
$(V)$(MAKE) $(MAKEOPTS) clean
$(V)$(SH) tools/grade.sh
touch:
$(V)$(foreach f,$(TOUCH_FILES),$(TOUCH) $(f))
print-%:
@echo $($(shell echo $(patsubst print-%,%,$@) | $(TR) [a-z] [A-Z]))
.PHONY: clean dist-clean handin packall tags
clean:
$(V)$(RM) $(GRADE_GDB_IN) $(GRADE_QEMU_OUT) cscope* tags
-$(RM) -r $(OBJDIR) $(BINDIR)
dist-clean: clean
-$(RM) $(HANDIN)
handin: packall
@echo Please visit http://learn.tsinghua.edu.cn and upload $(HANDIN). Thanks!
packall: clean
@$(RM) -f $(HANDIN)
@tar -czf $(HANDIN) `find . -type f -o -type d | grep -v '^\.*$$' | grep -vF '$(HANDIN)'`
tags:
@echo TAGS ALL
$(V)rm -f cscope.files cscope.in.out cscope.out cscope.po.out tags
$(V)find . -type f -name "*.[chS]" >cscope.files
$(V)cscope -bq
$(V)ctags -L cscope.files
調用make V=
後結果如下
##lab1僅是ucore的開始,部分函數未完成調用會出現警告,此處可以無視
#kernel
+ cc kern/init/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
kern/init/init.c:95:1: warning: ‘lab1_switch_test’ defined but not used [-Wunused-function]
lab1_switch_test(void) {
^
+ cc kern/libs/stdio.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/libs/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/debug/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/debug/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
kern/debug/kdebug.c:251:1: warning: ‘read_eip’ defined but not used [-Wunused-function]
read_eip(void) {
^
+ cc kern/debug/kmonitor.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/driver/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
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/picirq.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/driver/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/trap/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
kern/trap/trap.c:14:13: warning: ‘print_ticks’ defined but not used [-Wunused-function]
static void print_ticks() {
^
kern/trap/trap.c:30:26: warning: ‘idt_pd’ defined but not used [-Wunused-variable]
static struct pseudodesc idt_pd = {
^
+ cc kern/trap/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/trap/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/mm/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/string.c
gcc -Ilibs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -c libs/string.c -o obj/libs/string.o
+ cc libs/printfmt.c
gcc -Ilibs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -c libs/printfmt.c -o obj/libs/printfmt.o
#鏈接生成kernel
+ ld bin/kernel
ld -m elf_i386 -nostdlib -T tools/kernel.ld -o bin/kernel obj/kern/init/init.o obj/kern/libs/stdio.o obj/kern/libs/readline.o obj/kern/debug/panic.o obj/kern/debug/kdebug.o obj/kern/debug/kmonitor.o obj/kern/driver/clock.o obj/kern/driver/console.o obj/kern/driver/picirq.o obj/kern/driver/intr.o obj/kern/trap/trap.o obj/kern/trap/vectors.o obj/kern/trap/trapentry.o obj/kern/mm/pmm.o obj/libs/string.o obj/libs/printfmt.o
#bootblock
+ cc boot/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
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
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
+ ld bin/bootblock
#鏈接生成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!
#生成ucore.img
dd if=/dev/zero of=bin/ucore.img count=10000
記錄了10000+0 的讀入
記錄了10000+0 的寫出
5120000 bytes (5.1 MB, 4.9 MiB) copied, 0.0144903 s, 353 MB/s
dd if=bin/bootblock of=bin/ucore.img conv=notrunc
記錄了1+0 的讀入
記錄了1+0 的寫出
512 bytes copied, 0.000149204 s, 3.4 MB/s
dd if=bin/kernel of=bin/ucore.img seek=1 conv=notrunc
記錄了146+1 的讀入
記錄了146+1 的寫出
74828 bytes (75 kB, 73 KiB) copied, 0.000223667 s, 335 MB/s
1.2 一個被系統認爲是符合規範的硬盤主引導扇區的特徵是什麼?
- 磁盤主引導扇區只有512字節
- 磁盤最後兩個字節爲0x55AA
在tools/sign.c
中有如下代碼用來檢測是否符合要求
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;
}
練習2:使用qemu執行並調試lab1種的軟件
調用make debug
前需要注意Makefile
裏debug
條目下有一句"gdb -q -tui -x tools/gdbinit"
,故先修改tools/gdbinit
如下
#old-gdbinit
file bin/kernel
target remote:1234
break kern_init
continue
#new-gdbinit
file obj/bootblock.o
target remote:1234
set architecture i8086
b *0x7c00
continue
x /2i $pc
調用make debug
後結果如下,與源文件一致,斷點測試正常
練習3:分析bootloader進入保護模式的過程
從bootasm.S
的部分源碼中分析過程
# start address should be 0:7c00, in real mode, the beginning address of the running bootloader從0x7c00開始運行bootloader
.globl start
start:
.code16
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
# 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和seta20.2是開啓A20地址線的代碼,涉及到8042鍵盤控制器,當開啓後32條地址線可用
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
# 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.
#通過描述符加載GDT表
lgdt gdtdesc
#將CR0寄存器第一位置1來開啓保護模式
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.
#長跳轉到32位模式下的下一條指令,將$PROT_MODE_CSEG加載到CS中,將$protcseg加載到IP中
ljmp $PROT_MODE_CSEG, $protcseg
.code32 # Assemble for 32-bit mode
protcseg:
# Set up the protected-mode data segment registers
#將數據段選擇子加載到ax中
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)
#調用bootmain
movl $0x0, %ebp
movl $start, %esp
call bootmain
# If bootmain returns (it shouldn't), loop.
spin:
jmp spin
# 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
練習4:分析bootloader加載ELF格式的OS的過程
從bootmain
源碼分析加載過程
/* bootmain - the entry of bootloader */
void
bootmain(void) {
// read the 1st page off disk實際readseg函數循環調用了readsect讀取磁盤信息
readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);
// is this a valid ELF?判斷是否是合法的ELF文件
if (ELFHDR->e_magic != ELF_MAGIC) {
goto bad;
}
struct proghdr *ph, *eph;
// load each program segment (ignores ph flags)
// ELF頭部有program header表的位置偏移,使ph指向program header表的第一項,並循環將所有程序加載進內存
ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
// eph即end of ph標記了ELF文件頭部的結尾
eph = ph + ELFHDR->e_phnum;
// 按照程序頭表的描述將ELF文件中的數據加載進內存
for (; ph < eph; ph ++) {
readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
}
// call the entry point from the ELF header
// note: does not return
//程序加載完成,根據ELF頭表的入口信息找到內核的入口並開始執行,不返回
((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
bad:
outw(0x8A00, 0x8A00);
outw(0x8A00, 0x8E00);
/* do nothing */
while (1);
}
readsect()
從設備的secno扇區讀取數據到dst位置,IO部分參考
/* readsect - read a single sector at @secno into @dst */
static void
readsect(void *dst, uint32_t secno) {
// wait for disk to be ready
// 等待磁盤準備就緒
waitdisk();
// 設置讀取扇區的數目爲1
outb(0x1F2, 1); // count = 1
// 0x1F3-0x1F6這裏的4條指令設定了0-27位偏移量、28位(0)表示訪問Disk 0、29-31位強制設爲1
outb(0x1F3, secno & 0xFF);
outb(0x1F4, (secno >> 8) & 0xFF);
outb(0x1F5, (secno >> 16) & 0xFF);
outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0);
// 0x20命令讀取扇區
outb(0x1F7, 0x20); // cmd 0x20 - read sectors
// wait for disk to be ready
waitdisk();
// read a sector
// 讀取到dst位置,以DW爲單位,SECTSIZE = 512字節,故讀512/4=128個4字節的DW
insl(0x1F0, dst, SECTSIZE / 4);
}
練習5:實現函數調用堆棧跟蹤函數
5.1 函數堆棧基本原理
一個函數調用動作可分解爲:零到多個PUSH指令(用於參數入棧),一個CALL指令。CALL指令內部其實還暗含了一個將返回地址(即CALL指令下一條指令的地址)壓棧的動作(由硬件完成)。幾乎所有本地編譯器都會在每個函數體之前插入類似如下的彙編指令
pushl %ebp
movl %esp , %ebp
這樣在程序執行到一個函數的實際指令前,已經有以下數據順序入棧:參數、返回地址、ebp寄存器。由此得到類似如下的棧結構
一般而言,ss:[ebp+4]處爲返回地址,ss:[ebp+8]處爲第一個參數值(最後一個入棧的參數值,此處假設其佔用4字節內存),ss:[ebp-4]處爲第一個局部變量,ss:[ebp]處爲上一層ebp值。由於ebp中的地址處總是“上一層函數調用時的ebp值”,而在每一層函數調用中,都能通過當時的ebp值“向上(棧底方向)”能獲取返回地址、參數值,“向下(棧頂方向)”能獲取函數局部變量值。如此形成遞歸,直至到達棧底。這就是函數調用棧。
5.2 print_stackframe的實現
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();
uint32_t eip = read_eip();
for(int i = 0; i < STACKFRAME_DEPTH && ebp != 0; i++)
{
cprintf("ebp:0x%08x eip:0x%08x ", ebp, eip);
uint32_t *args = (uint32_t *)ebp + 2;
cprintf("args:0x%08x 0x%08x 0x%08x 0x%08x\n", args[0], args[1], args[2], args[3]);
print_debuginfo(eip-1);
eip = ((uint32_t *)ebp)[1];
ebp = ((uint32_t *)ebp)[0];
}
}
5.3 輸出結果
...
ebp:0x00007b38 eip:0x00100a28 args:0x00010094 0x00010094 0x00007b68 0x0010007f
kern/debug/kdebug.c:306: print_stackframe+22
ebp:0x00007b48 eip:0x00100d14 args:0x00000000 0x00000000 0x00000000 0x00007bb8
kern/debug/kmonitor.c:125: mon_backtrace+10
ebp:0x00007b68 eip:0x0010007f args:0x00000000 0x00007b90 0xffff0000 0x00007b94
kern/init/init.c:48: grade_backtrace2+19
ebp:0x00007b88 eip:0x001000a1 args:0x00000000 0xffff0000 0x00007bb4 0x00000029
kern/init/init.c:53: grade_backtrace1+27
ebp:0x00007ba8 eip:0x001000be args:0x00000000 0x00100000 0xffff0000 0x00100043
kern/init/init.c:58: grade_backtrace0+19
ebp:0x00007bc8 eip:0x001000df args:0x00000000 0x00000000 0x00000000 0x00103260
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:0x00007d6e args:0xc031fcfa 0xc08ed88e 0x64e4d08e 0xfa7502a8
<unknow>: -- 0x00007d6d --
...
練習6:完善中斷初始化和處理
6.1 中斷描述符表(也可簡稱爲保護模式下的中斷向量表)中一個表項佔多少字節?其中哪幾位代表中斷處理代碼的入口?
參考mmu.h
中對中斷描述符表表項數據結構的定義,中斷描述符一個表項佔8字節,其中0~15位和48~63位分別是offset
的低16位和高16位,16~31位是段選擇子,段選擇子獲得段基址加上偏移量就是入口
/* 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
};
6.2 中斷向量表初始化函數idt_init的實現
利用SETGATE
,其定義在mmu.h
中
/* *
* 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){...}
注意到實驗手冊中提到,因此在初始化完其他實現後,需要單獨重新處理T_SYSCALL
【注意】除了系統調用中斷(T_SYSCALL)使用陷阱門描述符且權限爲用戶態權限以外,其它中斷均使用特權級(DPL)爲0的中斷門描述符,權限爲內核態權限;而ucore的應用程序處於特權級3,需要採用`int 0x80`指令操作(這種方式稱爲軟中斷,軟件中斷,Tra中斷,在lab5會碰到)來發出系統調用請求,並要能實現從特權級3到特權級0的轉換,所以系統調用中斷(T_SYSCALL)所對應的中斷門描述符中的特權級(DPL)需要設置爲3。
/* idt_init - initialize IDT to each of the entry points in kern/trap/vectors.S */
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.
* (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
* (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!
*/
extern uintptr_t __vectors[];
for (int i = 0; i < 256; i ++) {
SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL);
}
// 單獨處理T_SYSCALL,這裏的GD_KETXT定義在memlayout.h中,表示內核代碼
SETGATE(idt[T_SYSCALL], 1, GD_KTEXT, __vectors[T_SYSCALL], DPL_USER);
lidt(&idt_pd);
}
參考PIAZZA論壇給出一個更爲合理的答案,區分出異常和中斷,否則按原方法所有異常會被作爲中斷處理,不同之處在於中斷門會將IF flag清零而異常門不會,則操作系統在處理異常時將無法響應可屏蔽中斷,例如程序產生了一個異常,本可以通過鍵盤強制終止程序,然而由於IF flag被清零鍵盤的中斷信號無法被接收,導致必須要等到異常處理結束才能得到響應
for(int i = 0; i < sizeof(idt) / sizeof(struct gatedesc){
if(i == T_SYSCALL){
SETGATE(idt[T_SYSCALL], 1, GD_KTEXT, __vectors[T_SYSCALL], DPL_USER);
}
else if(i < IRQ_OFFSET){
SETGATE(idt[i], 1, GD_KTEXT, __vectors[i], DPL_KERNEL);
}
else{
SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL);
}
}
6.3 時鐘中斷處理函數的實現
較爲簡單,直接給出代碼
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!
*/
ticks ++;
if (ticks % TICK_NUM == 0) {
print_ticks();
}
break;