Lab1

清華實驗lab1

本markdown遵循markdown plus與簡書與Typora編輯器規(guī)則

若需要使用目錄,請使用markdown plus或Typora

markdwon plus 在線編輯器

[TOC]

練習1

理解通過 make 生成執(zhí)行文件的過程。(要求在報告中寫出對下述問題的回答)
在此練習中,大家需要通過閱讀代碼來了解:

  1. 操作系統(tǒng)鏡像文件 ucore.img 是如何一步一步生成的?(需要比較詳細地解釋 Makefile 中
    每一條相關命令和命令參數(shù)的含義,以及說明命令導致的結果)
  2. 一個被系統(tǒng)認為是符合規(guī)范的硬盤主引導扇區(qū)的特征是什么?

練習1.1

1. 生成ucore.img

查看makefile源碼,在178行處有注釋:create ucore.img
以下是此部分代碼

UCOREIMG    := $(call totarget,ucore.img)

$(UCOREIMG): $(kernel) $(bootblock)
    $(V)dd if=/dev/zero of=$@ count=10000
    $(V)dd if=$(bootblock) of=$@ conv=notrunc
    $(V)dd if=$(kernel) of=$@ seek=1 conv=notrunc

$(call create_target,ucore.img)

此處參考dd命令參數(shù) " Linux 下的dd命令使用詳解 "可知道,創(chuàng)建了一塊10000字節(jié)的塊,并且將bootblock復制過去。并且把kernel接在之后的位置。(有seek = 1可知 復制的時候從文件開始跳過一個塊,再存放。但此處bootblock為什么只占了一個塊,會不會發(fā)生文件覆寫 存疑)。

2. 生成kernel

可以看到上文的依賴中有kernel,搜索全文得到

# create kernel target
kernel = $(call totarget,kernel)

$(kernel): tools/kernel.ld

$(kernel): $(KOBJS)
    @echo + ld $@
    $(V)$(LD) $(LDFLAGS) -T tools/kernel.ld -o $@ $(KOBJS)
    @$(OBJDUMP) -S $@ > $(call asmfile,kernel)
    @$(OBJDUMP) -t $@ | $(SED) '1,/SYMBOL TABLE/d; s/ .* / /; /^$$/d' > $(call symfile,kernel)

$(call create_target,kernel)

此處涉及到tools/kernel.ld文件,可運行查看make指令的實際樣子

+ ld bin/kernel
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

可以得出結論,此處鏈接的是如上的文件,也就是kern目錄下的全部文件編譯而成的文件。

gcc -Ikern/init/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector
此處的全部文件使用類如上格式編譯 相關的編譯參數(shù)有

  • 不適用c語言的內(nèi)建函數(shù),解決函數(shù)名沖突的情況
  • 打開警告開關
  • 生成gdb調(diào)試信息
  • 生成32位環(huán)境代碼
  • 生成stabs格式的調(diào)試信息
  • 不使用標準庫,內(nèi)核代碼不需要標準io
  • 禁用堆棧保護,這一條的作用如下 from stackoverflow ,并沒有讀的很懂
    (In the standard/stock GCC, stack protector is off by default. However, some Linux distributions have patched GCC to turn it on by default. In my opinion, this is rather harmful, as it breaks the ability to compile anything that's not linked against the standard userspace libraries unless the Makefile specifically disables stack protector. It would even break the Linux kernel build except that the distributions with this hack added additional hacks to GCC to detect that the kernel is being built and disable it.)

3.生成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)

代碼如上,一樣通過運行來查看實際的運行情況

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

此處有一個用sign規(guī)范bootblock.o到bin/block.o的操作

'obj/bootblock.out' size: 472 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) copied, 0.0237835 s, 215 MB/s

猜想是規(guī)范為 "符合規(guī)范的硬盤主引導扇區(qū)文件"

以上就是makefile所做的事情,下面加上一張圖以示

makefile編譯流程.jpg


練習1.2

根據(jù)剛剛的猜想,閱讀sign.c文件

char buf[512];
memset(buf, 0, sizeof(buf));    
···
buf[510] = 0x55;
buf[511] = 0xAA;

這三行代碼表明了 主引導扇區(qū)

  • 大小為512字節(jié)
  • 最后的兩位為AA55(小端機,低位在前)
  • 初始化為全零

練習2,練習3

  1. 輸入make debug進入調(diào)試界面
  2. 輸入 b *0x7c00設置斷點
  3. continue運行
  4. 查看接下的指令,與boot/bootasm.S和bootblock.asm里的內(nèi)容十分類似
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

    # 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
    jnz seta20.1

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

seta20.2:
    inb $0x64, %al                                  # Wait for not busy(8042 input buffer empty).
    testb $0x2, %al
    jnz seta20.2

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

    # Switch from real to protected mode, using a bootstrap GDT
    # and segment translation that makes virtual addresses
    # identical to physical addresses, so that the
    # effective memory map does not change during the switch.
    lgdt gdtdesc
    movl %cr0, %eax
    orl $CR0_PE_ON, %eax
    movl %eax, %cr0

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

.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

    # If bootmain returns (it shouldn't), loop.
spin:
    jmp spin

# Bootstrap GDT
.p2align 2                                          # force 4 byte alignment
gdt:
    SEG_NULLASM                                     # null seg
    SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff)           # code seg for bootloader and kernel
    SEG_ASM(STA_W, 0x0, 0xffffffff)                 # data seg for bootloader and kernel

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

代碼后面基本上都帶有注釋,在此處總結一下流程

  1. 禁止中斷
  2. 復位標志寄存器方向標志位
  3. 初始化ds,es, ss三個段(設置為0)
  4. 使能A20(擴大尋址空間從1M)
  5. 跳轉(zhuǎn)到gdtdes中,加載GDT(全局描述符表)
  6. 使能cr0,切換到保護模式
  7. 切換到32位模式
  8. 修改保護模式下各個寄存器的值(0x10)
  9. 設置堆棧以調(diào)用c語言
  10. 調(diào)用bootmain.c

按照練習3中的要求具體分析切換保護模式

1. 為何開啟A20

A20的歷史可以通過閱讀wiki/A20_line得知:
大概是8086的時候使用段加偏移訪問的時候,1M以上的空間必須要第21根線來尋址,所以才有了A20。
發(fā)展到后來的80286也沿用8086的方式,80386出來了保護模式。此時地址線已經(jīng)是32根,如果不使能A20的話,A20將保持低電平,訪問的空間減少了一半(第21位恒為零)。

2. 如何初始化GDT表

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

3.如何使能和進入保護模式

使能cr0的wp位為1,進入保護模式。


練習4

使用source insight可以很方便的查看各種數(shù)據(jù)類型和結構體的定義

void
bootmain(void) {
    // read the 1st page off disk
    readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);

    // is this a valid ELF?
    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函數(shù)讀取扇區(qū),readseg函數(shù)則是循環(huán)調(diào)用readsect函數(shù)來讀取每一個扇區(qū)的內(nèi)容。

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);
    outb(0x1F7, 0x20);                      // cmd 0x20 - read sectors

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

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

此處代碼具體的讀取方式存疑,猜想是通過outb:I/O 上寫入 8 位數(shù)據(jù) ( 1 字節(jié) )的方法往不同的io端口寫入指令來實現(xiàn)不同的功能。

扇區(qū)內(nèi)容讀完以后,進入判斷函數(shù)判斷是否為ELF格式的文件“ Loading ELF Binaries ” 配合相關ELF加載方法文檔,將ELF Header的信息讀如ph中。
按照ph里的信息,將ELF文件載入內(nèi)存,然后再通過過函數(shù)入口信息,加載內(nèi)核的入口。


練習5

棧相關的寄存器兩個,ebp(基址寄存器)和esp(棧指針寄存器),棧的增長方向是由高到低
eip是程序指令指針,當前程序運行的指令

此時ebp是sum函數(shù)棧的基址,然后eip里面是sum函數(shù)中的第一條指令
sum函數(shù)執(zhí)行完之后,sum函數(shù)棧的內(nèi)容全部出棧,
eip=((uint_t)ebp+1),就是sum函數(shù)之后的指令的地址,然后函數(shù)參數(shù)出棧
然后ebp重新變成main函數(shù)的函數(shù)?;?,ebp=((uint_t)ebp)

我們通過ebp獲得當前函數(shù)棧的基址,eip獲得程序當前運行的位置。
然后打印兩個值,由指導方案的示意圖與注釋輸出參數(shù)表

函數(shù)調(diào)用棧結構

然后在調(diào)用print_debuginfo輸出其他的信息。
再通過結構示意圖,將ebp+1的內(nèi)容(返回地址)給eip
eip = *((uint32_t*)ebp + 1);
再吧ebp的內(nèi)容給ebp回到上層調(diào)用函數(shù)繼續(xù)輸出
ebp = *((uint32_t*)ebp);

原代碼沒有判斷ebp是否為0,輸出了很多為零的ebp信息,在循環(huán)條件處判斷ebp是否為零就可以了

最后一行的信息由 print_debuginfo函數(shù)輸出

void
print_debuginfo(uintptr_t eip) {
    struct eipdebuginfo info;
    if (debuginfo_eip(eip, &info) != 0) {
        cprintf("    <unknow>: -- 0x%08x --\n", eip);
    }
    else {
        char fnname[256];
        int j;
        for (j = 0; j < info.eip_fn_namelen; j ++) {
            fnname[j] = info.eip_fn_name[j];
        }
        fnname[j] = '\0';
        cprintf("    %s:%d: %s+%d\n", info.eip_file, info.eip_line,
                fnname, eip - info.eip_fn_addr);
    }
}

輸出的信息分別是:源碼所在文件,源碼所在行數(shù),函數(shù)名,當前位置與函數(shù)指針的差(即函數(shù)源碼的長度)

至于
最深層ebp

這里的被ebp為什么是7bf8。
其實很好理解,因為每個函數(shù)體之前在編譯的時候都會被插入

pushl %ebp
movl %esp,%ebp

所以7c00-0008 = 7bf8


練習6

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

image.png

由指導方案的上圖可知,一個表項占32*2位,8個字節(jié)。0到15位和48到63位為偏移量的低位和高位。16到31位是段選擇子。 通過這幾個數(shù)據(jù)來找到中斷處理代碼的入口。

2.補全idt_init函數(shù)
void idt_init(void)
{
    /* LAB1 YOUR CODE : STEP 2 */
    /* (1) Where are the entry addrs of each Interrupt Service Routine (ISR)?
      *     All ISR's entry addrs are stored in __vectors. where is uintptr_t __vectors[] ?
      *     __vectors[] is in kern/trap/vector.S which is produced by tools/vector.c
      *     (try "make" command in lab1, then you will find vector.S in kern/trap DIR)
      *     You can use  "extern uintptr_t __vectors[];" to define this extern variable which will be used later.
      * (2) Now you should setup the entries of ISR in Interrupt Description Table (IDT).
      *     Can you see idt[256] in this file? Yes, it's IDT! you can use SETGATE macro to setup each item of IDT
      * (3) After setup the contents of IDT, you will let CPU know where is the IDT by using 'lidt' instruction.
      *     You don't know the meaning of this instruction? just google it! and check the libs/x86.h to know more.
      *     Notice: the argument of lidt is idt_pd. try to find it!
      */
    int i = 0;
    extern uintptr_t __vectors[];
    for(i = 0; i < 255; ++i)
    {
        SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], 0);
    }
    SETGATE(idt[T_SWITCH_TOK], 1, GD_KTEXT, __vectors[T_SWITCH_TOK], 3);
    lidt(&idt_pd);
}
  • 根據(jù)上面的注釋,先申明外部變量__vectors[],也就是偏移量。
  • 然后使用SETGATE宏來設置idt表。第二個參數(shù)按照要求設為零(for 中斷)
    第三個參數(shù)按照實驗手冊上說的通常設置為內(nèi)核代碼段,猜想應該折折以一個有關內(nèi)存管理的宏定義,查看mm文件下的.h文件,發(fā)現(xiàn)在memlayout.h里定義了相關的宏
/* This file contains the definitions for memory management in our OS. */

/* 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
  • 第四個參數(shù)設置為偏移量__vector[i]
  • 第五個參數(shù)按照指導手冊上所寫,除了系統(tǒng)調(diào)用使用特權級3以外,均使用特權級0
  • 然后是專門設置系統(tǒng)調(diào)用中斷,即用戶態(tài)切換到內(nèi)核態(tài),使用陷阱門描述符,并且特權級為3
  • 最后使用lidt加載中斷描述符表。此處雖然注釋上寫參數(shù)是idt_pd,這個變量類型是struct gatedesc 類型。但在寫的時候發(fā)現(xiàn)lidt的申明是lidt(struct pseudodesc *pd) 所以將idt_pd取地址傳入。

練習ex 1

擴展proj4,增加syscall功能,即增加一用戶態(tài)函數(shù)(可執(zhí)行一特定系統(tǒng)調(diào)用:獲得時鐘計數(shù)值),當內(nèi)核初始完畢后,可從內(nèi)核態(tài)返回到用戶態(tài)的函數(shù),而用戶態(tài)的函數(shù)又通過系統(tǒng)調(diào)用得到內(nèi)核態(tài)的服務

此處參考《操作系統(tǒng)設計與實現(xiàn)》中的代碼

case T_SWITCH_TOU:
        if (tf->tf_cs != USER_CS) {
            switchk2u = *tf;
            switchk2u.tf_cs = USER_CS;
            switchk2u.tf_ds = USER_DS;
            switchk2u.tf_es = USER_DS;
            switchk2u.tf_ss = USER_DS;
            switchk2u.tf_esp = (uint32_t)tf + sizeof(struct trapframe) - 8;
        
            //設置EFLAG的i/o特權位,在用戶態(tài)也能使用i/o指令
            switchk2u.tf_eflags |= (3<<12);
        
            //設置臨時棧,以便cpu用switchk2u中恢復數(shù)據(jù)
            *((uint32_t *)tf - 1) = (uint32_t)&switchk2u;
        }
        break;  
    case T_SWITCH_TOK:
        if (tf->tf_cs != KERNEL_CS) {
            tf->tf_cs = KERNEL_CS;
            tf->tf_ds = tf->tf_es = KERNEL_DS;

            //關閉用戶態(tài)使用io的特權位
            tf->tf_eflags &= ~(3<<12);
            switchu2k = (struct trapframe *)(tf->tf_esp - (sizeof(struct trapframe) - 8));
            memmove(switchu2k, tf, sizeof(struct trapframe) - 8);
            *((uint32_t *)tf - 1) = (uint32_t)switchu2k;
        }
        break;
棧情況K2U.png

棧情況恢復寄存器.png

圖片來源網(wǎng)絡,權侵刪

根據(jù)操作系統(tǒng)設計與實現(xiàn)里的指導,先申明臨時變量
struct trapframe switchk2u, *switchu2k;

然后在trap_disptach函數(shù)中,設置寄存器為用戶/內(nèi)核。
最后根據(jù)上面的兩張圖保存棧里的值。

在trap函數(shù)運行結束后,在trapentry.s里的iret的值返回以后,以用戶態(tài)/內(nèi)核態(tài)繼續(xù)執(zhí)行。

在init.c中啟動測試函數(shù),

static void
lab1_switch_to_user(void) {
    //LAB1 CHALLENGE 1 : TODO
    asm volatile (
        "sub $0x8, %%esp \n"      //從中斷返回的時候會多pop兩位用來更新ss和sp,所以先把棧壓兩位
        "int %0 \n"
        "movl %%ebp, %%esp"    //修復esp
        : 
        : "i"(T_SWITCH_TOU)
    );
}

static void
lab1_switch_to_kernel(void) {
    //LAB1 CHALLENGE 1 :  TODO
    asm volatile (
        "int %0 \n"
        "movl %%ebp, %%esp \n"
        : 
        : "i"(T_SWITCH_TOK)
    );
}

兩段內(nèi)聯(lián)匯編的作用與含義依然存在疑問。


練習ex 2

(未成功實現(xiàn)
嘗試在trap.c里重寫switch_to_u/k函數(shù),然后通過鍵盤中斷調(diào)用系統(tǒng)調(diào)用中斷,使用中斷嵌套的方式實現(xiàn)

 case IRQ_OFFSET + IRQ_KBD:
        c = cons_getc();
        cprintf("kbd [%03d] %c\n", c, c);
        if('0' == c && tf->tf_cs != KERNEL_CS){
            print_trapframe(tf);

            lab1_switch_to_kernel();
            cprintf("switch to kernel : \n");
            print_trapframe(tf);
            
        }
        else if('3' == c && tf->tf_cs != USER_CS){
            print_trapframe(tf);

            lab1_switch_to_userl();
            cprintf("switch to user : \n");
            print_trapframe(tf);

        }else
            
            break;

實際運行的時候,首先是無法觀察到寄存器的變化。理論在調(diào)用switch函數(shù)的時候,會有新的中斷產(chǎn)生,會再次調(diào)用trap函數(shù),在trap函數(shù)返回的時候,.s文件里的iret會執(zhí)行,寄存器的值被寫入應該會有不同的輸出。

然后就是內(nèi)核態(tài)在調(diào)用鍵盤中斷時轉(zhuǎn)入用戶態(tài)是成功了的,但是之后的用戶態(tài)轉(zhuǎn)入內(nèi)核態(tài)一直失敗,原因也仍在探索。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容