Linux 中 x86 的內聯彙編(轉)

ba發表於2007-08-12
Linux 中 x86 的內聯彙編(轉)[@more@]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 程式中的彙編指令(可以是單個指令,也可以是一組指令)。每條指令都應該由雙引號括起,或者整組指令應該由雙引號括起。每條指令還應該用一個定界符結尾。有效的定界符為新行 ( ) 和分號 (;)。 ' ' 後可以跟一個 tab( ) 作為格式化符號,增加 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 " :"=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 " : :"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

rep


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(可靠性、可用性和適用性)的研究。他感興趣的其它領域包括作業系統本質和處理器體系結構。可以透過 與他聯絡。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10617731/viewspace-950421/,如需轉載,請註明出處,否則將追究法律責任。

相關文章