ucore_lab1實驗報告

此篇是學習ucore操作系統lab1的實驗報告,參考了很多資料和文章,也學到了不少。

先看要求

爲了實現lab1的目標,lab1提供了6個基本練習和1個擴展練習,要求完成實驗報告。

對實驗報告的要求:

  • 基於markdown格式來完成,以文本方式爲主。
  • 填寫各個基本練習中要求完成的報告內容
  • 完成實驗後,請分析ucore_lab中提供的參考答案,並請在實驗報告中說明你的實現與參考答案的區別
  • 列出你認爲本實驗中重要的知識點,以及與對應的OS原理中的知識點,並簡要說明你對二者的含義,關係,差異等方面的理解(也可能出現實驗中的知識點沒有對應的原理知識點)
  • 列出你認爲OS原理中很重要,但在實驗中沒有對應上的知識點

練習1:理解通過make生成執行文件的過程。

操作系統鏡像文件ucore.img是如何一步一步生成的?

首先通過視頻課程可知,通過sudo make V=可編譯成ucore.img,那麼,我們追蹤make的過程應該可以知道是如何一步一步生成ucore.img的,而要追蹤這個過程,應該要去看make的過程和Makefile.

Makefile文件相關命令參考Makefile

make的過程如下圖所示:

結合Makefile與function.mk的內容我們來看

按照Makefile的格式

targets : prerequisites
    command

我們可以順藤摸瓜的找一下調用鏈:

TARGETS :=

TARGETS: $(TARGETS)

TARGETS += $$(temp_target)

temp_target = $(call totarget,$(1))

totarget = $(addprefix $(BINDIR)$(SLASH),$(1))

BINDIR := bin

也就是說target是在bin目錄生成文件

#ls
bootblock  kernel  sign  ucore.img

繼續看Makefile,找到關於ucore.img的內容

# UCOREIMG會在bin目錄下生成ucore.img,需要kernel與bootblock,
UCOREIMG	:= $(call totarget,ucore.img)
$(UCOREIMG): $(kernel) $(bootblock)
# 輸入文件:/dev/zero,個數1000個,輸出文件默認
# 輸入文件:bootblock,不截短輸出文件,輸出文件默認
# 輸入文件:kernel,不截短輸出文件,輸出文件默認,從輸出文件開頭跳過1個塊後再開始複製
	$(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)

/dev/null,或稱空設備,是一個特殊的設備文件,它丟棄一切寫入其中的數據(但報告寫入操作成功),讀取它則會立即得到一個EOF。

/dev/zero 是一個特殊的文件,當你讀它的時候,它會提供無限的空字符(NULL, ASCII NUL, 0x00)

也就是說,這段會把bootblock編譯好的kernel和bootblock寫入ucore.img文件中

看kernel的內容:

# create kernel target
# 在bin目錄生成kernel文件
kernel = $(call totarget,kernel)

# 需要tools/kernel.ld
$(kernel): tools/kernel.ld

# 需要KOBJS
$(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)

執行邏輯:

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

Makefile裏面的內容:

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

dfdafa

執行的邏輯:

ld -m    elf_i386 -nostdlib -N -e start -Ttext 0x7C00 obj/boot/bootasm.o obj/boot/bootmain.o -o obj/bootblock.o

1、先創建一個數量爲10000,大小爲默認512bytes即5.12M大小的ucore.img的空文件

2、把bin下面的bootblock拷貝進這個ucore.img文件(bootblock大小爲512bytes)

3、把bin下面的kernel拷貝進這個ucore.img文件(kernel大小爲78912bytes)

這樣,創建出來一個第一個扇區(512bytes)爲bootblock(啓動引導),然後跟着0x55AA,再之後就是kernel。

一個被系統認爲是符合規範的硬盤主引導扇區的特徵是什麼?

大小512字節,最近兩個是 0x55AA。

可以看sign.c此函數

下面這些是GCC的一些參數作用:

  • -ggdb 生成可供gdb使用的調試信息

  • -m32 生成適用於32位環境的代碼

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

  • -nostdinc 不使用標準庫

  • -fno-stack-protector 不生成用於檢測緩衝區溢出的代碼

  • -Os 爲減小代碼大小而進行優化

  • -I添加搜索頭文件的路徑

  • -fno-builtin 不進行builtin函數的優化

  • -m 模擬爲i386上的連接器

  • -N 設置代碼段和數據段均可讀寫

  • -e 指定入口

  • -Ttext 指定代碼段開始位置

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

首先在環境配置好,之前,可以執行sudo make debug的命令,即可開啓調試,tools/gdbinit裏面的內容爲:

file bin/bootblock

set architecture i8086
target remote :1234

b *0x7c00

執行sudo make debug

報這個錯是因爲bootblock不是elf格式,需要再通過執行target remote:1234,可以看到:

image-20220104100110351

可以看到CS:EIP的內容:

image-20220104100147537

CS:EIP指向:0xffff0

image-20220104100245057

通過指令:b *0x7c00可以把斷點打到:0x7c00處:

image-20220104100422098

可以看到0x7c00處的代碼就是boot/bootasm.S裏面的代碼:

# start address should be 0:7c00, in real model, the beginning address of thre running bootloader
.globl start
start:
.code16                                             # Assemble for 16-bit mode
    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

通過一步步執行可以看到,後面會從彙編指令調用C的函數:

    # 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

可以看到

image-20220104100744467

可以看到call 0x7d0d調用到bootmain。

當然前面的指令做了很多工作,比如開啓A20,加載全局描述符表等

練習3:分析bootloader進入保護模式的過程。

首先什麼是A20:

在Intel的早期處理器中,地址總線有20位A0~A19。處理器能訪問2^20bytes=1,048,576=1,048,576/512=1024KB=1MB,也就是說能訪問1MB的空間。然而,地址寄存器能訪問的是16位。這樣要訪問1MB的空間就需要做一些特殊的處理。通過兩個寄存器(CS、IP),CS左移4位+IP(segment × 16 + offset.)就剛好20位,這樣就可以訪問20位的地址總線。1MB的16位表示爲:0x100000

而因爲內存地址是0x00000000~0x000FFFFF所以當訪問0x000FFFFF+1,這個地址,物理地址是不存在的。此時就要通過"wrap around"去指向物理地址:0x00000000。

而後續的處理器80286 ,內存達到16MB,且有了實模式,此時訪問0x000FFFFF+1就可以訪問到0x100000。但是這樣有個問題就是之前支持8086系列處理器的代碼(wrap around)可能就不能正常工作。所以爲了解決這個問題,在處理器和系統總線之間在20 line處加了一個邏輯門,即:Gate-A20

A20可以被軟件來開啓或者阻止。在計算機啓動時,BIOS會先開啓(Enable)A20,去檢測所有的系統內存,在把控制權交給操作系統之前disabled。

爲何開啓A20,以及如何開啓A20

開啓A20後能正常訪問>1MB的地址空間,如何開啓,可以看bootasm.S代碼:

    # 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, %al                                 # 如果 %al 第低2位爲1,則ZF = 0, 則跳轉
    jnz seta20.1                                    # 如果 %al 第低2位爲0,則ZF = 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

看8042鍵盤控制器:

鍵盤控制器8042的邏輯結構圖

8042 鍵盤控制器的 IO 端口是 0x60 ~ 0x6f,實際上 IBM PC/AT 使用的只有 0x60 和 0x64 兩個端口(0x61、0x62 和 0x63 用於與 XT 兼容目的)。8042 通過這些端口給鍵盤控制器或鍵盤發送命令或讀取狀態。位 1(P21 引腳)用戶控制 A20 信號線的開啓與否。系統向輸入緩衝(端口 0x64)寫入一個字節,即發送一個鍵盤控制器命令。可以帶一個參數。參數是通過 0x60 端口發送的。 命令的返回值也從端口 0x60 去讀。

如何初始化GDT表

    # 通過lgdt指令把gdtdesc的地址載入到gdtr
    lgdt gdtdesc
    
# 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

而GDT表的內容可以結合全局描述符表的結構分析代碼asm.h中的兩個宏定義:SEG_NULLASMSEG_ASM

image-20220104163552060

可以看到,GDT爲64bits,也就是8個字節。看一下代碼裏面關於段描述符的數據結構定義:

/* segment descriptors */
struct segdesc {
    unsigned sd_lim_15_0 : 16;        // low bits of segment limit
    unsigned sd_base_15_0 : 16;        // low bits of segment base address
    unsigned sd_base_23_16 : 8;        // middle bits of segment base address
    unsigned sd_type : 4;            // segment type (see STS_ constants)
    unsigned sd_s : 1;                // 0 = system, 1 = application
    unsigned sd_dpl : 2;            // descriptor Privilege Level
    unsigned sd_p : 1;                // present
    unsigned sd_lim_19_16 : 4;        // high bits of segment limit
    unsigned sd_avl : 1;            // unused (available for software use)
    unsigned sd_rsv1 : 1;            // reserved
    unsigned sd_db : 1;                // 0 = 16-bit segment, 1 = 32-bit segment
    unsigned sd_g : 1;                // granularity: limit scaled by 4K when set
    unsigned sd_base_31_24 : 8;        // high bits of segment base address
};

宏定義代碼:

/* 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)

SEG_NULLASM爲一個空的GDT表,而後面的宏定義我們可以寫的更直觀一點,如下圖所示:

image-20220104165437469

然後我們看代碼段:SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff)

STA_X|STA_R = 0x8 | 0x2 = 00001000 | 00000010 = 00001010 = 0x0A,代入宏定義裏面得:

(0xC0 | (((0xffffffff) >> 28) & 0xf))

11111111111111111111111111111111 >> 28 = 1111 (11000000 | (1111 & 1111) = (11000000 | 1111) = (11001111) = 0xCF

(0x90|0xA) = (10010000 | 00001010) = (10011010) = 0x9A

最後得出代碼段的結構爲:

11111111 11001111 10011010 11111111
11111111 11111111 11111111 11111111

數據段的結構爲:

11111111 11001111 10010010 11111111
11111111 11111111 11111111 11111111

可以看到兩者的差異爲TYPE不同,TYPE 字段共 4 位,用於指示描述符的子類型,或者說是類別。

對於數據段來說, 這 4 位分別是 X、 E、 W、 A 位;而對於代碼段來說,這 4 位則分別是 X、 C、 R、 A 位。如下表所示

img

代碼段的TYPE爲:1010(執行、讀)

數據段的TYPE爲:0010(只讀,向下擴展)

三個段結構爲:

# NULL SEGMENT
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
# CODE SEGMENT
11111111 11001111 10011010 11111111
11111111 11111111 11111111 11111111
# DATA SEGMENT
11111111 11001111 10010010 11111111
11111111 11111111 11111111 11111111

如何使能和進入保護模式

使能和進入保護模式的代碼:

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

CR0是32位的控制寄存器,在Intel的手冊裏面可以找到:

The control registers (CR0, CR2, CR3, and CR4) contain a variety of flags and data fields for controlling system-level operations. Other flags in these registers are used to indicate support for specific processor capabilities within the operating system or executive. See also: Section 2.5, “Control Registers.”

CR0-CR4都是控制寄存器,包含有幾個標誌變更和數據來控制系統級別的操作。這些寄存器的其他標誌用來支持特定的處理能力。

image-20220104135359868

​ 單獨看CR0寄存器:

 ucore132431412

Reserved是被保留的。

第0位(PE)就是Protected-Mode Enable (PE) Bit.

PE=0表示CPU處於實模式;PE=1表CPU處於保護模式,並使用分段機制。也就是說,只要把CR0的PE位置爲1就可以使能和進入保護模式。

第31位(PG)就是**Paging Enable(PG)Bit.**控制分頁機制(後面的實驗會開啓分頁機制)

PG=1表示啓動分頁機制;PG=0表示不使用分頁機制

下圖爲IA-32 System-Level Registers and Data Structures(IA-32系統級別寄存器和數據結構):

image-20220104134845916

練習4:分析bootloader加載ELF格式的OS的過程。

通過這一步對從硬盤讀數據形成初步瞭解

bootloader如何讀取硬盤扇區的?

bootasm.S的代碼call bootmain會從彙編調到bootmain.c的bootmain函數

/* bootmain - the entry of bootloader */
void
bootmain(void) {
    // read the 1st page off disk
    // 從硬盤讀第一頁(第一個扇區)
    readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);

    // is this a valid ELF?
    // 通過檢測magic值來確定是不是ELF文件,如果不是ELF走bad的條件分支
    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 = ph + ELFHDR->e_phnum;
    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
    ((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();

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

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

readseg函數就是從硬盤讀數據,結合傳進去的3個參數

// 第1個參數
#define ELFHDR          ((struct elfhdr *)0x10000)      // scratch space
// 第2個參數 SECTSIZE*8 = 512*8 = 4096 = 0x1000
#define SECTSIZE        512
// 第3個參數  offset = 0
static void
readseg(uintptr_t va, uint32_t count, uint32_t offset) {
    
    // end_va即虛擬地址的結束 0x10000+0x1000 = 0x11000
    uintptr_t end_va = va + count;

    // 向下舍入到扇區邊界 0%512=0
    // round down to sector boundary
    va -= offset % SECTSIZE;

    // translate from bytes to sectors; kernel starts at sector 1
    // (0/512)+1 = 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.
    // va 從0開始,每循環一次加512
    for (; va < end_va; va += SECTSIZE, secno ++) {
        readsect((void *)va, secno);
    }
}

接下來就是readsect函數來讀取扇區的數據,前面簡單提到過硬盤訪問的

/* readsect - read a single sector at @secno into @dst */
// dst 即把內容讀到這個地址
// secno 扇區號
static void
readsect(void *dst, uint32_t secno) {
    // wait for disk to be ready
    waitdisk();

    // 讀寫1個扇區
    outb(0x1F2, 1);                         // count = 1
    // LBA參數的0-7位:1 & 11111111 = 00000001
    outb(0x1F3, secno & 0xFF);
    // LBA參數的8-15位:(1 >> 8) & 11111111 = 00000000
    outb(0x1F4, (secno >> 8) & 0xFF);
    // LBA參數的16-23位:(1 >> 16) & 11111111 = 00000000
    outb(0x1F5, (secno >> 16) & 0xFF);
    // 第0~3位:如果是LBA模式就是24-27位 第4位:爲0主盤;爲1從盤
    // LBA參數的24-27位:(1 >> 24) & 11100000 = 000xxxxx
    outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0);
    // 寫入0x20表示請求讀硬盤
    outb(0x1F7, 0x20);                      // cmd 0x20 - read sectors

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

    // read a sector
    insl(0x1F0, dst, SECTSIZE / 4);
}

/* waitdisk - wait for disk ready */
// 如果 (inb(0x1F7) & 0xC0) != 40,那麼就一直等
// inb是IO操作的指令,inb 從I/O端口讀取一個字節(BYTE, HALF-WORD) ;inb(0x1F7)是從0x1F7讀取一個字節的數據,然後與0xC0,0xC0 = 11000000, 0x40 = 01000000
// 0x1F7狀態和命令寄存器。操作時先給命令,再讀取,如果不是忙狀態就從0x1f0端口讀數據
// 既是命令端口,又是狀態端口。作爲命令端口時,寫入0x20表示請求讀硬盤;寫入0x30表示請求寫硬盤。作爲狀態端口時,第0位爲1表示前一個命令執行錯誤,具體原因可訪問端口0x1f1;第3位爲1表示硬盤已經準備好和主機進行數據交互;第7位爲1表示硬盤忙。
// 0x40(01000000)二進制的第7位爲1,所以只要硬盤忙,就會什麼也不做,一直循環,當第7位爲0時,就可以從硬盤讀數據
static void
waitdisk(void) {
    while ((inb(0x1F7) & 0xC0) != 0x40)
        /* do nothing */;
}

// 內聯彙編
// inb函數內聯了inb指令,用於從指定端口讀取1字節數據。
// outb函數內聯了outb指令,用於向指定端口寫入1字節數據。
static inline uint8_t
inb(uint16_t port) {
    uint8_t data;
    asm volatile ("inb %1, %0" : "=a" (data) : "d" (port));
    return data;
}

// 內聯彙編
// insl函數內聯了cld; rep insl指令,cld用於清除方向標誌,使偏移量向正方向移動,這個偏移量其實就是傳入的addr,會被關聯到edi,反彙編的結果中可以看到,請大家自己實驗。rep前綴用於重複執行insl,重複的次數由ecx決定,即傳入的參數cnt。最終數據會被連續讀取到addr指向的內存處。
// CLD與STD是用來操作方向標誌位DF(Direction Flag)。CLD使DF復位,即DF=0,STD使DF置位,即DF=1.用於串操作指令中。
// repne 不等於時重複 
// insl(0x1F0, dst, SECTSIZE / 4);
// 0x1F0讀數據
// dst目標地址
// cnt 重複次數
static inline void
insl(uint32_t port, void *addr, int cnt) {
    asm volatile (
            "cld;"
            "repne; insl;"
            : "=D" (addr), "=c" (cnt)
            : "d" (port), "0" (addr), "1" (cnt)
            : "memory", "cc");
}

我們看硬盤訪問概述裏所講:

一般主板有2個IDE通道,每個通道可以接2個IDE硬盤。訪問第一個硬盤的扇區可設置IO地址寄存器0x1f0-0x1f7實現的,具體參數見下表。一般第一個IDE通道通過訪問IO地址0x1f0-0x1f7來實現,第二個IDE通道通過訪問0x170-0x17f實現。每個通道的主從盤的選擇通過第6個IO偏移地址寄存器來設置。

IO地址 功能
0x1f0 讀數據,當0x1f7不爲忙狀態時,可以讀。
16位數據端口,用於讀取或寫入數據,每次讀寫1個字,循環直到讀完所有數據。
0x1f1 讀取時的錯誤信息或寫入時的額外參數。
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從盤
4位保存LBA地址的前4位,高4位指定訪問模式和訪問的設備。其中第4位指示硬盤號,0表示主盤,1表示從盤;第6位指定訪問模式,0表示CHS 模式1表示LBA 模式。第5、7位爲1
0x1f7 狀態和命令寄存器。操作時先給命令,再讀取,如果不是忙狀態就從0x1f0端口讀數據
既是命令端口,又是狀態端口。作爲命令端口時,寫入0x20表示請求讀硬盤;寫入0x30表示請求寫硬盤。作爲狀態端口時,第0位爲1表示前一個命令執行錯誤,具體原因可訪問端口0x1f1;第3位爲1表示硬盤已經準備好和主機進行數據交互;第7位爲1表示硬盤忙。

Bootloader是如何加載ELF格式的OS?

在bootmain.c的bootmain方法中,當從硬盤讀完數據後有下面的代碼

void
bootmain(void) {
    ......
    struct proghdr *ph, *eph;

    // load each program segment (ignores ph flags) 加載每個程序的段
    // e_phoff -> file position of program header or 0,
    // ph 即指向program header
    ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
    // e_phnum(入口個數):number of entries in program header or 0
    // 
    // proghdr:program section header
    // 讀進對應的地址ph->p_va。
    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
    ((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
    ......
}

所以大概可以看到bootmain函數的流程大概如下:

image-20220104134845916

而這個入口就是kern/init.c裏面的kern_init函數,從tools/kernel.ld文件中也可以看到有:

ENTRY(kern_init)

後面需要好好學習一下編譯原理

練習5:實現函數調用堆棧跟蹤函數 (需要編程)

ESP:棧指針寄存器(extended stack pointer),其內存放着一個指針,該指針永遠指向系統棧最上面一個棧幀的棧頂。

EBP:基址指針寄存器(extended base pointer),其內存放着一個指針,該指針永遠指向系統棧最上面一個棧幀的底部。

參考課程資料,及C的函數調用分析,完成後的代碼如下:

void
print_stackframe(void) {
    // 通過內聯彙編讀取ebp、eip的值
    uint32_t ebp = read_ebp();
    uint32_t eip = read_eip();

    // STACKFRAME_DEPTH = 20,也就是最多打印20個調用棧,且因爲剛開始(第一個)調用開始時ebp = 0,所以這裏要保證調用不會溢出
    int index;
    for (index = 0; index < STACKFRAME_DEPTH && ebp != 0; index++) {
        
        // ebp eip 
        printf("ebp = 0x%08x\t eip = 0x%08x\t", ebp, eip);
        // arguments 一般而言,ss:[ebp+4]處爲返回地址,ss:[ebp+8]處爲第一個參數值
        // 而我們這裏uint32_t佔4個字節,根據C指針的特性,ebp指針+2就是第一個參數,
        // 這樣我們可以定義一個長度爲4的數據,這樣依次就對應1、2、3、4參數
        uint32_t args[4] = (uint32_t)ebp + 2;
        printf("args:0x%80x\t0x%80x\t0x%80x\t0x%80x\t", args[0], args[1], args[2], args[3]);
        printf("\n");
        print_debuginfo(eip-1);
        // 如果eip的值轉化爲指針,那麼這個指針指向的就是caller函數(當前函數的調用者)的ebp
        // 對應的指針+1就是返回值(保存調用者要執行的地址)
        ebp = ((uint32_t *)ebp)[0];
        eip = ((uint32_t *)ebp)[1];
    }
}

練習6:完善中斷初始化和處理 (需要編程)

中斷描述符表(也可簡稱爲保護模式下的中斷向量表)中一個表項佔多少字節?其中哪幾位代表中斷處理代碼的入口?

先看一下中斷描述符表的結構(分三類:Task Gate、Interrupt Gate、Trap Gate)(參考Intel的手冊):

ucore_os_architecture_idt_0001

然後是不同的描述符:

image-20220116204829346

可以看到,中斷描述符表一個表項佔有8個字節,

段選擇子->LDT->找到Base Address + Offset可以找到中斷服務例程的入口地址。

請編程完善kern/trap/trap.c中對中斷向量表進行初始化的函數idt_init。在idt_init函數中,依次對所有中斷入口進行初始化。使用mmu.h中的SETGATE宏,填充idt數組內容。每個中斷的入口由tools/vectors.c生成,使用trap.c中聲明的vectors數組即可。

代碼如下:

void
idt_init(void) {
      // 根據提示,首先要__vectors,extern是外部變量聲明,__vectors是通過tools/vector.c生成的vectors.S裏面定義的
      extern uintptr_t __vectors[];
      // 對2562箇中斷向量表初始化
      int i;
      for (i = 0; i < (sizeof(idt) / (sizeof(struct gatedesc))); i++)
      {
        // idt數據裏面的每一個,也可以用指針表示,
        // 0表示是一個interrupt gate
        // segment selector設置爲GD_KTEXT(代碼段)
        // offset設置爲__vectors對應的內容
        // DPL設置爲0
        SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL);
      }

      // 再把從用戶態切換到內核態使用的Segment Descriptor改一下
      // 需要注意的是,我們使用的segment都是一樣的,都是GD_KTEXT
      // 而有一點不同的是這裏的DPL是DPL_USER,即從user->kernel時,需要的該段的權限級別
      // 因爲Privilege Check需要滿足:DPL >= max {CPL, RPL} 
      // 所以如果不單獨改這個會造成Privilege Check失敗,無法正確處理user->kernel的流程
      SETGATE(idt[T_SWITCH_TOK], 0, GD_KTEXT, __vectors[T_SWITCH_TOK], DPL_USER);

      // 通過lidt加載
      lidt(&idt_pd);
}

    // 通過lidt加載
    lidt(&idt_pd);
}

請編程完善trap.c中的中斷處理函數trap,在對時鐘中斷進行處理的部分填寫trap函數中處理時鐘中斷的部分,使操作系統每遇到100次時鐘中斷後,調用print_ticks子程序,向屏幕上打印一行文字”100 ticks”。

這個相對簡單,累計100次打印即可

代碼如下:

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 % 100 == 0) {
            print_ticks();
        }
        break;

擴展練習 Challenge 1(需要編程)

這道題難度太大,一點提示也沒有,只能各種查資料,並且看答案來理解。

準備知識

爲了更好的保護好多個任務,80386使用了幾種特別的數據結構。但是,並沒有使用特別的指令來控制 多任務。相反,當遇到轉移指令是訪問的特別的數據結構時,它用不同的方法來解析控制轉移。用來控 制多任務的寄存器和數據結構是: 1、 任務狀態段(Task state segment) 2、 任務狀態段描述符(Task state segment descriptor) 3、 任務寄存器(Task register) 4、 任務門描述符(Task gate descriptor) 有了這些數據結構,80386可以快速的從一個任務切換到另一個任務中去,把原先任務的上下文 (context)保存起來,以便以後可以重起該任務。除了任務切換以外,80386還進行以下兩個任務管 理: 1、 中斷和異常可以引起任務切換(如果系統設計需要的話)。處理器不但切換到中斷處理程序的任務 中,而且當中斷處理完後還會自動返回原任務。中斷任務可以中斷低特權級的任務,無論多少級。 2、 當每一次切換到另一個任務時,80386也會切換到另一個LDT和另一個頁目錄去。這樣,每個任務都 有了不同的邏輯地址——線性地址,和線性地址——物理地址的映射了。這是另一個保護的特性,它把 任務獨立開來,以防止它們之間的相互干涉。

需要從Intel手冊裏面瞭解的知識:

(01)任務狀態段(Task State Segment)

一個任務由兩部分構成:任務執行的空間和TSS(Task-State Segment)。而任務執行的空間則由:代碼段(Code Segment)、數據段(一個或者多個)(Data Segment)、棧段(Stack Segment)組成。

TSS存儲了處理器管理任務所需要的信息(也就是把寄存器裏面的內容都保存下來)。結構如下圖指所示:

image-20220121113226659

可以看到和我們代碼裏面的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));

TSS 的字段屬於兩類:

  1. 處理器在每次切換時更新的動態集 任務。該集合包括存儲的字段:

    • 通用寄存器(EAX、ECX、EDX、EBX、ESP、EBP、ESI、EDI)。

    • 段寄存器(ES、CS、SS、DS、FS、GS)。

    • 標誌寄存器(EFLAGS)。

    • 指令指針(EIP)。

    • 之前執行任務的 TSS 的選擇器(僅在會返回時更新)。

  2. 處理器讀取但不會更改的靜態集。這套 包括存儲的字段:

    • DGT或者LDT的選擇子(Selector)
    • 包含任務頁面目錄(僅在啓用分頁時讀取)的基地址寄存器(PDBR)(在lab1我們不會關注到)
    • 指向特權級別 0-2 的堆棧的指針。(在lab1我們不會關注到)
    • T 位(調試陷阱位)導致處理器產生一個 發生任務切換時的調試異常。 (在lab1我們不會關注到)
    • I/O 映射庫(在lab1我們不會關注到)

(02)TSS Descriptor

和其他的segment一樣,Task State Segment也是被一個描述符定義的,結構如下所示:

image-20220121144633212

TYPE裏面的B代表是否busy,9 = 1001,B = 0,代表空閒,11 = 1011,B = 1,代表忙。task不可重入,處理器可以根據這個位的值來判斷。

BASE、LIMIT、DPL、G這些和其他的描述符差不多。而LIMIT,必需>=103。如果小於103會引發異常。如果存在 I/O 權限映射,則需要更大的限制。 如果附加數據存儲在與 TSS 相同的段中,則更大的LIMIT對系統軟件更方便。

一個訪問TSS描述符的例程可能會引起一個task切換。多數系統裏面DPL需要設置爲0,這樣只有被信任的軟件纔有權限執行任務切換。

訪問 TSS 描述符並不賦予程序讀取或修改 TSS 的權利。 只能通過重新定義一個 TSS作爲數據段(data segment)讀取和修改。 嘗試將 TSS 描述符加載到任何段寄存器(CS、SS、DS、ES、FS、GS)都會引起異常。

TSS描述符只能放在GDT裏面,嘗試從TI=1的選擇子裏面(LDT)去導向一個TSS也異常。

(03)TaskRegister

任務寄存器(TR)指向當前正在執行的任務的TSS,如下圖所示

image-20220121154241774

和其他的寄存器一樣,分爲可見部分和不可見部分。可見部分,通過selector從GDT中,查找到TSS描述符。處理器通過不可見部分緩存TSS描述符的BASE和LIMIT,這樣處理器就不需要重複通過index(selector)從內存中獲取。

可以通過LTR和STR指令去修改和讀取可見部分。這兩個指令都採用一個操作數,如16-bit(兩個字節的)選擇子或通用寄存器。

LTR(Load task register)通過selector operand加載TR寄存器的可見部分(必需能從GDT中找到TSS Descriptor)。LTR指令也可以把根據selector從GDT裏面找到的TSS的信息(BASE、LIMIT)加載到不可見部分。且LTR是有權限的,可能只能在CPL爲0的時候來執行。

STR (Store task register) 在普通的寄存器或者內存裏面存放task register的可見部分,且沒有權限級別要求。

可以寫一個函數(內聯彙編)來記取TR寄存器的selector。


 

(04)Task Gate Descriptor

一個task gate descriptor(任務門描述符)提供一個間接的、受保護的指向TSS的引用。

image-20220121160736692

任務門的SELECTOR指向一個TSS描述符。selector裏面的RPL並不是被處理器使用。

任務門的DPL字段控制任務切換時使用描述符的權限。除非滿足要求:max{RPL, CPL} <= DPL。這樣的約束可以防止不被任務的程序(例程)引起任務切換(需要注意,當一個任務門正在使用,目標TSS描述符的DPL不會用來做Privilege Check)。

像可以訪問 TSS 描述符的程序一樣,可以訪問任務門的也可以引起任務切換(task switch), 除了 TSS 描述符之外,80386 還具有任務門,以滿足三個需求:

1、一個任務(task)需要有一個busy-bit。因爲busy-bit放在TSS描述符中,而每個任務都需要有一個這樣的描述符。然而可能幾個任務門(task gate)使用一個TSS descriptor。

2、需要提供訪問任務的選擇性(GDT、LDT)。任務門滿足這一要求,因爲任務門放在LDT中,且有一個不同於TSS descriptor的DPL。如果一個例程沒有權限訪問GDT中的TSS descriptor(GDT中TSS descriptor的DPL通常爲0),但能訪問其LDT中的task gate,也可以切換到另一個task。

3、需要中斷或者異常來引起任務切換。task gate可能在GDT或者LDT(ucore這裏都是GDT)中,這樣使得使用LDT的task也能引起task switch。當中斷或異常向量指向一個包含任務門的IDT,80386也可以正確的切換到指定的任務。因此,系統中的所有任務都可以受益於中斷任務隔離所提供的保護

下圖說明了LDT(Local Descriptor Table)和IDT(Interrupt Descriptor Table)中的task gate可以確定同一個任務。

image-20220121161927698

(05)Task Switching

80386 在以下四種情況下將切換執行另一個任務:

  1. 當前任務執行引用TSS描述符的JMP或者CALL指令。
  2. 當前任務執行引用任務門的JMP或者CALL指令。
  3. 指向IDT中任務門的中斷或者異常。
  4. 當前任務在設置NT(Nested Task)標誌時執行IRET指令。

一個任務切換過程包含下幾個步驟:

  1. 檢查當前任務是否能切換到指定的任務。JMP或CALL指令需要數據訪問特權。也就是TSS描述符或者task gate的DPL需要<=門選擇子的CPL和RPL最大值。異常、中斷、和IRET指令可以在不管目標TSS描述符或者task gate的DPL的情況下切換任務。
  2. 檢查新任務的TSS descriptor存在,且LIMIT可用。到目前爲止任何錯誤都發生在傳出任務的上下文中。 錯誤是可以重新啓動的,且可以以對應用程序透明的方式進行處理。
  3. 保存當前任務的狀態。處理器查找當前任務緩存在TR寄存器裏面的TSS的基址,且(處理器)拷貝寄存器(EAX, ECX, EDX, EBX, ESP, EBP, ESI,EDI, ES, CS, SS, DS, FS, GS, EFLAGS)的值到當前的TSS裏面。TSS 的 EIP 字段指向導致任務切換的指令之後的指令(比如中斷INT n指令的下一條指令)。
  4. 使用傳入任務的 TSS 描述符的選擇器加載任務寄存器,將傳入任務的 TSS 描述符標記爲忙,並設置 MSW 的 TS(任務切換)位。 選擇子要麼是控制轉移指令的操作數(比如 INT $80,那麼選擇子就是80),要麼取自任務門。
  5. 從 TSS 加載傳入任務(切換後任務)的狀態並恢復執行。加載的寄存器是 LDT 寄存器、 標誌寄存器、 通用寄存器 EIP、EAX、ECX、EDX、EBX、ESP、EBP、ESI、EDI;段寄存器 ES、CS、SS、DS、FS 和 GS; 和 PDBR。 在此步驟中檢測到的任何錯誤都發生在傳入任務的上下文中(也就是trapframe裏面的error no)。需要在新任務的第一條指令尚未執行前處理異常。
  6. 要滿足:CPL <= DPL且RPL <= DPL或者DPL >= max {CPL, RPL} 。

需要注意的是,舊任務的所有狀態在任務切換的時候應總是被完整保存下來,以便再切換回來時能夠正確恢復執行。

在傳入任務(new task)中恢復執行的特權級別不受傳出任務(old task)執行時的特權級別的限制或影響。 因爲任務被它們各自的地址空間和 TSS 隔離,並且因爲可以使用特權規則來防止對 TSS 的不當訪問,所以不需要特權規則來約束任務的 CPL 之間的關係。 新任務(new task)以從 TSS 加載的 CS 選擇器值的 RPL 指示的特權級別開始執行。

(06)指令:INT、IRET、CALL、RET、JMP、LEAVE

  • INT n會產生中斷,Linux裏面軟中斷(Software Interrupt)是INT 0x80。 中斷號從0255(也就是 02^8-1),ucore代碼裏面vectors.S可見,練習6時的extern uintptr_t __vectors[];即爲中斷向量號數組。根據中斷號索引到IDT裏面對應的gate descriptor,對應ucore裏面練習6設置的IDT。INT n會產生一個far call在返回地址前會把一些標誌寄存器壓棧。INT n會把eflags,cs,eip,errorCode壓棧,然後跳轉到根據中斷號索引到的長地址。簡要過程如下:

    • 取中斷類型碼n

    • SS入棧(返回權限發生變化,ucore需要手動壓棧)

    • ESP入棧(返回權限發生變化,ucore需要手動壓棧)

    • 標誌寄存器入棧,IF=0、TF=0

    • CS、IP入棧

    • error no

    • (IP)=(n∗4),(CS)=(n∗4+2)

  • IRET也就是Interrupt Return中斷服務程序的最後一條指令。IRET指令將推入堆棧的段地址和偏移地址彈出,使程序返回到原來發生中斷的地方。其作用是從中斷中恢復中斷前的狀態,具體作用有如下三點:

    • 恢復IP(instruction pointer):IP←((SP)+1:(SP)),SP←SP+2

    • 恢復CS(code segment):CS←((SP)+1:(SP)), SP←SP+2

    • 恢復中斷前的PSW(program status word),即恢復中斷前的標誌寄存器的狀態。

      FR←((SP)+1:(SP)),SP←SP+2

    • 恢復ESP(返回權限發生變化)

    • 恢復SS(返回權限發生變化)

    以上操作按順序進行。

    【指令手冊原文】

    the IRET instruction pops the return instruction pointer, return code segment selector, and EFLAGS image from the stack to the EIP, CS, and EFLAGS registers, respectively, and then resumes execution of the interrupted program or procedure. If the return is to another privilege level, the IRET instruction also pops the stack pointer and SS from the stack, before resuming program execution.

    IRET指令影響所有標誌位。

  • CALL在C語言的函數調用對應彙編就是call指令,比如:call 0x101e92 <trap_dispatch+304>,CALL指令執行時,進行兩步操作:

    (1)將程序下一條指令的位置的IP壓入堆棧中;

    (2)轉移到調用的子程序

    (CALL近調用,LCALL遠調用,CALL 尋址2K空間範圍,LCALL 尋址64K空間範圍) 先壓CS,再壓IP。CALL與RET結合使用,當CALL調用的子程序運行到RET命令時,壓入堆棧的IP彈出,跳出子程序,執行CALL的下一條語句。

  • RET指令與CALL結合,程序運行到RET命令時,壓入堆棧的IP彈出,跳出子程序,執行CALL的下一條語句。

  • JMP跳轉指令,比如ucore裏面執行kernel->user的中斷:INT 0x78,會根據0x78(120)跳轉到vectors.S的

    .globl vector120
    vector120:
      pushl $0
      pushl $120
      jmp __alltraps
  • leave指令

    在CALL執行跳轉到一個新的函數(子程序)後,都會執行:

    push %ebp
    move %esp, %ebp

    這樣的指令,從而根據ebp形成一個函數調用鏈,且可根據ebp ± n來取變量返回地址等數據。

    而leave就相當於和這兩條彙編指令反過來,即:

    ;在16位彙編下相當於:
    mov %bp, %sp
    pop %bp
     
    ;在32位彙編下相當於:
    mov %ebp, %esp//將ebp指向(ebp內部應當保存一個地址,所謂指向即這個地址對應的空間)的值賦給esp
    pop %ebp 
     
    /* leave指令將EBP寄存器的內容複製到ESP寄存器中,
    以釋放分配給該過程的所有堆棧空間。然後,它從堆棧恢復EBP寄存器的舊值。*/

過程分析

有了對TSS、TR、INT、IRET等知識點的瞭解再來分析一下ucore裏面的kernel->useruser->kernel的過程

(7.1)內核態到用戶態

由於特權級變化,我們需要手動壓棧ss和esp,但是由於其實ucore是沒用到ss(因爲ucore的內核態、用戶態使用相同的代碼段、數據段),所以也可以直接通過sub $0x8, %esp 這樣的操作,空出兩個位置就可以。

  1. 手動壓棧SSESP或者sub $0x8, %esp

  2. INT 0x78

  3. 處理器壓棧EFLAGS、CS、EIP

  4. ucore的vectors.S壓棧 Error Code(ucore這裏都是0)pushl $0和中斷向量號:pushl $120,然後通過jmp __alltraps跳轉到trapentry.S的__alltraps

  5. 寄存器信息壓棧:先壓段寄存器(DS->ES->FS->GS),然後pushal壓棧通用寄存器(EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI)

  6. 修改DS、ES數據段(data segments)爲GD_KDATA = 0x10 = 16 = 00010000

  7. ESP入棧,傳遞trapframe指針(push %esp to pass a pointer to the trapframe as an argument to trap())

  8. call trap調用trap.c的trap方法再調用到trap_dispatch

  9. case T_SWITCH_TOU:處理

    涉及棧的切換,參考圖示:

    kernel_to_user_h001

(7.2)用戶態到內核態

  1. INT 0x79

  2. 處理器壓棧EFLAGS、CS、EIP

  3. ucore的vectors.S壓棧 Error Code(ucore這裏都是0)pushl $0和中斷向量號:pushl $121,然後通過jmp __alltraps跳轉到trapentry.S的__alltraps`。

  4. 寄存器信息壓棧:先壓段寄存器(DS->ES->FS->GS),然後pushal壓棧通用寄存器(EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI)

  5. 修改DS、ES數據段(data segments)爲GD_KDATA = 0x10 = 16 = 00010000

  6. ESP入棧,傳遞trapframe指針(push %esp to pass a pointer to the trapframe as an argument to trap())

  7. call trap調用trap.c的trap方法再調用到trap_dispatch

  8. case T_SWITCH_TOK:處理

    無棧的切換,參考圖示:

    user_to_kernel_h001

開始操作

直接亮代碼

kernel->user

static void
lab1_switch_to_user(void) {

/**
--------------------------------------------------------
	"sub $0x8, %%esp \n" 
	讓 SS 和 ESP 這兩個寄存器 有機會 POP 出時 更新 SS 和 ESP
	因爲 從內核態進入中斷 它的特權級沒有改變 是不會 push 進 SS 和 ESP的 但是我們又需要通過 POP SS 和 ESP 去修改它們
	進入 T_SWITCH_TOU(120) 中斷
	將原來的棧頂指針還給esp棧底指針
--------------------------------------------------------
*/

    //LAB1 CHALLENGE 1 : TODO
    __asm__ __volatile__ (
        "sub $0x8, %%esp \n"
        "INT %0 \n"
        "movl %%ebp, %%esp \n"       // 因爲這裏主動調INT之後,彙編不會幫我們把類似leave的指令補齊,所以需要我們自己加上去
        :
        :"irq"(T_SWITCH_TOU)
    );

/** 
 * ----------------------------第二種方式-----------------------------------
 * 中斷髮生前壓入的SS實際不會被使用,所以代碼中僅僅是壓入了%eax佔位
 * 主動push esp
 * 	__asm__ __volatile__ (
		"pushl %%eax\n\t"
		"pushl %%esp\n\t"
		"int %0\n\t"
		:
		:"i" (T_SWITCH_TOU)
	);
 * ----------------------------第二種方式-----------------------------------
 * /

/**
--------------------------------------------------------
中斷處理例程處於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。
--------------------------------------------------------
*/
}

trap.c

case T_SWITCH_TOU:
	// 封裝一個函數,challenge2還可以複用 
    switch2user(tf);
    break;
/** -------------------------------------------------------- */
void switch2user(struct trapframe *tf)
{
  // eflags
  // 0x3000 = 00110000 00000000
  // 把nested task位置1,也就是可以嵌套
  tf->tf_eflags |= 0x3000;

  // USER_CS = 3 << 3 | 3 = 24 | 3 = 27 = 0x1B = 00011011;
  // 如果當前運行的程序不是在用戶態的代碼段執行(從內核切換過來肯定不會是)
  if (tf->tf_cs != USER_CS)
  {
    switchk2u = *tf;
    switchk2u.tf_cs = USER_CS;
    // 設置數據段爲USER_DS
    switchk2u.tf_ds = switchk2u.tf_es = switchk2u.tf_ss = USER_DS;
    // 因爲內存是從高到低,
    // 而這是從內核態切換到用戶態(沒有ss,sp)
    // (uint32_t)tf + sizeof(struct trapframe) - 8 即 tf->tf_esp的地址
    // 也就是switchk2u.tf_esp,指向舊的tf_esp的值
    switchk2u.tf_esp = (uint32_t)tf + sizeof(struct trapframe) - 8;

    //  eflags 設置IOPL
    switchk2u.tf_eflags | FL_IOPL_MASK;

    // (uint32_t *)tf是一個指針,指針的地址-1就
    // *((uint32_t *)tf - 1) 這個指針指向的地址設置爲我們新樊籠出來的tss(switchk2u)

    *((uint32_t *)tf - 1) = (uint32_t)&switchk2u;
  }
}

user->kernel

static void
lab1_switch_to_kernel(void) {
    //LAB1 CHALLENGE 1 :  TODO
    // 當內核初始完畢後,可從內核態返回到用戶態的函數
    __asm__ __volatile__ (
        "INT %0 \n"
        "movl %%ebp, %%esp \n"
        :
        :"irq"(T_SWITCH_TOK)
    );
}

trap.c

 

case T_SWITCH_TOK:
    // 同樣封裝一個函數,challenge2還可以複用 
    switch2kernel(tf);
    break;
/** -------------------------------------------------------- */
void switch2kernel(struct trapframe *tf)
{
  if (tf->tf_cs != KERNEL_CS)
  {
    // 設置CS爲 KERNEL_CS = 0x8 = 1000 =  00001|0|00 -> Index = 1, GDT, RPL = 0 
    tf->tf_cs = KERNEL_CS;
    // KERNEL_DS = 00010|0|00 -> Index = 2, GDT, RPL = 0 
    tf->tf_ds = tf->tf_es = KERNEL_DS;

    // FL_IOPL_MASK = 0x00003000 = 0011000000000000 = 00110000 00000000
    // I/O Privilege Level bitmask
    // tf->tf_eflags = (tf->tf_eflags) & (~FL_IOPL_MASK)
    // = (tf->tf_eflags) & (11111111 11111111 11001111 11111111)
    // 也就是把IOPL設置爲0
    // IOPL(bits 12 and 13) [I/O privilege level field]   
    // 指示當前運行任務的I/O特權級(I/O privilege level),
    // 正在運行任務的當前特權級(CPL)必須小於或等於I/O特權級才能允許訪問I/O地址空間。
    // 這個域只能在CPL爲0時才能通過POPF以及IRET指令修改。
    tf->tf_eflags &= ~FL_IOPL_MASK;

    // 由於內存佈局是從高到低,所以這裏修改switchu2k,指向
    switchu2k = (struct trapframe *)(tf->tf_esp - (sizeof(struct trapframe) - 8));

    /* *
    * memmove - copies the values of @n bytes from the location pointed by @src to
    * the memory area pointed by @dst. @src and @dst are allowed to overlap.
    * @dst        pointer to the destination array where the content is to be copied
    * @src        pointer to the source of data to by copied
    * @n:        number of bytes to copy
    *
    * The memmove() function returns @dst.
    * */
    // 相當於是把tf,拷貝到switchu2k
    memmove(switchu2k, tf, sizeof(struct trapframe) - 8);

    // 修改tf - 1處,指向新的trapframe
    *((uint32_t *)tf - 1) = (uint32_t)switchu2k;
  }
}

可能存在的問題

只要寫好,其實kernel->user用同一個棧也沒問題

任務寄存器tr保存 16 位的段選擇子、32 位基地址、16 位段界限和當前任務的 TSS屬性。它引用 GDT 中的 TSS 描述符。基地址指明 TSS 的第一個字節(字節 0)的線性地址,段界限確定 TSS 的字節個數。TR寄存器包含了當前正在CPU運行的進程的TSSD(任務段描述符)選擇符。也包含了兩個隱藏的非編程域:TSSD的base 和limit域。通過這種方式處理器就能直接對TSS尋址,而不用從GDT中索引TSS的地址

TR寄存器---->GDT中的TSS描述符---->硬件上下文的具體數據。任務切換中

img

cpu會把當前寄存器的數據保存到當前(舊的)tr寄存器所指向的tss數據結構裏,然後把新的tss數據複製到當前寄存器裏。這些操作是通過cpu的硬件實現的

爲了調試時能打印出trapframe裏面的內容需要修改Makefile,在gcc的選項上加上-g3

-g            //在編譯時生成原生格式的調試符號信息,可以使用 gdb 或 ddx 等調試器調試。-g 分爲三個級別,默認爲 -g2,其中 -g3 除包含 -g2 中的所有調試信息外,還包含源代碼中定義的宏。

如下圖所示:

image-20220116101014006

擴展練習 Challenge 2(需要編程)

用鍵盤實現用戶模式內核模式切換。具體目標是:“鍵盤輸入3時切換到用戶模式,鍵盤輸入0時切換到內核模式”。 基本思路是借鑑軟中斷(syscall功能)的代碼,並且把trap.c中軟中斷處理的設置語句拿過來。

做完Challenge 1,Challenge 2基本也就做完了,如下:

case IRQ_OFFSET + IRQ_KBD:
        c = cons_getc();
        cprintf("kbd [%03d] %c\n", c, c);
		if ( c =='3'){
			switch2user(tf);	
			print_trapframe(tf);
		}
		if ( c =='0'){
			switch2kernel(tf);
			print_trapframe(tf);
		}   		
		break;

地址轉換

在保護模式下有分段和分頁機制,不同的機制地址轉換方式有所不同

邏輯地址(Logical Address 應用程序員看到的地址)

物理地址(Physical Address 實際的物理內存地址)

分段機制下的地址轉換

可以看到的是段機制下的邏輯地址(logical address)=段選擇子(selector)+段偏移(offset)

而找到物理地址(線性地址)的方法是先要根據段選擇子從段描述符表中(descriptor table)找到對應的段描述符,然後再加上段偏移(offset)得到線性地址(linear address)在未開啓頁機制(僅開啓段機制)的情況下,此線性地址就是物理地址(實際物理內存地址)

image-20220105101459295

 

參考資料

一些資料信息來源於 http://pdos.csail.mit.edu/6.828/2014/reference.html

search from internet

ucore lab1 - 不告訴你我是誰 - 博客園 (cnblogs.com)

操作系統-uCore-Lab-1

7.1 任務狀態段(Task State Segment) | Intel 80386 程序員參考手冊 (gitbooks.io)

操作系統 uCore Lab 1 含 Challenge

8086 CPU 寄存器簡介 - 小寶馬的爸爸 - 博客園 (cnblogs.com)

ucore-analysis/challenge.md at master · oscourse-tsinghua/ucore-analysis · GitHub

ucore lab1及challenge_DynaZang的博客-CSDN博客

ucore lab1 - 不告訴你我是誰 - 博客園 (cnblogs.com)

ucore lab1 實驗報告 · Xris (xr1s.me)

lab1 - ucore step by step (gitbook.io)

TSS詳解 ——《x86彙編語言:從實模式到保護模式》讀書筆記33_車子(chezi)-CSDN博客_彙編tss

tr 寄存器

call指令_百度百科 (baidu.com)

iret_百度百科 (baidu.com)

leave(彙編leave指令)_百度百科 (baidu.com)

UNIX general info

building or reading a small OS

some OS course

x86 Emulation

x86 Assembly Language

Multiprocessor references:

x86系統結構與編程

General BIOS and PC bootstrap

VGA display - console.c

8253/8254 Programmable Interval Timer (PIT)

8259/8259A Programmable Interrupt Controller (PIC)

16550 UART Serial Port

IEEE 1284 Parallel Port

IDE hard drive controller

make過程日誌

通過sudo make V=

make過程打印內容如下:

+ 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]
   95 | 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
+ 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: In function ‘print_trapframe’:
kern/trap/trap.c:107:16: warning: taking address of packed member of ‘struct trapframe’ may result in an unaligned pointer value [-Waddress-of-packed-member]
  107 |     print_regs(&tf->tf_regs);
      |                ^~~~~~~~~~~~
+ 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
+ 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
+ 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
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: 492 bytes
build 512 bytes boot sector: 'bin/bootblock' success!
dd if=/dev/zero of=bin/ucore.img count=10000
10000+0 records in
10000+0 records out
5120000 bytes (5.1 MB, 4.9 MiB) copied, 0.021646 s, 237 MB/s
dd if=bin/bootblock of=bin/ucore.img conv=notrunc
1+0 records in
1+0 records out
512 bytes copied, 0.000546975 s, 936 kB/s
dd if=bin/kernel of=bin/ucore.img seek=1 conv=notrunc
163+1 records in
163+1 records out
83936 bytes (84 kB, 82 KiB) copied, 0.000939863 s, 89.3 MB/s

需要再學習理解

實模式下的內存佈局

在這裏插入圖片描述

雙向循環鏈表

在“數據結構”課程中,如果創建某種數據結構的雙循環鏈表,通常採用的辦法是在這個數據結構的類型定義中有專門的成員變量 data, 並且加入兩個指向該類型的指針next和prev。例如:

typedef struct foo {
    ElemType data;
    struct foo *prev;
    struct foo *next;
} foo_t;

雙向循環鏈表的特點是尾節點的後繼指向首節點,且從任意一個節點出發,沿兩個方向的任何一個,都能找到鏈表中的任意一個節點的data數據。由雙向循環列表形成的數據鏈如下所示:

雙向循環鏈表

這種雙向循環鏈表數據結構的一個潛在問題是,雖然鏈表的基本操作是一致的,但由於每種特定數據結構的類型不一致,需要爲每種特定數據結構類型定義針對這個數據結構的特定鏈表插入、刪除等各種操作,會導致代碼冗餘。

在uCore內核(從lab2開始)中使用了大量的雙向循環鏈表結構來組織數據,包括空閒內存塊列表、內存頁鏈表、進程列表、設備鏈表、文件系統列表等的數據組織(在[labX/libs/list.h]實現),但其具體實現借鑑了Linux內核的雙向循環鏈表實現,與“數據結構”課中的鏈表數據結構不太一樣。下面將介紹這一數據結構的設計與操作函數。 uCore的雙向鏈表結構定義爲:

struct list_entry {
    struct list_entry *prev, *next;
};

需要注意uCore內核的鏈表節點list_entry沒有包含傳統的data數據域,,而是在具體的數據結構中包含鏈表節點。以lab2中的空閒內存塊列表爲例,空閒塊鏈表的頭指針定義(位於lab2/kern/mm/memlayout.h中)爲:

/* free_area_t - maintains a doubly linked list to record free (unused) pages */
typedef struct {
    list_entry_t free_list;         // the list header
    unsigned int nr_free;           // # of free pages in this free list
} free_area_t;

而每一個空閒塊鏈表節點定義(位於lab2/kern/mm/memlayout)爲:

/* *
 * struct Page - Page descriptor structures. Each Page describes one
 * physical page. In kern/mm/pmm.h, you can find lots of useful functions
 * that convert Page to other data types, such as phyical address.
 * */
struct Page {
    atomic_t ref;          // page frame's reference counter
    ……
    list_entry_t page_link;         // free list link
};

這樣以free_area_t結構的數據爲雙向循環鏈表的鏈表頭指針,以Page結構的數據爲雙向循環鏈表的鏈表節點,就可以形成一個完整的雙向循環鏈表,如下圖所示:

空閒塊雙向循環鏈表 圖 空閒塊雙向循環鏈表

從上圖中我們可以看到,這種通用的雙向循環鏈表結構避免了爲每個特定數據結構類型定義針對這個數據結構的特定鏈表的麻煩,而可以讓所有的特定數據結構共享通用的鏈表操作函數。在實現對空閒塊鏈表的管理過程(參見lab2/kern/mm/default_pmm.c)中,就大量使用了通用的鏈表插入,鏈表刪除等操作函數。有關這些鏈表操作函數的定義如下。

(1) 初始化

uCore只定義了鏈表節點,並沒有專門定義鏈表頭,那麼一個雙向循環鏈表是如何建立起來的呢?讓我們來看看list_init這個內聯函數(inline funciton):

static inline void
list_init(list_entry_t *elm) {
    elm->prev = elm->next = elm;
}

參看文件default_pmm.c的函數default_init,當我們調用list_init(&(free_area.free_list))時,就聲明一個名爲free_area.free_list的鏈表頭時,它的next、prev指針都初始化爲指向自己,這樣,我們就有了一個表示空閒內存塊鏈的空鏈表。而且我們可以用頭指針的next是否指向自己來判斷此鏈表是否爲空,而這就是內聯函數list_empty的實現。

(2) 插入

對鏈表的插入有兩種操作,即在表頭插入(list_add_after)或在表尾插入(list_add_before)。因爲雙向循環鏈表的鏈表頭的next、prev分別指向鏈表中的第一個和最後一個節點,所以,list_add_after和list_add_before的實現區別並不大,實際上uCore分別用list_add(elm, listelm, listelm->next)和list_add(elm, listelm->prev, listelm)來實現在表頭插入和在表尾插入。而__list_add的實現如下:

static inline void
__list_add(list_entry_t *elm, list_entry_t *prev, list_entry_t *next) {
    prev->next = next->prev = elm;
    elm->next = next;
    elm->prev = prev;
}

從上述實現可以看出在表頭插入是插入在listelm之後,即插在鏈表的最前位置。而在表尾插入是插入在listelm->prev之後,即插在鏈表的最後位置。注:list_add等於list_add_after。

(3) 刪除

當需要刪除空閒塊鏈表中的Page結構的鏈表節點時,可調用內聯函數list_del,而list_del進一步調用了__list_del來完成具體的刪除操作。其實現爲:

static inline void
list_del(list_entry_t *listelm) {
    __list_del(listelm->prev, listelm->next);
}
static inline void
__list_del(list_entry_t *prev, list_entry_t *next) {
    prev->next = next;
    next->prev = prev;
}

如果要確保被刪除的節點listelm不再指向鏈表中的其他節點,這可以通過調用list_init函數來把listelm的prev、next指針分別自身,即將節點置爲空鏈狀態。這可以通過list_del_init函數來完成。

(4) 訪問鏈表節點所在的宿主數據結構

通過上面的描述可知,list_entry_t通用雙向循環鏈表中僅保存了某特定數據結構中鏈表節點成員變量的地址,那麼如何通過這個鏈表節點成員變量訪問到它的所有者(即某特定數據結構的變量)呢?Linux爲此提供了針對數據結構XXX的le2XXX(le, member)的宏,其中le,即list entry的簡稱,是指向數據結構XXX中list_entry_t成員變量的指針,也就是存儲在雙向循環鏈表中的節點地址值, member則是XXX數據類型中包含的鏈表節點的成員變量。例如,我們要遍歷訪問空閒塊鏈表中所有節點所在的基於Page數據結構的變量,則可以採用如下編程方式(基於lab2/kern/mm/default_pmm.c):

//free_area是空閒塊管理結構,free_area.free_list是空閒塊鏈表頭
free_area_t free_area;
list_entry_t * le = &free_area.free_list;  //le是空閒塊鏈表頭指針
while((le=list_next(le)) != &free_area.free_list) { //從第一個節點開始遍歷
    struct Page *p = le2page(le, page_link); //獲取節點所在基於Page數據結構的變量
    ……
}

le2page宏(定義位於lab2/kern/mm/memlayout.h)的使用相當簡單:

// convert list entry to page
#define le2page(le, member)                 \
to_struct((le), struct Page, member)

而相比之下,它的實現用到的to_struct宏和offsetof宏(定義位於lab2/libs/defs.h)則有一些難懂:

/* Return the offset of 'member' relative to the beginning of a struct type */
#define offsetof(type, member)                                      \
((size_t)(&((type *)0)->member))

/* *
 * to_struct - get the struct from a ptr
 * @ptr:    a struct pointer of member
 * @type:   the type of the struct this is embedded in
 * @member: the name of the member within the struct
 * */
#define to_struct(ptr, type, member)                               \
((type *)((char *)(ptr) - offsetof(type, member)))

這裏採用了一個利用gcc編譯器技術的技巧,即先求得數據結構的成員變量在本宿主數據結構中的偏移量,然後根據成員變量的地址反過來得出屬主數據結構的變量的地址。

我們首先來看offsetof宏,size_t最終定義與CPU體系結構相關,本實驗都採用Intel X86-32 CPU,顧szie_t等價於 unsigned int。 ((type *)0)->member的設計含義是什麼?其實這是爲了求得數據結構的成員變量在本宿主數據結構中的偏移量。爲了達到這個目標,首先將0地址強制"轉換"爲type數據結構(比如struct Page)的指針,再訪問到type數據結構中的member成員(比如page_link)的地址,即是type數據結構中member成員相對於數據結構變量的偏移量。在offsetof宏中,這個member成員的地址(即“&((type *)0)->member)”)實際上就是type數據結構中member成員相對於數據結構變量的偏移量。對於給定一個結構,offsetof(type,member)是一個常量,to_struct宏正是利用這個不變的偏移量來求得鏈表數據項的變量地址。接下來再分析一下to_struct宏,可以發現 to_struct宏中用到的ptr變量是鏈表節點的地址,把它減去offsetof宏所獲得的數據結構內偏移量,即就得到了包含鏈表節點的屬主數據結構的變量的地址。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章