Linux內聯彙編 .

 

如果您是 Linux 內核的開發人員,您會發現自己經常要對與體系結構高度相關的功能進行編碼或優化代碼路徑。您很可能是通過將彙編語言指令插入到 C 語句的中間(又稱爲內聯彙編的一種方法)來執行這些任務的。讓我們看一下 Linux 中內聯彙編的特定用法。(我們將討論限制在 IA32 彙編。)

讓我們首先看一下 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" 內部使用寄存器(無論是明確還是隱含地),既不出現在輸入操作數列表中,也不出現在輸出操作數列表中,必須將它列爲修飾寄存器。

 



回頁首

 

總的來說,內聯彙編非常巨大,它提供的許多特性我們甚至在這裏根本沒有涉及到。但如果掌握了本文描述的基本材料,您應該可以開始對自己的內聯彙編進行編碼了。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章