1. 程式人生 > >翻譯 | “擴展asm”——用C表示操作數的匯編程序指令

翻譯 | “擴展asm”——用C表示操作數的匯編程序指令

逗號 repr 告訴 ica 輸出格式 就是 發生 防止 ESS

技術分享圖片

本文翻譯自GNU關於GCC7.2.0版本的官方說明文檔,第6.45.2小節。供查閱討論,如有不當處敬請指正……

通過擴展asm,可以讓你在匯編程序中使用C中的變量,並從匯編代碼跳轉到C語言標號。在匯編程序模板之後,擴展asm語法使用冒號“:”來界定操作參數。

asm [volatile] ( 匯編程序模板

: 輸出操作數

: 輸入操作數

: 互斥項 )

asm [volatile] goto ( 匯編程序模板

:

: 輸入操作數

: 互斥項

: 跳轉標號)

“asm”是GNU擴展關鍵詞,如果要使用“-ansi”或“-std”選項編譯代碼,使用“__asm__”代替“asm”(這兩個選項會禁用一些特定的關鍵詞如“asm”、“typeof”、“inline”等)。

限定符

  • volatile:使用擴展asm語句的主要目的是控制輸入值使之產生相應的輸出值。然而,這些asm語句也可能產生副作用,這時也許需要使用“volatile”限定符來禁用某些優化。

  • goto:該限定符告訴編譯器匯編聲明中的指令可能會跳轉到所列出的跳轉標號中去。

參數

  • 匯編程序模板:匯編樣式的字符串,它由固定文本(匯編指令)和標識符組成,標識符引用輸入、輸出和跳轉參數。

  • 輸出操作數:由匯編程序模板更改的以逗號分隔的C變量列表。允許使用空列表。

  • 輸入操作數:被匯編程序模板讀取的以逗號分隔的C表達式列表。允許使用空列表。

  • 互斥項:由匯編程序模板更改的輸出參數之外的寄存器或其它值列表。允許使用空列表。

  • 跳轉標號:當你使用跳轉指令,該部分包含了匯編程序模板中可能跳轉到的C標號。但一個擴展asm語句只能到跳轉列表中的標號,不能跳轉到另一個擴展asm語句,GCC的優化器並不知道這些跳轉,所以也就無法決定如何優化。

所有輸入、輸出以及跳轉操作數的總數被限制在30以內。

附註

asm語句使得你可以在C代碼中直接嵌入匯編代碼,這可以幫助你在時間敏感的代碼中獲得最大化的性能,或是訪問一些C語言不易表達的匯編指令。

註意擴展asm語句必須在函數內部,只有基礎匯編才能存在於函數外。用 naked attribute 聲明過的函數也需要基礎匯編。

“asm”語法用處多樣,它可幫助你將asm語句看作是一連串將輸入參數轉化為輸出參數的底層指令。一個簡單的i386處理器使用“asm”的例子如下:

int src = 1;

int dst;

asm ("mov %1, %0\n\t"

"add $1, %0"

: "=r" (dst)

: "r" (src));

printf("%d\n", dst);

該段代碼將src拷貝到dst,並對dst加1。

1. Volatile(易變的)

GCC優化器如果認定輸出變量並無必要,它有時會將asm語句丟棄。同樣,如果優化器相信某段代碼始終返回相同的值,它會將該段代碼移到循環外部。使用volatile限定符可以禁止這類優化,包含asm跳轉語句在

內的無輸出操作數的asm語句,都是默認使用了volatile的。

這段i386代碼展示了一個沒有使用volatile限定符的例子。假設它用於斷言檢查,其中使用“asm”實現驗證;另外dwRes沒有在它處被引用。結果就是,優化器丟棄了該asm語句,接著移除了整個DoCheck程序。

void DoCheck(uint32_t dwSomeValue)

{

uint32_t dwRes;

// Assumes dwSomeValue is not zero.

asm ("bsfl %1,%0"

: "=r" (dwRes)

: "r" (dwSomeValue)

: "cc");

assert(dwRes > 3);

}

當你不需要允許優化器來產生高效的代碼時,可以省略volatile關鍵詞。

下面展示了一個優化器能夠識別出輸入變量dwSomeValue在函數執行過程中從未改變的例子,因而優化器將該asm語句移至循環外部以產生更高效的代碼。同樣,使用volatile可以禁止這類優化。

void do_print(uint32_t dwSomeValue)

{

uint32_t dwRes;

for (uint32_t x=0; x < 5; x++)

{

// Assumes dwSomeValue is not zero.

asm ("bsfl %1,%0"

: "=r" (dwRes)

: "r" (dwSomeValue)

: "cc");

printf("%u: %u %u\n", x, dwSomeValue, dwRes);

}

}

下面展示了一個你需要使用volatile限定符的例子。它使用x86的rdtsc 指令來讀取計算機的時間戳計數器。如果不使用volatile,優化器將可能會認為該asm語句段始終返回相同的值,進而在第二次調用時將其優化掉。

uint64_t msr;

asm volatile ( "rdtsc\n\t" // Returns the time in EDX:EAX.

"shl $32, %%rdx\n\t" // Shift the upper bits left.

"or %%rdx, %0" // ‘Or‘ in the lower bits.

: "=a" (msr)

:

: "rdx");

printf("msr: %llx\n", msr);

// Do other work...

// Reprint the timestamp

asm volatile ( "rdtsc\n\t" // Returns the time in EDX:EAX.

"shl $32, %%rdx\n\t" // Shift the upper bits left.

"or %%rdx, %0" // ‘Or‘ in the lower bits.

: "=a" (msr)

:

: "rdx");

printf("msr: %llx\n", msr);

在上面的例子中,GCC優化器不會把這個asm語句當作non-volatile代碼看待,就算認為前一次調用的結果依然有效,也不會將它移到循環外或是刪除。

註意:編譯器依然可能將帶有volatile的asm語句和其它代碼關聯起來,包括做指令交叉。例如,在很多目標處理器中,由系統寄存器控制浮點操作數的取整,即使設置帶volatile的asm語句,它也不一定能有效工作,就像下面PowerPC 的一個例子:

asm volatile ("mtfsf 255,%1" : "=X" (sum) : "f" (fpenv));

sum = x + y;

編譯器可能會將加法移至asm語句語句之前。為了讓它能正確執行,在後面加上一個變量引用使加法語句對asm語句語句產生人為的依賴關系,例如:

asm volatile ("mtfsf 255,%1" : "=X" (sum) : "f" (fpenv));

sum = x + y;

在特定的情況下,GCC可能會在優化時拷貝(或移除拷貝)你的匯編代碼,如果你的匯編代碼定義了符號或標號,這可能會在編譯時造成多倍的符號錯誤。使用“%=”會幫助你解決這個問題。

2. Assembler Template(匯編模板)

匯編程序模板是一個含有匯編指令的字符串。編譯器用對應的輸入、輸出和跳轉標號替換模板中的標識符,然後將結果字符串交給匯編器。該字符串可以包含任何匯編器可以識別的內容。GCC不負責解析匯編指令,也不知道它們的含義以及它們是否為有效的匯編輸入,但卻會統計語句的個數。

你也許會把多條匯編指令放在一個asm字符串內,之間用匯編代碼中常用的符號分隔。在多數場合,分隔的辦法是重起一行,再加上一個tab制表符對應到指令區域(用“\n\t”表示)。一些匯編器接受用分號表示行分隔,但要註意的是有些匯編器也把分號當作註釋的開始。

即使使用了volatile限定符,在編譯之後也不要指望一組asm語句可以保持絕對的連續。如果有特定的指令需要在輸出端保持連續,可以將它們放入單獨的一個asm語句中。

如果不通過輸入/輸出操作數來訪問C程序中的數據(例如在asm語句中直接使用全局符號),可能不會獲得期望的結果。同樣地,直接在asm語句中調用函數,需要你對目標匯編器和ABI(Application Binary Interface,應用程序二進制接口)有足夠的了解。

由於GCC並不解析asm語句,它無法看到asm語句中引用的符號,除非它們在輸入、輸出或跳轉操作數中被列出,否則可能導致GCC把這些符號當作未被引用的內容而丟棄掉。

特殊格式字符串

除了輸入、輸出以及跳轉操作數所描述的標識符之外,一下這些標識符在asm語句中具有特殊的含義:

  • “%%”:在匯編代碼中輸出一個“%”。

  • “%=”:為整個編譯代碼中asm語句的每一個實例輸出一個唯一的數字。當單個模板產生了本地標號,並被多次引用時,這一選項非常有用。

  • “%{”、“%|”、“%}”:分別在匯編代碼中輸出“{”、“|”、“}”。當為非轉義字符時,它們對表現多種匯編格式具有特殊意義。

匯編模板中的多種匯編格式

以x86平臺為例,GCC支持多種匯編格式。-masm選項控制GCC使用何種匯編格式,內聯匯編默認使用缺省值。目標指定文件中的-masm選項包含了可支持格式的列表,如未指定則使用缺省值。當匯編代碼使用某種格式能正常工作,卻很有可能在使用另一種格式編譯時失敗,這時這一信息就變得很重要了。

如果你的代碼需要支持多種匯編格式(例如,你要編寫一個公共的頭文件來支持多種不同的編譯選項),使用如下的結構形式:

{ 格式0 | 格式1 | 格式2... }

使用格式#0編譯時,這種結構輸出格式0代碼進行編譯,使用格式#1編譯時,輸出格式1代碼進行編譯……如果代碼分支的條目少於編譯器支持的格式,這種結構將輸出空。

例如,假設x86編譯器支持兩種格式(“att”和“intel”),一個匯編模板如下:

"bt{l %[Offset],%[Base] | %[Base],%[Offset]}; jc %l2"

它等價於其中的一句:

"btl %[Offset],%[Base] ; jc %l2" /* att dialect */

"bt %[Base],%[Offset]; jc %l2" /* intel dialect */

對於同一編譯器,下面的代碼:

"xchg{l}\t{%%}ebx, %1"

等價於其中的一句:

"xchgl\t%%ebx, %1" /* att dialect */

"xchg\tebx, %1" /* intel dialect */

不支持格式間的嵌套!

3. Output Operands(輸出操作數)

一個asm語句有零到多個輸出操作數,用C變量表示並在匯編代碼中被修改。

在下面這個i386平臺的例子中,old(與匯編模板中的%0對應)和*Base(%1)是輸出,offset(%2)是輸入。

bool old;

__asm__ ("btsl %2,%1\n\t" // Turn on zero-based bit #Offset in Base.

"sbb %0,%0" // Use the CF to calculate old.

: "=r" (old), "+rm" (*Base)

: "Ir" (Offset)

: "cc");

return old;

操作數間用逗號分隔,每個操作數的格式如下:

[ [asmSymbolicName] ] constraint (cvariablename)

asmSymbolicName(匯編標識符名)

為操作數指定一個標識符,在匯編模板中用方括號將標識符名括起來(如“%[Value]”)。名稱的範圍是有效的匯編語句,也包括任何C變量名以及周邊已經定義過的名字。一個asm語句中的兩個操作數不能使用相同的標識符。

當不使用匯編標識符名時,可用asm語句中操作數的位置(從0開始)代替。例如:假設有三個輸出操作數,在匯編模板中使用“%0”表示第一個,“%1”表示第二個,“%2”表示第三個。

constraint(約束)

用於約束操作數放置位置的字符串。

輸出約束必須以“=”(可覆蓋已存在值)或“+”(讀寫)開頭。當使用“=”時,不要假定輸出的位置就位於已存在的匯編輸入中,除非該操作數和輸入綁定在一起。

在前綴之後,還需要一到多個附加約束來描述操作值存放的位置。常用的約束有“r”表示寄存器,“m”表示內存。當你列出多於一個位置時(如“=rm”),編譯器基於上下文選擇效率最優的一個。如果你列出了

所有asm語句支持的情況,則表示許可優化器選擇最優可能的代碼。如果你必須指定一個寄存器,但機器約束使得你無法充分選擇到指定的那個寄存器,這時本地寄存器可以提供解決方案。

cvariablename(C變量名)

為輸出指定一個C左值表達式,通常為變量名。圓括號是語法的一部分。

當編譯器使用寄存器來表示輸出操作數,它就絕不會使用互斥過的寄存器。

輸出操作數表達式必須是左值,編譯器無法檢查操作數是否對執行的指令為合理的數據類型。對於不能直接尋址的輸出表達式(例如位字段),約束必須允許寄存器。在這種情況下,GCC使用寄存器作為匯編的輸出,然後將寄存器存儲到輸出中。

使用“+”約束的操作數,在asm語句最多30個操作數的統計中記為兩個操作數(同時為輸入和輸出)。

在所有不可覆蓋輸入的輸出操作數中使用“&”約束修飾符。否則,GCC可能將輸出操作數分配到與之不相關的輸入操作數相同的寄存器中,前提是匯編代碼在生成輸出之前已將輸入使用完畢。如果匯編代碼實際上包含多個指令,那麽這個假設可能是錯誤的!

如果一個輸出參數(a)允許寄存器約束,而另一個輸出參數(b)允許內存約束,同樣的問題也會發生。GCC生成的用於訪問B內存地址的代碼可以包含一個可能由A共享的寄存器,而GCC將這些寄存器考慮進asm的輸入中。如上所述,GCC假設這樣的輸入寄存器在寫入任何輸出之前被消耗掉,如果匯編在使用b之前寫到a,這個假設可能會導致不正確的行為。

asm支持操作數上添加操作數修飾符(例如“%k2”,而不是簡單的“%2”)。通常這些限定符是依賴於硬件的。

如果asm後面的C代碼沒有使用它的任何輸出操作數,可以使用volatile關鍵詞來防止優化器丟棄asm語句。

下面這個代碼沒有使用可選的匯編標識符,因此,它將第一個輸出操作數引用為%0(如有第二個為%1……)。第一個輸入操作數的值比後一個輸出操作數大1,在這個i386的例子中,mask引用為%1。

uint32_t Mask = 1234;

uint32_t Index;

asm ("bsfl %1, %0"

: "=r" (Index)

: "r" (Mask)

: "cc");

下面是一些輸出操作數的例子:

uint32_t c = 1;

uint32_t d;

uint32_t *e = &c;

asm ("mov %[e], %[d]"

: [d] "=rm" (d)

: [e] "rm" (*e));

這裏,d可以對應寄存器,也可以對應內存。因為編譯器已經獲得了e指向的uint32_t區域中的值,你可以通過指定多個約束,讓編譯器選擇d的最佳位置。

4. Flag Output Operands(標誌輸出操作數)

有些目標有一個特殊的寄存器,它保存操作或比較結果的“標誌”。通常情況下,這種寄存器的內容被asm登記為不可修改,或是被放進asm的修改列表中。

在某些目標上,存在一種特殊的輸出操作數,其中標誌寄存器中的狀態可以是asm的輸出。所支持的狀態集是特定於目標的,但一般規則是輸出變量必須是整數標量,而值是布爾值。如果支持,目標定義預處理器符號 __GCC_ASM_FLAG_OUTPUTS__。

由於標誌輸出操作數的特殊性質,沒有替代的約束方案。

通常,目標只有一個標誌寄存器,因此是許多指令的隱含操作數。在這種情況下,不應該通過%0等在匯編模板中引用操作數,因為匯編語言中沒有對應的字符。

x86家族

對於x86家族的標誌輸出約束的形式為“=@條件”,其中條件是在jcc或setcc的ISA(指令集)中定義的標準條件。

"a" :大於或無符號大於

"ae":大於等於或無符號大於等於

"b" :小於或無符號小於

"be":小於等於或無符號小於等於

"c" :進位標誌置位

"e"/

"z" :等於或零標誌置位

"g" :有符號大於

"ge":有符號大於等於

"l" :有符號小於

"le":有符號小於等於

"o" :溢出標誌置位

"p" :奇偶標誌置位

"s" :符號標誌置位

n前綴表示以上條件的反面

"na"

"nae"

"nb"

"nbe"

"nc"

"ne"

"ng"

"nge"

"nl"

"nle"

"no"

"np"

"ns"

"nz"

5. Input Operands(輸入操作數)

輸入操作數從C變量和對匯編代碼有效的表達式中取值。操作數用逗號分隔,每個操作數格式如下:

[ [asmSymbolicName] ] constraint (cexpression)

asmSymbolicName(匯編標識符名)

為操作數指定一個標識符,在匯編模板中用方括號將標識符名括起來(如“%[Value]”)。名稱的範圍是有效的匯編語句,也包括任何C變量名以及周邊已經定義過的名字。一個asm語句中的兩個操作數不能使用相同的標識符。

當不使用匯編標識符名時,可用asm語句中操作數的位置(從0開始)代替。例如:假設有2個輸出操作數和3個輸入操作數,在匯編模板中使用“%2”表示第一個輸入操作數,“%3”表示第二個輸入操作數,“%4”表示第三個輸入操作數。

constraint(約束)

用於約束操作數放置位置的字符串。

輸入約束不能用“=”或“+”開頭。當你列出多於一個位置時(如“=irm”),編譯器基於上下文選擇效率最優的一個。如果你必須指定一個寄存器,但機器約束使得你無法充分選擇到指定的那個寄存器,這時本地寄存器可以提供解決方案。

輸入約束也可以是數字(例如,“0”)。這表明指定的輸入必須與輸出約束列表中該對應索引(從0開始)的輸出約束共享同一位置。當使用匯編標識符語法輸出操作數,你可以使用它們的名稱(括在[]中)代替數字。

cexpression(C表達式)

可將C變量或表達式作為輸入傳遞給asm,其中括號是必須的語法。

當編譯器使用寄存器來表示輸入操作數,它就絕不會使用互斥過的寄存器。

如果沒有輸出操作數,但有輸入操作數,輸出操作數處使用兩個連續的冒號:

__asm__ ("some instructions"

: /* No outputs. */

: "r" (Offset / 8));

警告:不要修改單輸入操作數的內容(與輸出相關聯的輸入除外)。編譯器假定在退出asm語句時,這些操作數包含與執行語句之前相同的值。這時不可能使用互斥量告知編譯器這些輸入值的變化,一個常見的手法是將不斷變化的輸入變量綁定到一個從未使用過的輸出變量。

輸出操作數表達式必須是左值,編譯器無法檢查操作數是否對執行的指令為合理的數據類型。對於不能直接尋址的輸出表達式(例如位字段),約束必須允許寄存器。在這種情況下,GCC使用寄存器作為匯編的輸出,然後將寄存器存儲到輸出中。

如果asm後面的C代碼沒有使用它的任何輸出操作數,優化器可能會出人意料地丟棄asm語句。

asm支持操作數上添加操作數修飾符(例如“%k2”,而不是簡單的“%2”)。通常這些限定符是依賴於硬件的。

在這個示例中,使用虛構的組合指令,輸入操作數1的約束“0”表示它必須占用與輸出操作數“%0”相同的位置。只有輸入操作數可以在約束中使用數字,它們必須都各自對應到一個輸出操作數。在約束中只有數字(或匯編標識符名)可以保證一個操作數與另一個操作位置相同。僅僅是兩個操作數的值均為foo這一事實,並不足以保證它們在生成的匯編代碼中處於相同的位置。

asm ("combine %2, %0"

: "=r" (foo)

: "0" (foo), "g" (bar));

這是一個使用標識符名的例子:

asm ("cmoveq %1, %2, %[result]"

: [result] "=r"(result)

: "r" (test), "r" (new), "[result]" (old));

6. Clobbers(互斥項)

當編譯器意識到輸出操作數列表中條目的變化時,內聯匯編代碼修改的可能不僅僅是輸出。例如,計算可能需要附加寄存器,或是處理器可能覆蓋寄存器作為特定匯編指令的副作用。為了通知編譯器這些變化,將它們列在互斥列表中,互斥列表的內容可以是寄存器名或特殊的互斥項(在下方列出)。每個互斥項為用雙括號括起的字符串,並用逗號分隔。

互斥項不能與任何的輸入或輸出操作數重合。例如,操作數不能使用互斥項列表中列出的寄存器;輸入輸出操作數中指定寄存器的變量不能在互斥項列表中描述。特別地,除非把輸入操作數同時指定為輸出操作數,否則沒法判斷輸入操作數會被修改。

編譯器在選擇哪個寄存器用於輸入輸出操作數時,它不會選擇互斥列表中列出的寄存器。結果就是,互斥寄存器在匯編代碼中可以用於任何用途。

這裏以VAX為例,顯示了互斥寄存器的使用實例:

asm volatile ("movc3 %0, %1, %2"

: /* No outputs. */

: "g" (from), "g" (to), "g" (count)

: "r0", "r1", "r2", "r3", "r4", "r5");

另外,還有兩個特殊的互斥項:

  • "cc":它預示著匯編代碼修改了標誌寄存器。在某些器件上,GCC將條件代碼表示為一個特定的硬件寄存器;“cc”用來命名這個寄存器。在其他器件上,條件代碼的表示不同,指定“cc”沒有作用。但不管目標器件是什麽,它都是有效的。

  • "memory":告訴編譯器,匯編代碼對輸入和輸出操作數列表之外的其它項執行內存讀或寫(例如,訪問由某個輸入參數指向的內存)。為了確保內存中包含正確的值,GCC可能需要在執行asm之前將特定寄存器值刷新到內存。此外,編譯器不保證任何在asm語句執行前從內存中讀的值,在asm執行完後依然保持不變,它將會根據需要重載。使用“memory”互斥項,有效地為編譯器形成一個讀/寫內存保護。

註意:使用該互斥項並不能阻止處理器投機地越過asm語句做數據讀取,為防止這種情況發生,你需要特定於處理器的柵欄指令。

把寄存器的值刷進內存會影響性能,這對時間敏感型代碼可能會是個問題,如果被訪問內存的大小在編譯階段已知的話,你可以使用一個小技巧來避免它。例如,假設訪問一個10字節大小的字符串,使用下面的內存輸入操作數:

{"m"( ({ struct { char x[10]; } *p = (void *)ptr ; *p; }) )}.

7. Goto Labels(跳轉標號)

asm允許匯編代碼跳轉到一個或多個C標號。在asm語句的跳轉標號部分包含所有匯編代碼可能跳轉的標號,並用逗號分隔。GCC假定asm執行後轉到下一條語句(如果不是這樣的話,考慮在asm語句後使用__builtin_unreachable屬性)。通過使用熱和冷標簽屬性,可以改進對asm跳轉的優化。

asm跳轉語句不能有輸出,這是由編譯器內部限制的:控制轉移指令不能有輸出。如果匯編代碼不修改任何東西,使用“memory”互斥項來強制將所有寄存器的值刷入內存,並在asm語句後重新載入它們。

需要註意的是,asm跳轉語句總是隱式地使用volatile。

要在匯編模板中引用標號,使用“%l”(或“%L”前綴),後面跟上標號在跳轉標號中的位置(從0開始+輸入操作數)。例如,假設asm有三個輸入和兩個跳轉標號,用“%l3”表示第一個跳轉標號,“%l4”表示第2個跳轉標號。

另外,可以使用包含在括號中的實際C標號名稱來引用標號。例如,為了引用名為“carry”的標號,可以使用“%l[carry]”。用這個方法時,標簽必須仍然被列在跳轉標號中。

這裏是一個i386使用asm跳轉的例子:

asm goto (

"btl %1, %0\n\t"

"jc %l2"

: /* No outputs. */

: "r" (p1), "r" (p2)

: "cc"

: carry);

return 0;

carry:

return 1;

下面展示了一個使用memory互斥項的asm跳轉:

int frob(int x)

{

int y;

asm goto ("frob %%r5, %1; jc %l[error]; mov (%2), %%r5"

: /* No outputs. */

: "r"(x), "r"(&y)

: "r5", "memory"

: error);

return y;

error:

return -1;

}

關於x86平臺的兩個小節略去:

8. x86 Operand Modifiers

……

9. x86 Floating-Point asm Operands

……

參考資料

【1】Extended Asm - Assembler Instructions with C Expression Operands.

https://gcc.gnu.org/onlinedocs/gcc-7.2.0/gcc/Extended-Asm.html#Extended-Asm

·END·

想進一步跟蹤本博客動態,歡迎關註我的個人微信訂閱號:信號君

信號君:尋求簡單之道

技術成長 | 讀書筆記 | 認知升級

技術分享圖片

掃描二維碼關註信號君

翻譯 | “擴展asm”——用C表示操作數的匯編程序指令