ucore作業系統學習筆記(一) ucore lab1系統啟動流程分析

小熊餐館發表於2020-09-28

一、ucore作業系統介紹

  作業系統作為一個基礎系統軟體,對下控制硬體(cpu、記憶體、磁碟網路卡等外設),遮蔽了底層複雜多樣的硬體差異;對上則提供封裝良好的應用程式介面,簡化應用程式開發者的使用難度。站在應用程式開發人員的角度來看,日常開發中常見的各種關於併發、I/O、程式通訊的問題等都和作業系統相關,因此一定程度上了解底層的作業系統工作原理是有必要的。

  另一方面,由於作業系統自身功能的複雜性,整體設計一般會有一個好的模組化架構;作業系統作為基礎服務,對效能效率的要求也很高,底層會用到許多關於資料結構和演算法相關的知識。如果仔細的研究一個作業系統的原始碼,既可以學習設計一個複雜軟體的架構知識,又可以看到偏理論的資料結構和演算法知識是如何被運用在實際場景中的,更深刻的體會不同資料結構、演算法在特定場景下的效能差異。

  然而對於初學者而言,學習作業系統並不是一件輕鬆的事情。作業系統理論的學習過於抽象,往往看了就忘。而主流商業作業系統動輒十萬、百萬級的核心原始碼也令想要一窺究竟的普通人望而卻步。對於一個已經迭代發展相當一段時間的系統,個人認為好的學習方法不是從最新的,相對複雜的版本開始瞭解,而是從最初始的,較為簡單的版本起步,研究其是如何一步步優化、迭代至如今的這個版本。經過無數人迭代、優化的最新版本linux核心固然無比複雜,但90年代早期釋出的版本卻簡單太多,更容易理解和學習,在掌握了相對簡單的早期版本後,能降低後續學習更復雜的版本的難度。

  對於作業系統的學習而言,有不少大牛都出版了關於如何實現一個簡易版作業系統的書籍,例如《Orange'S:一個作業系統的實現》《30天自制作業系統》等等。不少大學也開始對作業系統的課程進行改革,不再是枯燥的灌輸理論知識點,而是嘗試著讓學生親自動手實現一個demo作業系統,加深對知識內容的理解。其中麻省理工大學的公開課MIT 6.828是出品較早,久負盛名的。

  本系列部落格的主角,是由清華大學出品的作業系統網上公開課,其中的實驗課程就需要學生通過一個個的迭代實驗,逐步實現一個名為ucore的作業系統。其實驗指導書上對ucore os的評價是"麻雀雖小,五臟俱全",非常適合作業系統初學者進行學習。

ucore專案github倉庫連結:https://github.com/chyyuu/os_kernel_lab (master分支)

ucore實驗指導書連結:https://chyyuu.gitbooks.io/ucore_os_docs/content/

ucore公開課視訊連結(學堂線上):https://www.xuetangx.com/course/THU08091000267

二、學習ucore所需要的準備知識

  工欲善其事,必先利其器。作業系統作為一門綜合性的課程,需要掌握一定的前置基礎知識才能順利的完成ucore作業系統的學習。

1.C語言  

  ucore核心中絕大多數的功能都是使用C語言實現的,掌握C語言是學習ucore的基礎。除了熟練掌握C語言的基礎語法知識之外,最好能對巨集、指標有一定了解。

  推薦學習書籍:《C primer》

2.x86組合語言(32位)

  ucore核心是執行在80386這一32位x86架構的cpu之上的。雖然ucore的核心主要是由C語言實現的,但由於作業系統是貼近底層,與硬體有頻繁互動的系統程式。在ucore中,CPU加電後的核心載入程式以及中斷、與特定硬體互動時的部分都需要通過x86彙編來實現。

  如果是組合語言的初學者,強烈建議先學習8086組合語言,建立一個對底層CPU工作原理的基本知識結構後,再學習更為複雜的32位彙編。  

  需要注意的一點是,ucore中的x86彙編程式碼是以AT&T格式編寫的,和Intel格式的x86彙編雖然邏輯等價,但寫法上有較大差異。對於通過Intel格式入門彙編的初學者來說,需要稍微適應一下。

  推薦學習書籍:《組合語言》王爽著(8086彙編)、《X86彙編從真實模式到保護模式》(80386彙編)

3.80386CPU工作原理

  組合語言對應的是機器碼,其中的很多功能都與CPU硬體緊密關聯。80386的分頁、中斷,特權級等功能在ucore作業系統的實現中扮演了重要的角色。如果對80386的工作原理了解不夠,在閱讀ucore與之相關的原始碼時會有困難。

  推薦學習書籍:《X86彙編從真實模式到保護模式》、《intel 80386參考手冊》

4.C語言機器實現底層(與x86彙編的關聯)

  在ucore中經常會出現c和彙編程式碼互相呼叫的地方。要想理解其工作原理,需要去理解C語言編譯後生成的底層機器指令(彙編),統一的站在組合語言的角度來思考。你需要了解C中的結構體、陣列等資料結構在記憶體中的是如何排布的,C中的指標操作是如何被轉換成各種記憶體定址指令的,C中的函式呼叫與返回過程中,由於引數壓棧出棧等等棧上資料的是如何變化的等等。

  其實C中的指標等比較難理解的概念,在有了一定的組合語言基礎後會理解的更加透徹。C中的指標和結構體使得程式設計師不必再去思考彙編層面中繁瑣的記憶體訪問偏移量計算,統統的交由編譯器處理,C程式設計師的腦力得到解放,能夠站在更高的抽象層面去思考更復雜的業務問題。

  有了C語言和彙編的基礎後,可以通過編寫簡單的C程式,檢視其反彙編程式碼來進行相關的學習(通過32位的編譯器)。

  推薦學習書籍:《深入理解計算機系統》(Computer Systems A Programmer's perspective  CSAPP)

5.基本的資料結構知識

  ucore中所涉及到的通用資料結構並不多,只需要對雙向連結串列和雜湊表有一定了解即可。

  雖然在後續的實驗中參考linux的實現引入了紅黑樹等複雜資料結構優化一些演算法的實現,但並不涉及核心流程,如果不是學有餘力,在ucore的學習過程中當做一個黑盒子去看待就行。

  推薦學習公開課視訊: 清華大學出品的資料結構公開課(鄧俊輝)

  初學者在學習ucore的過程中碰到的一個很大的困難就是lab1作為最初始的一個實驗,為了搭建起一個能實際執行的系統,一下子引入了很多內容。這裡面既有生成img映象的功能,也有bootloader載入核心的功能,還有許多與硬體互動的程式碼邏輯,這些資訊鋪天蓋地的湧來,容易勸退初學者。當時的我就差點被勸退了,但由於自己強烈的好奇心以及實驗指導書首頁的提醒:“lab1和lab2比較困難,有些同學由於畏難而止步與此,很可惜。通過lab1和lab2後,對計算機原理中的中斷、段頁表機制、特權級等的理解會更深入,等會有等同於打通了任督二脈,後面的實驗將一片坦途。”,最終還是堅持了下來。實際的感覺也確實如此,如果能理解lab1、lab2中諸多硬體相關的知識和C核心實現中很多巧妙但晦澀的指標、巨集的用法,後續的實驗將簡單很多。

  在整個ucore的學習過程中,除了公開課的視訊和資料外,網上很多關於ucore學習的部落格也給了我很大幫助,所以我也希望能通過部落格分享自己的學習心得,幫助到更多對作業系統、ucore感興趣的人。如果實驗中碰到不懂的地方,多通過關鍵字去搜尋相關資料以及網上關於ucore學習的部落格能夠起到事半功倍的作用。

三、ucore作業系統lab1 系統載入啟動過程分析

  下面進入正題,開始分析ucore在實驗課程lab1中的內容:ucore系統載入啟動過程的分析。

  ucore的lab1專案結構從整體來看,按照執行的流程順序分為三部分:img磁碟映像的生成引導核心的bootloader程式ucore核心的初始化

ucore的img磁碟映像生成

  ucore整體是一個makefile專案。通過make指令,解析專案中的makefile檔案後會生成一個ucore.img磁碟映像。(lab1的實驗課視訊演示中可以詳細的看到構建的全過程)

  這個磁碟映像主要由兩大部分組成:位於第一個扇區即引導扇區的ucore bootloader程式,以及第二個扇區開始往後的ucore kernel核心程式。

  80386CPU在加電啟動之初,會執行固化在BIOS中的程式。BIOS由於容量有限,自身不提供載入完整作業系統核心的功能,而是約定好會讀取磁碟中第一個扇區(引導扇區)中的內容,將其載入至記憶體地址空間0x7c00處,在載入完畢後,令CS:IP指向0x7c00,跳轉執行引導扇區中的載入程式的第一條指令。為了避免所載入的磁碟引導扇區是一個無效扇區(可能引導扇區中的內容就是空的或是亂碼),要求512位元組大小的扇區在其最後兩位元組必須是0x55AA(其餘的空餘空間可以用0填充),否則無法通過BIOS的校驗,引導失敗。

  ucore的makefile檔案中,將專案中位於boot資料夾下的程式放入了ucore的第一個扇區,在makefile的"#create bootblock"註釋開頭的段中有所體現。其中呼叫了/tool/sign.c來生成寫入一個合法的引導扇區。

  由於專案中的makefile檔案中有不少複雜指令碼,如果對makefile工作原理不熟悉,在ucore的學習中可以降低要求,大致瞭解一下每一部分的程式碼大概在幹什麼即可,不必強求理解每一行,避免在學習之初就產生太強的挫敗感。

如果想對通過makefile是如何一步步完整的生成磁碟映像感興趣,可以參考以下內容:

  1. lab1專案目錄下的report.md實驗報告示例

  2. https://www.jianshu.com/p/2f95d38afa1d  其中對lab1中makefile的分析非常詳細

ucore的bootloader載入程式

  當BIOS載入完引導扇區的內容至記憶體後,CPU便會跳轉到0x7c00執行命令,此時CPU的控制權便交給了ucore的載入程式bootloader。載入程式主體由boot資料夾下的bootasm.S和bootasm.c共同組成,其中bootasm.S由於構建時靠前,是先執行的。

令CPU進入保護模式

  bootasm.S的主要工作就是令80386從加電時預設的真實模式切換到32位保護模式,通過程式碼的註釋可以看到,由於一些歷史原因要令80386正確的進入保護模式還是有點小麻煩的(並不是簡單的調整一個開關位就行)。在《X86組合語言 從真實模式到保護模式》一書中對此有更加詳細的介紹。

  在通過彙編指令完成80386從真實模式至保護模式的切換後,通過call bootmain指令,跳轉至bootmain.c中的bootmain函式完成引導核心的工作。

bootasm.S:

  1 #include <asm.h>
  2 
  3 # Start the CPU: switch to 32-bit protected mode, jump into C.
  4 # The BIOS loads this code from the first sector of the hard disk into
  5 # memory at physical address 0x7c00 and starts executing in real mode
  6 # with %cs=0 %ip=7c00.
  7 
  8 # 80386CPU為了相容8086程式,最開始啟動時是以16位的真實模式進行工作的
  9 # 生成img磁碟映像時,bootasm.S中的引導程式碼將會被放在引導扇區
 10 # 80386CPU加電啟動後,會執行BIOS中的預設載入程式,BIOS載入程式會將引導扇區中(第一個磁碟塊)的內容讀入記憶體,並放置在0x7C00(16位)/0x00007c00(32位)處
 11 # 隨後CPU會跳轉到0x7c00處開始第一條指令的執行,即bootasm.S的第一條指令(start:)
 12 
 13 .set PROT_MODE_CSEG,        0x8                     # kernel code segment selector
 14 .set PROT_MODE_DSEG,        0x10                    # kernel data segment selector
 15 .set CR0_PE_ON,             0x1                     # protected mode enable flag
 16 
 17 # start address should be 0:7c00, in real mode, the beginning address of the running bootloader
 18 .globl start
 19 start:
 20 .code16                                             # Assemble for 16-bit mode
 21     cli                                             # Disable interrupts
 22     cld                                             # String operations increment
 23 
 24     # Set up the important data segment registers (DS, ES, SS).
 25     xorw %ax, %ax                                   # Segment number zero
 26     movw %ax, %ds                                   # -> Data Segment
 27     movw %ax, %es                                   # -> Extra Segment
 28     movw %ax, %ss                                   # -> Stack Segment
 29 
 30     # Enable A20:
 31     #  For backwards compatibility with the earliest PCs, physical
 32     #  address line 20 is tied low, so that addresses higher than
 33     #  1MB wrap around to zero by default. This code undoes this.
 34 
 35     # 為了進入32位保護模式,必須先開啟A20(第21位記憶體訪問線),否則在32位定址模式下給出的記憶體地址第21位始終為0,造成錯誤
 36     # 為什麼需要特意開啟A20匯流排?
 37     # 在早期的8086CPU中,記憶體匯流排是20位的,由高16位的段基址和低16位的段內偏移共同構成一個20位的記憶體地址
 38     # 但事實上在段基址和段內偏移比較大的情況下,其實際得出的結果是超過了20位的(例如0xFFFF段基址 <<< 4 + 0xFFFF段內偏移 > 0xFFFFF),出現了溢位
 39     # 而8086中對這種溢位是相容的,這種溢位在8086上會體現為繞回0x00000低端
 40     # “程式設計師,你是知道的,他們喜歡鑽研,更喜歡利用硬體的某些特性來展示自己的技術,很難說在當年有多少程式在依賴這個迴繞特性工作著”
 41     # 摘自《X86組合語言 從真實模式到保護模式》 11.5 關於第21條地址線A20的問題
 42     # 到了更新版的80286時代,24位的記憶體匯流排,如果不預設關閉A20匯流排,那麼就無法相容使用迴繞特性的8086程式了
 43     # 而80386作為80286的後一代,也繼承了80286這一特性
 44 seta20.1:
 45     inb $0x64, %al                                  # Wait for not busy(8042 input buffer empty).
 46     testb $0x2, %al
 47     jnz seta20.1
 48 
 49     movb $0xd1, %al                                 # 0xd1 -> port 0x64
 50     outb %al, $0x64                                 # 0xd1 means: write data to 8042’s P2 port
 51 
 52 seta20.2:
 53     inb $0x64, %al                                  # Wait for not busy(8042 input buffer empty).
 54     testb $0x2, %al
 55     jnz seta20.2
 56 
 57     movb $0xdf, %al                                 # 0xdf -> port 0x60
 58     outb %al, $0x60                                 # 0xdf = 11011111, means set P2’s A20 bit(the 1 bit) to 1
 59 
 60     # Switch from real to protected mode, using a bootstrap GDT
 61     # and segment translation that makes virtual addresses
 62     # identical to physical addresses, so that the
 63     # effective memory map does not change during the switch.
 64     # 設定GDT,修改CRO暫存器中的保護模式允許位,進入保護模式
 65     lgdt gdtdesc
 66     movl %cr0, %eax
 67     orl $CR0_PE_ON, %eax
 68     movl %eax, %cr0
 69 
 70     # Jump to next instruction, but in 32-bit code segment.
 71     # Switches processor into 32-bit mode.
 72     # 通過一個遠跳轉指令指向protcseg處的指令,令CPU清空之前在真實模式下儲存在流水線中的指令(當前處於保護模式下執行真實模式的指令會出現各種問題)
 73     ljmp $PROT_MODE_CSEG, $protcseg
 74 
 75 # 下面的都是X86-32的彙編程式
 76 .code32                                             # Assemble for 32-bit mode
 77 protcseg:
 78     # Set up the protected-mode data segment registers
 79     # 跳轉至保護模式後,需要重新整理資料段暫存器(因為引入了特權級保護,避免資料段暫存器之前的值不對而出現漏洞)
 80     movw $PROT_MODE_DSEG, %ax                       # Our data segment selector
 81     movw %ax, %ds                                   # -> DS: Data Segment
 82     movw %ax, %es                                   # -> ES: Extra Segment
 83     movw %ax, %fs                                   # -> FS
 84     movw %ax, %gs                                   # -> GS
 85     movw %ax, %ss                                   # -> SS: Stack Segment
 86 
 87     # Set up the stack pointer and call into C. The stack region is from 0--start(0x7c00)
 88     # 設定棧段暫存器 棧基址0x0,棧頂指標指向start段所在位置(0x7c00)
 89     movl $0x0, %ebp
 90     movl $start, %esp
 91     # 呼叫跳轉至bootmain.c中的bootmain函式,完成核心的引導
 92     call bootmain
 93 
 94     # If bootmain returns (it shouldn’t), loop.
 95     # 自旋死迴圈(但如果載入程式和核心實現正確,bootmain函式將永遠不會返回並執行至此。因為作業系統核心本身就是通過自旋迴圈常駐記憶體的)
 96 spin:
 97     jmp spin
 98 
 99 # Bootstrap GDT
100 .p2align 2                                          # force 4 byte alignment
101 # SEG_ASM是位於asm.h中的巨集,用於構造GDT中的段描述符
102 # 按照GDT的約定,第一個為NULL段。ucore採用的是平坦記憶體模型,所以程式碼段和資料段在核心中均只存在一個。
103 gdt:
104     SEG_NULLASM                                     # null seg
105     SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff)           # code seg for bootloader and kernel
106     SEG_ASM(STA_W, 0x0, 0xffffffff)                 # data seg for bootloader and kernel
107 
108 gdtdesc:
109     .word 0x17                                      # sizeof(gdt) - 1
110     .long gdt                                       # address gdt

bootloader引導載入核心

  bootloader載入程式是位於裝置的第一個扇區,即引導扇區的,而ucore的核心程式則是從第二個磁碟扇區開始往後存放的。bootmain.c的任務就是將kernel核心部分從磁碟中讀出並載入記憶體,並將程式的控制流轉移至指定的核心入口處。

  ucore的核心檔案在生成磁碟映像時是以ELF(Executable and linking format)格式儲存的。ELF檔案是Unix/Linux下通用的一種可執行檔案,對於ELF的詳細介紹在《深入理解計算機系統》的"連結"一章中有較為詳細的介紹。

  要想徹底的理解ELF格式的檔案是如何被編譯器、連結器等工具生成的,需要對編譯原理相關的知識進行系統的學習,難度很大。因此在ucore的學習過程中,如果不是很瞭解ELF,可以簡單的理解為ELF的檔案頭中標識了一個可執行程式中包含了哪些部分,比如程式碼段、資料段(只讀資料段、可讀寫資料段)、棧段等等,分別儲存在哪裡;並指明瞭需要為這些段分配多少記憶體空間、需要被載入到記憶體的什麼地址(虛擬地址)等。

  ucore核心生成ELF檔案的關鍵配置在/tools/kernel.ld中,可以清楚的看到核心載入的.text程式碼段基址為0x100000,後面緊跟著各種型別的資料段等。

kernel.ld:

/* Simple linker script for the JOS kernel.
   See the GNU ld 'info' manual ("info ld") to learn the syntax. */

OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")
OUTPUT_ARCH(i386)
ENTRY(kern_init)

SECTIONS {
    /* Load the kernel at this address: "." means the current address */
    . = 0x100000;

    .text : {
        *(.text .stub .text.* .gnu.linkonce.t.*)
    }

    PROVIDE(etext = .);    /* Define the 'etext' symbol to this value */

    .rodata : {
        *(.rodata .rodata.* .gnu.linkonce.r.*)
    }

    /* Include debugging information in kernel memory */
    .stab : {
        PROVIDE(__STAB_BEGIN__ = .);
        *(.stab);
        PROVIDE(__STAB_END__ = .);
        BYTE(0)        /* Force the linker to allocate space
                   for this section */
    }

    .stabstr : {
        PROVIDE(__STABSTR_BEGIN__ = .);
        *(.stabstr);
        PROVIDE(__STABSTR_END__ = .);
        BYTE(0)        /* Force the linker to allocate space
                   for this section */
    }

    /* Adjust the address for the data segment to the next page */
    . = ALIGN(0x1000);

    /* The data segment */
    .data : {
        *(.data)
    }

    PROVIDE(edata = .);

    .bss : {
        *(.bss)
    }

    PROVIDE(end = .);

    /DISCARD/ : {
        *(.eh_frame .note.GNU-stack)
    }
}

  在libs/elf.h中定義了兩個ELF相關的結構體,elfhdrproghdr,用於對映讀取出來的核心ELF頭資訊。

elf.h:

#ifndef __LIBS_ELF_H__
#define __LIBS_ELF_H__

#include <defs.h>

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

/* file header */
struct elfhdr {
    uint32_t e_magic;     // must equal ELF_MAGIC
    uint8_t e_elf[12];
    uint16_t e_type;      // 1=relocatable, 2=executable, 3=shared object, 4=core image
    uint16_t e_machine;   // 3=x86, 4=68K, etc.
    uint32_t e_version;   // file version, always 1
    uint32_t e_entry;     // entry point if executable
    uint32_t e_phoff;     // file position of program header or 0
    uint32_t e_shoff;     // file position of section header or 0
    uint32_t e_flags;     // architecture-specific flags, usually 0
    uint16_t e_ehsize;    // size of this elf header
    uint16_t e_phentsize; // size of an entry in program header
    uint16_t e_phnum;     // number of entries in program header or 0
    uint16_t e_shentsize; // size of an entry in section header
    uint16_t e_shnum;     // number of entries in section header or 0
    uint16_t e_shstrndx;  // section number that contains section name strings
};

/* program section header */
struct proghdr {
    uint32_t p_type;   // loadable code or data, dynamic linking info,etc.
    uint32_t p_offset; // file offset of segment
    uint32_t p_va;     // virtual address to map segment
    uint32_t p_pa;     // physical address, not used
    uint32_t p_filesz; // size of segment in file
    uint32_t p_memsz;  // size of segment in memory (bigger if contains bss)
    uint32_t p_flags;  // read/write/execute bits
    uint32_t p_align;  // required alignment, invariably hardware page size
};

#endif /* !__LIBS_ELF_H__ */

bootmain.c:

#include <defs.h>
#include <x86.h>
#include <elf.h>

/* *********************************************************************
 * This a dirt simple boot loader, whose sole job is to boot
 * an ELF kernel image from the first IDE hard disk.
 *
 * DISK LAYOUT
 *  * This program(bootasm.S and bootmain.c) is the bootloader.
 *    It should be stored in the first sector of the disk.
 *         這個程式(bootasm.S and bootmain.c)是引導載入器程式,應該被儲存在磁碟的第一個扇區
 *
 *  * The 2nd sector onward holds the kernel image.
 *         第二個扇區往後儲存著核心映像
 *
 *  * The kernel image must be in ELF format.
 *         核心映像必須必須是ELF格式的
 *
 * BOOT UP STEPS
 *  * when the CPU boots it loads the BIOS into memory and executes it
 *
 *  * the BIOS intializes devices, sets of the interrupt routines, and
 *    reads the first sector of the boot device(e.g., hard-drive)
 *    into memory and jumps to it.
 *
 *  * Assuming this boot loader is stored in the first sector of the
 *    hard-drive, this code takes over...
 *
 *  * control starts in bootasm.S -- which sets up protected mode,
 *    and a stack so C code then run, then calls bootmain()
 *
 *  * bootmain() in this file takes over, reads in the kernel and jumps to it.
 * */
unsigned int    SECTSIZE  =      512 ;  // 一個磁碟扇區的大小為512位元組
struct elfhdr * ELFHDR    =      ((struct elfhdr *)0x10000) ;     // scratch space

/* waitdisk - wait for disk ready */
static void
waitdisk(void) {
    // 讀資料,當0x1f7不為忙狀態時,可以讀
    while ((inb(0x1F7) & 0xC0) != 0x40)
        /* do nothing */;
}

/* readsect - read a single sector at @secno into @dst */
// 讀取一個單獨的扇區(由@secno指定)到@dst指標指向的記憶體中
static void
readsect(void *dst, uint32_t secno) {
    // https://chyyuu.gitbooks.io/ucore_os_docs/content/lab1/lab1_3_2_3_dist_accessing.html
    // 實驗指導書lab1中的對ide硬碟的訪問中有詳細介紹

    // 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);
}

/* *
 * readseg - read @count bytes at @offset from kernel into virtual address @va,
 * might copy more than asked.
 * */
static void
readseg(uintptr_t va, uint32_t count, uint32_t offset) {
    uintptr_t end_va = va + count;

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

    // translate from bytes to sectors; kernel starts at sector 1
    // 計算出需要讀取的磁碟扇區號,由於第1個扇區被bootloader佔據,kernel核心從第二個扇區開始(下標為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指標的自增,一個一個扇區的迴圈讀取資料寫入va指向的記憶體區域
    for (; va < end_va; va += SECTSIZE, secno ++) {
        readsect((void *)va, secno);
    }
}

/* bootmain - the entry of bootloader */
void
bootmain(void) {
    // read the 1st page off disk
    // 從硬碟中讀取出核心檔案ELF檔案頭資料,存入ELFHDR指標指向的記憶體區域 (大小為8個扇區)
    readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);

    // is this a valid ELF? 校驗讀取出來的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); // 根據elf檔案頭,獲得程式段的起始
    eph = ph + ELFHDR->e_phnum; // 程式段起始指標(*ph)指標偏移程式段數目(ELFHDR->e_phnum) = 最後一段程式的頭部
    for (; ph < eph; ph ++) {
        // 迴圈往復,將各個程式段的內容讀取至指定的記憶體位置(ph->p_va)
        readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
    }

    // call the entry point from the ELF header
    // note: does not return
    // 通過函式指標的方式,跳轉至ELFHDR->e_entry指定的程式初始執行入口(即核心入口)
    // 在makefile的配置中,ELFHDR->e_entry指向的是kern/init/init.c中的kern_init函式 (kernel.ld中的ENTRY(kern_init))
    ((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();

bad:
    // 跳轉至核心之後,不應該返回
    outw(0x8A00, 0x8A00);
    outw(0x8A00, 0x8E00);

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

ucore核心結構分析

  在bootloader將ucore的kernel核心完整的載入至記憶體,並通過ELF檔案頭中指定的entry入口跳轉至核心入口,即/kern/init.c中的kern_init函式。

  kern_init函式是核心的總控函式,核心中的各個組成部分都在kern_init函式中完成初始化。

核心總控函式kern_init 

  總控函式一方面負責初始化與各種硬體的互動(例如與顯示卡、中斷控制器、定時器等),另一方面初始化各種核心功能(比如初始化實體記憶體管理器、中斷描述符表IDT等),之後便通過一個自旋死迴圈令作業系統常駐記憶體,通過監聽各種中斷提供作業系統服務。

init.c(主體部分):

#include <defs.h>
#include <stdio.h>
#include <string.h>
#include <console.h>
#include <kdebug.h>
#include <picirq.h>
#include <trap.h>
#include <clock.h>
#include <intr.h>
#include <pmm.h>
#include <kmonitor.h>
void kern_init(void) __attribute__((noreturn));
void grade_backtrace(void);
static void lab1_switch_test(void);

/**
 * 核心入口 總控函式
 * */
void
kern_init(void){
    extern char edata[], end[];
    memset(edata, 0, end - edata);

    // 初始化控制檯(控制顯示卡互動),只有設定好了對顯示卡的控制後,std_out輸出的資訊(例如cprintf)才能顯示在控制檯中
    cons_init();                // init the console

    const char *message = "(THU.CST) os is loading ...";
    cprintf("%s\n\n", message);

    print_kerninfo();

    grade_backtrace();

    // 初始化實體記憶體管理器
    pmm_init();                 // init physical memory management

    // 初始化中斷控制器
    pic_init();                 // init interrupt controller
    // 初始化中斷描述符表
    idt_init();                 // init interrupt descriptor table

    // 初始化定時晶片
    clock_init();               // init clock interrupt
    // 開中斷
    intr_enable();              // enable irq interrupt

    //LAB1: CAHLLENGE 1 If you try to do it, uncomment lab1_switch_test()
    // user/kernel mode switch test
    lab1_switch_test();

    /* do nothing */
    // 陷入死迴圈,避免核心程式退出。通過監聽中斷事件進行服務
    while (1);
}

  從kern_init函式的程式碼中可以看出,其依次完成了如下的幾個主要工作:

  1. cons_init  初始化控制檯(控制顯示卡互動)

  2. pmm_init  初始化實體記憶體管理器(lab1中裡面暫時只是完成了GDT的重新設定,比較簡單。而在lab2的實體記憶體管理的實現中,pmm_init才成為主角)

  3. pic_init 初始化中斷控制器(內部通過與8259A中斷控制器晶片進行互動,令ucore能夠接收到來自硬體的各種中斷請求)

  4. idt_init 初始化中斷描述符表(在下面的中斷機制一節中詳細介紹)

  5. clock_init 初始化定時器(進行8253定時器的相關設定,將其設定為10ms發起一次時鐘中斷)

  6. intr_enable 完成了核心結構的初始化後,開啟中斷,至此ucore核心正式開始執行

  在kern_init的核心初始化過程中,涉及到的與顯示卡、定時器等硬體互動的地方,要想深入理解其工作原理,除了仔細閱讀ucore的程式碼外,還需通過硬體手冊等資料熟悉不同硬體提供的互動介面,限於篇幅就不再展開了。個人認為這一部分內容並不屬於ucore的核心,如果不是特別感興趣,可以將其暫時視為一個黑盒子,理解大致工作原理即可。

ucore的中斷工作機制

  ucore在lab1中實現的一個非常重要的功能,就是建立了一個可以工作的中斷服務框架。可以說作業系統的工作是離不開硬體提供的中斷機制的。

  下面分析ucore的中斷機制是如何工作的。

80386中斷工作機制介紹

  前面提到過學習ucore的一個前提是對80386CPU的硬體工作原理有一定了解,這裡先回顧一下80386的中斷工作機制。

  1. 在80386中,為了更好的支援對中斷服務例程的特權級保護,使用中斷描述符表代替了8086中的中斷向量表。和8086中斷向量表被固定在低位記憶體不一樣,80386CPU通過中斷描述符表暫存器IDTR來定位中斷描述符表IDT的位置,這給了作業系統的設計者一定的自主權。

  2. 80386在執行完每條指令後,都會檢查當前是否存在中斷請求。如果沒有發現中斷請求,則接著執行後續指令;如果發現存在中斷請求,則會根據中斷訊號中給出的中斷型別碼,從中斷描述符表中查詢到對應的中斷描述符,在中斷描述符中記錄了對應的中斷服務例程的入口。

  3. 隨後,CPU硬體會打斷當前控制流,在棧上壓入CS、EIP、EFLAGS等暫存器的內容(用於中斷服務例程的返回),跳轉到對應的中斷服務例程入口,進行中斷請求的處理。當中斷服務返回時,通過之前壓入棧中的CS,EIP等返回到之前被中斷請求打斷的控制流中,恢復現場,繼續執行。

  由於80386中斷工作機制相對比較複雜,限於篇幅這裡的流程介紹省略了不少細節。如果對這一塊內容不熟悉的話需要通過實驗指導書或是有關資料進行學習,或者參考我之前寫的部落格 80386學習(四) 80386中斷,裡面對此有較為詳細的介紹。

ucore中斷功能的組成部分

  ucore的中斷工作機制大致可以分為以下幾個部分:

  1. IDT中斷描述符表的建立

  2. 中斷棧幀的生成

  3. 接收到中斷棧幀,通過對應的中斷服務例程進行處理

  4. 中斷服務例程處理完畢,中斷返回

IDT中斷描述符表的建立

  在ucore中,對於中斷描述符表IDT的初始化,是在kern_init總控函式中通過idt_init函式進行的。

 idt_init函式:

/* *
 * Interrupt descriptor table:
 *
 * Must be built at run time because shifted function addresses can't
 * be represented in relocation records.
 * */
static struct gatedesc idt[256] = {{0}};

static struct pseudodesc idt_pd = {
    sizeof(idt) - 1, (uintptr_t)idt
};

/* idt_init - initialize IDT to each of the entry points in kern/trap/vectors.S */
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!
      */
    extern uintptr_t __vectors[];
    int i;
    // 首先通過tools/vector.c通過程式生成/kern/trap/verctor.S,並在載入核心時對之前已經宣告的全域性變數__vectors進行整體的賦值
    // __vectors陣列中的每一項對應於中斷描述符的中斷服務例程的入口地址,在SETGATE巨集的使用中可以體現出來
    // 將__vectors陣列中每一項關於中斷描述符的描述設定到下標相同的idt中,通過巨集SETGATE構造出最終的中斷描述符結構
    for (i = 0; i < sizeof(idt) / sizeof(struct gatedesc); i ++) {
        // 遍歷idt陣列,將其中的內容(中斷描述符)設定進IDT中斷描述符表中(預設的DPL特權級都是核心態DPL_KERNEL=0)
        SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL);
    }
    // set for switch from user to kernel
    // 使用者態與核心態的互相轉化是通過中斷實現的,單獨為其一箇中斷描述符
    // 由於需要允許使用者態的程式訪問使用該中斷,DPL特權級為使用者態DPL_USER=3
    SETGATE(idt[T_SWITCH_TOK], 0, GD_KTEXT, __vectors[T_SWITCH_TOK], DPL_USER);
    // load the IDT 令IDTR中斷描述符表暫存器指向idt_pd,載入IDT
    // idt_pd結構體中的前16位為描述符表的界限,pd_base指向之前完成了賦值操作的idt陣列的起始位置
    lidt(&idt_pd);
}

  通過上述程式碼的註釋可以發現,在idt_init函式中,通過構建專案時自動生成的中斷描述符元資訊陣列__vectors,在一個迴圈中,通過SETGATE巨集,將idt[i]中的每一項都賦值了一箇中斷描述符。 可以看到中斷描述符和gatedesc門描述符結構體的對應關係。(C中結構體的欄位在記憶體中排布的順序是按照定義的順序,從低位到高位的)

中斷門示意圖:

  

gatedesc結構體:

/* 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
};

SETGATE巨集:

/* *
 * 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.
 * */
#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;        \
}

  最終構建出了一個48位的結構體pseudodesc,前16位標識著中斷描述符表的大小(pd_lim),後32位標識著中斷描述符表IDT的基址(pd_base)。

  如果熟悉80386中斷機制的話,就會發現這一結構與IDTR暫存器所需要的結構一致。在idt_init函式的最後,通過lidt函式執行彙編指令lidt,完成了對IDTR暫存器的賦值。至此,ucore的中斷描述符表設定完成。

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

中斷棧幀的生成

  下面接著分析,中斷描述符表裡到底存放了什麼資料結構,在ucore的中斷服務功能的建立中是如何發揮作用的?

  開啟之前用於構造中斷描述符陣列,為vertors賦值的/kern/trap/vector.S,可以看到其中的每一項的中斷服務例程的程式碼都一樣。有的項首先push了一個0,有的沒有(下面會介紹為什麼會有這種差異)。接下來將下標push壓入棧中,便統一jmp跳轉到了__alltraps處了。

vector.S:(很長,幾乎都是指令碼生成的模板程式碼)

ucore作業系統學習筆記(一)  ucore lab1系統啟動流程分析
# handler
.text
.globl __alltraps
.globl vector0
vector0:
  pushl $0
  pushl $0
  jmp __alltraps
.globl vector1
vector1:
  pushl $0
  pushl $1
  jmp __alltraps
.globl vector2
vector2:
  pushl $0
  pushl $2
  jmp __alltraps
.globl vector3
vector3:
  pushl $0
  pushl $3
  jmp __alltraps
.globl vector4
vector4:
  pushl $0
  pushl $4
  jmp __alltraps
.globl vector5
vector5:
  pushl $0
  pushl $5
  jmp __alltraps
.globl vector6
vector6:
  pushl $0
  pushl $6
  jmp __alltraps
.globl vector7
vector7:
  pushl $0
  pushl $7
  jmp __alltraps
.globl vector8
vector8:
  pushl $8
  jmp __alltraps
.globl vector9
vector9:
  pushl $9
  jmp __alltraps
.globl vector10
vector10:
  pushl $10
  jmp __alltraps
.globl vector11
vector11:
  pushl $11
  jmp __alltraps
.globl vector12
vector12:
  pushl $12
  jmp __alltraps
.globl vector13
vector13:
  pushl $13
  jmp __alltraps
.globl vector14
vector14:
  pushl $14
  jmp __alltraps
.globl vector15
vector15:
  pushl $0
  pushl $15
  jmp __alltraps
.globl vector16
vector16:
  pushl $0
  pushl $16
  jmp __alltraps
.globl vector17
vector17:
  pushl $17
  jmp __alltraps
.globl vector18
vector18:
  pushl $0
  pushl $18
  jmp __alltraps
.globl vector19
vector19:
  pushl $0
  pushl $19
  jmp __alltraps
.globl vector20
vector20:
  pushl $0
  pushl $20
  jmp __alltraps
.globl vector21
vector21:
  pushl $0
  pushl $21
  jmp __alltraps
.globl vector22
vector22:
  pushl $0
  pushl $22
  jmp __alltraps
.globl vector23
vector23:
  pushl $0
  pushl $23
  jmp __alltraps
.globl vector24
vector24:
  pushl $0
  pushl $24
  jmp __alltraps
.globl vector25
vector25:
  pushl $0
  pushl $25
  jmp __alltraps
.globl vector26
vector26:
  pushl $0
  pushl $26
  jmp __alltraps
.globl vector27
vector27:
  pushl $0
  pushl $27
  jmp __alltraps
.globl vector28
vector28:
  pushl $0
  pushl $28
  jmp __alltraps
.globl vector29
vector29:
  pushl $0
  pushl $29
  jmp __alltraps
.globl vector30
vector30:
  pushl $0
  pushl $30
  jmp __alltraps
.globl vector31
vector31:
  pushl $0
  pushl $31
  jmp __alltraps
.globl vector32
vector32:
  pushl $0
  pushl $32
  jmp __alltraps
.globl vector33
vector33:
  pushl $0
  pushl $33
  jmp __alltraps
.globl vector34
vector34:
  pushl $0
  pushl $34
  jmp __alltraps
.globl vector35
vector35:
  pushl $0
  pushl $35
  jmp __alltraps
.globl vector36
vector36:
  pushl $0
  pushl $36
  jmp __alltraps
.globl vector37
vector37:
  pushl $0
  pushl $37
  jmp __alltraps
.globl vector38
vector38:
  pushl $0
  pushl $38
  jmp __alltraps
.globl vector39
vector39:
  pushl $0
  pushl $39
  jmp __alltraps
.globl vector40
vector40:
  pushl $0
  pushl $40
  jmp __alltraps
.globl vector41
vector41:
  pushl $0
  pushl $41
  jmp __alltraps
.globl vector42
vector42:
  pushl $0
  pushl $42
  jmp __alltraps
.globl vector43
vector43:
  pushl $0
  pushl $43
  jmp __alltraps
.globl vector44
vector44:
  pushl $0
  pushl $44
  jmp __alltraps
.globl vector45
vector45:
  pushl $0
  pushl $45
  jmp __alltraps
.globl vector46
vector46:
  pushl $0
  pushl $46
  jmp __alltraps
.globl vector47
vector47:
  pushl $0
  pushl $47
  jmp __alltraps
.globl vector48
vector48:
  pushl $0
  pushl $48
  jmp __alltraps
.globl vector49
vector49:
  pushl $0
  pushl $49
  jmp __alltraps
.globl vector50
vector50:
  pushl $0
  pushl $50
  jmp __alltraps
.globl vector51
vector51:
  pushl $0
  pushl $51
  jmp __alltraps
.globl vector52
vector52:
  pushl $0
  pushl $52
  jmp __alltraps
.globl vector53
vector53:
  pushl $0
  pushl $53
  jmp __alltraps
.globl vector54
vector54:
  pushl $0
  pushl $54
  jmp __alltraps
.globl vector55
vector55:
  pushl $0
  pushl $55
  jmp __alltraps
.globl vector56
vector56:
  pushl $0
  pushl $56
  jmp __alltraps
.globl vector57
vector57:
  pushl $0
  pushl $57
  jmp __alltraps
.globl vector58
vector58:
  pushl $0
  pushl $58
  jmp __alltraps
.globl vector59
vector59:
  pushl $0
  pushl $59
  jmp __alltraps
.globl vector60
vector60:
  pushl $0
  pushl $60
  jmp __alltraps
.globl vector61
vector61:
  pushl $0
  pushl $61
  jmp __alltraps
.globl vector62
vector62:
  pushl $0
  pushl $62
  jmp __alltraps
.globl vector63
vector63:
  pushl $0
  pushl $63
  jmp __alltraps
.globl vector64
vector64:
  pushl $0
  pushl $64
  jmp __alltraps
.globl vector65
vector65:
  pushl $0
  pushl $65
  jmp __alltraps
.globl vector66
vector66:
  pushl $0
  pushl $66
  jmp __alltraps
.globl vector67
vector67:
  pushl $0
  pushl $67
  jmp __alltraps
.globl vector68
vector68:
  pushl $0
  pushl $68
  jmp __alltraps
.globl vector69
vector69:
  pushl $0
  pushl $69
  jmp __alltraps
.globl vector70
vector70:
  pushl $0
  pushl $70
  jmp __alltraps
.globl vector71
vector71:
  pushl $0
  pushl $71
  jmp __alltraps
.globl vector72
vector72:
  pushl $0
  pushl $72
  jmp __alltraps
.globl vector73
vector73:
  pushl $0
  pushl $73
  jmp __alltraps
.globl vector74
vector74:
  pushl $0
  pushl $74
  jmp __alltraps
.globl vector75
vector75:
  pushl $0
  pushl $75
  jmp __alltraps
.globl vector76
vector76:
  pushl $0
  pushl $76
  jmp __alltraps
.globl vector77
vector77:
  pushl $0
  pushl $77
  jmp __alltraps
.globl vector78
vector78:
  pushl $0
  pushl $78
  jmp __alltraps
.globl vector79
vector79:
  pushl $0
  pushl $79
  jmp __alltraps
.globl vector80
vector80:
  pushl $0
  pushl $80
  jmp __alltraps
.globl vector81
vector81:
  pushl $0
  pushl $81
  jmp __alltraps
.globl vector82
vector82:
  pushl $0
  pushl $82
  jmp __alltraps
.globl vector83
vector83:
  pushl $0
  pushl $83
  jmp __alltraps
.globl vector84
vector84:
  pushl $0
  pushl $84
  jmp __alltraps
.globl vector85
vector85:
  pushl $0
  pushl $85
  jmp __alltraps
.globl vector86
vector86:
  pushl $0
  pushl $86
  jmp __alltraps
.globl vector87
vector87:
  pushl $0
  pushl $87
  jmp __alltraps
.globl vector88
vector88:
  pushl $0
  pushl $88
  jmp __alltraps
.globl vector89
vector89:
  pushl $0
  pushl $89
  jmp __alltraps
.globl vector90
vector90:
  pushl $0
  pushl $90
  jmp __alltraps
.globl vector91
vector91:
  pushl $0
  pushl $91
  jmp __alltraps
.globl vector92
vector92:
  pushl $0
  pushl $92
  jmp __alltraps
.globl vector93
vector93:
  pushl $0
  pushl $93
  jmp __alltraps
.globl vector94
vector94:
  pushl $0
  pushl $94
  jmp __alltraps
.globl vector95
vector95:
  pushl $0
  pushl $95
  jmp __alltraps
.globl vector96
vector96:
  pushl $0
  pushl $96
  jmp __alltraps
.globl vector97
vector97:
  pushl $0
  pushl $97
  jmp __alltraps
.globl vector98
vector98:
  pushl $0
  pushl $98
  jmp __alltraps
.globl vector99
vector99:
  pushl $0
  pushl $99
  jmp __alltraps
.globl vector100
vector100:
  pushl $0
  pushl $100
  jmp __alltraps
.globl vector101
vector101:
  pushl $0
  pushl $101
  jmp __alltraps
.globl vector102
vector102:
  pushl $0
  pushl $102
  jmp __alltraps
.globl vector103
vector103:
  pushl $0
  pushl $103
  jmp __alltraps
.globl vector104
vector104:
  pushl $0
  pushl $104
  jmp __alltraps
.globl vector105
vector105:
  pushl $0
  pushl $105
  jmp __alltraps
.globl vector106
vector106:
  pushl $0
  pushl $106
  jmp __alltraps
.globl vector107
vector107:
  pushl $0
  pushl $107
  jmp __alltraps
.globl vector108
vector108:
  pushl $0
  pushl $108
  jmp __alltraps
.globl vector109
vector109:
  pushl $0
  pushl $109
  jmp __alltraps
.globl vector110
vector110:
  pushl $0
  pushl $110
  jmp __alltraps
.globl vector111
vector111:
  pushl $0
  pushl $111
  jmp __alltraps
.globl vector112
vector112:
  pushl $0
  pushl $112
  jmp __alltraps
.globl vector113
vector113:
  pushl $0
  pushl $113
  jmp __alltraps
.globl vector114
vector114:
  pushl $0
  pushl $114
  jmp __alltraps
.globl vector115
vector115:
  pushl $0
  pushl $115
  jmp __alltraps
.globl vector116
vector116:
  pushl $0
  pushl $116
  jmp __alltraps
.globl vector117
vector117:
  pushl $0
  pushl $117
  jmp __alltraps
.globl vector118
vector118:
  pushl $0
  pushl $118
  jmp __alltraps
.globl vector119
vector119:
  pushl $0
  pushl $119
  jmp __alltraps
.globl vector120
vector120:
  pushl $0
  pushl $120
  jmp __alltraps
.globl vector121
vector121:
  pushl $0
  pushl $121
  jmp __alltraps
.globl vector122
vector122:
  pushl $0
  pushl $122
  jmp __alltraps
.globl vector123
vector123:
  pushl $0
  pushl $123
  jmp __alltraps
.globl vector124
vector124:
  pushl $0
  pushl $124
  jmp __alltraps
.globl vector125
vector125:
  pushl $0
  pushl $125
  jmp __alltraps
.globl vector126
vector126:
  pushl $0
  pushl $126
  jmp __alltraps
.globl vector127
vector127:
  pushl $0
  pushl $127
  jmp __alltraps
.globl vector128
vector128:
  pushl $0
  pushl $128
  jmp __alltraps
.globl vector129
vector129:
  pushl $0
  pushl $129
  jmp __alltraps
.globl vector130
vector130:
  pushl $0
  pushl $130
  jmp __alltraps
.globl vector131
vector131:
  pushl $0
  pushl $131
  jmp __alltraps
.globl vector132
vector132:
  pushl $0
  pushl $132
  jmp __alltraps
.globl vector133
vector133:
  pushl $0
  pushl $133
  jmp __alltraps
.globl vector134
vector134:
  pushl $0
  pushl $134
  jmp __alltraps
.globl vector135
vector135:
  pushl $0
  pushl $135
  jmp __alltraps
.globl vector136
vector136:
  pushl $0
  pushl $136
  jmp __alltraps
.globl vector137
vector137:
  pushl $0
  pushl $137
  jmp __alltraps
.globl vector138
vector138:
  pushl $0
  pushl $138
  jmp __alltraps
.globl vector139
vector139:
  pushl $0
  pushl $139
  jmp __alltraps
.globl vector140
vector140:
  pushl $0
  pushl $140
  jmp __alltraps
.globl vector141
vector141:
  pushl $0
  pushl $141
  jmp __alltraps
.globl vector142
vector142:
  pushl $0
  pushl $142
  jmp __alltraps
.globl vector143
vector143:
  pushl $0
  pushl $143
  jmp __alltraps
.globl vector144
vector144:
  pushl $0
  pushl $144
  jmp __alltraps
.globl vector145
vector145:
  pushl $0
  pushl $145
  jmp __alltraps
.globl vector146
vector146:
  pushl $0
  pushl $146
  jmp __alltraps
.globl vector147
vector147:
  pushl $0
  pushl $147
  jmp __alltraps
.globl vector148
vector148:
  pushl $0
  pushl $148
  jmp __alltraps
.globl vector149
vector149:
  pushl $0
  pushl $149
  jmp __alltraps
.globl vector150
vector150:
  pushl $0
  pushl $150
  jmp __alltraps
.globl vector151
vector151:
  pushl $0
  pushl $151
  jmp __alltraps
.globl vector152
vector152:
  pushl $0
  pushl $152
  jmp __alltraps
.globl vector153
vector153:
  pushl $0
  pushl $153
  jmp __alltraps
.globl vector154
vector154:
  pushl $0
  pushl $154
  jmp __alltraps
.globl vector155
vector155:
  pushl $0
  pushl $155
  jmp __alltraps
.globl vector156
vector156:
  pushl $0
  pushl $156
  jmp __alltraps
.globl vector157
vector157:
  pushl $0
  pushl $157
  jmp __alltraps
.globl vector158
vector158:
  pushl $0
  pushl $158
  jmp __alltraps
.globl vector159
vector159:
  pushl $0
  pushl $159
  jmp __alltraps
.globl vector160
vector160:
  pushl $0
  pushl $160
  jmp __alltraps
.globl vector161
vector161:
  pushl $0
  pushl $161
  jmp __alltraps
.globl vector162
vector162:
  pushl $0
  pushl $162
  jmp __alltraps
.globl vector163
vector163:
  pushl $0
  pushl $163
  jmp __alltraps
.globl vector164
vector164:
  pushl $0
  pushl $164
  jmp __alltraps
.globl vector165
vector165:
  pushl $0
  pushl $165
  jmp __alltraps
.globl vector166
vector166:
  pushl $0
  pushl $166
  jmp __alltraps
.globl vector167
vector167:
  pushl $0
  pushl $167
  jmp __alltraps
.globl vector168
vector168:
  pushl $0
  pushl $168
  jmp __alltraps
.globl vector169
vector169:
  pushl $0
  pushl $169
  jmp __alltraps
.globl vector170
vector170:
  pushl $0
  pushl $170
  jmp __alltraps
.globl vector171
vector171:
  pushl $0
  pushl $171
  jmp __alltraps
.globl vector172
vector172:
  pushl $0
  pushl $172
  jmp __alltraps
.globl vector173
vector173:
  pushl $0
  pushl $173
  jmp __alltraps
.globl vector174
vector174:
  pushl $0
  pushl $174
  jmp __alltraps
.globl vector175
vector175:
  pushl $0
  pushl $175
  jmp __alltraps
.globl vector176
vector176:
  pushl $0
  pushl $176
  jmp __alltraps
.globl vector177
vector177:
  pushl $0
  pushl $177
  jmp __alltraps
.globl vector178
vector178:
  pushl $0
  pushl $178
  jmp __alltraps
.globl vector179
vector179:
  pushl $0
  pushl $179
  jmp __alltraps
.globl vector180
vector180:
  pushl $0
  pushl $180
  jmp __alltraps
.globl vector181
vector181:
  pushl $0
  pushl $181
  jmp __alltraps
.globl vector182
vector182:
  pushl $0
  pushl $182
  jmp __alltraps
.globl vector183
vector183:
  pushl $0
  pushl $183
  jmp __alltraps
.globl vector184
vector184:
  pushl $0
  pushl $184
  jmp __alltraps
.globl vector185
vector185:
  pushl $0
  pushl $185
  jmp __alltraps
.globl vector186
vector186:
  pushl $0
  pushl $186
  jmp __alltraps
.globl vector187
vector187:
  pushl $0
  pushl $187
  jmp __alltraps
.globl vector188
vector188:
  pushl $0
  pushl $188
  jmp __alltraps
.globl vector189
vector189:
  pushl $0
  pushl $189
  jmp __alltraps
.globl vector190
vector190:
  pushl $0
  pushl $190
  jmp __alltraps
.globl vector191
vector191:
  pushl $0
  pushl $191
  jmp __alltraps
.globl vector192
vector192:
  pushl $0
  pushl $192
  jmp __alltraps
.globl vector193
vector193:
  pushl $0
  pushl $193
  jmp __alltraps
.globl vector194
vector194:
  pushl $0
  pushl $194
  jmp __alltraps
.globl vector195
vector195:
  pushl $0
  pushl $195
  jmp __alltraps
.globl vector196
vector196:
  pushl $0
  pushl $196
  jmp __alltraps
.globl vector197
vector197:
  pushl $0
  pushl $197
  jmp __alltraps
.globl vector198
vector198:
  pushl $0
  pushl $198
  jmp __alltraps
.globl vector199
vector199:
  pushl $0
  pushl $199
  jmp __alltraps
.globl vector200
vector200:
  pushl $0
  pushl $200
  jmp __alltraps
.globl vector201
vector201:
  pushl $0
  pushl $201
  jmp __alltraps
.globl vector202
vector202:
  pushl $0
  pushl $202
  jmp __alltraps
.globl vector203
vector203:
  pushl $0
  pushl $203
  jmp __alltraps
.globl vector204
vector204:
  pushl $0
  pushl $204
  jmp __alltraps
.globl vector205
vector205:
  pushl $0
  pushl $205
  jmp __alltraps
.globl vector206
vector206:
  pushl $0
  pushl $206
  jmp __alltraps
.globl vector207
vector207:
  pushl $0
  pushl $207
  jmp __alltraps
.globl vector208
vector208:
  pushl $0
  pushl $208
  jmp __alltraps
.globl vector209
vector209:
  pushl $0
  pushl $209
  jmp __alltraps
.globl vector210
vector210:
  pushl $0
  pushl $210
  jmp __alltraps
.globl vector211
vector211:
  pushl $0
  pushl $211
  jmp __alltraps
.globl vector212
vector212:
  pushl $0
  pushl $212
  jmp __alltraps
.globl vector213
vector213:
  pushl $0
  pushl $213
  jmp __alltraps
.globl vector214
vector214:
  pushl $0
  pushl $214
  jmp __alltraps
.globl vector215
vector215:
  pushl $0
  pushl $215
  jmp __alltraps
.globl vector216
vector216:
  pushl $0
  pushl $216
  jmp __alltraps
.globl vector217
vector217:
  pushl $0
  pushl $217
  jmp __alltraps
.globl vector218
vector218:
  pushl $0
  pushl $218
  jmp __alltraps
.globl vector219
vector219:
  pushl $0
  pushl $219
  jmp __alltraps
.globl vector220
vector220:
  pushl $0
  pushl $220
  jmp __alltraps
.globl vector221
vector221:
  pushl $0
  pushl $221
  jmp __alltraps
.globl vector222
vector222:
  pushl $0
  pushl $222
  jmp __alltraps
.globl vector223
vector223:
  pushl $0
  pushl $223
  jmp __alltraps
.globl vector224
vector224:
  pushl $0
  pushl $224
  jmp __alltraps
.globl vector225
vector225:
  pushl $0
  pushl $225
  jmp __alltraps
.globl vector226
vector226:
  pushl $0
  pushl $226
  jmp __alltraps
.globl vector227
vector227:
  pushl $0
  pushl $227
  jmp __alltraps
.globl vector228
vector228:
  pushl $0
  pushl $228
  jmp __alltraps
.globl vector229
vector229:
  pushl $0
  pushl $229
  jmp __alltraps
.globl vector230
vector230:
  pushl $0
  pushl $230
  jmp __alltraps
.globl vector231
vector231:
  pushl $0
  pushl $231
  jmp __alltraps
.globl vector232
vector232:
  pushl $0
  pushl $232
  jmp __alltraps
.globl vector233
vector233:
  pushl $0
  pushl $233
  jmp __alltraps
.globl vector234
vector234:
  pushl $0
  pushl $234
  jmp __alltraps
.globl vector235
vector235:
  pushl $0
  pushl $235
  jmp __alltraps
.globl vector236
vector236:
  pushl $0
  pushl $236
  jmp __alltraps
.globl vector237
vector237:
  pushl $0
  pushl $237
  jmp __alltraps
.globl vector238
vector238:
  pushl $0
  pushl $238
  jmp __alltraps
.globl vector239
vector239:
  pushl $0
  pushl $239
  jmp __alltraps
.globl vector240
vector240:
  pushl $0
  pushl $240
  jmp __alltraps
.globl vector241
vector241:
  pushl $0
  pushl $241
  jmp __alltraps
.globl vector242
vector242:
  pushl $0
  pushl $242
  jmp __alltraps
.globl vector243
vector243:
  pushl $0
  pushl $243
  jmp __alltraps
.globl vector244
vector244:
  pushl $0
  pushl $244
  jmp __alltraps
.globl vector245
vector245:
  pushl $0
  pushl $245
  jmp __alltraps
.globl vector246
vector246:
  pushl $0
  pushl $246
  jmp __alltraps
.globl vector247
vector247:
  pushl $0
  pushl $247
  jmp __alltraps
.globl vector248
vector248:
  pushl $0
  pushl $248
  jmp __alltraps
.globl vector249
vector249:
  pushl $0
  pushl $249
  jmp __alltraps
.globl vector250
vector250:
  pushl $0
  pushl $250
  jmp __alltraps
.globl vector251
vector251:
  pushl $0
  pushl $251
  jmp __alltraps
.globl vector252
vector252:
  pushl $0
  pushl $252
  jmp __alltraps
.globl vector253
vector253:
  pushl $0
  pushl $253
  jmp __alltraps
.globl vector254
vector254:
  pushl $0
  pushl $254
  jmp __alltraps
.globl vector255
vector255:
  pushl $0
  pushl $255
  jmp __alltraps

# vector table
.data
.globl __vectors
__vectors:
  .long vector0
  .long vector1
  .long vector2
  .long vector3
  .long vector4
  .long vector5
  .long vector6
  .long vector7
  .long vector8
  .long vector9
  .long vector10
  .long vector11
  .long vector12
  .long vector13
  .long vector14
  .long vector15
  .long vector16
  .long vector17
  .long vector18
  .long vector19
  .long vector20
  .long vector21
  .long vector22
  .long vector23
  .long vector24
  .long vector25
  .long vector26
  .long vector27
  .long vector28
  .long vector29
  .long vector30
  .long vector31
  .long vector32
  .long vector33
  .long vector34
  .long vector35
  .long vector36
  .long vector37
  .long vector38
  .long vector39
  .long vector40
  .long vector41
  .long vector42
  .long vector43
  .long vector44
  .long vector45
  .long vector46
  .long vector47
  .long vector48
  .long vector49
  .long vector50
  .long vector51
  .long vector52
  .long vector53
  .long vector54
  .long vector55
  .long vector56
  .long vector57
  .long vector58
  .long vector59
  .long vector60
  .long vector61
  .long vector62
  .long vector63
  .long vector64
  .long vector65
  .long vector66
  .long vector67
  .long vector68
  .long vector69
  .long vector70
  .long vector71
  .long vector72
  .long vector73
  .long vector74
  .long vector75
  .long vector76
  .long vector77
  .long vector78
  .long vector79
  .long vector80
  .long vector81
  .long vector82
  .long vector83
  .long vector84
  .long vector85
  .long vector86
  .long vector87
  .long vector88
  .long vector89
  .long vector90
  .long vector91
  .long vector92
  .long vector93
  .long vector94
  .long vector95
  .long vector96
  .long vector97
  .long vector98
  .long vector99
  .long vector100
  .long vector101
  .long vector102
  .long vector103
  .long vector104
  .long vector105
  .long vector106
  .long vector107
  .long vector108
  .long vector109
  .long vector110
  .long vector111
  .long vector112
  .long vector113
  .long vector114
  .long vector115
  .long vector116
  .long vector117
  .long vector118
  .long vector119
  .long vector120
  .long vector121
  .long vector122
  .long vector123
  .long vector124
  .long vector125
  .long vector126
  .long vector127
  .long vector128
  .long vector129
  .long vector130
  .long vector131
  .long vector132
  .long vector133
  .long vector134
  .long vector135
  .long vector136
  .long vector137
  .long vector138
  .long vector139
  .long vector140
  .long vector141
  .long vector142
  .long vector143
  .long vector144
  .long vector145
  .long vector146
  .long vector147
  .long vector148
  .long vector149
  .long vector150
  .long vector151
  .long vector152
  .long vector153
  .long vector154
  .long vector155
  .long vector156
  .long vector157
  .long vector158
  .long vector159
  .long vector160
  .long vector161
  .long vector162
  .long vector163
  .long vector164
  .long vector165
  .long vector166
  .long vector167
  .long vector168
  .long vector169
  .long vector170
  .long vector171
  .long vector172
  .long vector173
  .long vector174
  .long vector175
  .long vector176
  .long vector177
  .long vector178
  .long vector179
  .long vector180
  .long vector181
  .long vector182
  .long vector183
  .long vector184
  .long vector185
  .long vector186
  .long vector187
  .long vector188
  .long vector189
  .long vector190
  .long vector191
  .long vector192
  .long vector193
  .long vector194
  .long vector195
  .long vector196
  .long vector197
  .long vector198
  .long vector199
  .long vector200
  .long vector201
  .long vector202
  .long vector203
  .long vector204
  .long vector205
  .long vector206
  .long vector207
  .long vector208
  .long vector209
  .long vector210
  .long vector211
  .long vector212
  .long vector213
  .long vector214
  .long vector215
  .long vector216
  .long vector217
  .long vector218
  .long vector219
  .long vector220
  .long vector221
  .long vector222
  .long vector223
  .long vector224
  .long vector225
  .long vector226
  .long vector227
  .long vector228
  .long vector229
  .long vector230
  .long vector231
  .long vector232
  .long vector233
  .long vector234
  .long vector235
  .long vector236
  .long vector237
  .long vector238
  .long vector239
  .long vector240
  .long vector241
  .long vector242
  .long vector243
  .long vector244
  .long vector245
  .long vector246
  .long vector247
  .long vector248
  .long vector249
  .long vector250
  .long vector251
  .long vector252
  .long vector253
  .long vector254
  .long vector255
View Code

   __alltraps是定義在同一目錄下即/kern/trap/trapentry.S中的。

  在__alltraps中,按照順序將當前的各個常用的暫存器的值壓入了棧中,隨後將ds、es等資料段暫存器載入了核心的資料段選擇子(這是因為中斷可能來自使用者態,而中斷服務例程必須在核心態執行以擁有所有資源的訪問許可權,避免核心中的中斷服務例程由於特權級不夠,訪問資料時出現問題)。

  隨後棧中壓入esp的值,便通過call trap跳轉到了核心的中斷服務分發函式trap中。trap函式位於/kern/trap/trap.c中。

trapentry.S:

#include <memlayout.h>

# vectors.S sends all traps here.
.text
.globl __alltraps
__alltraps:
    # push registers to build a trap frame
    # therefore make the stack look like a struct trapframe
    pushl %ds
    pushl %es
    pushl %fs
    pushl %gs
    pushal

    # load GD_KDATA into %ds and %es to set up data segments for kernel
    movl $GD_KDATA, %eax
    movw %ax, %ds
    movw %ax, %es

    # push %esp to pass a pointer to the trapframe as an argument to trap()
    pushl %esp

    # call trap(tf), where tf=%esp
    call trap

    # pop the pushed stack pointer
    popl %esp

    # return falls through to trapret...
.globl __trapret
__trapret:
    # restore registers from stack
    popal

    # restore %ds, %es, %fs and %gs
    popl %gs
    popl %fs
    popl %es
    popl %ds

    # get rid of the trap number and error code
    addl $0x8, %esp
    iret

trap函式入口:

/* *
 * trap - handles or dispatches an exception/interrupt. if and when trap() returns,
 * the code in kern/trap/trapentry.S restores the old CPU state saved in the
 * trapframe and then uses the iret instruction to return from the exception.
 * */
void
trap(struct trapframe *tf) {
    // dispatch based on what type of trap occurred
    trap_dispatch(tf);
}

  trap函式的引數是trapframe結構體。仔細觀察可以看到,trapframe中欄位屬性的定義和trapentry.S中入棧的順序是相反的。

  1. 第一個欄位pushregs儲存了pushal壓入棧中的資料,後續的tf_gs + tf_padding0兩個欄位是因為pushl會壓入一個32位的資料,而gs資料段暫存器本身儲存的段選擇子是16位的,需要一個16位的padding空閒欄位合併起來與之對應。後面的fs、es、ds原理一樣。

  2. 之後的tf_trapno屬性對應著跳轉至__alltraps之前所壓入棧中的中斷向量號,tf_err對應的是在上面的pushl $0,即錯誤號。根據註釋可以看到,包括tf_err在內的中斷錯誤號都是x86CPU硬體在中斷髮生時自動壓入棧中的。但並不是所有的硬體中斷都會被自動壓入錯誤號(需要去閱讀硬體手冊才能知道具體細節),為了能夠以一個統一的介面去處理所有的中斷請求,在vertor.S中對於沒有錯誤號的中斷請求預設加上了pushl $0,壓入一個預設的錯誤號0;對於CPU硬體會壓入錯誤號的中斷向量則沒有進行預設處理,例如vertor8、vertor9等等。

  3. 發生中斷時,x86CPU會預設按照順序依次壓入eflags、cs和eip用於中斷後的現場恢復。對應的是tf_eip、tf_cs + tf_padding4以及tf_eflags。而當發生了CPL特權級的變化時,x86CPU硬體會發生不同特權級棧的切換,因此還會先依次壓入切換特權級前的ss棧段暫存器和esp棧頂指標的值入棧,便於中斷返回後回到對應的特權級中。一個很典型的例子就是,當使用者程式執行系統呼叫時(系統呼叫是通過中斷機制實現的,在ucore的lab5中實現了這一功能),會從使用者的CPL特權級ring3切換到核心的特權級ring0,系統呼叫的服務例程是在分配好的核心棧中執行的。

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

為什麼tramframe中斷棧幀的結構屬性的定義順序會和入棧時的順序相反?

  要理解這個需要對C語言編譯後的底層機器程式碼模型有一定了解。

  1.C語言結構體定義的記憶體排布是按照欄位定義順序,從記憶體的低位到高位延伸的。

  2.棧在壓入資料時,棧頂指標是遞減的,由高位往低位延伸的。

  3.trap函式的引數trapframe指標指向的是當前棧頂,相對處於低位,而對所包含的欄位則是在這個指標的基礎上向高位偏移對應的N個位元組來訪問的。要想在C中通過一個結構體對映出棧上的記憶體資料便於後續的訪問,那麼必須以和入棧順序相反的順序來定義結構體。棧幀結構體trapframe定義最後的__attribute__((packed))指的是強制令C編譯器使用緊湊模式處理該結構體,避免編譯器在欄位的處理上進行額外的記憶體對齊操作,導致訪問時最後生成的記憶體地址訪問偏移量計算錯誤。

  這裡需要注意的一點是,當處理沒有發生特權級切換的中斷時,trapframe對應的最後三個欄位是不存在於棧上的,此時如果通過tf_ss等屬性訪問時,會越界訪問到原本在棧上不相關的資料。所以在訪問這幾個欄位時,必須先判斷是否發生了特權級的變化,避免損壞棧上資料,令程式出錯甚至崩潰。

中斷服務例程進行處理

  現在分析當中斷髮生時,trap函式接收到trapframe中斷幀引數後是如何進行中斷服務處理的。

  在lab1中,trap函式只是簡單的呼叫了同一檔案中的trap_dispatch函式,在trap_dispatch中通過對tf->tf_trapno,即中斷向量號進行判斷,將控制流轉移至中斷向量號對應的中斷服務例程中。(這裡trap函式只是簡單的呼叫trap_dispatch,是因為後續的lab中,會在trap函式中在中斷處理服務開始前後加入許多邏輯,預先將對中斷請求的分發邏輯抽取了出來)

  比如第一個case塊便是用於處理時鐘中斷的。在lab1中,通過一個被volatile關鍵字修飾的ticks全域性變數,在每次時鐘中斷時累加1,當次數每達到TICK_NUM時(預設100,對應的是10ms一次的時鐘中斷),便列印一段話。體現在lab1實驗中便是,ucore核心啟動完成後,控制檯每秒鐘週期性的列印出"100ticks"。

  可以看到,雖然80386CPU的硬體設計者希望作業系統的設計者直接在中斷描述符中設定對應的中斷服務例程的入口地址,但ucore卻沒有充分利用這一特性,而是選擇了在中斷服務例程的入口處簡單壓入幾個資料後將中斷服務的控制流程統一的指向了__alltraps,最後通過trap_dispatch函式進行分發。這樣的設計雖然在效能上可能有微小的損失,但是卻使得ucore的中斷服務實現更加靈活、可控。(作為一個java後端程式設計師,這一設計令我想到了springmvc框架基於下層servlet的封裝機制:通過一個/*的servlet獲得所有請求的控制權,再由框架靈活封裝各種引數,最後將引數和控制權交給對應的controller方法進行處理)

trap_dispatch函式:

/* trap_dispatch - dispatch based on what type of trap occurred */
static void
trap_dispatch(struct trapframe *tf) {
    char c;

    switch (tf->tf_trapno) {
    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 % TICK_NUM == 0) {
            print_ticks();
        }
        break;
    case IRQ_OFFSET + IRQ_COM1:
        c = cons_getc();
        cprintf("serial [%03d] %c\n", c, c);
        break;
    case IRQ_OFFSET + IRQ_KBD:
        c = cons_getc();
        cprintf("kbd [%03d] %c\n", c, c);
        break;
    //LAB1 CHALLENGE 1 : YOUR CODE you should modify below codes.
    case T_SWITCH_TOU:
        if (tf->tf_cs != USER_CS) {
            switchk2u = *tf;
            switchk2u.tf_cs = USER_CS;
            switchk2u.tf_ds = switchk2u.tf_es = switchk2u.tf_ss = USER_DS;
            switchk2u.tf_esp = (uint32_t)tf + sizeof(struct trapframe) - 8;
        
            // set eflags, make sure ucore can use io under user mode.
            // if CPL > IOPL, then cpu will generate a general protection.
            switchk2u.tf_eflags |= FL_IOPL_MASK;
        
            // set temporary stack
            // then iret will jump to the right stack
            *((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;
            tf->tf_eflags &= ~FL_IOPL_MASK;
            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;
    case IRQ_OFFSET + IRQ_IDE1:
    case IRQ_OFFSET + IRQ_IDE2:
        /* do nothing */
        break;
    default:
        // in kernel, it must be a mistake
        if ((tf->tf_cs & 3) == 0) {
            print_trapframe(tf);
            panic("unexpected trap in kernel.\n");
        }
    }
}  

中斷返回

  中斷服務例程用於處理突發性的中斷事件,一般來說都是短小精悍的服務程式碼,會很快的執行完畢並返回。下面接著分析ucore在中斷返回時的處理機制。

  在trap函式返回後,程式碼的控制流回到了trapentry.S中,即CPU指令指向call trap的下一條指令。為了保證中斷服務返回後之前被中斷程式上下文的正確性,需要將執行call trap之前的壓入的資料一一彈出還原。

  1. 按照相反的順序彈出、還原各個常用暫存器的值(popl esp、popal、popl gs/fs/es/ds)。

  2. 通過addl $0x8, %esp,以直接上移棧頂指標的方式,略過之前壓入的中斷號tf_trapno和錯誤碼tf_err。

  3. 執行iret指令,iret指令會將之前硬體自動壓入的eip、cs、eflags按照順序彈出。當CPU發現彈出時的cs值和當前cs值不一致,則認定此次中斷髮生了特權級的變化。此時CPU會接著彈出之前壓入了的esp、ss暫存器的值,令其返回到中斷髮生前對應的特權級棧中繼續執行。

  CPU認為只有之前發生特權級變化時才會額外壓入ss、esp,所以中斷返回時如果發現彈出的cs與當前cs不一致時,除了恢復之前棧上的cs(也恢復了CPL),同時會額外的彈出esp、ss。

  這特權級一機制在lab1的挑戰練習lab1 challenge1中被利用了起來,挑戰練習1需要模擬出核心態轉化至使用者態,再從使用者態再轉換回核心態的過程。

lab1挑戰練習1實現原理分析

  在kern_init總控函式中,最後通過lab1_switch_test來實現這一過程。

  lab1_switch_to_user函式中,通過內聯彙編執行了int命令,觸發了一個軟中斷,中斷號為T_SWITCH_TOU。控制流最終會指向trap_dispatch函式中對應的case塊中,在其中通過修改當前中斷棧幀中的cs程式碼段暫存器、ds、es、ss等資料段暫存器的值,使得中斷棧幀上的CS的段選擇子的值為使用者態。這樣在中斷返回時,便"欺騙"了CPU,使得CPU在中斷返回後將當前的特權級由核心態切換到了使用者態。在後續的實驗中,例如通過系統呼叫載入並執行一個使用者態應用程式,就是通過這一"欺騙"機制巧妙地實現特權級的切換。

  lab1_switch_to_kernel函式中,同樣通過內聯彙編執行int命令觸發軟中斷,中斷號為T_SWITCH_TOK。控制流最終指向trap_dispatch函式中對應的case塊中,通過設定cs程式碼段暫存器的值為核心程式碼段、ds、es設定為核心資料段來實現中斷返回後,令CPU再從使用者態回到核心態。

static void
lab1_switch_test(void) {
    lab1_print_cur_status();
    cprintf("+++ switch to  user  mode +++\n");
    lab1_switch_to_user();
    lab1_print_cur_status();
    cprintf("+++ switch to kernel mode +++\n");
    lab1_switch_to_kernel();
    lab1_print_cur_status();
}

static void
lab1_switch_to_user(void) {
    //LAB1 CHALLENGE 1 : TODO
    asm volatile (
        "sub $0x8, %%esp \n"
        "int %0 \n"
        "movl %%ebp, %%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)
    );
}

總結

  從年初接觸ucore到現在完成學習,通過部落格總結心得已經過去了大半年。學習ucore就像攀爬一座高山一樣,最初的我由於彙編、C等基礎的前置知識掌握的不牢靠,導致每每想研究ucore原始碼時都因為看不懂程式碼而宣告失敗,因此我下定決心將彙編和C重新學習了一遍。雖然最初的動機是為了更好的學習ucore,但在學習過程中我卻收穫頗豐,領略了登山途中的好風景。一方面使我對計算機底層的執行機制建立起了一個大致的知識框架,理解了CPU的執行機制、中斷等硬體的工作原理(主要還是單核CPU的工作原理)。另一方面,隨著對彙編、C語言的進一步學習,也慢慢的理解了《黑客與畫家》中對於程式語言抽象能力的看法,為什麼計算機硬體不斷髮展,抽象程度更高但效能較低的程式語言會變得越來越流行。

  正如C語言最初作為一種"高階組合語言"而出現,其提供的陣列、結構體、指標等機制簡化了彙編中令人頭疼的訪問資料時的地址偏移問題。同時C還提供了標準庫,在絕大多數場景下能夠遮蔽掉不同硬體、作業系統平臺的差異,使其做到一次編寫,到處編譯。而C++作為C的高階版本,提供了物件導向的程式設計機制,由編譯器提供多型等諸多語法糖,由編譯器來自動完成之前需要C程式設計師通過函式指標集合等方式手動實現的物件導向邏輯。java作為C++的後繼者,認為C++為了相容C依然保留了太多應用程式開發時不需要的底層功能,便將包括指標、goto在內的許多機制都隱藏起來了,不讓程式設計師直接接觸,通過jvm在絕大多數場景下遮蔽了不同作業系統平臺的差異。而Lisp語言的抽象程度則更高,正同《程式設計師的吶喊》中所說的:“Lisp假裝作業系統不存在”。如果不考慮在當前馮.諾依曼架構機器上的執行效率,LISP倡導的就是肆無忌憚的進行函式遞迴而不必擔心棧溢位,為了使函式呼叫無副作用可以任意的copy資料,而不必擔心記憶體不足和垃圾回收的負擔,最重要的是程式的可讀性、可維護性,怎麼方便人思考怎麼來,不太關心空間、時間效能。隨著機器效能的不斷提升,未來的程式語言實現中也許真的可以用kv Map完全的替代陣列,甚至用丘奇數來替代整數以追求數學上極致簡約的美?Orz

  任意程式語言的內容主要分為兩部分,一是基礎語法,另一部分則是在基於的特定平臺上功能的封裝。例如javascript由ECMA語法和對其工作平臺瀏覽器相關功能的封裝組成,而java、nodejs等通用程式語言則是由語法和對作業系統功能的封裝。作為一個以java作為日常開發語言的我來說,學習ucore讓我對java中諸如併發同步、BIO/NIO等機制有了進一步的理解,解開了不少對於jvm底層與作業系統互動機制的困惑。總而言之,學習作業系統還是能學到很多知識的,而ucore os網上公開課就是一個很好的學習方式。

  這是我ucore學習系列部落格的第一篇部落格,未來會不斷的更新後續實驗的學習心得,部落格中會嘗試著儘量將初學者可能碰到的各種疑惑一一解答。希望能幫助到對作業系統、ucore os感興趣的人。

  這篇部落格的完整程式碼在我的github上:https://github.com/1399852153/ucore_os_lab (fork自官方倉庫)中的lab1_answer,存在許多不足之處,還請多多指教。

相關文章