歡迎光臨
每天分享高質量文章

gcc內嵌彙編詳解

轉載自: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中)

因此這條嵌入式彙編會轉換為至少三條彙編指令(非最佳化):

  1. 將angle變數的值載入到暫存器%1

  2. fsinx彙編指令,源暫存器%1,標的暫存器%0

  3. 將暫存器%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或者是個好辦法。


贊(0)

分享創造快樂