1. 程式人生 > >操作系統 Lab1

操作系統 Lab1

內存 兼容 con fdt esp 系統服務 ide into syn

練習1

1 ucore.img 是如何生成的
使用 make V= 查看詳細的步驟
cc kern/init/init.c 使用cc工具進行預處理
gcc
-Idir 將dir 作為查找目錄(頭文件)
-ggdb 符加信息到允許gdb進行debug的程度
-gstabs 將符加信息以 stabs format 格式輸出 這一個是主要被dbx 在大多數bsd 系統上使用
-m32
-Wall 輸出所有的警告信息
-fno-buildin 不允許不易build__in__開頭的buildin 函數
-nostdinc 不查找默認的c程序
-fno-stakc-portector 不使用棧保護機制
-C 生成.o 可重定位目標模塊
-m32 生成32位系統的Application Binary Interface

ld
將多個可重定位模塊進行重定位
-T 制定需要的腳本
kernel.ld 簡析:
output_format 指定為 elf32-i386 輸出的bfd格式
output_arch( 同上
entry 程序入口點
程序加載的位置 .=0x100000;

.text 節包括 .text .stub .text.* gnu.linkonce.t.*
provide 定義符號 etext=. //我才是額外的位置text

.rodata 節包括 rodata rodata.* .un.linkonce.r.
調試信息在下面
.stab
定義 STAB_BEGIN 的位置
.stab
定義__STAB_END__的位置

.=align(0x1000) 限定Data 的書對其

.data 包括 *.data

定義 edata在這裏

.bss {

}

定義 end 在這裏
/DISCARD/ eh.frame .note.GNU-stack

大概的順序 是先構成 kernel
構成 bootblock
構築 sign 運行了 sgin
使用sign在bootblock 最後510 511 個字節標價 0x55 0xaa

使用dd 命令 在一塊區域寫0 count=10000
在那塊寫 bootblock 512 個字節
然後寫 kernel

練習2

在 *0x7c00 出的斷點語句 是cli
cli 將 IF 置0 屏蔽可屏蔽中斷
對應的sti 將 IF 置1 回復相應課屏蔽中斷

第二條指令 cld
cld 置 df(direction flag) 為0 說明內存地址向高地址增加
std 置 df 為1 說明內存地址向低地址增加

ntel文檔使用MOVSD傳送雙字,而GNU文檔使用MOVSL傳送雙字。
源操作數和目的操作數分別使用寄存器(e)si和(e)di進行間接尋址;沒執行一次串操作,源指針(e)si和目的指針(e)di將自動進行修改:±1、±2、±4,其對應的分別是字節操作、字操作和雙字操作。註:

附:
inb 的含義
inb 是x86.h 下的一個函數 定義如下
static inline uint8_t inb(uint16_t port){
uint_t data;
asm volatile(“inb %1,%0” :"=a"(data),"d"(port));
}
這裏 %1 綁定到 a(data) 上 指定變量data 使用=a eax 寄存器 edi指定port 使用
於是inb 函數的具體含義應該是 從port 讀取一個byte 到eax 返回

waitdisk(void){
while((inb(0x1f7)&0xc0)!=0x40)
從0x1f7 端口讀取的數 如果不是 01** **** 就等待
}

readsect 接受 void*dst uint32_t secno
等待硬盤工作
將 1 寫入端口 0x1f2
將 secno的低位1字節 寫入端口0x1f3
將第二個字節 寫入端口0x1f4
...
第四個字節的低位部分寫入 端口 0x1f6 (secno>>24)& 0xf | 0xe0
將 0x20 寫入端口0x1f7

等待硬盤

練習3

  • a20的開啟
  • GDT表初始化
  • 如何使能進入保護模式

從這裏了解的:http://hengch.blog.163.com/blog/static/107800672009013104623747/
非常好的文章

以前的是只有1M的內存空間 有20根地址線
但是尋址使用的是段尋址:
segment 和 offset 寄存器都是16位的
所以最大值就是 0xffffh
兩個寄存器合起來就是
0x10ffef h 大約是1088k 就是超過了20位地址線的限制,所以在這時候就出現了回滾現象。

後來地址線變長了以後但是為了向前兼容,就是用了a20 Gate 屏蔽第21根地址線。

在保護模式下這樣就只能訪問奇數M的內存,這顯然是不能接受的

所以需要打開 a20Gate

下面是源碼閱讀 bootasm.S 計算機執行的第一批指令
cli 先屏蔽中斷
cld 置1說明指令向高位增長

xorw %ax %ax
movw %ax %ds
movw %ax %es
movw %ax %ss
置零關鍵的寄存器 段寄存器ds es ss

開始開始a20

inb $0x64 %al 從0x64 端口讀入一個字節(等待不忙(8042 input buffer empty))
testb $0x2 %al 直到status register 第二個bit 是0

向port 0x64 輸入0xd1
movb $0xd1 %al
outb %al $0x64

重復等待 0x64端口的 2bit 是0(不忙)
inb $0x64 %al 從0x64 端口讀入一個字節(等待不忙(8042 input buffer empty))
testb $0x2 %al 直到status register 第二個bit 是0
向0x60端口 發送0xdf (含義 是設置p2‘a20 bit to 1) df 11011111 1bit

附:分段存貯管理機制

只有在保護模式下才能使用分段存貯管理記住
邏輯地址
段描述符(描述段的屬性)
段描述符表(包含段描述符的數組)
段選擇子(段寄存器,用於定位段描述符表中表項的索引)

邏輯轉換地址(程序員使用的地址) 到物理地址(實際的物理內存地址) 分為以下兩步

[1] 分段地址轉換: CPU將邏輯地址(由段選擇子 和 段偏移offser 構成) 中的段選擇子的內容作為段描述符表的索引,找到表中對應的段描述符。
然後吧段描述符中保存的段基址加上段偏移值,形成線性地址(Linear address)

如果沒有分頁存貯管理機制 線性地址就是實際地址
[2]分頁地址轉換,線性地址 轉換成實際地址

要建立好段描述符 和段描述符表
gate a20開啟之後就 使用命令: lgdt gdtdesc
來具體看看 gdt 是如何構建的

.p2align 2 強制使用4 bytes 進行對齊(2 單位應該是 word)
gdt :
seg_nullasm : 生成一個空段
seg_asm: 分別是kern 的代碼段和數據段
seg_asm:

gdtdesc: //生成了加載近GDTR的48位內容
.word 0x17
.long gdt
//全局描述符表目前只有三個條目

段描述符

每一個段 由如下三個參數進行定義:
段基地址(Base Address) 段界限(Limit) 和 段屬性(Attributes)
定義在kern/m/mmu.h 中定義了一個段描述符的具體格式

  • 段基地址:
    規定線性地址空間中 段的起始地址。 在80386保護模式下,段基地址長32位。因為基地址長度與尋址地址長度相同。 送一任何一個段可以從任何一個字節開始。(與實模式不同,實模式下的邊界 必須被16整除)

  • 段界限:
    規定段的大小。 80386保護模式下 ,段界限由20位表示。可以以字節為單位或者4K 為段位;

  • 段屬性(Granularity)
    確定段的各種性質,用符號G 來表示
    G=0 表示 界限以字節為單位 那麽20位的空間 就有1M的返回 增量是1字節
    G=1 表示 界限以4K 為單位 對應的範圍就是 4G

類型 (TYPE):用語區別 不同類型的描述符 : 可表示描述的段是代碼段還是數據段,訪問權限:讀寫執行 ,段的擴展方向

描述符特權級(Descriptor Privilege Level) : 用來實現保護機制

段存在位(Segment-Presenr bit ): 如果這一位位0, 那麽這個描述符是非法的,不能用來實現地址轉換。 如果一個非法的描述符被加載進了一個段寄存器。處理器會立即產生異常。 ?操作系統可以使用任何被標記位 available 的位

已訪問位:當處理器訪問該段(當一個指向該 段描述符的選擇子被加載進一個段寄存器時) 將自動設置訪問位。操作系統可以清除該位。

段描述符的結構如下
技術分享圖片

全局 段描述符表
是一個保存多個段描述符的數組,其起始地址保存在全局段描述符表寄存器 GDTR 中。

GDTR 長48位 其中高32為基地址 低16位為段界限。
由於GDT不能有GDT本身之內的描述符進行描述定義?
所以處理器采用 GDTR 作為FDT 這一特殊的系統段。

註意,全局描述符表中第一個描述符設定為空段描述符。
GDTR 中的段界限以字節為單位,對於含有N個描述符的描述符表的段界限通常可以設為 8N-1。具體可以參考 boot/bootasm.S 中的gdt地址和 kern/mm/pmm.c 中的全局變量數組 gdt[] 分別基於匯編語言和 c 語言的 全局描述符表的實現

選擇子
線性地址部分的選擇子 是用來選擇哪個描述符表 和在該表中索引一個描述符的,選擇子可以作為指針變量的一部分。 從而對程序員是可見的 ,一般由加載器來設置。
選擇子的格式
技術分享圖片

索引(Index):在描述符表中從8192個描述符中選擇一個描述符。處理器自動將這個索引值乘以8(描述符的長度),再加上描述符表的基址來索引描述符表,從而選出一個合適的描述符。

表指示位(Table Indicator,TI):選擇應該訪問哪一個描述符表。0代表應該訪問全局描述符表(GDT),1代表應該訪問局部描述符表(LDT)。

請求特權級(Requested Privilege Level,RPL):保護機制,在後續試驗中會進一步講解。

全局描述符表的第一項不能被cpu 使用,當一個段選擇子的 索引 inde 和表指示位(Table Indicator) 都等於0時, 可以當作一個空的選擇子,既是 mmu.h 中的 SEG_NULL. 當段寄存器加載空選擇子的時候,處理器不會產生異常。 但是當使用一個空選擇子 訪問內存的時候 會產生異常。

保護模式下的特權級

在保護模式下,特權級總共由4個 編號從0(最高特權) 到3 (最低特權) 。 三種主要資源收到保護, 內存。IO 端口 。 以及執行特殊機器指令的能力。 在任一時刻,x86 CPU都是在一個特定的特權級下運行的,從而決定了代碼可以做什麽,不可以做什麽。這些特權級經常被稱為為保護環(protection ring),最內的環(ring 0)對應於最高特權0,最外面的環(ring 3)一般給應用程序使用,對應最低特權3。在ucore中,CPU只用到其中的2個特權級:0(內核態)和3(用戶態)。

有大約15條機器指令被CPU限制只能在內核態執行,這些機器指令如果被用戶模式的程序所使用,就會顛覆保護模式的保護機制並引起混亂,所以它們被保留給操作系統內核使用。如果企圖在ring 0以外運行這些指令,就會導致一個一般保護異常(general-protection exception)。對內存和I/O端口的訪問也受類似的特權級限制。

數據段選擇子的內容可由程序直接加載到各個段寄存器(SS 或 DS 等) 當中。

這些內容裏包含了請求特權級(RPL ) 字段。
當然,代碼段寄存器 CS 的內容不能由裝載指令MOV 直接設置,只能被那些話ui改變程序執行順序的指令(JMP INT CALL) 間接的設置,而且 CS 擁有一個由CPU 維護的當前特權級的字段 CPL (current Privilege Level)
二者的結構如下圖所示

技術分享圖片

代碼段寄存器中的值 總是等於 CPU 當前的特權級, 只需知道CS中的CPL 就可以知道此刻的特權級了

CPU會在兩個關鍵點上保護內存:當一個段選擇符被加載時,以及,當通過線性地址訪問一個內存頁時。因此,保護也反映在內存地址轉換的過程之中,既包括分段又包括分頁。當一個數據段選擇符被加載時,就會發生下述的檢測過程:

技術分享圖片

當前的特權級 要加載的特權級

因為越高的數值代表越低的特權,上圖中的MAX()用於選擇CPL和RPL中特權最低的一個,並與描述符特權級(Descriptor Privilege Level,簡稱DPL)比較。如果DPL的值大於等於它,那麽這個訪問可正常進行了。RPL背後的設計思想是:允許內核代碼加載特權較低的段。比如,你可以使用RPL=3的段描述符來確保給定的操作所使用的段可以在用戶模式中訪問。但堆棧段寄存器是個例外,它要求CPL,RPL和DPL這3個值必須完全一致,才可以被加載。下面再總結一下CPL、RPL和DPL:

保護模式的進入

計算機的cr0寄存器中去取出內容,然後置PE為 on 再設置 cro寄存器

ljmp 指令兩個參數一個是 prot_mode_cseg,protcseg :下面的地址
0x10就是代碼段描述符的地址
這個指令是將 prot_mode_cseg 加載到cs 寄存器中
protseg:

movw $PROT_MODE_DSEG , %ax 將數據段描述符的位置放在ax位置

movw %ax %ds data Segment
movw %ax %es extra Segment
movw %ax %fs
movw %ax %gs
movw %ax %ss Stack Segment

mov $0x0 %ebp 將ebp 初始化為0
mov \(start %esp 將esp 初始化為\)start
call bootmain 調用bootloader

bootmain 做了那些事

將硬盤上的第一頁讀到 0x10000 中的一個 hlfhdr中去
readseg 讀取一個Segment 參數是 va count offset
count 的大小是 SectSize
end_va= va +count 讀取的末尾地址
secsize 是512 一個扇區的大小

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

secno=(offset/SECTSIZE) + 1 kernel starts at sectors 1

for(;va

判斷加載進來的內核是否是一個合法的內核

struct proghdr ph,eph

ph=ELFHDR+ELFHDR+e_phoffset ph 指向program header
eph=ph +ELFHDR->e_phnum; 指向prpgram header 結束的地方
for(;ph<eph;ph++){ //遍歷所有的program header
readseg(ph->p_va & 0xffffff ,ph->p_memsz,ph->offset) //

}

(void (*)(void) )()(ELFHDR->e_entry & 0xffffff))() ; 調用這個可執行文件的入口 //不在返回

練習4 加載過程

1 如何讀取扇區
使用readsect 讀取扇區
第一個參數 *dst 表示都出來存放的位置,
第二個參數 secno 表示第幾個扇區

使用IO端口進行讀取
ps:
內核是從第一個扇區開始的
第0個扇區是引導扇區

2 如何加載ELF格式的內核的
使用readseg 函數接受三個參數: va 應該在內存中的起始虛擬地址, count 讀入的大小, offset 相對於第一個扇區的 offset

因為必須讀一整個扇區,所以讀入的時候有可能會破壞低位 內存的東西 ?(不過我們是從高位向低位讀 所以沒有問題)

先讀取ELFHDR,然後根據ELFHDR 內的信息逐個讀入程序的段

最後調用ELFHDR 中的 e_entry & 0xffffff 字段進入內核

練習5 輸出函數棧信息

主要內容在 kdebug.c 中完成 下面將分析給出的一些實現好的功能,然後嘗試自己完成
1
static void stab_binsearch(const struct stab stabs,int region_left,int region_right,int type, uintptr_t addr){
int l=
region_left,r=region_right,any_matches=0;
while(l<r){
int true_m=(l+r)/2,m=true_m;
while(m>=l && stabs[m].n_type !=type){ //匹配到第一個類型相符的 類型不對就向下尋找
m--;
}
if(m<=l){
l=true_m+1;
continue;
}
any_matches=1;
if(stabs[m].n_value<addr){ //符號的值 應該也是一個地址
region_left=m;
l=true_m+1;
}else if(stab[m].n_values>addr){
region_right=m-1; //返回的實際上是 包含addr的最大地址 (不包括下一個匹配(類型))
r=m-1
}else{ //找右側區域
region_left=m;
l=m;
addr++;
}
}
if(!any_matches){
region_right=region_left-1;
}else{
l=
region_right;
for(;l>region_left&& stabs[l].n_type!=type;l--);
region_left=l;
}
}
int debuginfo_eip(uintptr_t addr,struct eipdebuginto *info){ //將addr 處的結構信息填入 debuginfo
}

需要自己實現的部分
void print_stack_frame(){

    uint32_t mebp=read_ebp();
uint32_t meip=read_eip();
int i;
for(i=0;i<STACKFRAME_DEPTH && ebp!=0 ;i++){
    cprintf("ebp : %08x , eip: %08x \n",mebp,meip);
    cprintf("calling arguments: %08x  %08x %08x %08x\n",*(uint32_t*)(mebp+2),*(uint32_t*)(mebp+4),*(uint32_t*)(mebp+6),*(uint32_t*)(mebp+8));
    
    print_debuginfo(meip-1);
    mebp=((uint32_t *)mebp)[0];  //這裏是有錯誤的 ebp 只需要記錄上次調用這的位置 應該是  eip=((uint32_t *)mebp )[1]  ebp=((uint32_t *)ebp)ebp[0]
    meip=((uint32_t *)mebp)[1];     
            //錯誤的根源是我沒有意識到read_eip 是一個 noninline-function 這樣做是有好處的,可以快捷的讀取到當前的eip值 真是6 // read_ebp 就是 inline的
}

}

Lab 6 中斷服務的建立

中斷與異常
由三種特殊的中斷事件. 由CPU外部設備引起的外部事件如I/O 中斷,時鐘中斷,控制臺終端等 是異步產生的(即產生的時刻不確定),我們稱之為異步中斷(asynchronous interrupt) 也稱為外部中斷 簡稱中斷
而把在CPU執行指令期間檢測到不正常的或非法的條件(如除零錯、地址訪問越界)所引起的內部事件稱作同步中斷(synchronous interrupt),也稱內部中斷,簡稱異常(exception)。
。把在程序中使用請求系統服務的系統調用而引發的事件,稱作陷入中斷(trap interrupt),也稱軟中斷(soft interrupt),系統調用(system call)簡稱trap。在後續試驗中會進一步講解系統調用。
本實驗只描述保護模式下的處理過程。當CPU收到中斷(通過8259A完成,有關8259A的信息請看附錄A)或者異常的事件時,它會暫停執行當前的程序或任務,通過一定的機制跳轉到負責處理這個信號的相關處理例程中,在完成對這個事件的處理後再跳回到剛才被打斷的程序或任務中。中斷向量和中斷服務例程的對應關系主要是由IDT(中斷描述符表)負責。操作系統在IDT中設置好各種中斷向量對應的中斷描述符,留待CPU在產生中斷後查詢對應中斷服務例程的起始地址。而IDT本身的起始地址保存在idtr寄存器中。

(1) 中斷描述符表(Interrupt Descriptor Table) 中斷描述符表把每個中斷或異常編號和一個指向中斷服務例程的描述符聯系起來。同GDT一樣,IDT是一個8字節的描述符數組,(GDT 第一個條則是空段描述符)但IDT的第一項可以包含一個描述符。CPU把中斷(異常)號乘以8做為IDT的索引。IDT可以位於內存的任意位置,CPU通過IDT寄存器(IDTR)的內容來尋址IDT的起始地址。指令LIDT和SIDT用來操作IDTR。兩條指令都有一個顯示的操作數:一個6字節表示的內存地址。指令的含義如下:

LIDT(Load IDT Register)指令:使用一個包含線性地址基址和界限的內存操作數來加載IDT。操作系統創建IDT時需要執行它來設定IDT的起始地址。這條指令只能在特權級0執行。(可參見libs/x86.h中的lidt函數實現,其實就是一條匯編指令)
SIDT(Store IDT Register)指令:拷貝IDTR的基址和界限部分到一個內存地址。這條指令可以在任意特權級執行。
技術分享圖片

在保護模式下,最多會存在256個Interrupt/Exception Vectors。範圍[0,31]內的32個向量被異常Exception和NMI使用,但當前並非所有這32個向量都已經被使用,有幾個當前沒有被使用的,請不要擅自使用它們,它們被保留,以備將來可能增加新的Exception。範圍[32,255]內的向量被保留給用戶定義的Interrupts。Intel沒有定義,也沒有保留這些Interrupts。用戶可以將它們用作外部I/O設備中斷(8259A IRQ),或者系統調用(System Call 、Software Interrupts)等。

(2) IDt gate descriptors
Interrupts/Exceptions應該使用Interrupt Gate和Trap Gate,它們之間的唯一區別就是:當調用Interrupt Gate時,Interrupt會被CPU自動禁止;而調用Trap Gate時,CPU則不會去禁止或打開中斷,而是保留它原來的樣子。

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

在IDT中,可以包含如下3種類型的Descriptor:

Task-gate descriptor (這裏沒有使用)
Interrupt-gate descriptor (中斷方式用到)
Trap-gate descriptor(系統調用用到)

技術分享圖片
圖9 X86的各種門的格式
可參見kern/mm/mmu.h中的struct gatedesc數據結構對中斷描述符的具體定義。

(3) 中斷處理中硬件負責完成的工作
中斷服務例程包括具體負責處理中斷(異常)的代碼是操作系統的重要組成部分。需要註意區別的是,有兩個過程由硬件完成:

硬件中斷處理過程1(起始):從CPU收到中斷事件後,打斷當前程序或任務的執行,根據某種機制跳轉到中斷服務例程去執行的過程。其具體流程如下:
CPU在執行完當前程序的每一條指令後,都會去確認在執行剛才的指令過程中中斷控制器(如:8259A)是否發送中斷請求過來,如果有那麽CPU就會在相應的時鐘脈沖到來時從總線上讀取中斷請求對應的中斷向量;
CPU根據得到的中斷向量(以此為索引)到IDT中找到該向量對應的中斷描述符,中斷描述符裏保存著中斷服務例程的段選擇子;
CPU使用IDT查到的中斷服務例程的段選擇子從GDT中取得相應的段描述符,段描述符裏保存了中斷服務例程的段基址和屬性信息,此時CPU就得到了中斷服務例程的起始地址,並跳轉到該地址;
CPU會根據CPL和中斷服務例程的段描述符的DPL信息確認是否發生了特權級的轉換。比如當前程序正運行在用戶態,而中斷程序是運行在內核態的,則意味著發生了特權級的轉換,這時CPU會從當前程序的TSS信息(該信息在內存中的起始地址存在TR寄存器中)裏取得該程序的內核棧地址,即包括內核態的ss和esp的值,並立即將系統當前使用的棧切換成新的內核棧。這個棧就是即將運行的中斷服務程序要使用的棧。緊接著就將當前程序使用的用戶態的ss和esp壓到新的內核棧中保存起來;
CPU需要開始保存當前被打斷的程序的現場(即一些寄存器的值),以便於將來恢復被打斷的程序繼續執行。這需要利用內核棧來保存相關現場信息,即依次壓入當前被打斷程序使用的eflags,cs,eip,errorCode(如果是有錯誤碼的異常)信息;
CPU利用中斷服務例程的段描述符將其第一條指令的地址加載到cs和eip寄存器中,開始執行中斷服務例程。這意味著先前的程序被暫停執行,中斷服務程序正式開始工作。
硬件中斷處理過程2(結束):每個中斷服務例程在有中斷處理工作完成後需要通過iret(或iretd)指令恢復被打斷的程序的執行。CPU執行IRET指令的具體過程如下:
程序執行這條iret指令時,首先會從內核棧裏彈出先前保存的被打斷的程序的現場信息,即eflags,cs,eip重新開始執行;
如果存在特權級轉換(從內核態轉換到用戶態),則還需要從內核棧中彈出用戶態棧的ss和esp,這樣也意味著棧也被切換回原先使用的用戶態的棧了;
如果此次處理的是帶有錯誤碼(errorCode)的異常,CPU在恢復先前程序的現場時,並不會彈出errorCode。這一步需要通過軟件完成,即要求相關的中斷服務例程在調用iret返回之前添加出棧代碼主動彈出errorCode。
下圖顯示了從中斷向量到GDT中相應中斷服務程序起始位置的定位方式

技術分享圖片

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

技術分享圖片
8個字節,從圖來看對於 interrupt gate 來說0..15 到48-63 是偏移
16-31是段選擇子

操作系統 Lab1