轉載自:Linuxer公眾號
作者:byeyear
有時候我們希望在C/C++程式碼中使用嵌入式彙編,因為C中沒有對應的函式或語法可用。比如我最近在ARM上寫FIR程式時,需要對最後的結果進行飽和處理,但gcc沒有提供ssat這樣的函式,於是不得不在C程式碼中嵌入彙編指令。
1. 入門
在C中嵌入彙編的最大問題是如何將C語言變數與指令運算元相關聯。當然,gcc都幫我們想好了。下麵是是一個簡單例子。
asm(“fsinx %1, %0”:”=f”(result):”f”(angle));
這裡我們不需要關註fsinx指令是乾啥的;只需要知道這條指令需要兩個浮點暫存器作為運算元。作為專職處理C語言的gcc編譯器,它是沒辦法知道fsinx這條彙編指令需要什麼樣的運算元的,這就要求程式猿告知gcc相關資訊,方法就是指令後面的”=f”和”f”,表示這是兩個浮點暫存器運算元。這被稱為運算元規則(constraint)。規則前面加上”=”表示這是一個輸出運算元,否則是輸入運算元。constraint後面括號內的是與該暫存器關聯的變數。這樣gcc就知道如何將這條嵌入式彙編陳述句轉成實際的彙編指令了:
-
fsinx:彙編指令名
-
%1, %0:彙編指令運算元
-
“=f”(result):運算元%0是一個浮點暫存器,與變數result關聯(對輸出運算元,“關聯”的意思就是說gcc執行完這條彙編指令後會把暫存器%0的內容送到變數result中)
-
“f”(angle):運算元%1是一個浮點暫存器,與變數angle關聯(對輸入運算元,“關聯”的意思是就是說gcc執行這條彙編指令前會先將變數angle的值讀取到暫存器%1中)
因此這條嵌入式彙編會轉換為至少三條彙編指令(非最佳化):
-
將angle變數的值載入到暫存器%1
-
fsinx彙編指令,源暫存器%1,標的暫存器%0
-
將暫存器%0的值儲存到變數result
當然,在高最佳化級別下上面的敘述可能不適用;比如源運算元可能本來就已經在某個浮點暫存器中了。
這裡我們也看到constraint前加”=”符號的意義:gcc需要知道這個運算元是在執行嵌入彙編前從變數載入到暫存器,還是在執行後從暫存器儲存到變數中。
常用的constraints有以下幾個(更多細節參見gcc手冊):
-
m 記憶體運算元
-
r 暫存器運算元
-
i 立即數運算元(整數)
-
f 浮點暫存器運算元
-
F 立即數運算元(浮點)
從這個慄子也可以看出嵌入式彙編的基本格式:
asm(“彙編指令”:”=輸出運算元規則”(關聯變數):”輸入運算元規則”(關聯變數));
輸出運算元必須為左值;這個顯然。
2. 多個運算元,或沒有輸出運算元
如果某個指令有多個輸入或輸出運算元怎麼辦?例如arm有很多指令是三運算元指令。這個時候用逗號分隔多個規則:
asm(“add %0, %1, %2”:”=r”(sum):”r”(a), “r”(b));
每條運算元規則按順序對應運算元%0, %1, %2。
對於沒有輸出運算元的情況,在彙編指令後就沒有輸出規則,於是就出現兩個連續冒號,後跟輸入規則。
3. 輸入-輸出(或讀-寫)運算元
有時候一個運算元既是輸入又是輸出,比如x86下的這條指令:
add %eax, %ebx
註意指令使用AT&T;格式而不是Intel格式。暫存器ebx同時作為輸入運算元和輸出運算元。對這樣的運算元,在規則前使用”+”字元:
asm(“add %1, %0” : “+r”(a) : “r”(b));
對應C語言陳述句a=a+b。
註意這樣的運算元不能使用”=”符號,因為gcc看到”=”符號會認為這是一個單輸出運算元,於是在將嵌入彙編轉換為真正彙編的時候就不會預先將變數a的值載入到暫存器%0中。
另一個辦法是將讀-寫運算元在邏輯上拆分為兩個運算元:
asm(“add %2, %0” : “=r”(a) : “0”(a), “r”(b));
對“邏輯”輸入運算元1指定數字規則”0”,表示這個邏輯運算元佔用和運算元0一樣的“位置”(佔用同一個暫存器)。這種方法的特點是可以將兩個“邏輯”運算元關聯到兩個不同的C語言變數上:
asm(“add %2, %0” : “=r”(c) : “0”(a), “r”(b));
對應於C程式陳述句c=a+b。
數字規則僅能用於輸入運算元,且必須取用到輸出運算元。拿上例來說,數字規則”0”位於輸入規則段,且取用到輸出運算元0,該數字規則自身佔用運算元計數1。
這裡要註意,透過同名C語言變數是無法保證兩個運算元佔用同一“位置”的。比如下麵這樣的寫法是不行的:
(錯誤寫法)asm(“add %2, %0”:”=r”(a):”r”(a), “r”(b));
4. 指定暫存器
有時候我們需要在指令中使用指定的暫存器;典型的慄子是系統呼叫,必須將系統呼叫碼和引數放在指定暫存器中。為了達到這個目的,我們要在宣告變數時使用擴充套件語法:
register int a asm(“%eax”) = 1; // statement 1
register int b asm(“%ebx”) = 2; // statement 2
asm(“add %1, %0” : “+r”(a) : “r”(b)); // statement 3
註意只有在執行彙編指令時能確定a在eax中,b在ebx中,其他時候a和b的存放位置是不可知的。
另外,在這麼用的時候要註意,防止statement 2在執行時改寫了eax。例如statement 2改成下麵這句:
register int b asm(“%ebx”) = func();
函式呼叫約定會將func()的傳回值放在eax裡,於是破壞了statement 1對a的賦值。這個時候可以先用一條陳述句將func傳回值放在臨時變數裡:
int t = func();
register int a asm(“%eax”) = 1; // statement 1
register int b asm(“%ebx”) = t; // statement 2
asm(“add %1, %0” : “+r”(a) : “r”(b)); // statement 3
5. 隱式改變暫存器
有的彙編指令會隱含修改一些不在指令運算元中的暫存器,為了讓gcc知道這個情況,將隱式改變暫存器規則列在輸入規則之後。下麵是VAX機上的慄子:
asm volatile(“movc3 %0,%1,%2”
: /* no outputs */
:”g”(from),”g”(to),”g”(count)
:”r0”,”r1”,”r2”,”r3”,”r4”,”r5”);
(movc3是一條字元塊移動(Move characters)指令)
這裡要註意的是輸入/輸出規則中列出的暫存器不能和隱含改變規則中的暫存器有交叉。比如在上面的慄子裡,規則“g”中就不能包含r0-r5。以指定暫存器語法宣告的變數,所佔用的暫存器也不能和隱含改變規則有交叉。這個應該好理解:隱含改變規則是告訴gcc有額外的暫存器需要照顧,自然不能和輸入/輸出暫存器有交集。
另外,如果你在指令裡顯式指定某個暫存器,那麼這個暫存器也必須列在隱式改變規則之中(有點繞了哈)。上面我們說過gcc自身是不瞭解彙編指令的,所以你在指令中顯式指定的暫存器,對gcc來說是隱式的,因此必須包含在隱式規則之中。另外,指令中的顯式暫存器前需要一個額外的%,比如%%eax。
6. volatile
asm volatile通知gcc你的彙編指令有side effect,千萬不要給最佳化沒了,比如上面的慄子。
如果你的指令只是做些計算,那麼不需要volatile,讓gcc可以最佳化它;除此以外,無腦給每個asm加上volatile或者是個好辦法。