順序、條件、迴圈語句的底層解釋
我們都清楚,絕大多數編譯器都把組合語言作為中間語言,把組合語言程式變成可執行的二進位制檔案早就解決了,所以現在的高階語言基本上只需要把自己翻譯成組合語言就可以了。
彙編指令總共只有那麼多,大多數指令都是對資料進行操作,比如常見的資料傳送指令 mov
。不難理解,被操作資料無非有三種形式, 立即數 ,即用來表示常數值; 暫存器 ,此時的資料即存放在指定暫存器中的內容; 記憶體引用 ,它會根據計算出來的地址訪問某個記憶體位置。
需要注意的是,到了彙編層級,就不像高階語言那樣隨隨便便 int
就能和 long
型別的資料相加減,他們在底層所佔有的位元組是不一樣的,彙編指令是區分操作資料大小的,比如資料傳送指令,就有下面這些品種(x86-64 對資料傳送指令加了一條限制:兩個運算元不能都指向記憶體位置)。

壓棧與彈棧
對於棧,我想不必多講,IT 行業的同學都清楚,它是一種線性資料結構,其中的資料遵循“先進後出”原則,暫存器 %rsp
儲存著棧頂元素的地址,即棧頂指標。一個程式要執行起來,離不開棧這種資料結構。
棧使用最多的就是彈棧 popq
和壓棧 pushq
操作。比如將一個四字值壓入棧中,棧頂指標首先要減 8(棧向下增長),然後將值寫到新的棧頂地址;而彈棧則需要先將棧頂資料讀出,然後再將棧指標加 8。所以 pushq
和 popq
指令就可以表示為下面的形式。
// 壓棧 subq $8, %rsp movq %rbp, (%rsp) // 彈棧 movq (%rsp), %rax addq $8, %rsp 複製程式碼
其他還有算術、邏輯、載入有效地址、移位等等指令,可以查閱相關文件瞭解,不作過多介紹,彙編看起來確實枯燥乏味。
條件結構
前面講的都是順序結構,我們的程式中不可能只有順序結構,條件結構是必不可缺的元素,那麼彙編又是如何實現條件結構的呢?
首先你需要知道,除了整數暫存器,CPU 還維護著一組 條件碼暫存器 ,我們主要是瞭解如何把高階語言的條件結構轉換為組合語言,不去關注這些條件碼暫存器,只需要知道彙編可以通過檢測這些暫存器來執行條件分支指令。
if-else 語句
下面是 C 語言中的 if-else
語句的通用形式。
if(test-expr){ then-statement }else{ else-statement } 複製程式碼
組合語言通常會將上面的 C 語言模板轉換為下面的控制流形式,只要使用條件跳轉和無條件跳轉,這種形式的控制流就可以和彙編程式碼一一對應,我們以 C 語言形式給出。
t = test-expr; if(!t){ goto false; } then-statement; goto done; false: else-statement; done: 複製程式碼
但是這種條件控制轉移形式的程式碼在現代處理器上可能會很低效。原因是它無法事先確定要跳轉到哪個分支,我們的處理器通過 流水線 來獲得高效能,流水線的要求就是事先明確要執行的指令順序,而這種形式的程式碼只有當條件分支求值完成後,才能決定走哪一個分支。即使處理器採用了非常精密的分支預測邏輯,但是還是有錯誤預測的情況,一旦預測錯誤,那將會浪費 15 ~ 30 個時鐘週期,導致效能下降。
在流水線中,把一條指令分為多個階段,每個階段只執行所需操作的一小部分,比如取指令、確定指令型別、讀資料、運算、寫資料以及更新程式計數器。流水線通過重疊連續指令的步驟來獲得高效能,比如在取一條指令的同時,執行它前面指令的算術運算。所以如果事先不知道指令執行順序,那麼事先所做的預備工作就白乾了。
為了提高效能,可以改寫成使用條件資料傳送的程式碼,比如下面的例子。
v = test-expr ? then-expr : else-expr; // 使用條件資料傳送方法 v = then-expr; ve = else-expr; t = test-expr; if(!t){ v = ve; } 複製程式碼
這樣改寫,就能提高程式的效能了,但是並不是所有的條件表示式都可以使用條件傳送來編譯,一般只有當兩個表示式都很容易計算時,編譯器才會採用條件資料傳送的方式,大部分都還是使用條件控制轉移方式編譯。
switch 語句
switch
語句可以根據一個整數索引值進行多重分支,在處理具有多種可能結果的測試時,這種語句特別有用。為了讓 switch
的實現更加高效,使用了一種叫做 跳轉表 的資料結構(Radis 也是用的跳錶)。跳轉表是一個數組,表項 i 是一個程式碼段的地址,當開關情況數量比較多的時候,就會使用跳轉表。
我們舉個例子,還是採用 C 語言的形式表是控制流,要理解的是執行 switch
語句的關鍵步驟就是通過跳轉表來訪問程式碼的位置。
void switch_eg(long x, long n, long *dest){ long val = x; switch(n){ case 100: val *= 13; break; case 102: val += 10; case 103: val += 11; break; case 104: case 105: val *= val; break; default: val = 0; } *dest = val; } 複製程式碼
要注意的是,上面的程式碼中有的分支沒有 break
,這種問題在筆試中會經常遇到,沒有 break
會繼續執行下面的語句,即變成了順序執行。上面的程式碼會被翻譯為下面這種控制流。
void switch_eg(long x, long n, long *dest){ static void *jt[7] = { &&loc_A, &&loc_def, &&loc_B, &&loc_C, &&loc_D, &&loc_def, &&loc_D }; unsigned long index = n - 100; long val; if(index > 6){ goto loc_def; } goto *jt[index]; loc_A: val = x * 13; goto done; loc_B: x = x + 10; loc_C: val = x + 11; goto done; loc_D: val = x * x; goto done; loc_def: val = 0; done: *dest = val; } 複製程式碼
迴圈結構
C 語言中有 do-while
、 while
和 for
三種迴圈結構,它們的通用形式一般都長下面那樣。
// do-while do body-statement while(test-expr); // while while(test-expr) body-statement // for for(init-expr; test-expr; update-expr) body-statement 複製程式碼
do-while
的特點是 body-statement
一定會執行一次,所以我們可以將 do-while
翻譯成下面的控制流形式,很容易就能聯想到它的彙編形式。
loop: body-statement; t = test-expr; if(t){ goto loop; } 複製程式碼
while
迴圈我們給出兩種形式的控制流,其中一種包含 do-while
形式,如下所示。
// 第一種形式 t = test-expr; if(!t){ goto done; } do body-statement; while(test-expr); done: // 第二種形式 goto test; loop: body-statement; test: t = test-expr; if(t){ goto loop; } 複製程式碼
面試的時候,有的面試官會問你 for
迴圈的執行順序,現在深入理解了三種迴圈的機制,再也不怕面試官啦。 for
迴圈可以轉換成如下的 while
形式。
init-expr; while(test-expr){ body-statement; update-expr; } 複製程式碼
有了這種形式的 for
迴圈,我們只需要將其中的 while
部分再翻譯一下就好了,前文給出了兩種 while
翻譯的方式,而具體採用哪種方式,取決於編譯器優化的等級。
總結
計算機就是用那麼幾條簡簡單單的指令就完成了各種複雜的操作,不得不折服於電腦科學家們的魅力。現在人工智慧被炒的很火熱,然後人是事件、情感驅動的,而計算機是控制流驅動的,所以從架構上就決定了,馮諾依曼體系計算機實現的都是弱人工智慧。