lab1的實驗練習答案

# Lab1 report

## [練習1]

[練習1.1] 操作系統鏡像文件 tos.img 是如何一步一步生成的?(需要比較詳細地解釋 Makefile 中
每一條相關命令和命令參數的含義,以及說明命令導致的結果)

bin/tos.img
| 生成tos.img的相關代碼爲
| (TOSIMG): (kernel) (bootblock)| (V)dd if=/dev/zero of=@count=10000| (V)dd if=(bootblock)of= @ conv=notrunc
| (V)ddif= (kernel) of=@seek=1conv=notrunc||tos.imgbootblockkernel||>bin/bootblock||bootblock|| (bootblock): (calltoobj, (bootfiles)) | (calltotarget,sign)||@echo+ld @
| | (V) (LD) (LDFLAGS)NestartTtext0x7C00 ^ \
| | -o (calltoobj,bootblock)||@ (OBJDUMP) -S (callobjfile,bootblock)> || (call asmfile,bootblock)
| | @(OBJCOPY)SObinary (call objfile,bootblock) \
| | (calloutfile,bootblock)||@ (call totarget,sign) (calloutfile,bootblock) (bootblock)
| |
| | 爲了生成bootblock,首先需要生成bootasm.o、bootmain.o、sign
| |
| |> obj/boot/bootasm.o, obj/boot/bootmain.o
| | | 生成bootasm.o,bootmain.o的相關makefile代碼爲
| | | bootfiles = (calllistfcc,boot)||| (foreach f,(bootfiles), (call cc_compile,(f), (CC),\
| | | $(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代碼爲
| | | (calladdfileshost,tools/sign.c,sign,sign)||| (call create_target_host,sign,sign)
| | |
| | | 實際命令爲
| | | 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):tools/kernel.ld|| (kernel): (KOBJS)||@echo+ld @
| | (V) (LD) (LDFLAGS)Ttools/kernel.ldo @ (KOBJS)||@ (OBJDUMP) -S @> (call asmfile,kernel)
| | @(OBJDUMP)t @ | (SED) ‘1,/SYMBOL TABLE/d; s/ .* / /; \  
|   |       /^
/d> (call symfile,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代碼爲
| | | (calladdfilescc, (call listf_cc,(KSRCDIR)),kernel, ||| (KCFLAGS))
| | | 這些.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: (TOSIMG) (V)(TERMINAL)e (QEMU) -S -s -d in_asm -D (BINDIR)/q.logparallelstdiohda < -serial null”
(V)sleep2 (V)$(TERMINAL) -e “gdb -q -tui -x tools/gdbinit”


在調用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指向的堆棧位置儲存着callerebp,以此爲線索可以得到所有使用堆棧的函數ebpss:ebp+4指向caller調用時的eipss: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;


發佈了198 篇原創文章 · 獲贊 36 · 訪問量 27萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章