-
【Linux 從頭學(xué)】是什么
-
古老的 Intel8086 處理器
-
主存儲器是什么?
-
寄存器是什么?
-
三個總線
-
CPU 如何對內(nèi)存進(jìn)行尋址?
-
我們是如何控制 CPU 的?
-
CPU 執(zhí)行指令流程
【Linux 從頭學(xué)】是什么
這兩年多以來,我的本職工作重心一直是在
x86 Linux 系統(tǒng)這一塊,從驅(qū)動到中間層,再到應(yīng)用層的開發(fā)。
隨著內(nèi)容的不斷擴(kuò)展,越發(fā)覺得之前很多
基礎(chǔ)的東西都差不多忘記了,比如下面這張表(《深入理解 LINUX 內(nèi)核》第47頁):
這張表描述了Linux系統(tǒng)中幾個
段描述符信息。
數(shù)據(jù)段和
代碼段,仔細(xì)看一下相關(guān)書籍就知道這些描述符代表什么意思,但是:
為什么這幾個段的
Base 地址都是0x00000000?
為什么
Limit 都是0xfffff?
為什么它們的
Type 類型和
優(yōu)先級 DPL 又各不相同?
如果沒有對x86平臺的一些基礎(chǔ)知識的理解,要啃完這本書真的是
挺費(fèi)力氣的!
更要命的是,隨著Linux內(nèi)核代碼的體積不斷膨脹,最新的
5.13 版本壓縮檔已經(jīng)是一百多兆了:
這么一個龐然大物,如何下手才能真正的學(xué)好Linux呢?!
即便是從
Linux 0.11 版本開始,其中的很多代碼看起來也是非常費(fèi)勁的!
周末在整理一些吃灰的書籍時,發(fā)現(xiàn)幾本以前看過的好書: 王爽的《匯編語言》,李忠的《從實模式到保護(hù)模式》,馬朝暉翻譯的《匯編語言程序設(shè)計》等等。
都是非常-非常-老的書籍,再次翻了一下,真心覺得內(nèi)容
寫得真好!
對一些概念、原理、設(shè)計思路的描述,清晰而透徹。
Linux系統(tǒng)中的很多關(guān)于分段、內(nèi)存、寄存器相關(guān)的設(shè)計,都可以在這些書籍中找到基礎(chǔ)支撐。
于是乎,我就有了一個
想法:是否可以把這些書籍中,與Linux系統(tǒng)相關(guān)的內(nèi)容進(jìn)行一次重讀和整理,但
絕不是簡單的知識搬運(yùn)。
考慮了一下,大概有下面幾個想法:
-
先確定最終目標(biāo)的目標(biāo):學(xué)習(xí) Linux 操作系統(tǒng);
-
這幾本書寫的都是匯編語言,以及比較基礎(chǔ)的底層知識。我們會淡化匯編語言部分,把重點(diǎn)放在與 Linux 操作系統(tǒng)有關(guān)聯(lián)的原理部分;
-
不會嚴(yán)格按照書中的內(nèi)容、順序來輸出文章,而是把幾本書中內(nèi)容相關(guān)的部分放在一起學(xué)習(xí)、討論;
-
有些內(nèi)容,可以與 Linux 2.6 版本中的相關(guān)部分進(jìn)行對比分析,這樣的話在以后學(xué)習(xí) Linux 內(nèi)核部分時,可以找到底層的支撐;
-
最后,希望我自己能堅持這個系列,也算是給自己的一個梳理吧。
一句話:
以基礎(chǔ)知識為主!
作為開篇第一章,本文將會描述下面這張圖的執(zhí)行步驟:
現(xiàn)在就開始吧!
古老的 Intel8086 處理器
8086是Intel公司的第一款16位處理器,誕生于1978年,應(yīng)該比各位小伙伴的年齡都大一些。
在Intel公司的所有處理器中,它占有很重要的地位,是整個Intel 32位架構(gòu)處理器(IA-32)的
開山鼻祖。
那么,問題來了,
什么叫 16 位的處理器?
有些人會把
處理器的位數(shù)與
地址總線的位數(shù)搞混在一起!
我們知道,CPU在訪問內(nèi)存的時候,是通過
地址總線來傳送
物理地址的。
8086 CPU有20位的地址線,可以傳送20位地址。
每一根地址線都表示一個bit,那么20個bit可以表示的最大值就是
2 的 20 次方。
也就是說:最大可以定位到1M地址的內(nèi)存,這稱作CPU的
尋址能力。
但是,8086處理器卻是16位的,因為:
-
運(yùn)算器一次最多可以處理 16 位的數(shù)據(jù);
-
寄存器的最大寬度為 16 位;
-
寄存器和運(yùn)算器之間的通路為 16 位;
也就是說:在8086處理器的內(nèi)部,能夠一次性
處理、傳輸、暫時存儲的最大長度是16位,因此,我們說它是
16 位結(jié)構(gòu)的 CPU。
主存儲器是什么?
計算機(jī)的本質(zhì)就是對數(shù)據(jù)的存儲和處理,那么參與計算的數(shù)據(jù)是從哪里來的呢?那就是一個稱作
存儲器(Storage 或 Memory)的物理器件。
從廣義上來說,只要能存儲數(shù)據(jù)的器件都可以稱作存儲器,比如:
硬盤、U盤等。
但是,在計算機(jī)
內(nèi)部,有一種專門與CPU相連接,用來存儲正在執(zhí)行的程序和數(shù)據(jù)的存儲器,一般稱作
內(nèi)存儲器或者主存儲器,簡稱:
內(nèi)存或主存。
內(nèi)存按照
字節(jié)來組織,單次訪問的
最小單位是1個字節(jié),這是最基本的
存儲單元。
每一個存儲單元,也就是一個字節(jié),都對應(yīng)著一個地址,如下圖所示:
CPU就通過
地址總線來確定:對內(nèi)存中的哪一個存儲單元中的數(shù)據(jù)進(jìn)行訪問。
第 1 個字節(jié)的地址是 0000H,第 2 個字節(jié)的地址是 0001H,后面以此類推。
圖中的這個內(nèi)存,最大存儲單元的地址是FFFFH,換算成十進(jìn)制就是65535,因此這個內(nèi)存的容量是65536字節(jié),也就是64 KB。
這里有一個
原子操作的問題可以考慮一下。
在Linux內(nèi)核代碼中,很多地方使用了原子操作,比如:互斥鎖的實現(xiàn)代碼。
為什么原子操作需要對變量的類型限制為int型呢?這就涉及到對內(nèi)存的讀寫操作了。
盡管內(nèi)存的
最小組成單位是
字節(jié),但是,經(jīng)過精心的設(shè)計和安排,不同位數(shù)的CPU,能夠按照
字節(jié)、字、雙字進(jìn)行訪問。
換句話說,僅通過單次訪問,16位處理器就能處理16位的二進(jìn)制數(shù),32位處理器就能處理32位的二進(jìn)制數(shù)。
寄存器是什么?
在CPU內(nèi)部,一些都是代表
0 或 1 的電信號,這些二進(jìn)制數(shù)字的
一組電信號出現(xiàn)在處理器內(nèi)部線路上,它們是一排高低電平的組合,代表著二進(jìn)制數(shù)中的每一位。
在處理器內(nèi)部,必須用一個稱為
寄存器的電路把這些數(shù)據(jù)鎖存起來。
因此,寄存器本質(zhì)上也屬于存儲器的一種。只不過它們位于
處理器的內(nèi)部,CPU訪問寄存器比訪問內(nèi)存的速度更快。
處理器總是很忙的,在它操作的過程中,所有數(shù)據(jù)在寄存器里面只能是
臨時存在一小會,然后再被送往別處,這就是為什么它被叫做
“寄存器”。
8086中的寄存器都是16位的,可以存放2個字節(jié),或者說1個字。
高字節(jié)在前(bit8 ~ bit15),
低字節(jié)在后(bit0 ~ bit7)。
8086中有下面這些寄存器:
剛才說了,這些寄存器都是16位的。由于需要與以前更古老的處理器兼容,其中的4個寄存器:
AX、BX、CX、DX 還可以當(dāng)成
2 個 8 位的寄存器來使用。
比如:AX代表一個16位的寄存器,AH、AL分別代表一個8位的寄存器。
mov AX, 5D 表示把 005D 送入 AX 寄存器(16 位)
mov AL, 5D 表示把 5D 送入 AL 寄存器(8 位)
三個總線
當(dāng)我們啟動一個應(yīng)用程序的時候,這個程序的
代碼和數(shù)據(jù)都被加載到
物理內(nèi)存中。
CPU無論是讀取指令,還是操作數(shù)據(jù),都需要與內(nèi)存進(jìn)行信息的交互:
-
確定存儲單元的地址(地址信息);
-
器件的選擇,讀或?qū)懙拿?控制信息);
-
讀或?qū)懙臄?shù)據(jù)(數(shù)據(jù)信息);
在計算機(jī)中,有專門連接CPU和其他芯片的數(shù)據(jù),稱為
總線。
從邏輯上來分類,包括下面3種總線:
地址總線:用來確定存儲單元的地址;
控制總線: CPU 對外部器件進(jìn)行控制;
數(shù)據(jù)總線: CPU 與內(nèi)存或其他器件之間傳送數(shù)據(jù);
8086 有20根地址線,稱作地址總線的
寬度,它可以尋址
2 的 20 次方個內(nèi)存單元。
同樣的道理,8086
數(shù)據(jù)總線的寬度是16,也就是一次性可以傳送16 bit的數(shù)據(jù)。
控制總線決定了CPU可以對外進(jìn)行多少種控制,決定了CPU對外部器件的
控制能力。
CPU 如何對內(nèi)存進(jìn)行尋址?
在Linux 2.6內(nèi)核代碼中,編譯器產(chǎn)生的地址叫做
虛擬地址(也稱作:
邏輯地址),這個邏輯地址經(jīng)過
段轉(zhuǎn)換之后,變成
線性地址,
線性地址再經(jīng)過
分頁轉(zhuǎn)換,就得到最終物理內(nèi)存上的
物理地址。
還記得文章開頭的那張
段描述符的表格嗎?
其中的代碼段和數(shù)據(jù)段描述符的
起始地址都是0x00000000,也就是說: 在數(shù)值上
虛擬地址和轉(zhuǎn)換后的
線性地址是相等的(稍后就會明白為什么是這樣)。
我們再來看看一下8086中更簡單的地址轉(zhuǎn)換。
剛才說到,內(nèi)存是一個
線性的存儲器件,CPU依賴地址來
定位每一個存儲單元。
對于8086 CPU來說,它有20根地址線,可以傳送20位地址,達(dá)到1MB的尋址能力。
但是8086又是16位的結(jié)構(gòu),在內(nèi)部一次性處理、傳輸、暫時存儲的地址
只有 16 位。
從內(nèi)部結(jié)構(gòu)來看,如果將地址從內(nèi)部簡單的發(fā)出到
地址總線上,只能送出16位的地址,這樣的話,尋址能力只有64KB。
那么應(yīng)該怎么才能充分利用20根地址線呢?
8086 CPU采用: 在內(nèi)部使用
兩個 16 位地址合成的方法,來形成一個20位的物理地址,如下所示:
第一個16位的地址稱為
段地址,第二個16位的地址稱為
偏移地址。
地址加法器采用下面的這個公式,來
“合成”得到一個20位的
物理地址:
物理地址 = 段地址 x 16 偏移地址
例如:我們編寫的程序,在加載到內(nèi)存中之后,放在一個內(nèi)存空間中。
CPU 在執(zhí)行這些指令的時候,把CS寄存器當(dāng)做
段寄存器,把IP寄存器當(dāng)做
偏移寄存器,然后計算 CS x 16 IP 的值,就得到了指令的
物理地址。
從以上的描述中可以看出:8086 CPU 似乎是因為寄存器
無法直接輸出20位的物理地址,不得已才使用這樣的地址合成方式。
其實
更本質(zhì)的原因是:8086 CPU 就是想通過
基地址 偏移量 的方式來對內(nèi)存進(jìn)行尋址(這里的基地址,就是段地址左移 4 位)。
也就是說,即使CPU有能力直接輸出一個20位的地址,它仍然可能會采用
基地址 偏移量的方式來進(jìn)行內(nèi)存尋址。
想一下:我們在Linux系統(tǒng)中編譯一個庫文件的時候,一般都會在編譯選項中添加-fPIC選項,表示編譯出來的動態(tài)庫是
地址無關(guān)的,在被加載到內(nèi)存時需要被重定位。
而基地址 偏移量的尋址模式,就為重定位提供了
底層支撐。
我們是如何控制 CPU 的?
CPU其實是一個
很純粹、很呆板的一個東西,它唯一做的事情就是:到
CS:IP 這兩個寄存器指定的內(nèi)存單元中取出一條
指令,然后執(zhí)行這條指令:
當(dāng)然了,還需要預(yù)先定義一套
指令集,在內(nèi)存中的指令區(qū)中,存儲的都必須是合法的指令,否則 CPU 就不認(rèn)識了。
每一條指令都是用某些特定的數(shù)(
指令碼)來指示CPU進(jìn)行特定的操作。
CPU認(rèn)識這些指令,一看到這些指令碼,CPU就知道這個指令碼后面還有幾個字節(jié)的操作數(shù)、需要進(jìn)行什么樣的操作。
例如:指令碼F4H 表示讓處理器停機(jī),當(dāng)CPU執(zhí)行這條指令的時候,就停止工作。
(其實這里說CPU已經(jīng)有點(diǎn)不準(zhǔn)確了,因為 CPU 是囊括了很多器件的一個整體,也許這里說CPU中的
執(zhí)行單元會更準(zhǔn)確些。)
另外有一點(diǎn)可以提前說一下:內(nèi)存中的一切都是數(shù)據(jù),至于把其中的哪一部分?jǐn)?shù)據(jù)
當(dāng)做指令來執(zhí)行,哪一部分?jǐn)?shù)據(jù)
當(dāng)做被指令操作的“變量”,這完全是由操作系統(tǒng)的設(shè)計者來規(guī)劃的。
在 8086 處理器的層面來說,只要是
CS:IP “指向”的內(nèi)存區(qū)域,都被當(dāng)做指令來執(zhí)行。
從以上描述可以看出:在CPU中,程序員能夠用指令讀寫的器件只有
寄存器,我們可以通過
改變寄存器中的內(nèi)容,來實現(xiàn)對 CPU 的控制。
更直白的說就是:我們可以通過改變
CS、IP 寄存器中的內(nèi)容,來控制CPU執(zhí)行目標(biāo)指令。
作為一名合格的嵌入式開發(fā)者,大家估計都配置過一些單片機(jī)里的寄存器,以達(dá)到一些功能定義、端口復(fù)用的目的,其實這些操作,都可以看做是我們對 CPU 的控制。
如果把 CPU 比作
木偶,那么 寄存器就是控制木偶的
繩索。
我們再把CPU與
工控領(lǐng)域的PLC編程進(jìn)行類比一下。
我們在拿到一個新的PLC設(shè)備之后,其中只有一個
運(yùn)行時(runtime),這個運(yùn)行時執(zhí)行的本職工作就是:
-
掃描所有的輸入端口,鎖存在輸入映象區(qū);
-
執(zhí)行一個運(yùn)算、控制邏輯,得到一些列輸出信號,鎖存到輸出映象區(qū);
-
把輸出映象區(qū)的信號,刷新到輸出端口;
在一個全新的 PLC 中,其中第 2 個步驟中需要的運(yùn)算、控制邏輯可能就不存在。
因此,單單一個runtime,PLC是無法完成一件有意義的工作的。
為了讓PLC完成一個具體的控制目標(biāo),我們還需要利用PLC廠家提供的
上位機(jī)編程軟件,開發(fā)一個運(yùn)算、控制邏輯程序,編程語言一般都是
梯形圖居多。
當(dāng)這個程序被下載到PLC中之后,它就可以控制
運(yùn)行時來做一些有意義的工作了。
我們可以簡單的認(rèn)為:
梯形圖就是用來控制 PLC 的運(yùn)行時。
對于CPU來說,想讓它執(zhí)行某個內(nèi)存單元的指令,只要修改寄存器CS和IP即可。
換句話說:只要對一個程序的內(nèi)存布局足夠的清楚,可以把 CPU 玩弄于股掌之間,讓它執(zhí)行哪里的代碼都可以。
CPU 執(zhí)行指令流程
現(xiàn)在我們已經(jīng)明白了地址轉(zhuǎn)換、內(nèi)存的尋址,距離CPU執(zhí)行一條指令需要的最小單元還剩下:
指令緩沖區(qū)和控制電路。
簡單來說:指令緩沖區(qū)用來
緩存從內(nèi)存中讀取的指令,控制電路用來協(xié)調(diào)各種器件對總線等資源的使用。
對于下面這張圖來說,它一共有4條指令:
以
第一條指令來舉例,它一共經(jīng)過5個步驟:
-
把 CS:IP 內(nèi)容送入地址加法器,計算得到 20 位的物理地址 20000H;
-
控制電路把 20 位的地址,送入到地址總線;
-
內(nèi)存中 20000H 單元處的指令 B8 23 01,經(jīng)過數(shù)據(jù)總線被送到指令緩沖區(qū);
-
指令偏移寄存器 IP 的值要加 3,指向下一條等待被執(zhí)行的偏移地址(因為指令碼 B8 代表當(dāng)前指令的長度是 3 個字節(jié));
-
執(zhí)行指令緩沖區(qū)中的指令: 把數(shù)值 0123H 送入寄存器 AX 中;
以上就是一條指令的執(zhí)行最基本步驟,當(dāng)然,現(xiàn)代處理器的指令執(zhí)行流程,比這里的要復(fù)雜的多得多。
------ End ------
萬丈高樓平地起!
這篇文章,僅僅描述了
CPU執(zhí)行一條
指令所需要的最小知識點(diǎn)。
下一篇文章,我們再繼續(xù)對內(nèi)存的
分段機(jī)制進(jìn)行更進(jìn)一步的窺探。