CPU執行的也不只是一條指令,一般一個程式包含很多條指令 因為有if…else、for這樣的條件和迴圈存在,這些指令也不會一路平直執行下去。 一個電腦程式是怎麼被分解成一條條指令來執行的呢 1 CPU如何執行指令 CPU里差不多幾百億個晶體管 實際上,一條條電腦指令執行起來非常複雜 好在CPU在 ...
CPU執行的也不只是一條指令,一般一個程式包含很多條指令
因為有if…else、for這樣的條件和迴圈存在,這些指令也不會一路平直執行下去。
一個電腦程式是怎麼被分解成一條條指令來執行的呢
1 CPU如何執行指令
CPU里差不多幾百億個晶體管
實際上,一條條電腦指令執行起來非常複雜
好在CPU在軟體層面已經為我們做好了封裝
對於程式員來說,我們只要知道,寫好的代碼變成了指令之後,是一條一條順序執行
不管幾百億的晶體管的背後是怎麼通過電路運轉起來的
邏輯上,我們可以認為,CPU其實就是由一堆寄存器組成的
而寄存器就是CPU內部,由多個觸發器(Flip-Flop)或者鎖存器(Latches)組成的簡單電路。
觸發器和鎖存器,其實就是兩種不同原理的數字電路組成的邏輯門
如果想要深入學習的話,可以學習數字電路的相關課程
N個觸發器或者鎖存器,就可以組成一個N位(Bit)的寄存器,能夠保存N位的數據
比方說,我們用的64位Intel伺服器,寄存器就是64位的
CPU里有很多種不同功能的
1.1 寄存器
寄存器(Register),是中央處理器內的其中組成部分。寄存器是有限存貯容量的高速存貯部件,它們可用來暫存指令、數據和地址。在中央處理器的控制部件中,包含的寄存器有指令寄存器(IR)和程式計數器。在中央處理器的算術及邏輯部件中,包含的寄存器有累加器。
在電腦體繫結構里,處理器中的寄存器是少量且速度快的電腦存儲器,藉由提供快速共同地訪問數值來加速電腦程式的運行:典型地說就是在已知時間點所作的之計算中間的數值。
寄存器是存儲器層次結構中的最頂端,也是系統操作數據的最快速途徑。寄存器通常都是以他們可以保存的比特數量來估量,舉例來說,一個8位寄存器或32位寄存器。寄存器現在都以寄存器數組的方式來實現,但是他們也可能使用單獨的觸發器、高速的核心存儲器、薄膜存儲器以及在數種機器上的其他方式來實現出來。
這個名詞通常都用來意指由一個指令之輸出或輸入可以直接索引到的寄存器組群。更適當的是稱他們為“架構寄存器”。例如,x86指令集定義八個32位寄存器的集合,但一個實現x86指令集的CPU可以包含比八個更多的寄存器。
1.1.1 PC寄存器(Program Counter Register)
亦稱指令地址寄存器(Instruction Address Register)
存放下一條需要執行的電腦指令的記憶體地址
1.1.2 指令寄存器(Instruction Register)
存放當前正在執行的指令
1.1.3 條件碼寄存器(Status Register)
用裡面的一個一個標記位(Flag),存放CPU進行算術或者邏輯計算的結果
CPU裡面還有更多用來存儲數據和記憶體地址的寄存器
這樣的寄存器通常一類裡面不止一個
通常根據存放的數據內容來給它們取名字,比如
- 常量寄存器
用來持有隻讀的數值(例如0、1、圓周率等等)。由於“其中的值不可更改”這一特殊性質,這些寄存器未必會有實體的硬體電路相對應,例如將從零常數寄存器讀的操作實現為接通目標寄存器的下拉電阻。
一般而言,即使真正在硬體中放置常數寄存器也未必會是出於體繫結構理論上的考慮,而很可能是由硬體描述語言為了簡化操作而自動生成的電路 - 整數寄存器
用來存儲整數數字(參考以下的浮點寄存器)。在某些簡單(或舊)的CPU,特別的數據寄存器是累加器,作為數學計算之用。 - 浮點數寄存器(FPRs)
用來存儲浮點數字。 - 向量寄存器
用來存儲由向量處理器運行SIMD指令所得到的數據。 - 地址寄存器
持有存儲器地址,以及用來訪問存儲器。在某些簡單/舊的CPU里,特別的地址寄存器是索引寄存器(可能出現一個或多個)。
有些寄存器既可以存放數據,又能存放地址,我們就叫它通用寄存器(GPRs)。
程式執行的時候,CPU會
- 根據PC寄存器里的地址
- 從記憶體裡面把需要執行的指令讀取到指令寄存器裡面執行
- 然後根據指令長度自增
- 開始順序讀取下一條指令
可以看到,一個程式的一條條指令,在記憶體里是連續保存的,也會一條條順序載入
而有些特殊指令,比如上一講我們講到J類指令,也就是跳轉指令,會修改PC寄存器裡面的地址值
這樣,下一條要執行的指令就不是從記憶體裡面順序載入的了
事實上,這些跳轉指令的存在,也是我們可以在寫程式的時候,使用
- if…else條件語句
- while/for迴圈語句
的原因
2 從if/else看程式的執行和跳轉
我們現在就來看一個包含if…else的簡單程式。
- test.c
用rand生成了一個隨機數r(0/1)
- 當r是0,我們把之前定義的變數a設成1
- 不然就設成2
我們把這個程式編譯成彙編代碼。你可以忽略前後無關的代碼,只關註於這裡的if…else條件判斷語句
- 對應的彙編代碼是這樣的
對於r == 0的條件判斷,被編譯成了cmp和jne兩條指令。
- cmp指令比較了前後兩個操作數的值
DWORD PTR 代表操作的數據類型是32位的整數
rbp-0x4則是一個寄存器的地址
第一個操作數就是從寄存器里拿到的變數r的值
第二個操作數0x0就是我們設定的常量0的16進位表示
cmp指令的比較結果,會存入到條件碼寄存器
狀態寄存器又名條件碼寄存器,它是電腦系統的核心部件——運算器的一部分
狀態寄存器用來存放兩類信息:
一類是體現當前指令執行結果的各種狀態信息(條件碼),如有無進位(CF位)、有無溢出(OF位)、結果正負(SF位)、結果是否為零(ZF位)、奇偶標誌位(P位)等
另一類是存放控制信息(PSW:程式狀態字寄存器),如允許中斷(IF位)、跟蹤標誌(TF位)等
有些機器中將PSW稱為標誌寄存器FR(Flag Register)。
如果比較結果 True,即 r == 0,就把零標誌條件碼(對應的條件碼是ZF,Zero Flag)設置為1
條件碼是CPU根據運算結果由硬體設置的位,體現當前指令執行結果的各種狀態信息
例如:算術運算產生的正、負、零或溢出等的結果。條件碼可被測試,作為分支運算的依據,此外,有些條件碼可被設置,例如對於最高位進位標誌C,可用指令對它置位和複位。
Intel的CPU下還有
- 進位標誌(CF,Carry Flag)
最近的操作使最高位產生了進位。可以用來檢查無符號操作數據的溢出。 - 符號標誌(SF,Sign Flag)
最近的操作得到的結果為負數。 - 溢出標誌(OF,Overflow Flag)
最近的操作導致一個補碼溢出--正溢出或負溢出
用在不同的判斷條件下。
cmp指令執行完成之後,PC寄存器會自增,開始執行下一條jne的指令
跟著的jne指令(jump if not equal),它會查看對應的零標誌位
如果為0,會跳轉到後面跟著的操作數4a的位置
4a,對應彙編代碼的行號,也就是else條件里的第一條指令
當跳轉發生,PC寄存器不再是自增變成下一條指令的地址,而被直接設置4a這個地址
這個時候,CPU再把4a地址里的指令載入到指令寄存器執行。
跳轉到執行地址為4a的指令,實際是一條mov指令
第一個操作數和前面的cmp指令一樣,是另一個32位整型的寄存器地址,以及對應的2的16進位值0x2
mov指令把2設置到對應的寄存器里去,相當於一個賦值操作
然後,PC寄存器里的值繼續自增,執行下一條mov指令。
這條mov指令的第一個操作數eax,代表累加寄存器
在中央處理器中,累加器 (accumulator) 是一種寄存器,用來儲存計算產生的中間結果。如果沒有像累加器這樣的寄存器,那麼在每次計算 (加法,乘法,移位等等) 後就必須要把結果寫回到 記憶體,也許馬上就得讀回來。然而存取主存的速度是比從算術邏輯單元到有直接路徑的累加器存取更慢。
第二個操作數0x0則是16進位的0的表示。這條指令其實沒有實際的作用,它的作用是一個占位符
if條件如果滿足,在賦值的mov指令執行完成之後,有一個jmp的無條件跳轉指令
跳轉的地址就是這一行的地址51
我們的main函數沒有設定返回值,而mov eax, 0x0 其實就是給main函數生成了一個預設的為0的返回值到累加器裡面
if條件裡面的內容執行完成之後也會跳轉到這裡,和else里的內容結束之後的位置是一樣的。
上一講我們講打孔卡的時候說到,讀取打孔卡的機器會順序地一段一段地讀取指令,然後執行。
執行完一條指令,它會自動地順序讀取下一條指令
如果執行的當前指令帶有跳轉的地址,比如往後跳10個指令,那麼機器會自動將卡片帶往後移動10個指令的位置,再來執行指令
同樣的,機器也能向前移動,去讀取之前已經執行過的指令
這也就是我們的while/for迴圈實現的原理。
如何通過if…else和goto來實現迴圈?
我們再看一段簡單的利用for迴圈的程式。我們迴圈自增變數i三次,三次之後,i>=3,就會跳出迴圈。整個程式,對應的Intel彙編代碼就是這樣的:
可以看到,對應的迴圈也是用1e這個地址上的cmp比較指令
和緊接著的jle條件跳轉指令來實現的
主要的差別在於,這裡的jle跳轉的地址,在這條指令之前的地址14,而非if…else編譯出來的跳轉指令之後
往前跳轉使得條件滿足的時候,PC寄存器會把指令地址設置到之前執行過的指令位置,重新執行之前執行過的指令,直到條件不滿足,順序往下執行jle之後的指令,整個迴圈才結束。
如果你看一長條打孔卡的話,就會看到卡片往後移動一段,執行了之後,又反向移動,去重新執行前面的指令。
jle和jmp指令,有點像程式語言裡面的goto命令,直接指定了一個特定條件下的跳轉位置
雖然我們在用高級語言開發程式的時候反對使用goto,但是實際在機器指令層面,無論是if…else…也好,還是for/while也好,都是用和goto相同的跳轉到特定指令位置的方式來實現的。
3 總結
學習了程式里的多條指令,究竟是怎麼樣一條一條被執行的
除了簡單地通過PC寄存器自增的方式順序執行外
條件碼寄存器會記錄下當前執行指令的條件判斷狀態
然後通過跳轉指令讀取對應的條件碼
修改PC寄存器內的下一條指令的地址
最終實現if…else以及for/while這樣的程式控制流程。
雖然我們可以用高級語言,可以用不同的語法,比如 if…else 這樣的條件分支,或者 while/for 這樣的迴圈方式,來實現不用的程式運行流程
但是回歸到電腦可以識別的機器指令級別,其實都只是一個簡單的地址跳轉而已,也就是一個類似於goto的語句。
想要在硬體層面實現這個goto語句,除了本身需要用來保存下一條指令地址,以及當前正要執行指令的PC寄存器、指令寄存器外
我們只需要再增加一個條件碼寄存器,來保留條件判斷的狀態。這樣簡簡單單的三個寄存器,就可以實現條件判斷和迴圈重覆執行代碼的功能。
4 推薦閱讀
- 《深入理解電腦系統》的第3章
詳細講解了C語言和Intel CPU的彙編語言以及指令的對應關係,以及Intel CPU的各種寄存器和指令集。
Intel指令集相對於之前的MIPS指令集要複雜一些
- 所有的指令是變長的
從1個位元組到15個位元組不等 - 即使是彙編代碼,還有很多針對操作數據的長度不同有不同的尾碼