Linux 中 x86 的內聯匯編
2024-07-21 02:38:19
供稿:網友
作者:Bharata B. Rao
將各個部分組合起來
假如您是 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 如何分配寄存器,以及它如何更