天天看點

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

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

公衆号:【IOT物聯網小鎮】,專注于:C/C++、Linux作業系統、應用程式設計、物聯網、單片機和嵌入式開發等領域。

文章目錄

  • ​​【Linux 從頭學】是什麼​​
  • ​​古老的 Intel8086 處理器​​
  • ​​主存儲器是什麼?​​
  • ​​寄存器是什麼?​​
  • ​​三個總線​​
  • ​​CPU 如何對記憶體進行尋址?​​
  • ​​我們是如何控制 CPU 的?​​
  • ​​CPU 執行指令流程​​

【Linux 從頭學】是什麼

這兩年多以來,我的本職工作重心一直是在 x86 Linux

随着内容的不斷擴充,越發覺得之前很多基礎的東西都差不多忘記了,比如下面這張表(《深入了解 LINUX 核心》第 ​

​47​

​ 頁):

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

這張表描述了 ​

​Linux​

​ 系統中幾個段描述符資訊。

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

為什麼這幾個段的 Base 位址都是 ​

​0x00000000​

​?

為什麼 Limit 都是 ​

​0xfffff​

​?

為什麼它們的 Type 類型和優先級 DPL

如果沒有對 ​

​x86​

​ 平台的一些基礎知識的了解,要啃完這本書真的是挺費力氣的!

更要命的是,随着 ​

​Linux​

​ 核心代碼的體積不斷膨脹,最新的 5.13

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

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

​Linux​

​ 呢?!

即便是從 Linux 0.11

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

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

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

​Linux​

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

于是乎,我就有了一個想法:是否可以把這些書籍中,與 ​

​Linux​

​ 系統相關的内容進行一次重讀和整理,但絕不是簡單的知識搬運。

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

  1. 先确定最終目标的目标:學習 Linux 作業系統;
  2. 這幾本書寫的都是彙編語言,以及比較基礎的底層知識。我們會淡化彙編語言部分,把重點放在與 Linux 作業系統有關聯的原理部分;
  3. 不會嚴格按照書中的内容、順序來輸出文章,而是把幾本書中内容相關的部分放在一起學習、讨論;
  4. 有些内容,可以與 Linux 2.6 版本中的相關部分進行對比分析,這樣的話在以後學習 Linux 核心部分時,可以找到底層的支撐;
  5. 最後,希望我自己能堅持這個系列,也算是給自己的一個梳理吧。

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

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

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

現在就開始吧!

古老的 Intel8086 處理器

​8086​

​​ 是 ​

​Intel​

​​ 公司的第一款 ​

​16​

​​ 位處理器,誕生于 ​

​1978​

​ 年,應該比各位小夥伴的年齡都大一些。

在 ​

​Intel​

​​ 公司的所有處理器中,它占有很重要的地位,是整個 ​

​Intel 32​

​ 位架構處理器(IA-32)的開山鼻祖。

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

有些人會把處理器的位數與位址總線的位數搞混在一起!

我們知道,​

​CPU​

​ 在通路記憶體的時候,是通過位址總線來傳送實體位址的。

​8086 CPU​

​​ 有 ​

​20​

​​ 位的位址線,可以傳送 ​

​20​

​ 位位址。

每一根位址線都表示一個 ​

​bit​

​​,那麼 ​

​20​

​​ 個 ​

​bit​

​ 可以表示的最大值就是 2 的 20 次方。

也就是說:最大可以定位到 ​

​1M​

​​ 位址的記憶體,這稱作 ​

​CPU​

​ 的尋址能力。

但是,​

​8086​

​​ 處理器卻是 ​

​16​

​ 位的,因為:

  1. 運算器一次最多可以處理 16 位的資料;
  2. 寄存器的最大寬度為 16 位;
  3. 寄存器和運算器之間的通路為 16 位;

也就是說:在 ​

​8086​

​​ 處理器的内部,能夠一次性處理、傳輸、暫時存儲的最大長度是 ​

​16​

​ 位,是以,我們說它是 16 位結構的 CPU。

主存儲器是什麼?

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

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

但是,在計算機内部,有一種專門與 ​

​CPU​

​ 相連接配接,用來存儲正在執行的程式和資料的存儲器,一般稱作記憶體儲器或者主存儲器,簡稱:記憶體或主存。

記憶體按照位元組來組織,單次通路的最小機關是 ​

​1​

​ 個位元組,這是最基本的存儲單元。

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

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

​CPU​

​ 就通過位址總線來确定:對記憶體中的哪一個存儲單元中的資料進行通路。

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

圖中的這個記憶體,最大存儲單元的位址是 ​

​FFFF​

​​H,換算成十進制就是 ​

​65535​

​​,是以這個記憶體的容量是 ​

​65536​

​​ 位元組,也就是 ​

​64 KB​

​。

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

在 ​

​Linux​

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

為什麼原子操作需要對變量的類型限制為 ​

​int​

​ 型呢?這就涉及到對記憶體的讀寫操作了。

盡管記憶體的最小組成機關是位元組,但是,經過精心的設計和安排,不同位數的 ​

​CPU​

​,能夠按照位元組、字、雙字進行通路。

換句話說,僅通過單次通路,​

​16​

​​ 位處理器就能處理 ​

​16​

​​ 位的二進制數,​

​32​

​​ 位處理器就能處理 ​

​32​

​ 位的二進制數。

寄存器是什麼?

在 ​

​CPU​

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

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

是以,寄存器本質上也屬于存儲器的一種。隻不過它們位于處理器的内部,​

​CPU​

​ 通路寄存器比通路記憶體的速度更快。

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

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

​8086​

​​ 中的寄存器都是 ​

​16​

​​ 位的,可以存放 ​

​2​

​​ 個位元組,或者說 ​

​1​

​ 個字。高位元組在前(bit8 ~ bit15),低位元組在後(bit0 ~ bit7)。

​8086​

​ 中有下面這些寄存器:

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

剛才說了,這些寄存器都是 ​

​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 與記憶體或其他器件之間傳送資料;

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

8086 有 ​

​20​

​ 根位址線,稱作位址總線的寬度,它可以尋址 2 的 20 次方個記憶體單元。

同樣的道理,8086 資料總線的寬度是 ​

​16​

​​,也就是一次性可以傳送 ​

​16 bit​

​ 的資料。

控制總線決定了 ​

​CPU​

​​ 可以對外進行多少種控制,決定了 ​

​CPU​

​ 對外部器件的控制能力。

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

在 ​

​Linux 2.6​

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

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

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

其中的代碼段和資料段描述符的起始位址都是 ​

​0x00000000​

​,也就是說: 在數值上虛拟位址和轉換後的線性位址是相等的(稍後就會明白為什麼是這樣)。

我們再來看看一下 ​

​8086​

​ 中更簡單的位址轉換。

剛才說到,記憶體是一個線性的存儲器件,​

​CPU​

​ 依賴位址來定位每一個存儲單元。

對于 ​

​8086 CPU​

​​ 來說,它有 ​

​20​

​​ 根位址線,可以傳送 ​

​20​

​​ 位位址,達到 ​

​1MB​

​ 的尋址能力。

但是 ​

​8086​

​​ 又是 ​

​16​

​ 位的結構,在内部一次性處理、傳輸、暫時存儲的位址隻有 16 位。

從内部結構來看,如果将位址從内部簡單的發出到位址總線上,隻能送出 ​

​16​

​​ 位的位址,這樣的話,尋址能力隻有 ​

​64KB​

​。

那麼應該怎麼才能充分利用 ​

​20​

​ 根位址線呢?

​8086 CPU​

​​ 采用: 在内部使用兩個 16 位位址合成的方法,來形成一個 ​

​20​

​ 位的實體位址,如下所示:

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

第一個 ​

​16​

​​ 位的位址稱為段位址,第二個 ​

​16​

​ 位的位址稱為偏移位址。

位址加法器采用下面的這個公式,來“合成”得到一個 ​

​20​

​ 位的實體位址:

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

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

CPU 在執行這些指令的時候,把 ​

​CS​

​​ 寄存器當做段寄存器,把 ​

​IP​

​ 寄存器當做偏移寄存器,然後計算 CS x 16 + IP 的值,就得到了指令的實體位址。

從以上的描述中可以看出:8086 CPU 似乎是因為寄存器無法直接輸出 ​

​20​

​ 位的實體位址,不得已才使用這樣的位址合成方式。

其實更本質的原因是:8086 CPU 就是想通過 基位址 + 偏移量

也就是說,即使 ​

​CPU​

​​ 有能力直接輸出一個 ​

​20​

​ 位的位址,它仍然可能會采用 基位址 + 偏移量的方式來進行記憶體尋址。

想一下:我們在 ​

​Linux​

​​ 系統中編譯一個庫檔案的時候,一般都會在編譯選項中添加 ​

​-fPIC​

​ 選項,表示編譯出來的動态庫是位址無關的,在被加載到記憶體時需要被重定位。

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

我們是如何控制 CPU 的?

​CPU​

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

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

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

每一條指令都是用某些特定的數(指令碼)來訓示 ​

​CPU​

​ 進行特定的操作。

​CPU​

​​ 認識這些指令,一看到這些指令碼,​

​CPU​

​ 就知道這個指令碼後面還有幾個位元組的操作數、需要進行什麼樣的操作。

例如:指令碼 ​

​F4​

​​H 表示讓處理器停機,當 ​

​CPU​

​ 執行這條指令的時候,就停止工作。

(其實這裡說 ​

​CPU​

​​ 已經有點不準确了,因為 CPU 是囊括了很多器件的一個整體,也許這裡說 ​

​CPU​

​ 中的執行單元會更準确些。)

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

在 8086 處理器的層面來說,隻要是 CS:IP

從以上描述可以看出:在 ​

​CPU​

​ 中,程式員能夠用指令讀寫的器件隻有寄存器,我們可以通過改變寄存器中的内容,來實作對 CPU 的控制。

更直白的說就是:我們可以通過改變 CS、IP 寄存器中的内容,來控制 ​

​CPU​

​ 執行目标指令。

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

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

我們再把 ​

​CPU​

​​ 與 工控領域的 ​

​PLC​

​ 程式設計進行類比一下。

我們在拿到一個新的 ​

​PLC​

​ 裝置之後,其中隻有一個運作時(runtime),這個運作時執行的本職工作就是:

  1. 掃描所有的輸入端口,鎖存在輸入映象區;
  2. 執行一個運算、控制邏輯,得到一些列輸出信号,鎖存到輸出映象區;
  3. 把輸出映象區的信号,重新整理到輸出端口;
Linux 從頭學 01:CPU 是如何執行一條指令的?

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

是以,單單一個 ​

​runtime​

​​,​

​PLC​

​ 是無法完成一件有意義的工作的。

為了讓 ​

​PLC​

​​ 完成一個具體的控制目标,我們還需要利用 ​

​PLC​

​ 廠家提供的上位機程式設計軟體,開發一個運算、控制邏輯程式,程式設計語言一般都是梯形圖居多。

當這個程式被下載下傳到 ​

​PLC​

​ 中之後,它就可以控制運作時來做一些有意義的工作了。

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

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

對于 ​

​CPU​

​​ 來說,想讓它執行某個記憶體單元的指令,隻要修改寄存器 ​

​CS​

​​ 和 ​

​IP​

​ 即可。

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

CPU 執行指令流程

現在我們已經明白了位址轉換、記憶體的尋址,距離 ​

​CPU​

​ 執行一條指令需要的最小單元還剩下:指令緩沖區和控制電路。

簡單來說:指令緩沖區用來緩存從記憶體中讀取的指令,控制電路用來協調各種器件對總線等資源的使用。

對于下面這張圖來說,它一共有 ​

​4​

​ 條指令:

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

以第一條指令來舉例,它一共經過 ​

​5​

​ 個步驟:

  1. 把 CS:IP 内容送入位址加法器,計算得到 20 位的實體位址 20000H;
  2. 控制電路把 20 位的位址,送入到位址總線;
  3. 記憶體中 20000H 單元處的指令 B8 23 01,經過資料總線被送到指令緩沖區;
  4. 指令偏移寄存器 IP 的值要加 3,指向下一條等待被執行的偏移位址(因為指令碼 B8 代表目前指令的長度是 3 個位元組);
  5. 執行指令緩沖區中的指令: 把數值 0123H 送入寄存器 AX 中;

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

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

繼續閱讀