ucore操作系統(tǒng)實驗筆記 - Lab1

最近一直都在跟清華大學的操作系統(tǒng)課程杈抢,這個課程最大的特點是有一系列可以實戰(zhàn)的操作系統(tǒng)實驗京腥。這些實驗總共有8個蜜另,我在這里記錄實驗中的一些心得和總結碉纳。

Task1

這個Task主要是為了熟悉Makfile以及如何生成操作系統(tǒng)的鏡像文件。Makefile會用就行了馏艾,并不用太深入的理解劳曹。

Task2

這個Task主要是為了熟悉GDB以及熟悉操作系統(tǒng)的啟動過程,下面是調試BIOS的一些過程琅摩。

首先修改gdbinit為:

set architecture i8086
target remote :1234
define hook-stop
x/i $pc
end

然后輸入

make debug

通過輸入

x/i $cs
x/i $eip

我們可以獲取當前 $cs$eip 的值铁孵。其中

$cs = 0xf000
$eip = 0xfff0

在實模式下,這個地址就是

$cs << 4 | $eip = 0xffff0

我們也可以看看這個地址的指令是什么

x/2i 0xffff0

得到的結果是

0xffff0:     ljmp   $0xf000,$0xe05b

也就是說房资,BIOS開始的地址應該是

$cs << 4 | 0xe05b = 0xfe05b

此時, 我們設置一個斷點到0x7c00:

b *0x7c00 /* 注意蜕劝,對于絕對地址來說,需要添加*將其作為地址 */

然后當程序運行起來后, 最后會停止在 0x7c00 這個地址轰异。這里存放的便是bootloader了岖沛。

Task3

這個Taks是這5個Taks中最重要的一個。通過這個Task我們可以了解:如何開啟A20搭独;CPU是如何從實模式轉換到保護模式婴削;如何初始化和使用GDT表。

如何開啟/關閉 A20

實模式下內存的訪問

在開啟A20前牙肝,我們先來說說i8086時CPU是如何訪問內存空間的唉俗。

在i8086時代嗤朴,CPU的數據總線是16bit,地址總線是20bit虫溜,寄存器是16bit雹姊,因此CPU只能訪問1MB以內的空間。因為數據總線和寄存器只有16bit衡楞,如果需要獲取20bit的數據, 我們需要做一些額外的操作吱雏,比如移位。實際上寺酪,CPU是通過對segment(每個segment大小恒定為64K) 進行移位后和offset一起組成了一個20bit的地址坎背,這個地址就是實模式下訪問內存的地址:

address = segment << 4 | offset

理論上,20bit的地址可以訪問1MB的內存空間(0x00000 - (2^20 - 1 = 0xFFFFF))寄雀。但在實模式下, 這20bit的地址理論上能訪問從0x00000 - (0xFFFF0 + 0xFFFF = 0x10FFEF)的內存空間得滤。也就是說,理論上我們可以訪問超過1MB的內存空間盒犹,但越過0xFFFFF后懂更,地址又會回到0x00000。

上面這個特征在i8086中是沒有任何問題的(因為它最多只能訪問1MB的內存空間)急膀,但到了i80286/i80386后沮协,CPU有了更寬的地址總線,數據總線和寄存器后卓嫂,這就會出現一個問題: 在實模式下, 我們可以訪問超過1MB的空間慷暂,但我們只希望訪問1MB以內的內存空間。為了解決這個問題晨雳, CPU中添加了一個可控制A20地址線的模塊行瑞,通過這個模塊,我們在實模式下將第20bit的地址線限制為0餐禁,這樣CPU就不能訪問超過1MB的空間了血久。進入保護模式后,我們再通過這個模塊解除對A20地址線的限制帮非,這樣我們就能訪問超過1MB的內存空間了氧吐。

A20開啟/關閉的過程

現在使用的CPU都是通過鍵盤控制器8042來控制A20地址線疙驾。默認情況下则北,A20地址線是關閉的(第20bit的地址線限制為0),因此在進入保護模式(需要訪問超過1MB的內存空間)前往堡,我們需要開啟A20地址線(第20bit的地址線可為0或者1)庄岖。A20的開啟過程請參考bootasm.S文件豁翎。

CPU是如何從實模式轉換到保護模式

這個特別簡單,我們需要在開啟A20地址線后隅忿,將$CR0(control register 0)的PE(bit0)置為1就行了心剥。具體代碼請參考bootasm.S文件邦尊。

如何初始化和使用GDT表

GDT詳解

在使用GDT前,我們需要先來了解什么是GDT优烧。GDT全稱是Global Descriptor Table蝉揍,也就是全局描述符表。在保護模式下畦娄,我們通過設置GDT將內存空間被分割為了一個又一個的segment(這些segment是可以重疊的)又沾,這樣我們就能實現不同的程序訪問不同的內存空間。
這和實模式下的尋址方式是不同的, 在實模式下我們只能使用

address = segment << 4 | offset

的方式進行尋址(雖然也是segment + offset的熙卡,但在實模式下我們并不會真正的進行分段)杖刷。在這種情況下,任何程序都能訪問整個1MB的空間驳癌。而在保護模式下滑燃,通過分段的方式,程序并不能訪問整個內存空間颓鲜。下面引用一段ucore實驗報告書上的說明:

【補充】保護模式下表窘,有兩個段表:GDT(Global Descriptor Table)和LDT(Local Descriptor Table),每一張段表可以包含8192 (2^13)個描述符1甜滨,因而最多可以同時存在2 * 2^13 = 214個段乐严。雖然保護模式下可以有這么多段,邏輯地址空間看起來很大衣摩,但實際上段并不能擴展物理地址空間昂验,很大程度上各個段的地址空間是相互重疊的。目前所謂的64TB(2(14+32)=246)邏輯地址空間是一個理論值艾扮,沒有實際意義凛篙。在32位保護模式下,真正的物理空間仍然只有232字節(jié)那么大栏渺。注:在ucore lab中只用到了GDT,沒有用LDT锐涯。

Reference: 1 3.5.1 Segment Descriptor Tables, Intel? 64 and IA-32 Architectures Software Developer’s Manual

除了GDT, 我們還需要了解另外幾個名詞:段描述符(segment descriptor)和段選擇子(segment selector)磕诊。段描述符就是GDT中的元素,段選擇子就是訪問GDT的索引纹腌。

段選擇子

在實模式下, 邏輯地址由段選擇子和段選擇子偏移量組成. 其中, 段選擇子16bit, 段選擇子偏移量是32bit. 下面是段選擇子的示意圖:

段選擇子示意圖
段選擇子示意圖
  1. 在段選擇子中霎终,其中的INDEX[15:3]是GDT的索引。
  2. TI[2:2]用于選擇表格的類型升薯,1是LDT莱褒,0是GDT。
  3. RPL[1:0]用于選擇請求者的特權級涎劈,00最高广凸,11最低阅茶。
段描述符

段描述符的形式比較復雜(為了兼容各種不同版本的CPU),這里我只給一個示意圖谅海,具體的內容請查找手冊脸哀。這里用到的最重要的是segment base和segment limit:

段描述符示意圖
段描述符示意圖
GDT的訪問

有了上面這些知識,我們可以來看看到底應該怎樣通過GDT來獲取需要訪問的地址了扭吁。我們通過這個示意圖來講解:

GDT的訪問
GDT的訪問
  1. 我們根據CPU給的邏輯地址分離出段選擇子撞蜂。
  2. 利用這個段選擇子選擇一個段描述符。
  3. 將段描述符里的Base Address和段選擇子的偏移量相加而得到線性地址侥袜。這個地址就是我們需要的地址蝌诡。

GDT的初始化和使用

因為在保護模式下我們需要使用分段的內存空間,因此在進入保護模式前枫吧,我們就需要初始化GDT浦旱。 下面就通過一些代碼來說明如何初始化和使用GDT。

下面是GDT初始化的代碼:

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

gdt:
    /* 有一個特殊的選擇子稱為空(Null)選擇子由蘑,它的Index=0闽寡,TI=0,而RP
    L字段可以為任意值尼酿∫罚空選擇子有特定的用途,當用空選擇子進行存儲訪
    問時會引起異常裳擎∠延溃空選擇子是特別定義的,它不對應于全局描述符表GDT
    中的第0個描述符鹿响,因此處理器中的第0個描述符總不被處理器訪問羡微,一
    般把它置成全0。*/
    SEG_NULLASM                                     # null seg
    
    /* 在Lab1中, code segment和data segment都可以訪問整個內存空間 */
    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:
    /* lgdt 要先載入GDT的大小, 然后才是gdt的地址 */
    .word 0x17                                      # sizeof(gdt) - 1
    .long gdt                                       # address gdt

理論上GDT可以存在內存中任何位置惶我,但這里我們是在實模式下初始化GDT的妈倔,因此GDT應該是存在最低的這1MB內存空間中。CPU通過lgdt指令讀入GDT的地址绸贡,之后我們就可以使用GDT了盯蝴。

.set PROT_MODE_CSEG,        0x8   
.set PROT_MODE_DSEG,        0x10

/* 載入GDT */
lgdt gdtdesc

/* 從實模式切換到保護模式*/
movl %cr0, %eax
orl $CR0_PE_ON, %eax
movl %eax, %cr0

# ljmp <imm1>, <imm2>
# %cs ← imm1
# %ip ← imm2
/* 將%cs(code segment)的值設置為0x8 */
ljmp $PROT_MODE_CSEG, $protcseg

...

protcseg:
    # Set up the protected-mode data segment registers
    /* 設置data segment 的值 */
    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

Task4

通過這個Task,我們可以了解OS是如何加載ELF鏡像文件的听怕。這里我并沒有仔細研究ELF文件格式以及如何使用捧挺。

Task5

這個task是為了讓我們了解函數的調用和堆棧的關系。對于函數調用的細節(jié)尿瞭,我在之前的文章中已經寫過了闽烙,具體請參見C函數調用過程原理及函數棧幀分析。這里主要分析下代碼声搁,源代碼在 kern/debug/kdebug.c文件中黑竞。

/*
棧底方向      高位地址
...          
...          
參數3        
參數2        
參數1        
返回地址     
上一層[ebp]   <-------- [esp/當前ebp]
局部變量      低位地址
*/
void
print_stackframe(void) {
    uint32_t cur_ebp, cur_eip; 
    uint32_t args[4]; 
    cur_ebp = read_ebp();
    cur_eip = read_eip();
    
    /* 假設最多有20層的函數調用 */
    for (int stack_level = 0; stack_level < STACKFRAME_DEPTH + 1; stack_level++) {
        cprintf("ebp: 0x%08x eip: 0x%08x ", cur_ebp, cur_eip);
        
        /* 假設函數最多有4個參數 */
        for (int arg_num = 0; arg_num < 4; arg_num++)
            args[arg_num] = *((uint32_t *)cur_ebp + (2 + arg_num));
        cprintf("args:0x%08x 0x%08x 0x%08x 0x%08x\n", args[0], args[1], args[2], args[3]);
        print_debuginfo(cur_eip);
        
        /* 獲取上一層函數的返回地址和$ebp的值 */
        cur_eip = *((uint32_t *)cur_ebp + 1); 
        cur_ebp = *((uint32_t *)cur_ebp);  
    }
}

Task6

這個Task主要是為了讓我們熟悉保護模式下的中斷捕发。在X86架構中,中斷可以分為3種:

  1. 和CPU無關的摊溶,比如外設的請求等爬骤,這些屬于Interrupt。
  2. 和CPU有關的莫换,比如除0霞玄,page fault等,這些屬于Exception拉岁。
  3. 系統(tǒng)調用坷剧,這些屬于Trap

中斷機制

當CPU收到中斷(通過8259A完成)或者異常的事件時,它會暫停執(zhí)行當前的程序或任務喊暖,通過一定的機制跳轉到負責處理這個信號的相關處理例程中惫企,在完成對這個事件的處理后再跳回到剛才被打斷的程序或任務中.

中斷向量和中斷服務例程

在X86架構中, 系統(tǒng)最多支持256種不同的中斷, 這些中斷都有一個相應的中斷向量與其對應. 每個中斷向量又有一個對應的中斷服務例程陵叽, 這個中斷服務例程用于處理中斷向量.

IDT

將中斷向量和中斷服務例程聯系在一起的是IDT(Interrupt Descriptor Table)狞尔,輸入一個中斷向量,我們可以找到并運行該中斷向量對應的中斷服務例程巩掺。IDT和GDT類似偏序,每個描述符都是8K,但IDT的第一項可以包含一個描述符胖替。IDT中的中斷描述符可以分為3種:

  1. Task Gate
  2. Interrupt Gate
  3. Trap Gate

在這個Lab中我們使用了后兩種中斷描述符.

中斷描述符
中斷描述符

Interrupt Gate和Trap Gate差不多研儒,但有些微小的區(qū)別,我直接引用老師的說明:

【補充】所謂“自動禁止”独令,指的是CPU跳轉到interrupt gate里的地址時端朵,在將EFLAGS保存到棧上之后,清除EFLAGS里的IF位燃箭,以避免重復觸發(fā)中斷冲呢。在中斷處理例程里,操作系統(tǒng)可以將EFLAGS里的IF設上,從而允許嵌套中斷招狸。但是必須在此之前做好處理嵌套中斷的必要準備碗硬,如保存必要的寄存器等。二在ucore中訪問Trap Gate的目的是為了實現系統(tǒng)調用瓢颅。用戶進程在正常執(zhí)行中是不能禁止中斷的,而當它發(fā)出系統(tǒng)調用后弛说,將通過Trap Gate完成了從用戶態(tài)(ring 3)的用戶進程進了核心態(tài)(ring 0)的OS kernel挽懦。如果在到達OS kernel后禁止EFLAGS里的IF位,第一沒意義(因為不會出現嵌套系統(tǒng)調用的情況)木人,第二還會導致某些中斷得不到及時響應信柿,所以調用Trap Gate時冀偶,CPU則不會去禁止中斷∮嫒拢總之进鸠,interrupt gate和trap gate之間沒有優(yōu)先級之分,僅僅是CPU在處理中斷時有不同的方法形病,供操作系統(tǒng)在實現時根據需要進行選擇客年。

根據實際需求,我們建立相應的IDT漠吻,在建立好IDT后量瓜,我們就需要告訴CPU我們建立的IDT在哪里。要實現這個目的途乃,我們需要使用一個專門的指令lidt將IDT的地址加載到IDTR寄存器中绍傲。這樣 CPU就通過這個寄存器便可以訪問IDT了。在IDTR寄存器中耍共,我們需要存入IDT的起始地址和大小烫饼。下面是IDTR寄存器的示意圖:

IDTR寄存器
IDTR寄存器

中斷實例

我這里通過該Task的代碼來說明如何建立IDT以及如何通過中斷向量來訪問相應的中斷服務例程。

建立中斷向量表

在這個lab中试读,中斷向量表是__vectors杠纵,該表的每一項存儲一個中斷向量的地址。中斷服務例程在__alltraps中被調用鹏往。 __alltraps除了調用中斷服務例程外淡诗,還會做現場保護等工作。

# kern/trap/vectors.S
.globl vector0
vector0:
  pushl $0
  pushl $0
  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 vector255
  
# kern/trap/trapentry.S
.globl __alltraps
__alltraps:
    ...
    # push %esp to pass a pointer to the trapframe as an argument to trap()
    # 我這里補充一下, 在call __alltraps 之前, $esp指向最后壓入的一個參數, 也就是interrupt number(比如pushl $255). 所以說這里 pushl %esp 就是把 $255 在stack中的地址壓入stack作為 trap() 的參數
    pushl %esp

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

建立IDT

在這個Lab中伊履,前32個中斷向量和T_SYSCALL使用的是Trap Gate韩容;其余的中斷向量都是使用Interrupt Gate。

void
idt_init(void) {
    extern uintptr_t __vectors[]; 
    
    for (int i = 0; i < 256; i++) {
        if (i < IRQ_OFFSET) { 
            SETGATE(idt[i], 1, GD_KTEXT, __vectors[i], DPL_KERNEL); 
        } else if (i == T_SYSCALL) { 
            SETGATE(idt[i], 1, GD_KTEXT, __vectors[i], DPL_USER);
        } else { 
            SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL);
        }
    }

    lidt(&idt_pd);
}

中斷處理流程

下圖是一個簡化版的中斷處理流程:

  1. 當系統(tǒng)接收到中斷后, 會根據中斷類型產生一個中斷向量唐瀑。
  2. 用這個中斷向量作為索引在IDT中找到相應的中斷描述符群凶。
  3. 利用中斷描述符中的Segment Selector在GDT中找到相應的Segment。
  4. 將3中找到的Segment和中斷描述符中的Offset(也就是中斷向量表中存儲的中斷向量的地址)相加得到中斷服務例程的地址哄辣。
  5. 調用這個中斷服務例程请梢。

詳細的中斷處理過程請參考中斷與異常lab1中對中斷的處理實現.

中斷處理過程請
中斷處理過程請
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市力穗,隨后出現的幾起案子毅弧,更是在濱河造成了極大的恐慌,老刑警劉巖当窗,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件够坐,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機元咙,發(fā)現死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門梯影,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人庶香,你說我怎么就攤上這事甲棍。” “怎么了赶掖?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵感猛,是天一觀的道長。 經常有香客問我倘零,道長唱遭,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任呈驶,我火速辦了婚禮拷泽,結果婚禮上,老公的妹妹穿的比我還像新娘袖瞻。我一直安慰自己司致,他們只是感情好,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布聋迎。 她就那樣靜靜地躺著脂矫,像睡著了一般。 火紅的嫁衣襯著肌膚如雪霉晕。 梳的紋絲不亂的頭發(fā)上庭再,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天,我揣著相機與錄音牺堰,去河邊找鬼拄轻。 笑死,一個胖子當著我的面吹牛伟葫,可吹牛的內容都是我干的恨搓。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼筏养,長吁一口氣:“原來是場噩夢啊……” “哼斧抱!你這毒婦竟也來了?” 一聲冷哼從身側響起渐溶,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤辉浦,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后茎辐,有當地人在樹林里發(fā)現了一具尸體盏浙,經...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡眉睹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了废膘。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡慕蔚,死狀恐怖丐黄,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情孔飒,我是刑警寧澤灌闺,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站坏瞄,受9級特大地震影響桂对,放射性物質發(fā)生泄漏。R本人自食惡果不足惜鸠匀,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一蕉斜、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧缀棍,春花似錦宅此、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至青瀑,卻和暖如春璧亮,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背斥难。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工枝嘶, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蘸炸。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓躬络,卻偏偏與公主長得像,于是被迫代替她去往敵國和親搭儒。 傳聞我的和親對象是個殘疾皇子穷当,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內容