日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 运维知识 > windows >内容正文

windows

ucore操作系统实验笔记 - Lab1

發(fā)布時間:2025/3/21 windows 35 豆豆
生活随笔 收集整理的這篇文章主要介紹了 ucore操作系统实验笔记 - Lab1 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

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

Task1

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

Task2

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

首先修改gdbinit為:

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

然后輸入

make debug

通過輸入

x/i $cs x/i $eip

我們可以獲取當(dāng)前 $cs 和 $eip 的值。其中

$cs = 0xf000 $eip = 0xfff0

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

$cs << 4 | $eip = 0xffff0

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

x/2i 0xffff0

得到的結(jié)果是

0xffff0: ljmp $0xf000,$0xe05b

也就是說,BIOS開始的地址應(yīng)該是

$cs << 4 | 0xe05b = 0xfe05b

此時, 我們設(shè)置一個斷點到0x7c00:

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

然后當(dāng)程序運行起來后, 最后會停止在 0x7c00 這個地址。這里存放的便是bootloader了。

Task3

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

如何開啟/關(guān)閉 A20

實模式下內(nèi)存的訪問

在開啟A20前,我們先來說說i8086時CPU是如何訪問內(nèi)存空間的。

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

address = segment << 4 | offset

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

上面這個特征在i8086中是沒有任何問題的(因為它最多只能訪問1MB的內(nèi)存空間),但到了i80286/i80386后,CPU有了更寬的地址總線,數(shù)據(jù)總線和寄存器后,這就會出現(xiàn)一個問題: 在實模式下, 我們可以訪問超過1MB的空間,但我們只希望訪問1MB以內(nèi)的內(nèi)存空間。為了解決這個問題, CPU中添加了一個可控制A20地址線的模塊,通過這個模塊,我們在實模式下將第20bit的地址線限制為0,這樣CPU就不能訪問超過1MB的空間了。進入保護模式后,我們再通過這個模塊解除對A20地址線的限制,這樣我們就能訪問超過1MB的內(nèi)存空間了。

A20開啟/關(guān)閉的過程

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

CPU是如何從實模式轉(zhuǎn)換到保護模式

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

如何初始化和使用GDT表

GDT詳解

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

address = segment << 4 | offset

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

【補充】保護模式下,有兩個段表:GDT(Global Descriptor Table)和LDT(Local Descriptor Table),每一張段表可以包含8192 (2^13)個描述符[1],因而最多可以同時存在2 * 2^13 = 2^14個段。雖然保護模式下可以有這么多段,邏輯地址空間看起來很大,但實際上段并不能擴展物理地址空間,很大程度上各個段的地址空間是相互重疊的。目前所謂的64TB(2^(14+32)=2^46)邏輯地址空間是一個理論值,沒有實際意義。在32位保護模式下,真正的物理空間仍然只有2^32字節(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. 下面是段選擇子的示意圖:

  • 在段選擇子中,其中的INDEX[15:3]是GDT的索引。

  • TI[2:2]用于選擇表格的類型,1是LDT,0是GDT。

  • RPL[1:0]用于選擇請求者的特權(quán)級,00最高,11最低。

  • 段描述符

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

    GDT的訪問

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

  • 我們根據(jù)CPU給的邏輯地址分離出段選擇子。

  • 利用這個段選擇子選擇一個段描述符。

  • 將段描述符里的Base Address和段選擇子的偏移量相加而得到線性地址。這個地址就是我們需要的地址。

  • GDT的初始化和使用

    因為在保護模式下我們需要使用分段的內(nèi)存空間,因此在進入保護模式前,我們就需要初始化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,而RPL字段可以為任意值。空選擇子有特定的用途,當(dāng)用空選擇子進行存儲訪問時會引起異常。空選擇子是特別定義的,它不對應(yīng)于全局描述符表GDT中的第0個描述符,因此處理器中的第0個描述符總不被處理器訪問,一般把它置成全0。*/SEG_NULLASM # null seg/* 在Lab1中, code segment和data segment都可以訪問整個內(nèi)存空間 */SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) # code seg for bootloader and kernelSEG_ASM(STA_W, 0x0, 0xffffffff) # data seg for bootloader and kernelgdtdesc:/* lgdt 要先載入GDT的大小, 然后才是gdt的地址 */.word 0x17 # sizeof(gdt) - 1.long gdt # address gdt

    理論上GDT可以存在內(nèi)存中任何位置,但這里我們是在實模式下初始化GDT的,因此GDT應(yīng)該是存在最低的這1MB內(nèi)存空間中。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)的值設(shè)置為0x8 */ ljmp $PROT_MODE_CSEG, $protcseg...protcseg:# Set up the protected-mode data segment registers/* 設(shè)置data segment 的值 */movw $PROT_MODE_DSEG, %ax # Our data segment selectormovw %ax, %ds # -> DS: Data Segmentmovw %ax, %es # -> ES: Extra Segmentmovw %ax, %fs # -> FSmovw %ax, %gs # -> GSmovw %ax, %ss # -> SS: Stack Segment

    Task4

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

    Task5

    這個task是為了讓我們了解函數(shù)的調(diào)用和堆棧的關(guān)系。對于函數(shù)調(diào)用的細節(jié),我在之前的文章中已經(jīng)寫過了,具體請參見C函數(shù)調(diào)用過程原理及函數(shù)棧幀分析。這里主要分析下代碼,源代碼在 kern/debug/kdebug.c文件中。

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

    Task6

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

  • 和CPU無關(guān)的,比如外設(shè)的請求等,這些屬于Interrupt。

  • 和CPU有關(guān)的,比如除0,page fault等,這些屬于Exception。

  • 系統(tǒng)調(diào)用,這些屬于Trap

  • 中斷機制

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

    中斷向量和中斷服務(wù)例程

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

    IDT

    將中斷向量和中斷服務(wù)例程聯(lián)系在一起的是IDT(Interrupt Descriptor Table),輸入一個中斷向量,我們可以找到并運行該中斷向量對應(yīng)的中斷服務(wù)例程。IDT和GDT類似,每個描述符都是8K,但IDT的第一項可以包含一個描述符。IDT中的中斷描述符可以分為3種:

  • Task Gate

  • Interrupt Gate

  • Trap Gate

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

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

    【補充】所謂“自動禁止”,指的是CPU跳轉(zhuǎn)到interrupt gate里的地址時,在將EFLAGS保存到棧上之后,清除EFLAGS里的IF位,以避免重復(fù)觸發(fā)中斷。在中斷處理例程里,操作系統(tǒng)可以將EFLAGS里的IF設(shè)上,從而允許嵌套中斷。但是必須在此之前做好處理嵌套中斷的必要準備,如保存必要的寄存器等。二在ucore中訪問Trap Gate的目的是為了實現(xiàn)系統(tǒng)調(diào)用。用戶進程在正常執(zhí)行中是不能禁止中斷的,而當(dāng)它發(fā)出系統(tǒng)調(diào)用后,將通過Trap Gate完成了從用戶態(tài)(ring 3)的用戶進程進了核心態(tài)(ring 0)的OS kernel。如果在到達OS kernel后禁止EFLAGS里的IF位,第一沒意義(因為不會出現(xiàn)嵌套系統(tǒng)調(diào)用的情況),第二還會導(dǎo)致某些中斷得不到及時響應(yīng),所以調(diào)用Trap Gate時,CPU則不會去禁止中斷。總之,interrupt gate和trap gate之間沒有優(yōu)先級之分,僅僅是CPU在處理中斷時有不同的方法,供操作系統(tǒng)在實現(xiàn)時根據(jù)需要進行選擇。

    根據(jù)實際需求,我們建立相應(yīng)的IDT,在建立好IDT后,我們就需要告訴CPU我們建立的IDT在哪里。要實現(xiàn)這個目的,我們需要使用一個專門的指令lidt將IDT的地址加載到IDTR寄存器中。這樣 CPU就通過這個寄存器便可以訪問IDT了。在IDTR寄存器中,我們需要存入IDT的起始地址和大小。下面是IDTR寄存器的示意圖:

    中斷實例

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

    建立中斷向量表

    在這個lab中,中斷向量表是__vectors,該表的每一項存儲一個中斷向量的地址。中斷服務(wù)例程在__alltraps中被調(diào)用。 __alltraps除了調(diào)用中斷服務(wù)例程外,還會做現(xiàn)場保護等工作。

    # kern/trap/vectors.S .globl vector0 vector0:pushl $0pushl $0jmp __alltraps... .globl vector255 vector255:pushl $0pushl $255jmp __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指向最后壓入的一個參數(shù), 也就是interrupt number(比如pushl $255). 所以說這里 pushl %esp 就是把 $255 在stack中的地址壓入stack作為 trap() 的參數(shù)pushl %esp# call trap(tf), where tf=%espcall 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); }

    中斷處理流程

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

  • 當(dāng)系統(tǒng)接收到中斷后, 會根據(jù)中斷類型產(chǎn)生一個中斷向量。

  • 用這個中斷向量作為索引在IDT中找到相應(yīng)的中斷描述符。

  • 利用中斷描述符中的Segment Selector在GDT中找到相應(yīng)的Segment。

  • 將3中找到的Segment和中斷描述符中的Offset(也就是中斷向量表中存儲的中斷向量的地址)相加得到中斷服務(wù)例程的地址。

  • 調(diào)用這個中斷服務(wù)例程。

  • 詳細的中斷處理過程請參考中斷與異常和lab1中對中斷的處理實現(xiàn).

    總結(jié)

    以上是生活随笔為你收集整理的ucore操作系统实验笔记 - Lab1的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。