# Lab1 report
## [練習1]
[練習1.1] 操作系統鏡像文件 tos.img 是如何一步一步生成的?(需要比較詳細地解釋 Makefile 中
每一條相關命令和命令參數的含義,以及說明命令導致的結果)
bin/tos.img
| 生成tos.img的相關代碼爲
|
|
| |
| | -o
| | @
| |
| |
| | 爲了生成bootblock,首先需要生成bootasm.o、bootmain.o、sign
| |
| |> obj/boot/bootasm.o, obj/boot/bootmain.o
| | | 生成bootasm.o,bootmain.o的相關makefile代碼爲
| | | bootfiles =
| | | $(CFLAGS) -Os -nostdinc))
| | | 實際代碼由宏批量生成
| | |
| | | 生成bootasm.o需要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
| | | 其中關鍵的參數爲
| | | -ggdb 生成可供gdb使用的調試信息。這樣才能用qemu+gdb來調試bootloader or tos。
| | | -m32 生成適用於32位環境的代碼。我們用的模擬硬件是32bit的80386,所以tos也要是32位的軟件。
| | | -gstabs 生成stabs格式的調試信息。這樣要tos的monitor可以顯示出便於開發者閱讀的函數調用棧信息
| | | -nostdinc 不使用標準庫。標準庫是給應用程序用的,我們是編譯tos內核,OS內核是提供服務的,所以所有的服務要自給自足。
| | | -fno-stack-protector 不生成用於檢測緩衝區溢出的代碼。這是for 應用程序的,我們是編譯內核,tos內核好像還用不到此功能。
| | | -Os 爲減小代碼大小而進行優化。根據硬件spec,主引導扇區只有512字節,我們寫的簡單bootloader的最終大小不能大於510字節。
| | | -I
| | |
| | | 生成bootmain.o需要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
| | | 新出現的關鍵參數有
| | | -fno-builtin 除非用_builtin前綴,
| | | 否則不進行builtin函數的優化
| |
| |> bin/sign
| | | 生成sign工具的makefile代碼爲
| | |
| | |
| | | 實際命令爲
| | | 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
| |
| | 首先生成bootblock.o
| | ld -m elf_i386 -nostdlib -N -e start -Ttext 0x7C00 \
| | obj/boot/bootasm.o obj/boot/bootmain.o -o obj/bootblock.o
| | 其中關鍵的參數爲
| | -m 模擬爲i386上的連接器
| | -nostdlib 不使用標準庫
| | -N 設置代碼段和數據段均可讀寫
| | -e 指定入口
| | -Ttext 制定代碼段開始位置
| |
| | 拷貝二進制代碼bootblock.o到bootblock.out
| | objcopy -S -O binary obj/bootblock.o obj/bootblock.out
| | 其中關鍵的參數爲
| | -S 移除所有符號和重定位信息
| | -O 指定輸出格式
| |
| | 使用sign工具處理bootblock.out,生成bootblock
| | bin/sign obj/bootblock.out bin/bootblock
|
|> bin/kernel
| | 生成kernel的相關代碼爲
| |
| |
| | @
| | /^
| |
| | 爲了生成kernel,首先需要 kernel.ld init.o readline.o stdio.o kdebug.o
| | kmonitor.o panic.o clock.o console.o intr.o picirq.o trap.o
| | trapentry.o vectors.o pmm.o printfmt.o string.o
| | kernel.ld已存在
| |
| |> obj/kern//.o
| | | 生成這些.o文件的相關makefile代碼爲
| | |
| | | 這些.o生成方式和參數均類似,僅舉init.o爲例,其餘不贅述
| |> obj/kern/init/init.o
| | | 編譯需要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
| |
| | 生成kernel時,makefile的幾條指令中有@前綴的都不必需
| | 必需的命令只有
| | ld -m elf_i386 -nostdlib -T tools/kernel.ld -o bin/kernel \
| | obj/kern/init/init.o obj/kern/libs/readline.o \
| | obj/kern/libs/stdio.o obj/kern/debug/kdebug.o \
| | obj/kern/debug/kmonitor.o obj/kern/debug/panic.o \
| | obj/kern/driver/clock.o obj/kern/driver/console.o \
| | obj/kern/driver/intr.o obj/kern/driver/picirq.o \
| | obj/kern/trap/trap.o obj/kern/trap/trapentry.o \
| | obj/kern/trap/vectors.o obj/kern/mm/pmm.o \
| | obj/libs/printfmt.o obj/libs/string.o
| | 其中新出現的關鍵參數爲
| | -T 讓連接器使用指定的腳本
|
| 生成一個有10000個塊的文件,每個塊默認512字節,用0填充
| dd if=/dev/zero of=bin/tos.img count=10000
|
| 把bootblock中的內容寫到第一個塊
| dd if=bin/bootblock of=bin/tos.img conv=notrunc
|
| 從第二個塊開始寫kernel中的內容
| dd if=bin/kernel of=bin/tos.img seek=1 conv=notrunc
[練習1.2] 一個被系統認爲是符合規範的硬盤主引導扇區的特徵是什麼?
從sign.c的代碼來看,一個磁盤主引導扇區只有512字節。且
第510個(倒數第二個)字節是0x55,
第511個(倒數第一個)字節是0xAA。
## [練習2]
[練習2.1] 從 CPU 加電後執行的第一條指令開始,單步跟蹤 BIOS 的執行。
練習2可以單步跟蹤,方法如下:
1 修改 lab1/tools/gdbinit,內容爲:
set architecture i8086
target remote :1234
2 在 lab1目錄下,執行
make debug
3 在看到gdb的調試界面(gdb)後,在gdb調試界面下執行如下命令
si
即可單步跟蹤BIOS了。
4 在gdb界面下,可通過如下命令來看BIOS的代碼
x /2i $pc //顯示當前eip處的彙編指令
> [進一步的補充]
改寫Makefile文件
debug:
在調用qemu時增加`-d in_asm -D q.log`參數,便可以將運行的彙編指令保存在q.log中。
爲防止qemu在gdb連接後立即開始執行,刪除了`tools/gdbinit`中的`continue`行。
[練習2.2] 在初始化位置0x7c00 設置實地址斷點,測試斷點正常。
在tools/gdbinit結尾加上
set architecture i8086 //設置當前調試的CPU是8086
b *0x7c00 //在0x7c00處設置斷點。此地址是bootloader入口點地址,可看boot/bootasm.S的start地址處
c //continue簡稱,表示繼續執行
x /2i $pc //顯示當前eip處的彙編指令
set architecture i386 //設置當前調試的CPU是80386
運行"make debug"便可得到
Breakpoint 2, 0x00007c00 in ?? ()
=> 0x7c00: cli
0x7c01: cld
0x7c02: xor %eax,%eax
0x7c04: mov %eax,%ds
0x7c06: mov %eax,%es
0x7c08: mov %eax,%ss
0x7c0a: in $0x64,%al
0x7c0c: test $0x2,%al
0x7c0e: jne 0x7c0a
0x7c10: mov $0xd1,%al
[練習2.3] 在調用qemu 時增加-d in_asm -D q.log 參數,便可以將運行的彙編指令保存在q.log 中。
將執行的彙編代碼與bootasm.S 和 bootblock.asm 進行比較,看看二者是否一致。
在tools/gdbinit結尾加上
b *0x7c00
c
x /10i $pc
便可以在q.log中讀到"call bootmain"前執行的命令
----------------
IN:
0x00007c00: cli
----------------
IN:
0x00007c01: cld
0x00007c02: xor %ax,%ax
0x00007c04: mov %ax,%ds
0x00007c06: mov %ax,%es
0x00007c08: mov %ax,%ss
----------------
IN:
0x00007c0a: in $0x64,%al
----------------
IN:
0x00007c0c: test $0x2,%al
0x00007c0e: jne 0x7c0a
----------------
IN:
0x00007c10: mov $0xd1,%al
0x00007c12: out %al,$0x64
0x00007c14: in $0x64,%al
0x00007c16: test $0x2,%al
0x00007c18: jne 0x7c14
----------------
IN:
0x00007c1a: mov $0xdf,%al
0x00007c1c: out %al,$0x60
0x00007c1e: lgdtw 0x7c6c
0x00007c23: mov %cr0,%eax
0x00007c26: or $0x1,%eax
0x00007c2a: mov %eax,%cr0
----------------
IN:
0x00007c2d: ljmp $0x8,$0x7c32
----------------
IN:
0x00007c32: mov $0x10,%ax
0x00007c36: mov %eax,%ds
----------------
IN:
0x00007c38: mov %eax,%es
----------------
IN:
0x00007c3a: mov %eax,%fs
0x00007c3c: mov %eax,%gs
0x00007c3e: mov %eax,%ss
----------------
IN:
0x00007c40: mov $0x0,%ebp
----------------
IN:
0x00007c45: mov $0x7c00,%esp
0x00007c4a: call 0x7d0d
----------------
IN:
0x00007d0d: push %ebp
其與bootasm.S和bootblock.asm中的代碼相同。
## [練習3]
分析bootloader 進入保護模式的過程。
從`%cs=0 $pc=0x7c00`,進入後
首先清理環境:包括將flag置0和將段寄存器置0
.code16
cli
cld
xorw %ax, %ax
movw %ax, %ds
movw %ax, %es
movw %ax, %ss
開啓A20:通過將鍵盤控制器上的A20線置於高電位,全部32條地址線可用,
可以訪問4G的內存空間。
seta20.1: # 等待8042鍵盤控制器不忙
inb $0x64, %al #
testb $0x2, %al #
jnz seta20.1 #
movb $0xd1, %al # 發送寫8042輸出端口的指令
outb %al, $0x64 #
seta20.1: # 等待8042鍵盤控制器不忙
inb $0x64, %al #
testb $0x2, %al #
jnz seta20.1 #
movb $0xdf, %al # 打開A20
outb %al, $0x60 #
初始化GDT表:一個簡單的GDT表和其描述符已經靜態儲存在引導區中,載入即可
lgdt gdtdesc
進入保護模式:通過將cr0寄存器PE位置1便開啓了保護模式
movl %cr0, %eax
orl $CR0_PE_ON, %eax
movl %eax, %cr0
通過長跳轉更新cs的基地址
ljmp $PROT_MODE_CSEG, $protcseg
.code32
protcseg:
設置段寄存器,並建立堆棧
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
轉到保護模式完成,進入boot主方法
call bootmain
## [練習4]
分析bootloader加載ELF格式的OS的過程。
首先看readsect函數,
`readsect`從設備的第secno扇區讀取數據到dst位置
static void
readsect(void *dst, uint32_t secno) {
waitdisk();
outb(0x1F2, 1); // 設置讀取扇區的數目爲1
outb(0x1F3, secno & 0xFF);
outb(0x1F4, (secno >> 8) & 0xFF);
outb(0x1F5, (secno >> 16) & 0xFF);
outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0);
// 上面四條指令聯合制定了扇區號
// 在這4個字節線聯合構成的32位參數中
// 29-31位強制設爲1
// 28位(=0)表示訪問"Disk 0"
// 0-27位是28位的偏移量
outb(0x1F7, 0x20); // 0x20命令,讀取扇區
waitdisk();
insl(0x1F0, dst, SECTSIZE / 4); // 讀取到dst位置,
// 幻數4因爲這裏以DW爲單位
}
readseg簡單包裝了readsect,可以從設備讀取任意長度的內容。
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;
// 加1因爲0扇區被引導佔用
// ELF文件從1扇區開始
for (; va < end_va; va += SECTSIZE, secno ++) {
readsect((void *)va, secno);
}
}
在bootmain函數中,
void
bootmain(void) {
// 首先讀取ELF的頭部
readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);
// 通過儲存在頭部的幻數判斷是否是合法的ELF文件
if (ELFHDR->e_magic != ELF_MAGIC) {
goto bad;
}
struct proghdr *ph, *eph;
// ELF頭部有描述ELF文件應加載到內存什麼位置的描述表,
// 先將描述表的頭地址存在ph
ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
eph = ph + ELFHDR->e_phnum;
// 按照描述表將ELF文件中數據載入內存
for (; ph < eph; ph ++) {
readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
}
// ELF文件0x1000位置後面的0xd1ec比特被載入內存0x00100000
// ELF文件0xf000位置後面的0x1d20比特被載入內存0x0010e000
// 根據ELF頭部儲存的入口信息,找到內核的入口
((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
bad:
outw(0x8A00, 0x8A00);
outw(0x8A00, 0x8E00);
while (1);
}
## [練習5]
實現函數調用堆棧跟蹤函數
ss:ebp指向的堆棧位置儲存着caller的ebp,以此爲線索可以得到所有使用堆棧的函數ebp。
ss:ebp+4指向caller調用時的eip,ss:ebp+8等是(可能的)參數。
輸出中,堆棧最深一層爲
ebp:0x00007bf8 eip:0x00007d68 \
args:0x00000000 0x00000000 0x00000000 0x00007c4f
<unknow>: -- 0x00007d67 --
其對應的是第一個使用堆棧的函數,bootmain.c中的bootmain。
bootloader設置的堆棧從0x7c00開始,使用"call bootmain"轉入bootmain函數。
call指令壓棧,所以bootmain中ebp爲0x7bf8。
## [練習6]
完善中斷初始化和處理
[練習6.1] 中斷向量表中一個表項佔多少字節?其中哪幾位代表中斷處理代碼的入口?
中斷向量表一個表項佔用8字節,其中2-3字節是段選擇子,0-1字節和6-7字節拼成位移,
兩者聯合便是中斷處理程序的入口地址。
[練習6.2] 請編程完善kern/trap/trap.c中對中斷向量表進行初始化的函數idt_init。
見代碼
[練習6.3] 請編程完善trap.c中的中斷處理函數trap,在對時鐘中斷進行處理的部分填寫trap函數
見代碼
## [練習7]
增加syscall功能,即增加一用戶態函數(可執行一特定系統調用:獲得時鐘計數值),
當內核初始完畢後,可從內核態返回到用戶態的函數,而用戶態的函數又通過系統調用得到內核態的服務
在idt_init中,將用戶態調用SWITCH_TOK中斷的權限打開。
SETGATE(idt[T_SWITCH_TOK], 1, KERNEL_CS, __vectors[T_SWITCH_TOK], 3);
在trap_dispatch中,將iret時會從堆棧彈出的段寄存器進行修改
對TO User
tf->tf_cs = USER_CS;
tf->tf_ds = USER_DS;
tf->tf_es = USER_DS;
tf->tf_ss = USER_DS;
對TO Kernel
tf->tf_cs = KERNEL_CS;
tf->tf_ds = KERNEL_DS;
tf->tf_es = KERNEL_DS;
在lab1_switch_to_user中,調用T_SWITCH_TOU中斷。
注意從中斷返回時,會多pop兩位,並用這兩位的值更新ss,sp,損壞堆棧。
所以要先把棧壓兩位,並在從中斷返回後修復esp。
asm volatile (
"sub $0x8, %%esp \n"
"int %0 \n"
"movl %%ebp, %%esp"
:
: "i"(T_SWITCH_TOU)
);
在lab1_switch_to_kernel中,調用T_SWITCH_TOK中斷。
注意從中斷返回時,esp仍在TSS指示的堆棧中。所以要在從中斷返回後修復esp。
asm volatile (
"int %0 \n"
"movl %%ebp, %%esp \n"
:
: "i"(T_SWITCH_TOK)
);
但這樣不能正常輸出文本。根據提示,在trap_dispatch中轉User態時,將調用io所需權限降低。
tf->tf_eflags |= 0x3000;