Linux 從頭學 01:CPU 是如何執行一條指令的?


作 者:道哥,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:物聯網

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM