作者 Bharata?B.?Rao IBM?Linux?技術中心,IBM?軟件實驗室,印度 2001?年?3?月?
Bharata?B.?Rao?提供了在?Linux?平臺上使用和構造?x86?內聯匯編的概括性介紹。他介紹了內聯匯編及其各種用法的基礎知識,提供了一些基本的內聯匯編編碼指導,并解釋了在?Linux?內核中內聯匯編代碼的一些實例。 如果您是?Linux?內核的開發人員,您會發現自己經常要對與體系結構高度相關的功能進行編碼或優化代碼路徑。您很可能是通過將匯編語言指令插入到?C?語句的中間(又稱為內聯匯編的一種方法)來執行這些任務的。讓我們看一下?Linux?中內聯匯編的特定用法。(我們將討論限制在?IA32?匯編。)
GNU?匯編程序簡述 讓我們首先看一下?Linux?中使用的基本匯編程序語法。GCC(用于?Linux?的?GNU?C?編譯器)使用?AT&T?匯編語法。下面列出了這種語法的一些基本規則。(該列表肯定不完整;只包括了與內聯匯編相關的那些規則。)
寄存器命名? 寄存器名稱有?%?前綴。即,如果必須使用?eax,它應該用作?%eax。?
源操作數和目的操作數的順序? 在所有指令中,先是源操作數,然后才是目的操作數。這與將源操作數放在目的操作數之后的?Intel?語法不同。?
mov?%eax,?%ebx,?transfers?the?contents?of?eax?to?ebx.
?
操作數大小? 根據操作數是字節?(byte)、字?(word)?還是長型?(long),指令的后綴可以是?b、w?或?l。這并不是強制性的;GCC?會嘗試通過讀取操作數來提供相應的后綴。但手工指定后綴可以改善代碼的可讀性,并可以消除編譯器猜測不正確的可能性。?
movb?%al,?%bl?--?Byte?move ????movw?%ax,?%bx?--?Word?move ????movl?%eax,?%ebx?--?Longword?move
?
立即操作數? 通過使用?$?指定直接操作數。?
movl?$0xffff,?%eax?--?will?move?the?value?of?0xffff?into?eax?register.
?
間接內存引用? 任何對內存的間接引用都是通過使用?(? ?來完成的。?
movb?(%esi),?%al?--?will?transfer?the?byte?in?the?memory?
?pointed?by?esi?into?al register
?
內聯匯編 GCC?為內聯匯編提供特殊結構,它具有以下格式:
GCG?的?"asm"?結構?
???asm?(?assembler?template ????
:?output?operands???????????????(optional) ????
:?input?operands????????????????(optional) ????
:?list?of?clobbered?registers??? ????(optional) ????
);??
?
本例中,匯編程序模板由匯編指令組成。輸入操作數是充當指令輸入操作數使用的?C?表達式。輸出操作數是將對其執行匯編指令輸出的?C?表達式。
內聯匯編的重要性體現在它能夠靈活操作,而且可以使其輸出通過?C?變量顯示出來。因為它具有這種能力,所以?"asm"?可以用作匯編指令和包含它的?C?程序之間的接口。
一個非常基本但很重要的區別在于?簡單內聯匯編只包括指令,而?擴展內聯匯編包括操作數。要說明這一點,考慮以下示例:?
內聯匯編的基本要素?
{ ????int?a=10,?b; ????asm?("movl?%1,?%%eax; ????
movl?%%eax,?%0;" ????????:"=r"(b)??/*?output?*/???? ????????:"r"(a)???????/*?input?*/ ????????:"%eax" ;?/*?clobbered?register?*/ }
?
在上例中,我們使用匯編指令使?"b"?的值等于?"a"。請注意以下幾點:
"b"?是輸出操作數,由?%0?引用,"a"?是輸入操作數,由?%1?引用。? "r"?是操作數的約束,它指定將變量?"a"?和?"b"?存儲在寄存器中。請注意,輸出操作數約束應該帶有一個約束修飾符?"=",指定它是輸出操作數。? 要在?"asm"?內使用寄存器?%eax,%eax?的前面應該再加一個?%,換句話說就是?%%eax,因為?"asm"?使用?%0、%1?等來標識變量。任何帶有一個?%?的數都看作是輸入/輸出操作數,而不認為是寄存器。? 第三個冒號后的修飾寄存器?%eax?告訴將在?"asm"?中修改?GCC?%eax?的值,這樣?GCC?就不使用該寄存器存儲任何其它的值。? movl?%1,?%%eax?將?"a"?的值移到?%eax?中,?movl?%%eax,?%0?將?%eax?的內容移到?"b"?中。? 因為?"b"?被指定成輸出操作數,因此當?"asm"?的執行完成后,它將反映出更新的值。換句話說,對?"asm"?內?"b"?所做的更改將在?"asm"?外反映出來。? 現在讓我們更詳細的了解每一項的含義。
匯編程序模板 匯編程序模板是一組插入到?C?程序中的匯編指令(可以是單個指令,也可以是一組指令)。每條指令都應該由雙引號括起,或者整組指令應該由雙引號括起。每條指令還應該用一個定界符結尾。有效的定界符為新行?(\n)?和分號?( 。?'\n'?后可以跟一個?tab(\t)?作為格式化符號,增加?GCC?在匯編文件中生成的指令的可讀性。?指令通過數?%0、%1?等來引用?C?表達式(指定為操作數)。
如果希望確保編譯器不會在?"asm"?內部優化指令,可以在?"asm"?后使用關鍵字?"volatile"。如果程序必須與?ANSI?C?兼容,則應該使用?__asm__?和?__volatile__,而不是?asm?和?volatile。
操作數 C?表達式用作?"asm"?內的匯編指令操作數。在匯編指令通過對?C?程序的?C?表達式進行操作來執行有意義的作業的情況下,操作數是內聯匯編的主要特性。
每個操作數都由操作數約束字符串指定,后面跟用括弧括起的?C?表達式,例如:"constraint"?(C?expression)。操作數約束的主要功能是確定操作數的尋址方式。
可以在輸入和輸出部分中同時使用多個操作數。每個操作數由逗號分隔開。
在匯編程序模板內部,操作數由數字引用。如果總共有?n?個操作數(包括輸入和輸出),那么第一個輸出操作數的編號為?0,逐項遞增,最后那個輸入操作數的編號為?n?-1。總操作數的數目限制在?10,如果機器描述中任何指令模式中的最大操作數數目大于?10,則使用后者作為限制。?
修飾寄存器列表 如果?"asm"?中的指令指的是硬件寄存器,可以告訴?GCC?我們將自己使用和修改它們。這樣,GCC?就不會假設它裝入到這些寄存器中的值是有效值。通常不需要將輸入和輸出寄存器列為?clobbered,因為?GCC?知道?"asm"?使用它們(因為它們被明確指定為約束)。不過,如果指令使用任何其它的寄存器,無論是明確的還是隱含的(寄存器不在輸入約束列表中出現,也不在輸出約束列表中出現),寄存器都必須被指定為修飾列表。修飾寄存器列在第三個冒號之后,其名稱被指定為字符串。
至于關鍵字,如果指令以某些不可預知且不明確的方式修改了內存,則可能將?"memory"?關鍵字添加到修飾寄存器列表中。這樣就告訴?GCC?不要在不同指令之間將內存值高速緩存在寄存器中。
操作數約束 前面提到過,"asm"?中的每個操作數都應該由操作數約束字符串描述,后面跟用括弧括起的?C?表達式。操作數約束主要是確定指令中操作數的尋址方式。約束也可以指定:
是否允許操作數位于寄存器中,以及它可以包括在哪些種類的寄存器中? 操作數是否可以是內存引用,以及在這種情況下使用哪些種類的地址? 操作數是否可以是立即數? 約束還要求兩個操作數匹配。
常用約束 在可用的操作數約束中,只有一小部分是常用的;下面列出了這些約束以及簡要描述。有關操作數約束的完整列表,請參考?GCC?和?GAS?手冊。
寄存器操作數約束?(r)? 使用這種約束指定操作數時,它們存儲在通用寄存器中。請看下例:?
asm?("movl?%%cr3,?%0\n"?:"=r"(cr3val));
?
這里,變量?cr3val?保存在寄存器中,%cr3?的值復制到寄存器上,cr3val?的值從該寄存器更新到內存中。指定?"r"?約束時,GCC?可以將變量?cr3val?保存在任何可用的?GPR?中。要指定寄存器,必須通過使用特定的寄存器約束直接指定寄存器名。
a???%eax
b???%ebx
c???%ecx
d???%edx
S???%esi
D???%edi
?
內存操作數約束?(m)? 當操作數位于內存中時,任何對它們執行的操作都將在內存位置中直接發生,這與寄存器約束正好相反,后者先將值存儲在要修改的寄存器中,然后將它寫回內存位置中。但寄存器約束通常只在對于指令來說它們是絕對必需的,或者它們可以大大提高進程速度時使用。當需要在?"asm"?內部更新?C?變量,而您又確實不希望使用寄存器來保存其值時,使用內存約束最為有效。例如,idtr?的值存儲在內存位置?loc?中:?
?("sidt?%0\n"?:?:"m"(loc));
?
匹配(數字)約束? 在某些情況下,一個變量既要充當輸入操作數,也要充當輸出操作數。可以通過使用匹配約束在?"asm"?中指定這種情況。?
asm?("incl?%0"?:"=a"(var):"0"(var));
?
在匹配約束的示例中,寄存器?%eax?既用作輸入變量,也用作輸出變量。將?var?輸入讀取到?%eax,增加后將更新的?%eax?再次存儲在?var?中。這里的?"0"?指定第?0?個輸出變量相同的約束。即,它指定?var?的輸出實例只應該存儲在?%eax?中。該約束可以用于以下情況:
輸入從變量中讀取,或者變量被修改后,修改寫回到同一變量中? 不需要將輸入操作數和輸出操作數的實例分開? 使用匹配約束最重要的意義在于它們可以導致有效地使用可用寄存器。
一般內聯匯編用法示例 以下示例通過各種不同的操作數約束說明了用法。有如此多的約束以至于無法將它們一一列出,這里只列出了最經常使用的那些約束類型。
"asm"?和寄存器約束?"r"?讓我們先看一下使用寄存器約束?r?的?"asm"。我們的示例顯示了?GCC?如何分配寄存器,以及它如何更新輸出變量的值。?
int?main(void) { ????int?x?=?10,?y; ???? ????asm?("movl?%1,?%%eax; ????
?"movl?%%eax,?%0;" ????????:"=r"(y)??/*?y?is?output?operand?*/ ????????:"r"(x)???????/*?x?is?input?operand?*/ ????????:"%eax" ;?/*?%eax?is?clobbered?register?*/ }
?
在該例中,x?的值復制為?"asm"?中的?y。x?和?y?都通過存儲在寄存器中傳遞給?"asm"。為該例生成的匯編代碼如下:
main:
pushl?%ebp
movl?%esp,%ebp
subl?$8,%esp
movl?$10,-4(%ebp)????
movl?-4(%ebp),%edx??/*?x=10?is?stored?in?%edx?*/ #APP????/*?asm?starts?here?*/???
movl?%edx,?%eax?????/*?x?is?moved?to?%eax?*/
movl?%eax,?%edx?????/*?y?is?allocated?in?edx?and?updated?*/
#NO_APP?/*?asm?ends?here?*/
movl?%edx,-8(%ebp)??/*?value?of?y?in?stack?is?updated?with? ???????????????? ?the?value?in?%edx?*/?
?
當使用?"r"?約束時,GCC?在這里可以自由分配任何寄存器。在我們的示例中,它選擇?%edx?來存儲?x。在讀取了?%edx?中?x?的值后,它為?y?也分配了相同的寄存器。
因為?y?是在輸出操作數部分中指定的,所以?%edx?中更新的值存儲在?-8(%ebp),堆棧上?y?的位置中。如果?y?是在輸入部分中指定的,那么即使它在?y?的臨時寄存器存儲值?(%edx)?中被更新,堆棧上?y?的值也不會更新。
因為?%eax?是在修飾列表中指定的,GCC?不在任何其它地方使用它來存儲數據。
輸入?x?和輸出?y?都分配在同一個?%edx?寄存器中,假設輸入在輸出產生之前被消耗。請注意,如果您有許多指令,就不是這種情況了。要確保輸入和輸出分配到不同的寄存器中,可以指定?&?約束修飾符。下面是添加了約束修飾符的示例。
int?main(void) { ????int?x?=?10,?y; ???? ????asm?("movl?%1,?%%eax; ????
?"movl?%%eax,?%0;" ????????:"=&r"(y)?/*?y?is?output?operand,?note?the???? ???????????????? ?&?constraint?modifier.?*/ ????????:"r"(x)???????/*?x?is?input?operand?*/ ????????:"%eax" ;?/*?%eax?is?clobbered?register?*/ }
?
以下是為該示例生成的匯編代碼,從中可以明顯地看出?x?和?y?存儲在?"asm"?中不同的寄存器中。
main:
pushl?%ebp
movl?%esp,%ebp
subl?$8,%esp
movl?$10,-4(%ebp)
movl?-4(%ebp),%ecx??/*?x,?the?input?is?in?%ecx?*/ #APP ????movl?%ecx,?%eax ????movl?%eax,?%edx?????/*?y,?the?output?is?in?%edx?*/
#NO_APP
movl?%edx,-8(%ebp)
?
特定寄存器約束的使用 現在讓我們看一下如何將個別寄存器作為操作數的約束指定。在下面的示例中,cpuid?指令采用?%eax?寄存器中的輸入,然后在四個寄存器中給出輸出:%eax、%ebx、%ecx、%edx。對?cpuid?的輸入(變量?"op")傳遞到?"asm"?的?eax?寄存器中,因為?cpuid?希望它這樣做。在輸出中使用?a、b、c?和?d?約束,分別收集四個寄存器中的值。
asm?("cpuid"
:?"=a"?(_eax),
"=b"?(_ebx),
"=c"?(_ecx),
"=d"?(_edx)
:?"a"?(op));
?
在下面可以看到為它生成的匯編代碼(假設?_eax、_ebx?等...?變量都存儲在堆棧上):
movl?-20(%ebp),%eax?/*?store?'op'?in?%eax?--?input?*/ #APP
cpuid #NO_APP
movl?%eax,-4(%ebp)??/*?store?%eax?in?_eax?--?output?*/
movl?%ebx,-8(%ebp)??/*?store?other?registers?in
movl?%ecx,-12(%ebp)? ?respective?output?variables?*/
movl?%edx,-16(%ebp)
?
strcpy?函數可以通過以下方式使用?"S"?和?"D"?約束來實現:
asm?("cld\n ????
rep\n ????
movsb" ????
:?/*?no?input?*/ ????
:"S"(src),?"D"(dst),?"c"(count));
?
通過使用?"S"?約束將源指針?src?放入?%esi?中,使用?"D"?約束將目的指針?dst?放入?%edi?中。因為?rep?前綴需要?count?值,所以將它放入?%ecx?中。
在下面可以看到另一個約束,它使用兩個寄存器?%eax?和?%edx?將兩個?32?位的值合并在一起,然后生成一個64?位的值:
#define?rdtscll(val)?\
?__asm__?__volatile__?("rdtsc"?:?"=A"?(val))
The?generated?assembly?looks?like?this?(if?val?has?a?64?bit?memory?space).
#APP
rdtsc #NO_APP
movl?%eax,-8(%ebp)??/*?As?a?result?of?A?constraint?
movl?%edx,-4(%ebp)?? ?%eax?and?%edx?serve?as?outputs?*/
Note?here?that?the?values?in?%edx:%eax?serve?as?64?bit?output.
?
使用匹配約束 在下面將看到系統調用的代碼,它有四個參數:
#define?_syscall4(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4)?\ type?name?(type1?arg1,?type2?arg2,?type3?arg3,?type4?arg4)?\ {?\ long?__res;?\ __asm__?volatile?("int?$0x80"?\
:?"=a"?(__res)?\
:?"0"?(__NR_##name),"b"?((long)(arg1)),"c"?((long)(arg2)),?\
"d"?((long)(arg3)),"S"?((long)(arg4)));?\ __syscall_return(type,__res);?\ }
?
在上例中,通過使用?b、c、d?和?S?約束將系統調用的四個自變量放入?%ebx、%ecx、%edx?和?%esi?中。請注意,在輸出中使用了?"=a"?約束,這樣,位于?%eax?中的系統調用的返回值就被放入變量?__res?中。通過將匹配約束?"0"?用作輸入部分中第一個操作數約束,syscall?號?__NR_##name?被放入?%eax?中,并用作對系統調用的輸入。這樣,這里的?%eax?既可以用作輸入寄存器,又可以用作輸出寄存器。沒有其它寄存器用于這個目的。另請注意,輸入(syscall?號)在產生輸出(syscall?的返回值)之前被消耗(使用)。
內存操作數約束的使用 請考慮下面的原子遞減操作:
__asm__?__volatile__(
"lock;?decl?%0"
:"=m"?(counter)
:"m"?(counter));
?
為它生成的匯編類似于:
#APP ????lock ????decl?-24(%ebp)?/*?counter?is?modified?on?its?memory?location?*/ #NO_APP.
?
您可能考慮在這里為?counter?使用寄存器約束。如果這樣做,counter?的值必須先復制到寄存器,遞減,然后對其內存更新。但這樣您會無法理解鎖定和原子性的全部意圖,這些明確顯示了使用內存約束的必要性。
使用修飾寄存器 請考慮內存拷貝的基本實現。
???asm?("movl?$count,?%%ecx; ????
up:?lodsl;?? ????
stosl; ????
loop?up;" ????????:???????????/*?no?output?*/ ????????:"S"(src),?"D"(dst)?/*?input?*/ ????????:"%ecx",?"%eax"? ;??/*?clobbered?list?*/????
?
當?lodsl?修改?%eax?時,lodsl?和?stosl?指令隱含地使用它。%ecx?寄存器明確裝入?count。但?GCC?在我們通知它以前是不知道這些的,我們是通過將?%eax?和?%ecx?包括在修飾寄存器集中來通知?GCC?的。在完成這一步之前,GCC?假設?%eax?和?%ecx?是自由的,它可能決定將它們用作存儲其它的數據。請注意,%esi?和?%edi?由?"asm"?使用,它們不在修飾列表中。這是因為已經聲明?"asm"?將在輸入操作數列表中使用它們。這里最低限度是,如果在?"asm"?內部使用寄存器(無論是明確還是隱含地),既不出現在輸入操作數列表中,也不出現在輸出操作數列表中,必須將它列為修飾寄存器。
結束語 總的來說,內聯匯編非常巨大,它提供的許多特性我們甚至在這里根本沒有涉及到。但如果掌握了本文描述的基本材料,您應該可以開始對自己的內聯匯編進行編碼了。
參考資料?
您可以參閱本文在?developerWorks?全球站點上的?英文原文.?
請參考?Using?and?Porting?the?GNU?Compiler?Collection?(GCC)手冊。?
請參考?GNU?Assembler?(GAS)手冊。?
仔細閱讀?Brennan's?Guide?to?Inline?Assembly。?
關于作者 Bharata?B.?Rao?擁有印度?Mysore?大學的電子和通信工程的學士學位。他從?1999?年就開始為?IBM?Global?Services,?India?工作了。他是?IBM?Linux?技術中心的成員之一,他在該中心中主要從事?Linux?RAS(可靠性、可用性和適用性)的研究。他感興趣的其它領域包括操作系統本質和處理器體系結構。可以通過?rbharata@in.ibm.com?與他聯系。??
|