咱們都清楚,絕大多數編譯器都把彙編語言做爲中間語言,把彙編語言程序變成可運行的二進制文件早就解決了,因此如今的高級語言基本上只須要把本身翻譯成彙編語言就能夠了。git
彙編指令總共只有那麼多,大多數指令都是對數據進行操做,好比常見的數據傳送指令mov
。不難理解,被操做數據無非有三種形式,當即數,即用來表示常數值;寄存器,此時的數據即存放在指定寄存器中的內容;內存引用,它會根據計算出來的地址訪問某個內存位置。github
須要注意的是,到了彙編層級,就不像高級語言那樣隨隨便便int
就能和long
類型的數據相加減,他們在底層所佔有的字節是不同的,彙編指令是區分操做數據大小的,好比數據傳送指令,就有下面這些品種(x86-64 對數據傳送指令加了一條限制:兩個操做數不能都指向內存位置)。面試
對於棧,我想沒必要多講,IT 行業的同窗都清楚,它是一種線性數據結構,其中的數據遵循「先進後出」原則,寄存器%rsp
保存着棧頂元素的地址,即棧頂指針。一個程序要運行起來,離不開棧這種數據結構。數組
棧使用最多的就是彈棧popq
和壓棧pushq
操做。好比將一個四字值壓入棧中,棧頂指針首先要減 8(棧向下增加),而後將值寫到新的棧頂地址;而彈棧則須要先將棧頂數據讀出,而後再將棧指針加 8。因此pushq
和popq
指令就能夠表示爲下面的形式。數據結構
// 壓棧
subq $8, %rsp
movq %rbp, (%rsp)
// 彈棧
movq (%rsp), %rax
addq $8, %rsp
複製代碼
其餘還有算術、邏輯、加載有效地址、移位等等指令,能夠查閱相關文檔瞭解,不做過多介紹,彙編看起來確實枯燥乏味。架構
前面講的都是順序結構,咱們的程序中不可能只有順序結構,條件結構是必不可缺的元素,那麼彙編又是如何實現條件結構的呢?oop
首先你須要知道,除了整數寄存器,CPU 還維護着一組條件碼寄存器,咱們主要是瞭解如何把高級語言的條件結構轉換爲彙編語言,不去關注這些條件碼寄存器,只須要知道彙編能夠經過檢測這些寄存器來執行條件分支指令。post
下面是 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
的實現更加高效,使用了一種叫作跳轉表的數據結構(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
翻譯的方式,而具體採用哪一種方式,取決於編譯器優化的等級。
計算機就是用那麼幾條簡簡單單的指令就完成了各類複雜的操做,不得不折服於計算機科學家們的魅力。如今人工智能被炒的很火熱,而後人是事件、情感驅動的,而計算機是控制流驅動的,因此從架構上就決定了,馮諾依曼體系計算機實現的都是弱人工智能。