作 者:道哥,10+年的嵌入式開發老兵。

公眾號:【IOT物聯網小鎮】,專注於:C/C++、Linux作業系統、應用程式設計、物聯網、微控制器和嵌入式開發等領域。 公眾號回覆【書籍】,獲取 Linux、嵌入式領域經典書籍。

轉 載:歡迎轉載文章,轉載需註明出處。

【Linux 從頭學】是什麼

這兩年多以來,我的本職工作重心一直是在 x86 Linux 系統這一塊,從驅動到中間層,再到應用層的開發。

隨著內容的不斷擴充套件,越發覺得之前很多基礎的東西都差不多忘記了,比如下面這張表(《深入理解 LINUX 核心》第 47 頁):

這張表描述了 Linux 系統中幾個段描述符資訊。

資料段和程式碼段,仔細看一下相關書籍就知道這些描述符代表什麼意思,但是:

為什麼這幾個段的 Base 地址都是 0x00000000

為什麼 Limit 都是 0xfffff

為什麼它們的 Type 型別和優先順序 DPL 又各不相同?

如果沒有對 x86 平臺的一些基礎知識的理解,要啃完這本書真的是挺費力氣的!

更要命的是,隨著 Linux 核心程式碼的體積不斷膨脹,最新的 5.13 版本壓縮檔已經是一百多兆了:

這麼一個龐然大物,如何下手才能真正的學好 Linux 呢?!

即便是從 Linux 0.11 版本開始,其中的很多程式碼看起來也是非常費勁的!

週末在整理一些吃灰的書籍時,發現幾本以前看過的好書: 王爽的《組合語言》,李忠的《從真實模式到保護模式》,馬朝暉翻譯的《組合語言程式設計》等等。

都是非常-非常-老的書籍,再次翻了一下,真心覺得內容寫得真好!

對一些概念、原理、設計思路的描述,清晰而透徹。

Linux 系統中的很多關於分段、記憶體、暫存器相關的設計,都可以在這些書籍中找到基礎支撐。

於是乎,我就有了一個想法:是否可以把這些書籍中,與 Linux 系統相關的內容進行一次重讀和整理,但絕不是簡單的知識搬運。

考慮了一下,大概有下面幾個想法:

  1. 先確定最終目標的目標:學習 Linux 作業系統;

  2. 這幾本書寫的都是組合語言,以及比較基礎的底層知識。我們會淡化組合語言部分,把重點放在與 Linux 作業系統有關聯的原理部分;

  3. 不會嚴格按照書中的內容、順序來輸出文章,而是把幾本書中內容相關的部分放在一起學習、討論;

  4. 有些內容,可以與 Linux 2.6 版本中的相關部分進行對比分析,這樣的話在以後學習 Linux 核心部分時,可以找到底層的支撐;

  5. 最後,希望我自己能堅持這個系列,也算是給自己的一個梳理吧。

一句話:以基礎知識為主!

作為開篇第一章,本文將會描述下面這張圖的執行步驟:

現在就開始吧!

古老的 Intel8086 處理器

8086Intel 公司的第一款 16 位處理器,誕生於 1978 年,應該比各位小夥伴的年齡都大一些。

Intel 公司的所有處理器中,它佔有很重要的地位,是整個 Intel 32 位架構處理器(IA-32)的開山鼻祖。

那麼,問題來了,什麼叫 16 位的處理器?

有些人會把處理器的位數與地址匯流排的位數搞混在一起!

我們知道,CPU 在訪問記憶體的時候,是通過地址匯流排來傳送實體地址的。

8086 CPU20 位的地址線,可以傳送 20 位地址。

每一根地址線都表示一個 bit,那麼 20bit 可以表示的最大值就是 2 的 20 次方。

也就是說:最大可以定位到 1M 地址的記憶體,這稱作 CPU 的定址能力。

但是,8086 處理器卻是 16 位的,因為:

  1. 運算器一次最多可以處理 16 位的資料;

  2. 暫存器的最大寬度為 16 位;

  3. 暫存器和運算器之間的通路為 16 位;

也就是說:在 8086 處理器的內部,能夠一次性處理、傳輸、暫時儲存的最大長度是 16 位,因此,我們說它是 16 位結構的 CPU。

主儲存器是什麼?

計算機的本質就是對資料的儲存和處理,那麼參與計算的資料是從哪裡來的呢?那就是一個稱作 儲存器(Storage 或 Memory)的物理器件。

從廣義上來說,只要能儲存資料的器件都可以稱作儲存器,比如:硬碟、U盤等。

但是,在計算機內部,有一種專門與 CPU 相連線,用來儲存正在執行的程式和資料的儲存器,一般稱作記憶體儲器或者主儲存器,簡稱:記憶體或主存。

記憶體按照位元組來組織,單次訪問的最小單位是 1 個位元組,這是最基本的儲存單元。

每一個儲存單元,也就是一個位元組,都對應著一個地址,如下圖所示:

CPU 就通過地址匯流排來確定:對記憶體中的哪一個儲存單元中的資料進行訪問。

第 1 個位元組的地址是 0000H,第 2 個位元組的地址是 0001H,後面以此類推。

圖中的這個記憶體,最大儲存單元的地址是 FFFFH,換算成十進位制就是 65535,因此這個記憶體的容量是 65536 位元組,也就是 64 KB

這裡有一個原子操作的問題可以考慮一下。

Linux 核心程式碼中,很多地方使用了原子操作,比如:互斥鎖的實現程式碼。

為什麼原子操作需要對變數的型別限制為 int 型呢?這就涉及到對記憶體的讀寫操作了。

儘管記憶體的最小組成單位是位元組,但是,經過精心的設計和安排,不同位數的 CPU,能夠按照位元組、字、雙字進行訪問。

換句話說,僅通過單次訪問,16 位處理器就能處理 16 位的二進位制數,32 位處理器就能處理 32 位的二進位制數。

暫存器是什麼?

CPU 內部,一些都是代表 0 或 1 的電訊號,這些二進位制數字的一組電訊號出現在處理器內部線路上,它們是一排高低電平的組合,代表著二進位制數中的每一位。

在處理器內部,必須用一個稱為暫存器的電路把這些資料鎖存起來。

因此,暫存器本質上也屬於儲存器的一種。只不過它們位於處理器的內部,CPU 訪問暫存器比訪問記憶體的速度更快。

處理器總是很忙的,在它操作的過程中,所有資料在暫存器裡面只能是臨時存在一小會,然後再被送往別處,這就是為什麼它被叫做“暫存器”。

8086 中的暫存器都是 16 位的,可以存放 2 個位元組,或者說 1 個字。高位元組在前(bit8 ~ bit15),低位元組在後(bit0 ~ bit7)。

8086 中有下面這些暫存器:

剛才說了,這些暫存器都是 16 位的。由於需要與以前更古老的處理器相容,其中的 4 個暫存器:AX、BX、CX、DX 還可以當成 2 個 8 位的暫存器來使用。

比如:AX 代表一個 16 位的暫存器,AH、AL 分別代表一個 8 位的暫存器。

mov AX, 5D  表示把 005D 送入 AX 暫存器(16 位)
mov AL, 5D 表示把 5D 送入 AL 暫存器(8 位)

三個匯流排

當我們啟動一個應用程式的時候,這個程式的程式碼和資料都被載入到實體記憶體中。

CPU 無論是讀取指令,還是操作資料,都需要與記憶體進行資訊的互動:

  1. 確定儲存單元的地址(地址資訊);

  2. 器件的選擇,讀或寫的命令(控制資訊);

  3. 讀或寫的資料(資料資訊);

在計算機中,有專門連線 CPU 和其他晶片的資料,稱為匯流排。

從邏輯上來分類,包括下面 3 種匯流排:

地址匯流排:用來確定儲存單元的地址;

控制匯流排: CPU 對外部期間進行控制;

資料匯流排: CPU 與記憶體或其他器件之間傳送資料;

8086 有 20 根地址線,稱作地址匯流排的寬度,它可以定址 2 的 20 次方個記憶體單元。

同樣的道理,8086 資料匯流排的寬度是 16,也就是一次性可以傳送 16 bit 的資料。

控制匯流排決定了 CPU 可以對外進行多少種控制,決定了 CPU 對外部器件的控制能力。

CPU 如何對記憶體進行定址?

Linux 2.6 核心程式碼中,編譯器產生的地址叫做虛擬地址(也稱作:邏輯地址),這個邏輯地址經過段轉換之後,變成線性地址,線性地址再經過分頁轉換,就得到最終實體記憶體上的實體地址。

還記得文章開頭的那張段描述符的表格嗎?

其中的程式碼段和資料段描述符的起始地址都是 0x00000000,也就是說: 在數值上虛擬地址和轉換後的線性地址是相等的(稍後就會明白為什麼是這樣)。

我們再來看看一下 8086 中更簡單的地址轉換。

剛才說到,記憶體是一個線性的儲存器件,CPU 依賴地址來定位每一個儲存單元。

對於 8086 CPU 來說,它有 20 根地址線,可以傳送 20 位地址,達到 1MB 的定址能力。

但是 8086 又是 16 位的結構,在內部一次性處理、傳輸、暫時儲存的地址只有 16 位。

從內部結構來看,如果將地址從內部簡單的發出到地址總線上,只能送出 16 位的地址,這樣的話,定址能力只有 64KB

那麼應該怎麼才能充分利用 20 根地址線呢?

8086 CPU 採用: 在內部使用兩個 16 位地址合成的方法,來形成一個 20 位的實體地址,如下所示:

第一個 16 位的地址稱為段地址,第二個 16 位的地址稱為偏移地址。

地址加法器採用下面的這個公式,來“合成”得到一個 20 位的實體地址:

實體地址 = 段地址 x 16 + 偏移地址

例如:我們編寫的程式,在載入到記憶體中之後,放在一個記憶體空間中。

CPU 在執行這些指令的時候,把 CS 暫存器當做段暫存器,把 IP 暫存器當做偏移暫存器,然後計算 CS x 16 + IP 的值,就得到了指令的實體地址。

從以上的描述中可以看出:8086 CPU 似乎是因為暫存器無法直接輸出 20 位的實體地址,不得已才使用這樣的地址合成方式。

其實更本質的原因是:8086 CPU 就是想通過 基地址 + 偏移量 的方式來對記憶體進行定址(這裡的基地址,就是段地址左移 4 位)。

也就是說,即使 CPU 有能力直接輸出一個 20 位的地址,它仍然可能會採用 基地址 + 偏移量的方式來進行記憶體定址。

想一下:我們在 Linux 系統中編譯一個庫檔案的時候,一般都會在編譯選項中新增 -fPIC 選項,表示編譯出來的動態庫是地址無關的,在被載入到記憶體時需要被重定位。

而基地址+偏移量的定址模式,就為重定位提供了底層支撐。

我們是如何控制 CPU 的?

CPU 其實是一個很純粹、很呆板的一個東西,它唯一做的事情就是:到 CS:IP 這兩個暫存器指定的記憶體單元中取出一條指令,然後執行這條指令:

當然了,還需要預先定義一套指令集,在記憶體中的指令區中,儲存的都必須是合法的指令,否則 CPU 就不認識了。

每一條指令都是用某些特定的數(指令碼)來指示 CPU 進行特定的操作。

CPU 認識這些指令,一看到這些指令碼,CPU 就知道這個指令碼後面還有幾個位元組的運算元、需要進行什麼樣的操作。

例如:指令碼 F4H 表示讓處理器停機,當 CPU 執行這條指令的時候,就停止工作。

(其實這裡說 CPU 已經有點不準確了,因為 CPU 是囊括了很多器件的一個整體,也許這裡說 CPU 中的執行單元會更準確些。)

另外有一點可以提前說一下:記憶體中的一切都是資料,至於把其中的哪一部分資料當做指令來執行,哪一部分資料當做被指令操作的“變數”,這完全是由作業系統的設計者來規劃的。

在 8086 處理器的層面來說,只要是 CS:IP “指向”的記憶體區域,都被當做指令來執行。

從以上描述可以看出:在 CPU 中,程式設計師能夠用指令讀寫的器件只有暫存器,我們可以通過改變暫存器中的內容,來實現對 CPU 的控制。

更直白的說就是:我們可以通過改變 CS、IP 暫存器中的內容,來控制 CPU 執行目標指令。

作為一名合格的嵌入式開發者,大家估計都配置過一些微控制器裡的暫存器,以達到一些功能定義、埠複用的目的,其實這些操作,都可以看做是我們對 CPU 的控制。

如果把 CPU 比作木偶,那麼 暫存器就是控制木偶的繩索。

我們再把 CPU 與 工控領域的 PLC 程式設計進行類比一下。

我們在拿到一個新的 PLC 裝置之後,其中只有一個執行時(runtime),這個執行時執行的本職工作就是:

  1. 掃描所有的輸入埠,鎖存在輸入映象區;

  2. 執行一個運算、控制邏輯,得到一些列輸出訊號,鎖存到輸出映象區;

  3. 把輸出映象區的訊號,重新整理到輸出埠;

在一個全新的 PLC 中,其中第 2 個步驟中需要的運算、控制邏輯可能就不存在。

因此,單單一個 runtimePLC 是無法完成一件有意義的工作的。

為了讓 PLC 完成一個具體的控制目標,我們還需要利用 PLC 廠家提供的上位機程式設計軟體,開發一個運算、控制邏輯程式,程式語言一般都是梯形圖居多。

當這個程式被下載到 PLC 中之後,它就可以控制執行時來做一些有意義的工作了。

我們可以簡單的認為:梯形圖就是用來控制 PLC 的執行時。

對於 CPU 來說,想讓它執行某個記憶體單元的指令,只要修改暫存器 CSIP 即可。

換句話說:只要對一個程式的記憶體佈局足夠的清楚,可以把 CPU 玩弄於股掌之間,讓它執行哪裡的程式碼都可以。

CPU 執行指令流程

現在我們已經明白了地址轉換、記憶體的定址,距離 CPU 執行一條指令需要的最小單元還剩下:指令緩衝區和控制電路。

簡單來說:指令緩衝區用來快取從記憶體中讀取的指令,控制電路用來協調各種器件對匯流排等資源的使用。

對於下面這張圖來說,它一共有 4 條指令:

以第一條指令來舉例,它一共經過 5 個步驟:

  1. 把 CS:IP 內容送入地址加法器,計算得到 20 位的實體地址 20000H;

  2. 控制電路把 20 位的地址,送入到地址匯流排;

  3. 記憶體中 20000H 單元處的指令 B8 23 01,經過資料匯流排被送到指令緩衝區;

  4. 指令偏移暫存器 IP 的值要加 3,指向下一條等待被執行的偏移地址(因為指令碼 B8 代表當前指令的長度是 3 個位元組);

  5. 執行指令緩衝區中的指令: 把數值 0123H 送入暫存器 AX 中;

以上就是一條指令的執行最基本步驟,當然,現代處理器的指令執行流程,比這裡的要複雜的多得多。

------ End ------

萬丈高樓平地起!

這篇文章,僅僅描述了 CPU 執行一條指令所需要的最小知識點。

下一篇文章,我們再繼續對記憶體的分段機制進行更進一步的窺探。

推薦閱讀

專輯0:精選文章

專輯1:C 語言

專輯2:應用程式設計

專輯3:Linux 作業系統

專輯4:物聯網

星標公眾號,能更快找到我!