如何在C代碼中使用內聯彙編之彙編指令模板

彙編指令模板
彙編模板:
彙編程序模板是包含彙編程序指令的文本字符串。
編譯器替換模板中引用輸入、輸出和goto標籤的標記,然後將生成的字符串輸出給彙編程序。
字符串可以包含彙編程序識別的任何指令,包括指示符。
GCC不分析彙編程序指令本身,也不知道它們的含義,甚至不知道它們是否是有效的彙編程序輸入。
可以將多個彙編程序指令放在一個asm字符串中,由系統彙編代碼中通常使用的字符分隔。
在大多數地方工作的組合是換行符,加上一個製表符移動到指令字段(寫爲’\n\t’)。
有些彙編程序允許分號作爲行分隔符。但是,請注意,一些彙編語言使用分號來開始註釋。
不要期望彙編後asm語句序列保持完全連續,即使使用volatile限定符也是如此。
如果某些指令需要在輸出中保持連續,請將它們放在一個多指令asm語句中
如果不使用輸入/輸出操作數(例如直接使用匯編器模板中的全局符號)就從C程序訪問數據,可能無法像預期的那樣工作。
類似地,直接從彙編器模板調用函數需要對目標彙編器和ABI有詳細的瞭解。
由於GCC不解析彙編器模板,所以它對引用的任何符號都不可見。
這可能導致GCC將這些符號作爲未引用丟棄,除非它們也被列出爲輸入、輸出或goto操作數。

特殊的格式字符串:
除了輸入、輸出和goto操作數所描述的令牌之外,下面的令牌在彙編器模板中還有特殊的含義:
‘%%’:
將單個’ % ‘輸出到彙編程序代碼中。
‘%=’:
輸出在整個編譯過程中asm語句的每個實例所特有的數字
當在生成多個彙編器指令的單個模板中創建本地標籤並多次引用它們時,此選項非常有用
‘%{’:
‘%|’:
‘%}’:
相應的將’ { ‘,’|以及’}'輸出到彙編程序代碼中。

asm模板中的多種彙編語言:

輸出操作數:
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;

輸出操作數的語法格式:
操作數使用逗號分隔。
約束條件
|
V
[ [asmSymbolicName] ] constraint (cvariablename)

asmSymbolicName:

爲操作數指定一個符號名。
通過將名稱括在方括號中,在彙編器模板中引用這個名稱(即’ %[Value] ‘)。
名稱的作用域是容納定義它的asm語句中。
任何有效的C變量名都是可以接受的,包括已經在周圍代碼中定義的名稱。
同一asm語句中的任何兩個操作數都不能使用相同的符號名。
當不使用asmSymbolicName時,請在彙編器模板中的操作數列表中使用操作數的(從零開始的)位置。
例如,如果有三個輸出操作數,在模板中使用’ %0 ‘引用第一個操作數,’ %1 ‘引用第二個操作數,’ %2 '引用第三個操作數。

constraint:

指定操作數位置約束的字符串常量。
輸出約束必須以’ = ‘(覆蓋現有值的變量)或’ + ‘(讀寫時)開始。
使用’ = '時,不要假定位置包含asm入口時的現有值,除非操作數綁定到輸入
在前綴之後,必須有一個或多個附加約束來描述值所在的位置。
常見的約束包括r:表示寄存器(register),m:表示內存(memory)
當您列出多個可能的位置(例如,“=rm”)時,編譯器將根據當前上下文選擇最有效的位置
如果在asm語句允許的範圍內列出儘可能多的備選項,則允許優化器生成儘可能好的代碼。
如果您必須使用特定的寄存器,但是您的機器約束沒有提供足夠的控制來選擇您想要的特定寄存器,那麼本地寄存器變量可以提供一個解決方案

cvariablename:

指定一個C 左值表達式來保存輸出,通常是一個變量名。
括號是語法中必需的一部分

輸出操作數表達式必須是左值。使用“+”約束脩飾符的操作數被計算爲兩個操作數(即同時作爲輸入和輸出),每個asm語句的操作數最多爲30個

對不能與輸入重疊的所有輸出操作數使用’ & '約束脩飾符。
否則,GCC可以在相同的寄存器中將輸出操作數分配爲一個不相關的輸入操作數,前提是彙編代碼在生成輸出之前使用它的輸入。如果彙編程序代碼實際上包含一條以上的指令,那麼這種假設可能是錯誤的。
如果一個輸出參數(a)允許寄存器約束,而另一個輸出參數(b)允許內存約束,也會出現同樣的問題。
GCC生成的用於訪問b中的內存地址的代碼可以包含可能由a共享的寄存器,GCC認爲這些寄存器是asm的輸入。
如上所述,GCC假設在編寫任何輸出之前都要使用這些輸入寄存器
這種假設可能導致不正確的行爲如果asm語句寫在使用b。結合“&”修改器和寄存器約束確保修改不影響地址引用b。否則,b是未定義的位置如果修改使用前b。
asm支持操作數上的操作數修飾符(例如“%k2”而不是簡單的“%2”)。通常,這些限定符依賴於硬件
如果asm後面的C代碼沒有使用任何輸出操作數,那麼使用volatile for asm語句,以防止優化器在不需要時丟棄asm語句
這段代碼沒有使用可選的asmSymbolicName。因此,它將第一個輸出操作數引用爲%0(如果有第二個操作數,則爲%1,依此類推)。第一個輸入操作數的個數大於最後一個輸出操作數的個數。在這個i386示例中,它使Mask引用爲%1:
uint32_t Mask = 1234;
uint32_t Index;

	  asm ("bsfl %1, %0"
		 : "=r" (Index)
		 : "r" (Mask)
		 : "cc");

該代碼覆蓋變量Index(' = '),將該值放在寄存器(' r ')中。

使用通用的“r”約束而不是特定寄存器的約束,允許編譯器選擇要使用的寄存器,這可以產生更高效的代碼
如果彙編程序指令需要特定的寄存器,這可能不可能。
下面的i386示例使用asmSymbolicName語法。
它產生的結果與上面的代碼相同,但是有些人可能認爲它更易於閱讀或更易於維護,因爲在添加或刪除操作數時不需要重新排序索引號。
名稱aIndex和aMask僅在本例中用於強調在何處使用哪些名稱。可以重用名稱索引和掩碼。
uint32_t Mask = 1234;
uint32_t Index;

	  asm ("bsfl %[aMask], %[aIndex]"
		 : [aIndex] "=r" (Index)
		 : [aMask] "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選擇最佳位置。

輸入操作數:
輸入操作數使來自C的變量和表達式的值對彙編代碼可用。
操作數之間使用逗號分隔。
輸入操作數的語法格式:
[ [asmSymbolicName] ] constraint (cexpression)

asmSymbolicName:

爲操作數指定符號名稱。
通過將名稱括在方括號中,在彙編器模板中引用該名稱(即’ %[Value] ‘)。
名稱的作用域是包含定義它的asm語句。
任何有效的C變量名都是可以接受的,包括已經在周圍代碼中定義的名稱。
同一asm語句中的任何兩個操作數都不能使用相同的符號名。
當不使用asmSymbolicName時,請在彙編器模板中的操作數列表中使用操作數的(從零開始的)位置。
例如,如果有兩個輸出操作數和三個輸入,在模板中使用’ %2 ‘引用第一個輸入操作數,’ %3 ‘引用第二個,’ %4 ‘引用第三個。
constraint:
指定操作數位置約束的字符串常量
輸入約束字符串不能以’ = ‘或’ + '開頭
當您列出多個可能的位置(例如,“irm”)時,編譯器將根據當前上下文選擇最有效的位置。
如果您必須使用特定的寄存器,但是您的機器約束沒有提供足夠的控制來選擇您想要的特定寄存器,那麼本地寄存器變量可以提供一個解決方案
輸入約束也可以是數字(例如,“0”)。這表明指定的輸入必須與輸出約束列表中(從零開始)索引處的輸出約束位於相同的位置。當爲輸出操作數使用asmSymbolicName語法時,您可以使用這些名稱(括在括號“[]”中)代替數字。
cexpression:
這是作爲輸入傳遞給asm語句的C變量或表達式。括號是語法中必需的一部分。
如果沒有輸出操作數,但有輸入操作數,則在輸出操作數的位置放置兩個連續冒號:
asm (“some instructions”
: /* No outputs. */
: “r” (Offset / 8));

警告:不要修改僅輸入操作數的內容(與輸出綁定的輸入除外)。編譯器假設從asm語句中退出時,這些操作數包含與執行語句之前相同的值。
不可能使用clobbers通知編譯器這些輸入中的值正在更改。一種常見的解決方法是將正在更改的輸入變量綁定到從未使用過的輸出變量。

但是,請注意,如果asm語句後面的代碼沒有使用任何輸出操作數,GCC優化器可能會將asm語句作爲不需要的語句丟棄(請參閱Volatile)
在本例中,使用虛構的combine指令,輸入操作數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));

Clobbers and Scratch Registers:
雖然編譯器知道對輸出操作數中列出的條目的更改,但是內聯asm代碼可能修改的不僅僅是輸出
例如,計算可能需要額外的寄存器,或者作爲特定彙編指令的副作用,處理器可能覆蓋寄存器。
爲了將這些更改通知編譯器,請在clobber列表中列出它們。
Clobber列表項可以是寄存器名,也可以是特殊的Clobber(如下所示)。每個clobber列表項都是一個字符串常量,用雙引號括起來,並用逗號分隔。
Clobber描述不能以任何方式與輸入或輸出操作數重疊。
另一個限制是clobber列表不應該包含堆棧指針寄存器。
這是因爲編譯器要求堆棧指針的值在asm語句之後與在語句入口時相同。
但是,以前版本的GCC沒有強制執行這個規則,允許堆棧指針出現在列表中,語義不清楚。這種行爲是不贊成的,在GCC的未來版本中,列出堆棧指針可能會成爲一個錯誤。

下面是一個實際的VAX例子,展示瞭如何使用Clobber寄存器:
	asm volatile ("movc3 %0, %1, %2"
					   : /* No outputs. */
					   : "g" (from), "g" (to), "g" (count)
					   : "r0", "r1", "r2", "r3", "r4", "r5", "memory");

此外,還有兩個特殊的clobber參數:

“cc”:
“cc”clobber表示彙編程序代碼修改標誌寄存器。
在某些機器上,GCC將條件代碼表示爲一個特定的硬件寄存器;“cc”用於命名此寄存器。在其他機器上,條件代碼處理是不同的,指定“cc”沒有效果。但無論目標是什麼,它都是有效的。
“memory”:
“memory”clobber告訴編譯器,彙編代碼執行對輸入和輸出操作數中列出的項以外的項的內存讀寫(例如,訪問由輸入參數之一指向的內存)。
爲了確保內存中包含正確的值,GCC可能需要在執行asm之前將特定的寄存器值刷新到內存中。
此外,編譯器並不假設在asm之前從內存中讀取的任何值在asm之後保持不變;它會根據需要重新加載它們。使用“內存”clobber可以有效地爲編譯器形成讀/寫內存屏障。
注意,這個clobber並不阻止處理器執行asm語句之後的推測性讀取。爲了防止這種情況,您需要特定於處理器的fence指令。
將寄存器刷新到內存中會影響性能,對於時間敏感的代碼來說可能是個問題。您可以向GCC提供更好的信息來避免這種情況,如下面的示例所示。
至少,別名規則允許GCC知道哪些內存不需要刷新。
這裏是一個虛構的平方和指令,它接受兩個指向內存中浮點值的指針,並生成一個浮點寄存器輸出。注意,x和y都出現在asm參數中兩次,一次指定訪問的內存,一次指定asm使用的基寄存器。
這樣做通常不會浪費寄存器,因爲GCC可以爲這兩個目的使用相同的寄存器。但是,如果在asm中同時使用%1和%3來表示x,並且期望它們是相同的,那就太愚蠢了。
事實上,%3很可能不是寄存器。它可能是x指向的對象的符號內存引用。
asm (“sumsq %0, %1, %2”
: “+f” (result)
: “r” (x), “r” (y), “m” (*x), “m” (*y));

這裏是一個虛構的*z++ = *x++ * *y++指令。注意,必須將x、y和z指針寄存器指定爲輸入/輸出,因爲asm會修改它們。
	asm ("vecmul %0, %1, %2"
		 : "+r" (z), "+r" (x), "+r" (y), "=m" (*z)
		 : "m" (*x), "m" (*y));	

一個x86示例,其中字符串內存參數的長度未知。
	asm("repne scasb"
		: "=c" (count), "+D" (p)
		: "m" (*(const char (*)[]) p), "0" (-1), "a" (0));


如果您知道上面只讀取一個10字節數組,那麼您可以使用類似於這樣的內存輸入:
		"m" (*(const char (*)[10]) p)

Goto Label:
asm goto允許彙編代碼跳轉到一個或多個C標籤。
asm goto語句中的GotoLabels部分包含一個逗號分隔的列表,其中包含彙編代碼可能跳轉到的所有C標籤。
GCC假設asm的執行會一直執行到下一個語句(如果不是這樣,請考慮在asm語句之後使用__builtin_unreachable )
asm goto的優化可以通過使用hot和cold標籤屬性來改進
asm goto語句不能有輸出。
是由於編譯器的內部限制:控件傳輸指令不能有輸出
如果彙編代碼確實修改了任何東西,那麼使用“memory”clobber強制優化器將所有寄存器值刷新到內存中,並在asm語句之後根據需要重新加載它們。
還要注意,asm goto語句總是被隱式地認爲是volatile語句。
要引用匯編器模板中的標籤,請在其前面加上“%l”(小寫的“l”),
在GotoLabels中加上它的(從零開始的)位置和輸入操作數。
例如,如果asm有三個輸入並引用兩個標籤,則將第一個標籤引用爲“%l3”,第二個標籤引用爲“%l4”)。
另外,您可以使用括號內的實際C標籤名稱引用標籤。例如,要引用一個名爲carry的標籤,可以使用“%l[carry]”。
使用這種方法時,標籤必須仍然列在GotoLabels部分中。
下面是一個例子asm goto for i386:
asm goto (
“btl %1, %0\n\t”
“jc %l2”
: /* No outputs. */
: “r” (p1), “r” (p2)
: “cc”
: carry);

	return 0;

	carry:
	return 1;


下面的例子顯示了一個使用內存clobber的asm goto。	
	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;
	}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章