前面的幾篇文章講到了頁表,以及構成頁表的頁表描述符,分頁機制(paging)是80386引入的,而在其之前的8086和80286,使用的是分段機制(segmentation)。一些新的處理器架構,比如ARM,從誕生之日,就只支持分頁機制。雖然分段機制看起來似乎是一個上古時代的,已經被淘汰的東西,但畢竟人家是分頁機制的前輩。以兼容性著稱的x86處理器,即便到了今天的64位時代,依然保留了對segmentation的向前兼容,而linux操作系統最開始也是基於i386寫的,所以也保留了對segmentation的支持。了解下歷史,往往可以讓我們對後來出現的某些事物有更好的理解。

提到segment,做嵌入式軟體的同學可能會想到elf(executable and linkable file)文件,一個elf文件包含了text段,data段,bss段。Linux操作系統在載入elf文件運行進程後,還會生成stack段, heap段,每個segment用一個vm_area_struct結構體管理。x86中segment劃分也是類似的,包括cs(code segment),ds(data segment),ss(stack segment)等。同elf和linux中軟體實現的segment有所不同,x86中的segmentation機制含有更多硬體的參與,比如硬體實現的對許可權位的檢測(類似於MMU對頁表描述符中許可權位的檢測)。此外,x86中還有一些特殊的system segment,比如存儲task相關信息,以提供對context switch的硬體支持的task state segment。

讓我們從1978年的8086開始,看看x86中的segmentation機制到底是怎樣一回事。

8086實模式時代

8086的CPU是16位的,但intel的工程師希望在不改變CPU寄存器

和指令集的基礎上,讓它可以可以定址更大的內存範圍。於是他們使用了一種叫segmentation的機制,即一個邏輯地址(logical address)由segment加上offset組成,一般表達為segement:offset的形式。

當segment的值放入segment register時,就是告訴CPU:現在要訪問這個segment對應的內存區域,然後再根據offset的值,在這個segment內找到對應的位元組。轉換後形成的地址被稱作線性地址(linear address),若offset為16位,linear address = segment<<4 + offset,則定址範圍就擴大成了20位,比如logical address為0xA000:0x5F00,轉換成linear address就是0xA5F00。這種內存訪問被稱為實模式(Real Mode),因為linear address是由segment基址移位加上一個偏移得到,因此實模式下的這種segmentation被稱為shift-and-add segmentation。

在實模式下segment register直接指向segment:

80286保護模式時代

80286也使用segmentation,但和8086不同的是,80286中segment register存的不再是segment的起始地址,而是一個segment selector,通過這個selector查找GDT表獲得segment descriptor,segment descriptor存的才是segment的起始地址,因此保護模式下的這種segmentation被稱為table-based segmentation(區別於實模式的shift-and-add方式)。

名詞轟炸有沒有?別慌,我們先從段描述符(segment descriptor)入手,看看它的組成是怎樣的。

一個segment descriptor佔8個位元組,居然和現在64位系統的page table descriptor一樣。一個segment對應的內存區域由base和limit確定,base佔32位,limit佔20位,最大為0xFFFFFH,這52位是表達地址的(頁表使用20到36位存儲地址),然後還剩下12個bit表達屬性(上圖紅框部分),居然又和頁表描述符是一樣的,那再具體看看這些屬性哪些和頁表描述符是相同的,哪些是不同的。

G - Granularity,粒度,byte或者page,因為limit佔20位,如果粒度為byte,則該segment的定址範圍是1MB,如果粒度為page(按4KB算),則該segment的定址範圍是4GB。所以,一個segment的size是由limit和G位共同確定的。

D/B - Default Size/Bound,為1表示在32位模式下運行,為0在16位模式下運行。

L - 僅在64位系統中有效,為1表示在64位模式下運行,為0表示在32位兼容模式下運行。

AVL - Available for software,留給軟體用的,但反正在linux里是被忽略的。

P - Present, 同頁表描述符里的P位。

DPL - Descriptor Privledge Level,表示可以訪問segment的最低級別。x86處理器的特權級別從ring 0到ring 3,數字越小,級別越高,通常用戶空間運行於ring3,內核空間運行於ring0,所以這個基本等同於頁表描述符里的U/S位。假設DPL為1,則只有當前特權級別為0或者1時,才可以訪問該decriptor指向的segment。

S - 對於code segment和data segment都是1,system segment(比如task state segment)為0。

Type - 這個對於task segment是沒有意義的,對於code segment和data segment主要是關於Writable,Executable的屬性,等同於頁表描述符中的R/W, XD。

stack段也是可讀寫的,區別僅在於stack通常是向下增長的,所以也可視為data段的一種。對於這種向下增長的,需要注意是其地址邊界不再是base+limit,而是base-limit。

A - Accessed,同頁表描述符里的A位一樣。

當task運行時一個page的映射關係被建立,操作系統就需要在page table中添加一個entry記錄映射的物理頁面號,並填寫許可權控制位,形成一個descriptor,同樣的,當一個segment建立,操作系統就需要在GDT中添加一個segment descriptor,那GDT又是什麼呢?請看下文分解。


推薦閱讀:
相关文章