預想了解Linux核心的一些原理,必先了解X86體系結構。x86體系基本等同于cpu的使用說明書,Linux的底層就是cpu,如果不了解底層提供的功能點,很多實作方式都搞不懂。就好像我們做一個需求,不知道底層有哪些接口可用,那需求做起來就太困難了。
x86架構起源于intel8086處理器,是intel公司70年代生産的16位處理器,推出前幾年都不溫不火,直到找到自己的業務方:ibm pc。ibm pc的研發沒有交給最牛的華生實驗室,而是交給另外一個團隊,且要求一年交貨,由于工期緊張,他們直接采用intel8086作為cpu,微軟ms-dos作為os。由于ibm pc的大賣,hp、康柏等公司開始做相容機,也是采用8086處理器,是以intel成為pc行業的标準,也就是x86。
1、體系結構總覽

cpu的核心邏輯是輸入資料 + 指令 = 輸出結果,那就要解決這麼幾個問題:1)get指令。2)get輸入資料。3)運算。4)store輸出結果。其中第3步運算邏輯基本在cpu的alu中完成,不需要關注。其中1、2、4主要包含get、store兩種操作,其實解決了資料的存儲、尋址,get和store也就ok了。
1.1 指令和資料的存儲
一個程序的能夠看到的記憶體空間是邏輯位址,最後操作時會映射到實體位址,一個程序的記憶體邏輯位址又分為以下幾個segment。
1)Text Segment:存放程序運作的指令,程序建立時,會将代碼編譯後的指令放到這個位置,也是靜态的,運作時不會改變。
2)Data Segment:代碼中已初始化值的變量。Bss是未初始化值的靜态變量。
3)heap:堆,用于進行記憶體的動态配置設定。
4)stack:函數運作的棧。
1.2 指令和資料的尋址
X86的尋址,在cpu的視角看,是
段基址 + 段偏移位址(至于為什麼用這麼挫的方式,在下一節段位址中描述)。以下是段基址寄存器:
1)CS:存儲代碼段的基址。
2)DS:存儲資料段的基址。
3)SS:存儲運作棧的基址。
段偏移位址寄存器:
1)IP:指令指針寄存器,根據CS + IP擷取到的指令存儲EIP(指令寄存器)中,供ALU使用。
2)EBP:棧基指針寄存器。
3)ESP:棧頂指針寄存器。
4)資料段的偏移位址一般存儲在通用寄存器中,擷取的資料一般也存到通用寄存器中,供ALU使用。
有了尋址方式,CPU直接将位址發送到位址總線上,即可以從資料總線收到資料。
2、分段記憶體管理
8086是16位的cpu,最大支援尋址空間為64K,但如何支援20位的位址總線呢?即支援1M的尋址空間(估計是ibm提的需求)。那怎麼辦呢?16位撐死就64K。那就兩個位址,段基址和段偏移,湊成20位位址。
邏輯位址 = 段基址 * 16(左移四位) + 段偏移位址
當32位cpu出現時,這種設計就a round peg in a square hole,但考慮到原有架構上已經有很多硬體和軟體,不得不做相容。但現在好不容易更新為32位,再用原有模式也無法支援4G的空間。于是IA32(32位處理器架構簡稱)和作業系統産生商量後達成一緻,IA32推出的标準:
1)尋址方式仍然為
線性位址->
邏輯位址(分段模式) -> 實體位址,保留分段模式。
2)在記憶體中建構兩個存儲段基址的table,
GDT(Global Descriptor Table,全局描述表),存儲核心程式的段基址,由寄存器GDTR儲存GDT入口位址,
LDT(Local Descriptor Table,本地描述符表),由寄存器LRTR指向LRT的入口,存儲各個使用者程序的段基址。table中的item存儲
段描述符,其中包括段基址、段界限和權限相關資訊。
3)CS、DS等段基址寄存器仍然為16位(相容以前),但改為存儲
段選擇子,即table的index,這樣段基址就存儲于記憶體中,為以後擴充也打下基礎。于是段基址存儲位址時叫“
實模式”,存儲段選擇子叫“
保護模式”,系統啟動時都是先處于實模式,當你需要更多記憶體時,切換到保護模式。通過切換模式做到無縫相容。
以上是硬體商給的解決方案,但Linux為了相容spark、arm等體系架構(這些體系都沒有分段位址模式),也是為了少一次位址映射,是以将所有段基址都設定為0,是以段偏移位址 = 線性位址(記憶體尋址的原理後面會細講),也就是所有程序的任一段基址都為0,是以Linux沒必要使用LDT,僅用了GDT。
3、函數調用棧
代碼都用函數來承載指令,通過函數調用來承載指令的跳轉。而 函數的運作是基于棧的,一個函數就是一個棧幀。
int bar(int c, int d)
{
int e = c + d;
return e;
}
int foo(int a, int b)
{
return bar(a, b);
}
int main(void)
{
foo(2, 3);
return 0;
}
1)stack segment處于線性位址的高位址,且向下擴充,是以esp棧頂位址是向低位址擴充,esp棧基位址反而處于高位址。
2)當A->B函數時,會進行如下操作:
1、push eip:儲存目前的EIP(指向A函數的某條指令),然後通過jmp或call指令将EIP設
置為B函數的起始指令。這樣當B函數傳回時,再将該EIP的值pop 到EIP寄存器中,使得A
函數可以繼續運作以下的指令。
2、push ebp:将A目前的ebp壓棧,有兩個作用:1)友善B函數擷取其入參。2)當B傳回
時,将ebp指向A的棧基址。
3、mov esp ebp:将目前的esp複制給ebp,相當于ebp從指向A的棧基址指向B的棧基
址,開始運作B函數的指令。
3)目前函數的入參,是在上一個調用函數的棧幀中,通過本棧幀中儲存的調用者的ebp和參
數的size進行計算入參的位址。
4. 狀态寄存器
eflags是狀态寄存器,控制單元會根據其值變更執行邏輯。比如IF就是中斷标志,當陷入中斷時,會通關關閉eflags.if标志來關中斷,此處先提一下,後面中斷會具體講到。
在看X86體系架構時,有些感觸:
1、業務很重要。對于intel,沒有保住ibm pc的大腿,現在可能也沒有intel,也沒有x86。
2、業務提的需求,很多時候條件都不具備,需要工程師是做trade off。比如16位cpu支援20位的尋址,分段模式不得不說是一個臨時方案,但為其赢得了市場。為了技術潔癖去做完美的方案是沒有出路的。對于架構師,當你知道了業務的不美好和代碼的龌蹉後,還能依然想把這個系統做好。
3、如果要做開放體系,相容是必須的,哪怕是原來做個的臨時方案。對于開放體系,一旦釋出方案,就意味着必須相容到底。好比java的泛型,現在還為了相容早期非泛型版本遺留着許多不完美。