ucore lab1


ucore lab1 report

這個報告是計算機1班第10組實驗報告。

exercise 1: 生成ucore的過程

通過make V=輸出的命令研究ucore生成的過程。

下面的命令是make實際執行的命令(23~24行除外)。

  1 gcc -Ikern/init/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/init/init.c -o obj/ke
  2 gcc -Ikern/libs/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/libs/stdio.c -o obj/k
  3 gcc -Ikern/libs/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/libs/readline.c -o ob
  4 gcc -Ikern/debug/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/debug/panic.c -o obj
  5 gcc -Ikern/debug/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/debug/kdebug.c -o ob
  6 gcc -Ikern/debug/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/debug/kmonitor.c -o 
  7 gcc -Ikern/driver/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/clock.c -o o
  8 gcc -Ikern/driver/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/console.c -o
  9 gcc -Ikern/driver/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/picirq.c -o 
 10 gcc -Ikern/driver/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/intr.c -o ob
 11 gcc -Ikern/trap/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/trap/trap.c -o obj/ke
 12 gcc -Ikern/trap/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/trap/vectors.S -o obj
 13 gcc -Ikern/trap/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/trap/trapentry.S -o o
 14 gcc -Ikern/mm/ -fno-builtin -fno-PIC -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
 15 gcc -Ilibs/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/  -c libs/string.c -o obj/libs/string.o
 16 gcc -Ilibs/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/  -c libs/printfmt.c -o obj/libs/printfmt.o
 17 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/
 18 gcc -Iboot/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootasm.S -o obj/boot/bootasm.o
 19 gcc -Iboot/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootmain.c -o obj/boot/bootmain.o
 20 gcc -Itools/ -g -Wall -O2 -c tools/sign.c -o obj/sign/tools/sign.o
 21 gcc -g -Wall -O2 obj/sign/tools/sign.o -o bin/sign
 22 ld -m    elf_i386 -nostdlib -N -e start -Ttext 0x7C00 obj/boot/bootasm.o obj/boot/bootmain.o -o obj/bootblock.o
 23 'obj/bootblock.out' size: 496 bytes
 24 build 512 bytes boot sector: 'bin/bootblock' success!
 25 dd if=/dev/zero of=bin/ucore.img count=10000
 26 dd if=bin/bootblock of=bin/ucore.img conv=notrunc
 27 dd if=bin/kernel of=bin/ucore.img seek=1 conv=notrunc

編譯過程

編譯ucore和編譯應用程序的過程相同,但是:

  1. 不使用任何外部的庫,比如C標准庫;
  2. 不使用動態鏈接,不生成pic(position-independent code)代碼;
  3. 不查找標准庫頭文件;

具體參數分析:

  1. 鏈接相關
  • -no-stdlib: 不鏈接標准庫和C初始化函數

由於ucore是操作系統,不應該使用外部的庫,而且應該自己指定程序的入口,所以用這個選項。

  • -fno-buitlin: 對於非__buitlin_開頭的函數,不使用對應的GCC內置(built in)函數

除了外部的庫,GCC內部內嵌的有庫libgcc.a,這個庫總是自動鏈接(即使使用了-no-stdlib選項)。大量的標准庫函數都有對應的內置版本,比如strcpy,對應有__builtin_strcpy,調用strcpy時GCC實際調用__builtin_strcpy。ucore既需要內置函數實現的C語言特性(比如va_list),又希望自己實現部分標准庫函數(比如strcpy),並且在調用時使用自定義版本而非內置版本。

  • -no-stdinc: 不搜索系統默認的頭文件目錄

ucore不使用外部庫,只是用libgcc.a和自定義的標准庫。如果不開啟這個選項,GCC會搜索到系統的標准庫頭文件,而非自定義的標准庫頭文件。

  • -fno-PIC: 不生成位置無關代碼(position-independent code)

GCC默認動態鏈接,生成pic代碼。ucore不進行動態鏈接,不生成pic代碼。

  1. 調試相關
  • -ggdb: 生成GDB專用格式的調試信息

ucore使用GDB調試,生成GDB專用的調試信息可以最大限度的增強GDB的調試能力。

  • -gstabs: 生成stabs格式的調試信息

ucore中內置了調試內核的函數,比如聯系5完成的print_stackframe()函數,這些函數解析stabs格式的調試信息。

  1. 目標平台
  • -m32: 生成IA-32位代碼

ucore是運行在IA-32處理器上的操作系統,所以要用這個選項。

  1. 代碼生成規格
  • -fno-stack-protector: 禁用stack-protector

現代GCC編譯時會使用stack-protector(在數組末尾添加金絲雀值(哨兵),如果金絲雀值被更改就會終止程序)防止緩沖區溢出。ucore添加這個選項可能是想簡化內存布局或者減少運行時的內存消耗。

鏈接過程

bootloader和kernel都是ucore項目的一部分,但卻是兩個獨立的程序(執行文件),所以分別鏈接。

ld選項:

  • -m: 指定可執行文件格式與目標平台

ucore使用elf_i386格式。

  • -nostdlib: 禁止鏈接標准庫和C程序初始函數

ucore是操作系統,不依賴與外部函數庫。

  • -T: 指定linker script

生成kernel的過程中使用tools/kernel.ld作為linker script,其中包含了設置kernel內部各段的相關信息;生成bootblock的過程中使用tools/boot.ld作為linker scrip,其中包含程序入口點。

  • -Ttext: 設置可執行文件的.text節的絕對地址

在生成bootblock的過程中使用了這個選項,使bootloader的.text節起始地址為絕對地址0x7c00。

  • -e: 設置可執行文件入口點

在生成bootblock的過程中使用了這個選項,指定start標記為bootloader的入口點。

  • -N: 將可執行文件的代碼節和數據節設置為讀/寫、禁止代碼節向頁大小對齊、禁止動態鏈接

_

啟動扇區的檢驗和生成

完成了全部的編譯鏈接后,還需要生成啟動扇區。使用tools/sign和objdump對目標文件bootblock.o進行修改得到。

在生成了bootblock.o,

  1. 調用objdump提取了bootblock.o中的代碼輸出到bootblock.out中;
  2. 調用tools/sign檢查bootblock.out是否小於510字節,如果大於510字節就報錯;
  3. tools/sign將bootblock.out輸出到一個bin/bootblock,並將文件最后兩個字節設置為0x55,0xAA。
	@$(OBJDUMP) -S $(call objfile,bootblock) > $(call asmfile,bootblock)
	@$(OBJDUMP) -t $(call objfile,bootblock) | $(SED) '1,/SYMBOL TABLE/d; s/ .* / /; /^$$/d' > $(call symfile,bootblock)
	@$(OBJCOPY) -S -O binary $(call objfile,bootblock) $(call outfile,bootblock)
	@$(call totarget,sign) $(call outfile,bootblock) $(bootblock)

$(call create_target,bootblock)

通過以上步驟得到的bin/bootblock包含了啟動扇區中的全部內容。

虛擬硬盤的制作

通過dd命令制作虛擬硬盤。
先將虛擬硬盤第一個扇區區初始化為0,在把bin/bootblock寫入其中制成啟動扇區,然后再將bin/kernel寫入之后的扇區中。

dd if=/dev/zero of=bin/ucore.img count=10000
dd if=bin/bootblock of=bin/ucore.img conv=notrunc
dd if=bin/kernel of=bin/ucore.img seek=1 conv=notrunc

exercise 2:使用qemu執行並調試lab1中的軟件

從CPU加電后執行的第一條指令開始,單步跟蹤BIOS的執行

修改gdbinit內容為:

set architecture i8086

target remote :1234

在lab1下執行:

make debug

反匯編 :

x /2i $pc

結果如圖:

在初始化位置0x7c00設置實地址斷點,測試斷點

gdbinit文件修改為:

set architecture i8086

target remote :1234

b *0x7c00

c

gdb中執行

x/2i $pc

結果如圖

從0x7c00開始跟蹤代碼運行,將單步跟蹤反匯編得到的代碼與bootasm.S和 bootblock.asm進行比較

改寫makefile文件:

debug: $(UCOREIMG)

\((V)\)(TERMINAL) -e "$(QEMU) -S -s -d in_asm -D $(BINDIR)/q.log -parallel stdio -hda $< -serial null"

​ $(V)sleep 2

\((V)\)(TERMINAL) -e "gdb -q -tui -x tools/gdbinit"

執行:

make debug

得到q.log文件

查看bootasm.S文件

查看bootblock.asm文件

查看bootblock.asm文件

從上面結果可以看出bootasm.S文件代碼和bootblock.asm是一樣的。

找一個bootloader或內核中的代碼位置,設置斷點並進行測試

在0x7c35設置斷點(bootmain函數)。修改gdbinit:

set architecture i8086

target remote :1234

break *0x7c4a

執行make debug,結果如下:

斷點正常。

exercise 3: bootloader進入保護模式的過程

練習一要求分析bootloader進入保護模式的過程。bootloader由三個文件實現,分別為asm.h(包含常量,初始值)、bootasm.S(開啟A20,設置GDT,並進入保護模式)、bootmain.c(加載kernel並執行)。這個練習僅涉及asm.h和bootasm.S。

bootasm.S完成了A20的開啟、GDT的設置、內核棧的設置后進入保護模式,並call到bootmain執行,進行kernel的加載工作。

常量與宏

bootasm.S中定義了三個常量:

.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

CRO_PE_ON作為切換到保護模式時修改CRO的掩碼。
PROT_MODE_CSEG和PROT_MODE_DSEG分別作為內核代碼段、數據段的選擇子,指向GDT[1]和GDT[2],RPL(requested privilege level)均設置為ring 0。

asm.h中定義了描述段描述符的宏。

A20的開啟

bootloader最初運行在16位實模式下。

bootloader首先關閉對中斷的響應,並將段寄存器ds,es,ss置零,之后開始A20(在8042芯片上)的開啟。
開啟A20的思路很簡單:等到8042芯片空閑時,將A20位設置為1即可。由於8042的設計,向8042寫入數據被才分成向0x64端口發送寫命令和向0x60端口發送數據兩步。

最終操作步驟如下:

  1. 等待8042芯片空閑
  2. 向P2端口發送寫命令
  3. 再次等待8042芯片空閑
  4. 像P2端口寫入數據

等待8042芯片空閑
等待8042空閑通過循環讀取8042的狀態寄存器到CPU寄存器al,判斷al是否為0x2(芯片初始系統狀態)來實現。

seta20.1:
    inb $0x64, %al                                  # read status information into al
    testb $0x2, %al
    jnz seta20.1

像8042寫入數據

向端口0x64發送寫命令

    movb $0xd1, %al                                 # 0xd1 -> port 0x64
    outb %al, $0x64                                 # 0xd1 means: write data to 8042's P2 port

向端口0x60寫入數據

    movb $0xdf, %al                                 # 0xdf -> port 0x60
    outb %al, $0x60                                 # 0xdf = 11011111, means set P2's A20 bit(the 1 bit) to 1

經過以上介紹的四步,A20就被開啟了,系統可以使用高於1M的線性空間。

設置GDT

GDT被設置為4字節對齊,僅定義了GDT[0]、GDT[1](內核代碼段)、GDT[2](內核數據段)。

# 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(executable and read-only)
    SEG_ASM(STA_W, 0x0, 0xffffffff)                 # data seg for bootloader and kernel(writable)

gdtdesc:
    .word 0x17                                      # sizeof(gdt) - 1
    .long gdt                                       # address gdt

```
其中的的宏定義在asm.h中,asm.h比較短,直接摘抄出來。
```
/* Normal segment */
#define SEG_NULLASM                                             \
    .word 0, 0;                                                 \
    .byte 0, 0, 0, 0

#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)

/* Application segment type bits */
#define STA_X       0x8     // Executable segment
#define STA_E       0x4     // Expand down (non-executable segments)
#define STA_C       0x4     // Conforming code segment (executable only)
#define STA_W       0x2     // Writeable (non-executable segments)
#define STA_R       0x2     // Readable (executable segments)
#define STA_A       0x1     // Accessed

分析以上兩段代碼可以發現:

  • GDT[0]設置為空,未使用;
  • GDT[1],GDT[2]分別作為內核代碼段、數據段;
  • 內核代碼段、數據段都被設置為最長4G,且基地址均為0x00;

內核代碼段、數據段被設置成這樣是有意削弱(避免)X86分段內存模型的影響,在32位的CPU上實現類似64位上的平坦內存模型,方便頁機制的實現。

加載GDT

lgdt gdtdesc

切換到保護模式

開啟A20、設置並加載GDT后,bootloader已經完成了切換到保護模式的全部准備工作。

開啟保護模式僅需要打開控制寄存器CR0中相應的標志位,通過異或之前定義的掩碼CRO_PE_ON實現。

    movl %cr0, %eax
    orl $CR0_PE_ON, %eax        
    movl %eax, %cr0

打開模式后,CPU真正進入了32位模式,默認使用分段內存模型,段寄存器中必須存放相應的選擇子。
首先設置cs和 eip的值,通過ljmp實現。ljmp僅僅是跳轉到了下一條指令(procseg處)

    # Jump to next instruction, but in 32-bit code segment.
    # Switches processor into 32-bit mode.
    ljmp $PROT_MODE_CSEG, $protcseg

.code32
procseg:

設置棧並跳轉到bootmain加載kernel

將所有的寄存器設置為PROT_MODE_DSEG(指向內核數據段)。將棧設置為為0x00~0x7c00(bootloader之下都是棧的空間),然后使用call指令執行bootmain,開始加載kernel。
bootmain函數正常情況不會返回,如果返回肯定是bootloader產生錯誤,進入死循環。

.code32                                             # Assemble for 32-bit mode
protcseg:
    # Set up the protected-mode data segment registers
    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)
    movl $0x0, %ebp
    movl $start, %esp
    call bootmain

    # bootmain should not return. if return, loop forever
spin:
    jmp spin

棧和bootloader的位置關系如下:

+--------------------+
+                    +
+                    +
+                    +
+                    +
+     not in use     +
+                    +
+                    +
+                    +
+                    +
+                    +
+--------------------+
+                    +
+                    +
+                    +
+      bootloader    +
+                    +
+                    +
+                    +
+                    +
+--------------------+ <-- 0x7c00 beginning of bootloader
+                 |  +
+                 |  +
+       stack     |  +
+                 |  +
+                 |  +
+                 V  +
+--------------------+ <-- 0x00 

至此,bootlader完成了初始化和各種設置,bootasm.S完成任務,剩下的任務交給bootmain函數完成。

execrise 4: bootloader加載ELF格式的OS的過程

在bootmain函數中,bootloader加載kernel到內存中。
先分析bootmain函數如何加載ELF格式的kernel,再分析具體讀取磁盤的機制。

加載ELF格式的kernel

為了理解如何加載ELF格式的kernel,就必須知道ELF格式的結構。在這里只涉及到了ELF32格式中的已鏈接的可執行文件,所以忽略共享目標文件和可重定位目標文件。

實驗手冊給的資料不足,參考《深入理解計算機系統》第三版第7章《鏈接》圖7-13“典型的ELF可執行目標文件”

典型的ELF可執行文件

圖中ELF頭和段頭部對應的ucore中的數據類型為struct elfhdr和struct proghdr,均定義在libs/elf.h中。這里只摘抄在這里用到的結構體成員。

#define ELF_MAGIC    0x464C457FU            // "\x7FELF" in little endian

struct elfhdr {
    ...
    uint32_t e_magic;     // must equal ELF_MAGIC
    ...
    uint32_t e_entry;     // entry point of executable
    ...
    uint16_t e_phnum;     // number of entries in program header or 0
    ...
}

struct proghdr {
    ...
    uint32_t p_offset; // file offset of segment
    uint32_t p_va;     // virtual address to map segment
    ...
    uint32_t p_memsz;  // size of segment in memory (bigger if contains bss)
    ...
}

知道這些信息后,加載ELF格式的kernel的機制就很清楚了:

  • 通過判斷ELF_MAGIC是否等於ELF頭中的e_magic確定kernel是否是合法的ELF32格式
  • 段頭部表是struct proghdr的數組,數組元素個數為e_phnum(在ELF頭中)
  • 內核各段應該加載到對應段頭部中記錄的p_va處,大小為p_memsz,位於於kernel文件的p_offset處。通過readseg實現。

bootmain邏輯流:

  1. 讀取kernel的ELF頭和段頭部表加載到ELFHDR(0x10000)處
  2. 判斷kerne是否是合法的ELF格式,如果不是則死循環
  3. 如果格式合法就根據kernel中的ELF頭和段頭部表中的信息將kernel各段加載到適當的位置
  4. 加載完成后,通過函數調用跳轉到entry point

具體實現代碼如下:

uintptr_t其實就是uint32_t代表32位地址,定義在lib/defs.h中。

/* bootmain - the entry of bootloader */
void
bootmain(void) {
    // read the 1st page off disk
    /** read SECTSIZE*8 bytes at offset 0 from kernel into virtual address ELFHDR(0x10000) **/
    readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);

    /* if the executable is not ELF, goto bad(infinite loop) */
    if (ELFHDR->e_magic != ELF_MAGIC) {
        goto bad;
    }

    struct proghdr *ph, *eph;

    // load each program segment (ignores ph flags)
    ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
    /* eph is the position after the end of the last program header */
    eph = ph + ELFHDR->e_phnum;
    for (; ph < eph; ph ++) {
        /** load executable code from kernel into corresponding virtual address according to infomation in program header **/
        readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
    }

    // call the entry point from the ELF header
    // note: does not return
    /* use function call to transfer control to kernel */
    ((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();

bad:
    outw(0x8A00, 0x8A00);
    outw(0x8A00, 0x8E00);

    /* do nothing */
    while (1);
}


ELF格式中記錄的地址是鏈接時設置的,這些地址是虛擬地址,在啟動虛擬內存系統之前,ucore設置了虛擬地址與物理地址之間的臨時映射,關系為:

virtual address = physical address + 0xC0000000

按位與掩碼0xFFFFFF獲取鏈接地址對應的物理地址。

讀取硬盤的機制

ucore為了簡化硬盤訪問的實現,使用的是可編程IO(programed IO)方式,並且假設硬盤使用IDE。一個IDE通道可以接兩個硬盤(主盤/從盤),ucore只讀取主盤的數據。

基本思路:

  1. 等待硬盤空閑
  2. 發送要讀取的扇區號為硬盤
  3. 等待硬盤空閑
  4. 讀取扇區數據到某個內存位置

ucore通過readsg()、readsec()和insl()三個函數實現讀取硬盤。readsg()和readsec()定義在bootmain.c中,insl()定義在libs/x86.h頭文件中。

/* 從讀取扇區號secno的數據到內存dst處 */
static void
readsect(void *dst, uint32_t secno);

/* 從kernel文件偏移offset處讀取count字節到內存va處 */
static void
readseg(uintptr_t va, uint32_t count, uint32_t offset);

/* 內聯匯編。從prot端口讀取cnt*4字節數據到內存addr處 */
static inline
void insl(uint32_t port, void *addr, int cnt) __attribute__((always_inline));

函數調用關系: readsg() --> readsec() --> insl()

insl()實現:

*/當%ecx(cnt)不為0使,從端口%edi(port)處讀取4字節到(%edi)(內存addr)處*/
static inline void
insl(uint32_t port, void *addr, int cnt) {
    /* edi - addr    
       ecx - cnt
       edi - port
    */
    asm volatile (
            "cld;"
            "repne; insl;"
            : "=D" (addr), "=c" (cnt)
            : "d" (port), "0" (addr), "1" (cnt)
            : "memory", "cc");
}

insl()函數是一個輔助函數,在讀取之前還需要像硬盤發送讀命令,讀取硬盤扇區由readsect()完成。

磁盤IO地址與功能(摘自ucore實驗手冊)如下:

static void
readsect(void *dst, uint32_t secno) {
    // wait for disk to be ready
    waitdisk();

    outb(0x1F2, 1);                         // count = 1
    outb(0x1F3, secno & 0xFF);
    outb(0x1F4, (secno >> 8) & 0xFF);
    outb(0x1F5, (secno >> 16) & 0xFF);
    outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0); /* 掩碼0xF將扇區號28~31位置零,僅保留24~27位。
                                                  異或0xE0避開了IO端口第4位,目的是讀取主盤。*/
    outb(0x1F7, 0x20);                      // cmd 0x20 - read sectors

    // wait for disk to be ready
    waitdisk();

    // read a sector
    insl(0x1F0, dst, SECTSIZE / 4);           /*SECTISE被定義為512。insl每次讀取4字節,所以SECTISE要除4
}

最終bootmain讀取磁盤使用的是readseg()函數。

static void
readseg(uintptr_t va, uint32_t count, uint32_t offset) {
    /* uintptr_t在/libs/defs.h中定義為uint32_t;
       uintprt_t避免了C語言指針運算時的自動伸展;
    */      
    uintptr_t end_va = va + count;

    // round down to sector boundary
    va -= offset % SECTSIZE;

    // translate from bytes to sectors; kernel starts at sector 1
    uint32_t secno = (offset / SECTSIZE) + 1;

    // If this is too slow, we could read lots of sectors at a time.
    // We'd write more to memory than asked, but it doesn't matter --
    // we load in increasing order.
    for (; va < end_va; va += SECTSIZE, secno ++) {
        readsect((void *)va, secno);
    }
}

readseg()函數提供的功能是從磁盤讀取count字節數據,但是讀取硬盤卻以512字節的扇區為為單位,所以實際讀入的字節數很可能大於要求讀入的字節數。
readseg()函數實際寫入的內存地址是地址參數向下舍入到512字節的邊界,所以實際寫入的內存地址很可能低於要求寫入的內存地址。

execrise 5:完成函數調用跟蹤函數

這個練習要求我們kern/debug/kdebug.c中的print_stackframe()函數。在print——stackframe()函數中,注釋已經給出了完整的步驟,難度不大,只要理解x86函數調用過程就可以做出來。
棧的設置是在啟動階段(跳轉到bootmain函數之前完成的),所以初始時棧結構如下:

+--------------------+ <-- 0x7c00 
+   bootmain frame   +    |g
+--------------------+    |r
+                    +    |o
+                    +    |w
+                    +    V
+                    +
+                    +
+--------------------+ <-- 0x00 

bootmain函數是不會返回的,所有的函數棧幀都在bootmain函數下面。在調用bootmain()之前,%ebp被設置了0,這個值被壓入了函數棧中,這個特殊的%ebp是跟蹤函數棧幀的結束標志。

x86函數調用的具體過程(同特權級):

  1. 把函數參數壓入棧中
  2. 把函數返回地址(%es,%eip)壓入棧中
  3. 把舊的%ebp壓入棧中,並把%ebp的值改為當前%esp

跟蹤函數堆棧就是利用了x86函數調用后堆棧的結構。

/* stack */
                        high address
+         %cs        +
+        %eip        +
+--------------------+<----------------------------------------------+
+        ...         +  local variables of calling function          +
+--------------------+                                               +
+     parameters     +  parameters passed by calling function        +
+--------------------+  <-----+                                      +
+         %cs        +        +                                      +
+--------------------+        +---- return address                   +
+        %eip        +        +                                      +
+--------------------+  <-----+                                      +
+      old %ebp      +-----------------------------------------------+
+--------------------+<----------------------------------------------+
+        ...         +  local variables of calling function          +
+--------------------+                                               +
+     parameters     +  parameters passed by calling function        +
+--------------------+  <-----+                                      +
+         %cs        +        +                                      +
+--------------------+        +---- return address                   +
+        %eip        +        +                                      +
+--------------------+  <-----+                                      +
+      old %ebp      +-----------------------------------------------+
+--------------------+  <----- current %ebp
+         ...        +
+                    +
                        low address

在上面的圖示是某時刻堆棧的布局。

每次函數調用,%ebp都被壓入棧中,並修改為當時棧頂指針%esp的值。棧中保存的%ebp總是指向上一次保存的%ebp處,而且棧中保存的%ebp上面4字節處就是調用函數的返回地址,返回地址之上就是被調用函數的參數。

利用當前的%ebp值和棧中保存的%ebp值就可以實現跟蹤堆棧的功能。

print_stackframe(void)函數要求了實現的具體步驟,所以最終補全的代碼跟答案結構是完全一樣的,這里直接給出了去掉步驟要求的答案代碼。

void
print_stackframe(void) {
    uint32_t ebp = read_ebp(), eip = read_eip();

    int i, j;
    for (i = 0; ebp != 0 && i < STACKFRAME_DEPTH; i ++) {
        cprintf("ebp:0x%08x eip:0x%08x args:", ebp, eip);
        uint32_t *args = (uint32_t *)ebp + 2;
        for (j = 0; j < 4; j ++) {
            cprintf("0x%08x ", args[j]);
        }
        cprintf("\n");
        print_debuginfo(eip - 1);
        eip = ((uint32_t *)ebp)[1];
        ebp = ((uint32_t *)ebp)[0];
    }
}

運行結果

在print_stackframe中對調試信息的解析和顯示由print_debuginfo()函數完成。這個函數接受一個代碼段中地址,並顯示相應的調試信息。
這個函數似乎有bug,只要把print_stackframe()函數的實現稍作改變,%eip的值偏移幾個字節,就會無法正確分析出代碼在kdebug.c文件中的位置,其他棧幀中函數所在文件似乎不受影響。

execrise 6:完善中斷初始化和處理

發生中斷后,x86根據終端去IDT中查找相應的描述符,並跳轉到描述符指向的中斷處理例程執行。
進行中斷初始化和處理,就相當於設置IDT中的描述符和描述符指向的中斷處理例程。

IDT描述符結構(摘自intel開發手冊)如下:
IDT描述符的結構

IDT表項占8個字節,其中031位、4763位代表中斷處理例程的入口。

中斷的設置

在kern/trap/vector.S中設置了256個中斷處理例程,並定義了一個指向函數的指針數組__vector。利用kern/mm/mmu.h中定義的SET_GATE函數宏就可以輕易的設置好IDT。

與初始化IDT相關的結構和宏如下:

/*
 * 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.
 */
/** defined in kern/mm/mmu.h **/
#define SETGATE(gate, istrap, sel, off, dpl) {            \
    (gate).gd_off_15_0 = (uint32_t)(off) & 0xffff;        \
    (gate).gd_ss = (sel);                                \
    (gate).gd_args = 0;                                    \
    (gate).gd_rsv1 = 0;                                    \
    (gate).gd_type = (istrap) ? STS_TG32 : STS_IG32;    \
    (gate).gd_s = 0;                                    \
    (gate).gd_dpl = (dpl);                                \
    (gate).gd_p = 1;                                    \
    (gate).gd_off_31_16 = (uint32_t)(off) >> 16;        \
}

/** defined in kern/mm/memlayout.h **/
#define SEG_KTEXT    1
#define GD_KTEXT    ((SEG_KTEXT) << 3)        // kernel text
#define DPL_KERNEL    (0)
#define DPL_KERNEL    (0)
#define USER_CS        ((GD_UTEXT) | DPL_USER)
#define KERNEL_CS    ((GD_KTEXT) | DPL_KERNEL)

/** defined in kern/mm/mmu.h **/
/* 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
};

/** defined in kern/trap/trap.c **/
static struct gatedesc idt[256] = {{0}};

/** defined in kern/trap/trap.h  **/
#define T_SYSCALL               0x80 // SYSCALL, ONLY FOR THIS PROJ

/** defined in libs/x86.h **/
/* Pseudo-descriptors used for LGDT, LLDT(not used) and LIDT instructions. */
struct pseudodesc {
    uint16_t pd_lim;        // Limit
    uint32_t pd_base;        // Base address
} __attribute__ ((packed));


設置IDT在/kern/trap/trap.c中的idt_init函數中完成:

void idt_init() 
{
	int intrno = 0;
	/* ucore don't use task gate.*/
	for(; intrno < 256; intrno++) 
		SETGATE(idt[intrno], 0, KERNEL_CS, __vectors[intrno], DPL_KERNEL);

	SETGATE(idt[T_SYSCALL], 1, KERNEL_CS, __vectors[T_SYSCALL], DPL_USER);
    ldt(&ldt_pd);
}

因為程序在調用中斷處理例程時,必須要滿足CPL<=中斷處理例程的DPL,把系統調用DPL設置為用戶態保證了用戶可以通過int指令使用系統調用,把其他中斷處理例程DPL設置為內核態確保了用戶態程序無法通過int指令調用中斷例程。

中斷的處理

中斷處理過程的調用鏈如下:

發生中斷N --> __vectors[N] --> __alltraps --> trap --> trap_dispatch

IDT中設置中斷處理程序僅僅是將中斷錯誤碼/0和中斷號壓入棧中,然后跳轉到__alltraps將其他寄存器壓棧,之后調用trap(trap_dispatch的包裝函數)將指向棧幀的指針傳遞給trap_dispatch,trap_dispatch根據棧幀中保存的中斷號對相應中斷進行處理。

__vectors[N]

  1. 對於有錯誤碼的中斷,壓入中斷號(錯誤碼由處理器壓入),跳轉到__alltraps。
  2. 對於無錯誤碼的中斷,壓入0和中斷號,跳轉到__alltraps。

__vectors[N]的這種處理方式同一了帶錯誤碼的中斷和不帶中斷碼的終端的堆棧布局,為統一兩種終端的處理過程提供了可能。

/** interrupt without error code **/
vector1:
  pushl $0
  pushl $1
  jmp __alltraps
  
/** interrupt with error code **/
.globl vector8
vector8:
  pushl $8
  jmp __alltraps


__alltraps

_alltraps對堆棧的布局進行進一步處理:

  1. 中斷發生時,將所有段寄存器、通用寄存器壓入棧中。
  2. 中斷處理完成后將傳遞給trap的指針出棧。

ucore使用struct trapframe描述堆棧的結構,定義如下:

/* registers as pushed by pushal */
struct pushregs {
    uint32_t reg_edi;
    uint32_t reg_esi;
    uint32_t reg_ebp;
    uint32_t reg_oesp;            /* Useless */
    uint32_t reg_ebx;
    uint32_t reg_edx;
    uint32_t reg_ecx;
    uint32_t reg_eax;
};

struct trapframe {
    struct pushregs tf_regs;
    uint16_t tf_gs;
    uint16_t tf_padding0;
    uint16_t tf_fs;
    uint16_t tf_padding1;
    uint16_t tf_es;
    uint16_t tf_padding2;
    uint16_t tf_ds;
    uint16_t tf_padding3;
    uint32_t tf_trapno;
    /* below here defined by x86 hardware */
    uint32_t tf_err;
    uintptr_t tf_eip;
    uint16_t tf_cs;
    uint16_t tf_padding4;
    uint32_t tf_eflags;
    /* below here only when crossing rings, such as from user to kernel */
    uintptr_t tf_esp;
    uint16_t tf_ss;
    uint16_t tf_padding5;
} __attribute__((packed)); /* disable align */


因為C語言中的結構體在內存中時連續存儲的,從低地址向高地址增長,代碼塊中的結構體沒有進行內存對齊,所以代碼中的結構體就可以看成是棧幀。

在reg_edi上方(更低地址)因為調用trap而壓入了指向trapframe的%sp指針,這個指針在中斷處理完成后由__alltraps出棧。

trap和trap_dispatch

trap作為trap_dispatch的包裝函數,功能僅僅是將指向棧幀的指針傳遞給trap_dispatch,trap_dispatch完成具體處理過程。

trap定義如下:

void
trap(struct trapframe *tf) {
   // dispatch based on what type of trap occurred
   trap_dispatch(tf);
}

trap_distrap接收到指向棧幀的指針后,使用switch語句根據棧幀中保存的中斷號對中斷進行針對性的處理。在ucore lab1中,trap_dispatch的的功能還很簡單,只有對少數幾種硬件中斷的處理能力。

我的時鍾中斷處理過程實現如下:

    case IRQ_OFFSET + IRQ_TIMER:
		ticks = (ticks + 1) % 100;
		if (ticks == 0)
			print_ticks();
        break;

執行結果:

拓展練習1

ucore的內核代碼段在boot/bootmain.c中被設置為可執行、可讀、非一致的,只有當CPL等於代碼段DPL且RPL小於等於RPL時才能夠成功訪問,所以不能夠通過直接修改CS的特權位實現特權級的切換。數據段也存在類似問題。

可行的方法是通過額外設置與內核代碼段指向相同、DPL不同的用戶態代碼段,與內核數據段指向相同、DPL不同的用戶態數據段來實現特權級切換。

當從用戶態切換到內核態時,程序的段寄存器指向對應的內核態段,偏移地址不變;當從內核態切換到用戶態時,程序的段寄存器指向對應的應用態段,偏移地址不變。

特權級切換時會發生堆棧切換,還必須設置好ring 3和ring 0的堆棧並記錄在TSS中。幸運的是,ucore在初始化過程中已經替我們完成了這些工作。

具體的設置在kern/mm/pmm.c中完成:

static struct taskstate ts = {0};

/*
 * Global Descriptor Table:
 *
 * The kernel and user segments are identical (except for the DPL). To load
 * the %ss register, the CPL must equal the DPL. Thus, we must duplicate the
 * segments for the user and the kernel. Defined as follows:
 *   - 0x0 :  unused (always faults -- for trapping NULL far pointers)
 *   - 0x8 :  kernel code segment
 *   - 0x10:  kernel data segment
 *   - 0x18:  user code segment
 *   - 0x20:  user data segment
 *   - 0x28:  defined for tss, initialized in gdt_init
 */
static struct segdesc gdt[] = {
    SEG_NULL,
    [SEG_KTEXT] = SEG(STA_X | STA_R, 0x0, 0xFFFFFFFF, DPL_KERNEL),
    [SEG_KDATA] = SEG(STA_W, 0x0, 0xFFFFFFFF, DPL_KERNEL),
    [SEG_UTEXT] = SEG(STA_X | STA_R, 0x0, 0xFFFFFFFF, DPL_USER),
    [SEG_UDATA] = SEG(STA_W, 0x0, 0xFFFFFFFF, DPL_USER),
    [SEG_TSS]    = SEG_NULL,
};


代表選擇子和特權級的常量定義在kern/mm/memlayout.h中:

/* global segment number */
#define SEG_KTEXT    1
#define SEG_KDATA    2
#define SEG_UTEXT    3
#define SEG_UDATA    4
#define SEG_TSS        5

/* global descriptor numbers */
#define GD_KTEXT    ((SEG_KTEXT) << 3)        // kernel text
#define GD_KDATA    ((SEG_KDATA) << 3)        // kernel data
#define GD_UTEXT    ((SEG_UTEXT) << 3)        // user text
#define GD_UDATA    ((SEG_UDATA) << 3)        // user data
#define GD_TSS        ((SEG_TSS) << 3)        // task segment selector

#define DPL_KERNEL    (0)
#define DPL_USER    (3)

#define KERNEL_CS    ((GD_KTEXT) | DPL_KERNEL)
#define KERNEL_DS    ((GD_KDATA) | DPL_KERNEL)
#define USER_CS        ((GD_UTEXT) | DPL_USER)
#define USER_DS        ((GD_UDATA) | DPL_USER)

為了使用中斷,還應該修改T_SWITCH_TOK和T_SWITCH_TOU兩個中斷的中斷門DPL,idt_init修改如下:

void
idt_init(void) {
	int intrno = 0;
	/* ucore don't use task gate.*/
	for(; intrno < 256; intrno++) 
		SETGATE(idt[intrno], 0, KERNEL_CS, __vectors[intrno], DPL_KERNEL);

	SETGATE(idt[T_SYSCALL], 1, KERNEL_CS, __vectors[T_SYSCALL], DPL_USER);
	SETGATE(idt[T_SWITCH_TOK], 0, KERNEL_CS, __vectors[T_SWITCH_TOK], DPL_USER);
	SETGATE(idt[T_SWITCH_TOU], 0, KERNEL_CS, __vectors[T_SWITCH_TOU], DPL_KERNEL);

	lidt(&idt_pd);

}

練習6理清了中斷處理的完整流程,中斷發生時會有大量的寄存器被保存到棧上,在執行IRET指令時恢復保存的寄存器。特權級的切換就利用了這個原理,在中斷處理過程中篡改相應的寄存器,欺騙IRET指令將這些值保存到寄存器中。

IRET的執行過程偽代碼如下:

PROTECTED-MODE:
    IF NT = 1
        THEN GOTO TASK-RETURN; (* PE = 1, VM = 0, NT = 1 *)
    FI;
    IF OperandSize = 32
        THEN
                EIP ← Pop();
                CS ← Pop(); (* 32-bit pop, high-order 16 bits discarded *)
                tempEFLAGS ← Pop();
        ELSE (* OperandSize = 16 *)
                EIP ← Pop(); (* 16-bit pop; clear upper bits *)
                CS ← Pop(); (* 16-bit pop *)
                tempEFLAGS ← Pop(); (* 16-bit pop; clear upper bits *)
    FI;
    IF tempEFLAGS(VM) = 1 and CPL = 0
        THEN GOTO RETURN-TO-VIRTUAL-8086-MODE;
        ELSE GOTO PROTECTED-MODE-RETURN;
    FI;
    PROTECTED-MODE-RETURN: (* PE = 1 *)
    IF CS(RPL) > CPL
        THEN GOTO RETURN-TO-OUTER-PRIVILEGE-LEVEL;
        ELSE GOTO RETURN-TO-SAME-PRIVILEGE-LEVEL; FI;
END;
RETURN-TO-OUTER-PRIVILEGE-LEVEL:
    IF OperandSize = 32
        THEN
                ESP ← Pop();
                SS ← Pop(); (* 32-bit pop, high-order 16 bits discarded *)
    ELSE IF OperandSize = 16
        THEN
                ESP ← Pop(); (* 16-bit pop; clear upper bits *)
                SS ← Pop(); (* 16-bit pop *)
        ELSE (* OperandSize = 64 *)
                RSP ← Pop();
                SS ← Pop(); (* 64-bit pop, high-order 48 bits discarded *)
    FI;
    IF new mode = 64-Bit Mode
        THEN
                IF EIP is not within CS limit
                        THEN #GP(0); FI;
        ELSE (* new mode = 64-bit mode *)
                IF RIP is non-canonical
                            THEN #GP(0); FI;
    FI;
    EFLAGS (CF, PF, AF, ZF, SF, TF, DF, OF, NT) ← tempEFLAGS;
    IF OperandSize = 32 or or OperandSize = 64
        THEN EFLAGS(RF, AC, ID) ← tempEFLAGS; FI;
    IF CPL ≤ IOPL
        THEN EFLAGS(IF) ← tempEFLAGS; FI;
    IF CPL = 0
        THEN
                EFLAGS(IOPL) ← tempEFLAGS;
                IF OperandSize = 32 or OperandSize = 64
                        THEN EFLAGS(VIF, VIP) ← tempEFLAGS; FI;
    FI;
    CPL ← CS(RPL);
    FOR each SegReg in (ES, FS, GS, and DS)
        DO
                tempDesc ← descriptor cache for SegReg (* hidden part of segment register *)
                IF (SegmentSelector == NULL) OR (tempDesc(DPL) < CPL AND tempDesc(Type) is (data or non-conforming code)))
                        THEN (* Segment register invalid *)
                            SegmentSelector ← 0; (*Segment selector becomes null*)
                FI;
        OD;
END;
RETURN-TO-SAME-PRIVILEGE-LEVEL: (* PE = 1, RPL = CPL *)
    IF new mode ≠ 64-Bit Mode
        THEN
                IF EIP is not within CS limit
                        THEN #GP(0); FI;
        ELSE (* new mode = 64-bit mode *)
                IF RIP is non-canonical
                            THEN #GP(0); FI;
    FI;
    EFLAGS (CF, PF, AF, ZF, SF, TF, DF, OF, NT) ← tempEFLAGS;
    IF OperandSize = 32 or OperandSize = 64
        THEN EFLAGS(RF, AC, ID) ← tempEFLAGS; FI;
    IF CPL ≤ IOPL
        THEN EFLAGS(IF) ← tempEFLAGS; FI;
    IF CPL = 0
            THEN
                    EFLAGS(IOPL) ← tempEFLAGS;
                    IF OperandSize = 32 or OperandSize = 64
                        THEN EFLAGS(VIF, VIP) ← tempEFLAGS; FI;
    FI;
END;

其中彈出錯誤碼是程序員手動完成的,在kern/trap/trapentry.S中的__trapret中已經完成了錯誤碼和堆棧上所有寄存器值的出棧。所以我們只需要關注硬件自動檢查的堆棧結構。

X86中斷發生時的堆棧結構:

從內核態到用戶態

中斷處理例程處於ring 0,所以內核態發生的中斷不發生堆棧切換,因此SS、ESP不會自動壓棧;但是是否彈出SS、ESP確實由堆棧上的CS中的特權位決定的。當我們將堆棧中的CS的特權位設置為ring 3時,IRET會誤認為中斷是從ring 3發生的,執行時會按照發生特權級切換的情況彈出SS、ESP。

利用這個特性,只需要手動地將內核堆棧布局設置為發生了特權級轉換時的布局,將所有的特權位修改為DPL_USER,保持EIP、ESP不變,IRET執行后就可以切換為應用態。

因為從內核態發生中斷不壓入SS、ESP,所以在中斷前手動壓入SS、ESP。中斷處理過程中會修改tf->tf_esp的值,中斷發生前壓入的SS實際不會被使用,所以代碼中僅僅是壓入了%eax占位。

為了在切換為應用態后,保存原有堆棧結構不變,確保程序正確運行,棧頂的位置應該被恢復到中斷發生前的位置。SS、ESP是通過push指令壓棧的,壓入SS后,ESP的值已經上移了4個字節,所以在trap_dispatch將ESP下移4字節。為了保證在用戶態下也能使用I/O,將IOPL降低到了ring 3。

static void
lab1_switch_to_user(void) {
    //LAB1 CHALLENGE 1 : TODO
	__asm__ __volatile__ (
		"pushl %%eax\n\t"
		"pushl %%esp\n\t"
		"int %0\n\t"
		:
		:"i" (T_SWITCH_TOU)
	);
}

    case T_SWITCH_TOU:
		if ((tf->tf_cs & 3) == 0) {
			tf->tf_cs = USER_CS;
			tf->tf_ss = tf->tf_ds = tf->tf_es = tf->tf_gs = tf->tf_fs = USER_DS;
			tf->tf_esp += 4;
			tf->tf_eflags |= FL_IOPL_MASK;
		}
		break;

堆棧結構如下:

     kernel stack

+          .         +
+          .         +
+          .         +   low address
+--------------------+
+         EIP        +
+                    +
+--------------------+
+         CS         +
+--------------------+
+       padding1     +
+--------------------+
+       EFLAGS       +
+                    +
+--------------------+------------------------------------+
+         ESP        +-----------+                        +
+                    +           |                        +
+--------------------+<----------+                        +
+         SS         +                                    +
+--------------------+                                    +------ push by hand             
+       padding0     +                                    +
+--------------------+<-- original stack pointer          +
+          .         + |                                  +
+          .         + |                                  +
+          .         + ++++++++++++++++++++++++++++++++++++
+                    +
  
                      high address

從用戶態到內核態

在用戶態發生中斷時堆棧會從用戶棧切換到內核棧,並壓入SS、ESP等寄存器。在篡改內核堆棧后IRET返回時會誤認為沒有特權級轉換發生,不會把SS、ESP彈出,因此從用戶態切換到內核態時需要手動彈出SS、ESP。

實現如下:

static void
lab1_switch_to_kernel(void) {
	__asm__ __volatile__ (
		"int %0\n\t"
		"popl %%esp\n\t"
		:
		:"i" (T_SWITCH_TOK)
	);
}

    case T_SWITCH_TOK:
		if ((tf->tf_cs & 3) != 0) {
			tf->tf_cs = KERNEL_CS;
			tf->tf_ss = tf->tf_ds = tf->tf_es = tf->tf_gs = tf->tf_fs = KERNEL_DS;
			tf->tf_eflags &= ~FL_IOPL_MASK;
		}
		break;

中斷處理過程中內核棧結構與上圖相同,但是tf->tf_esp指向發生中斷前用戶棧棧頂,IRET執行后程序仍處於內核態,內核堆棧結構如下:

     kernel stack
+          .         + low address
+          .         +
+          .         +   
+--------------------+<-- stack pointer after IRET       user stack
+         ESP        +---------------------------->+--------------------+               
+                    +                             +                    +                 
+--------------------+                             +                    +                 
+         SS         +                             +                    +
+--------------------+                             +                    +     
+       padding0     +                             +                    +                 
+--------------------+                             +                    +
+          .         +                                  
+          .         +                               
+          .         + 
+                    + high address                    

執行結果:

make grade結果:

拓展練習2

拓展練習2使用的技術與拓展練習1相同,通過篡改堆棧來欺騙IRET實現希望實現的功能。但是拓展練習1是在某個特定的函數中切換特權級,給了我們足夠的空間去篡改、修復堆棧。但是在拓展練習2中,中斷可以在任何時候發生,我們沒有從中斷處理返回后修復堆棧的機會,所有的操作都必須要在中斷處理的過程中完成。

從內核態切換到用戶態

按照拓展練習1中的思路,在中斷發生前要手動壓入SS、ESP,但在這里中斷可以在任意時刻發生,沒有機會提前壓入SS、ESP,所以實現從內核態切換到用戶態的關鍵就是如何在中斷處理過程補齊8個字節。這里的思路是:

  1. 將trapframe上保存的%esp(指向trapframe)減8字節
  2. 將trapframe及其上面(低地址)的全部堆棧內容向低地址平移8字節
  3. 修改%esp指向平移后的棧頂
  4. 修改堆棧中的%ebp,確保程序正確執行
  5. 設置堆棧中的SS、ESP

當trap函數返回時,__alltraps會將堆棧保存的%esp恢復到%esp寄存器中,使得棧頂指針指向trapframe。

    case IRQ_OFFSET + IRQ_KBD:
        c = cons_getc();
        cprintf("kbd [%03d] %c\n", c, c);
            switch (c) {
		case '0' :
		/* codes */
			break;
		case '3' :
			if (trap_in_kernel(tf)) {
				__asm__ __volatile__ (
					"pushl %%esi \n\t"
				        "pushl %%edi \n\t"
					"pushl %%ebx \n\t"
				/* set %esp(upon trapframe) to correct position */
					"subl $8, -4(%%edx) \n\t"
				/* move space opon trapframe and trapframe to low address */
					"movl %%esp, %%esi \n\t"
				/* %eax - the highest bound of trapframe */
					"movl %%eax, %%ecx \n\t"
					"subl %%esi, %%ecx \n\t"
					"incl %%ecx \n\t"
					"movl %%esp, %%edi \n\t"
					"subl $8, %%edi \n\t"
					"cld \n\t"
					"rep movsb \n\t"
				/* correct %esp and %ebp */
					"subl $8, %%esp \n\t"
					"subl $8, %%ebp \n\t"
					"movl %%ebp, %%ebx \n\t"
				"loop: \n\t"
					"subl $8, (%%ebx) \n\t"
					"movl (%%ebx), %%ebx \n\t"
					"cmpl %%ebx, %%eax \n\t"
					"jg loop \n\t"
				/* set esp and ss */
					"movl %%eax, -8(%%eax) \n\t"
					"movl %2, -4(%%eax) \n\t"

					"popl %%ebx \n\t"
					"popl %%edi \n\t"
					"popl %%esi \n\t"
					:
					:"a" ((uint32_t)(&tf->tf_esp)),
					 "d" ((uint32_t)(tf)),
					    "i" ((uint16_t)USER_DS)
					:"%ecx", "memory" 
				);
				correct_tf->tf_cs = USER_CS;
				correct_tf->tf_ds = tf->tf_es = tf->tf_gs = tf->tf_fs = USER_DS;
				correct_tf->tf_eflags |= FL_IOPL_MASK;
						 
			}
			break;

為了觀察執行結果,修改了kern/init/init.c中的循環,讓ucore定期顯示程序的特權級。

	long cnt = 0;
    while (1) {
	if ((++cnt) % 10000000 == 0)
	    lab1_print_cur_status();
	}

運行結果如下:

從用戶態切換到內核態

安裝拓展練習1的思路,要在IRET后修改ESP的值,但在這里沒有這個機會,所以關鍵就在於在中斷處理過程中修改堆棧的結構,使得IRET后ESP被修改到合適的值。思路如下:

  1. 修改內核堆棧中的各寄存器值
  2. 將trapframe中tf->tf_esp之上(更低地址)的部分復制到用戶堆棧上,確保使用用戶堆棧IRET后ESP指向中斷發生前的棧頂
  3. 將內核堆棧trapframe上面保存的%esp指向用戶態堆棧上的trapframe,確保IRET返回時使用的時用戶棧。

具體步驟圖示如下:

kernel stack
+--------------------+
+        ESP         +-----+
+                    +     |
+--------------------+-----+------+        +--------------------+ <--stack pointer
+                    +     |      |        +                    +    after IRET    
+                    +     |      |        +                    +
+                    +     |      |        +                    +
+                    +     |      |        +                    +
+                    +     |      |        +                    +
+                    +     |      |        +                    +
+                    +     |      |        +                    + 
+                    +     |      |        +                    +
+                    +     |      | copy   +                    +
+                    +     |      |=====>  +                    +
+                    +     |      |        +                    +
+                    +     |      |        +                    +
+                    +     +------+--+     +                    +
+                    +            |  |     +                    +      
+                    +            |  |     +                    + 
+--------------------+            |  +-->+-+--------------------+ <---original   
+        ESP         +          +-+------+ +                    +     user stack
+                    +          | |        +                    +     pointer
+--------------------+    copy  | |        +                    +
+        SS          + <========| |        +                    +
+--------------------+          | |        +                    +
+     padding 0      +  +-------+-+        +                    +
+--------------------+--+       +----------+                    +
                                           +                    +
                                           +                    +
                                           +                    +

具體實現如下:

    case IRQ_OFFSET + IRQ_KBD:
        c = cons_getc();
        cprintf("kbd [%03d] %c\n", c, c);
	switch (c) {
		case '0' :
			if (!trap_in_kernel(tf)) {
				tf->tf_cs = KERNEL_CS;
				tf->tf_ds = tf->tf_es = tf->tf_gs = tf->tf_fs = KERNEL_DS;
				tf->tf_eflags &= ~FL_IOPL_MASK;
				uintptr_t user_stack_ptr = (uintptr_t)tf->tf_esp;
				tf->tf_esp = *((uint32_t *) user_stack_ptr);
				tf->tf_ss = *((uint16_t *) (user_stack_ptr + 4));
				tf->tf_padding0 = *((uint16_t *) (user_stack_ptr + 6));
				user_stack_ptr -= (uintptr_t) (sizeof(struct trapframe) - 8);
				*((struct trapframe *) user_stack_ptr) = *tf;
				__asm__ __volatile__ (
					"movl %%ebx, -4(%%eax) \n\t"
					:
					:"a" ((uint32_t) tf),
					 "b" ((uint32_t) user_stack_ptr)
				);
			}
			break;
            ...

運行截圖:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM