GCC 的 inline assembly 讓我們可以在 C code 中嵌入 assembly code。比起直接撰寫 assembly code,C 程式碼的可讀性好多了。本文章將介紹如何使用 GCC inline assembly。
Table of Contents
Basic Asm
GCC 支援兩種 inline assembly 語法。第一種是 basic asm,它只包含 assembler instructions,而沒有 operands,其語法如下。
asm [qualifiers](AssemblerInstructions);
asm keyword 是一個 GNU extension。當使用 -ansi 或 -std 選項並選擇不帶 GNU extensions 的 C dialects 來編譯 C 程式碼時,要改為使用 __asm__。對於 C++,asm 是一個 standard keyword。
Basic asm 有兩個 qualifiers。
- volatile:沒有作用。所有 basic asm 默認為 volatile。
- inline:請參照 Size of an asm。
AssemblerInstructions 可以是任何 assembly code,包含 instructions 和 directives。GCC 不會解析其內容,而是直接輸出 AssemblerInstructions 字串。在 AssemblerInstructions 中,我們可以包含多行 assembly code,並使用 \n\t 來分隔每行 assembly code。
GCC 推薦使用 extended asm 而不是 basic asm。但是 basic asm 有一個好處是,它可以出現在任何地方,而不是一定要在函式裡面。
asm(".code16gcc");
void func()
{
asm("xorw %ax, %ax\n\t"
"movw %ax, %ds");
}Extended Asm
GCC 支援的第二種 inline assembly 是 extended asm。它包含 assembler instructions 和 operands,其語法如下。
asm [qualifiers](AssemblerTemplate
: OutputOperands
[: InputOperands
[: Clobbers]]);
asm [qualifiers](AssemblerTemplate
: OutputOperands
: InputOperands
: Clobbers
: GotoLables);asm keyword 是一個 GNU extension。當使用 -ansi 或 -std 選項並選擇不帶 GNU extensions 的 C dialects 來編譯 C 程式碼時,要改為使用 __asm__。對於 C++,asm 是一個 standard keyword。
Extended asm 有三個 qualifiers。
- volatile:Extended asm 的典型用法會有接受輸入值並產生輸出值。然而,你的 asm 語句可能會產生一些 side effects。所以,你可以使用 volatile 來 disable optimizations。
- inline:請參照 Size of an asm。
- goto:告知 GCC 此 asm 語句可能會執行 jump 到某個列在 GotoLabels 裡的 label。
與 basic asm 不同的是,GCC 會解析 AssemblerTemplate。所以,在 AssemblerTemplate 裡存取 register 時,要使用 %% 而不是 %,這樣 GCC 才會輸出一個 %。此外,extended asm 只能出現在函式裡面。與 basic asm 相同的是,我們可以包含多行 assembly code,並使用 \n\t 來分隔每行 assembly code。
void func()
{
asm("xorw %%ax, %%ax\n\t"
"movw %%ax, %%ds"
: );
}Constraints
在談 output 和 input operands 之前,我們要先談談 constraints,因為它會被用在 operands 裡。Constraints 是用來描述 operands 的字串。一個 operands 可以有多個 constraints。以下是幾種常用的 constraints,其他的 constraints 請參照 Simple Constraints。除了這些 simple constraints,每一種 architecture 支援一些特殊的 constraints,請參照 Constraints for Particular Machines。
| Constraint | Description |
|---|---|
m | Operand 可以是一個 memory address。 |
r | Operand 可以是一個 general-purpose register。 |
i | Operand 可以是一個 immediate integrer。 |
g | Operand 可以是一個 memory、general-purpose register、或 immediate integer。 |
0, 1, …, 9 | Operand 可以是該編號的 operand。 |
一個 constraint 字串可以包含多個 constraint。例如,rm 表示該 operand 可以是 general-purpose register 或 memory address。當 constraint 字串包含多個 constraints 時, GCC 會根據當前的 context 選擇最有效率的一個。
此外,GCC 還提供 constraint modifier 來修飾 operands。以下列出兩種 constraint modifiers,其他的 constraint modifiers 請參照 Constraint Modifier Characters。
| Constraint Modifier | Description |
|---|---|
= | 該 operand 會被該 instruction 寫入。 |
+ | 該 operand 會被該 instruction 讀取與寫入。 |
Operands
一個 asm 語句有 0 個或多個 operands,並用逗號分割。每個 operand 的格式如下。
[[asmSymbolicName]] constraint (cVariableName) [[asmSymbolicName]] constraint (cExpression)
每一個 operand 必須要有 constraint 和對應的 C 變數名稱或 C expression。在一個 asm 語句中,GCC 會對每個 operand 編號,從 0 開始。因此,我們可以透過編號,在 asm statement 中參照 operand。如下程式碼中,我們可以用 %1 參照 mask,%0 參照 index。
uint32_t foo = 1234;
uint32_t bar;
asm("movl %1, %0"
: "=r" (bar)
: "r" (foo));每一個 asm 語句最多只能有 30 operands。而且,當一個 operand 使用 + constraint modifier,它會被計算為 2 個 operands。
除了使用編號來參照 operands,GCC 也允許我們使用名稱來參照 operands,如下。
uint32_t foo = 1234;
uint32_t bar;
asm("movl %[aFoo], %[aBar]"
: [aBar] "=r" (bar)
: [aFoo] "r" (foo));Output Operands
一個 asm 語句有 0 個或多個 output operands,並用逗號分割。它們是用來指示該 assembly code 會修改到的 C 變數的名稱。一個 output operand 必須有 constraint modifier,而且必須是 =(write-only)或 +(read-write)。
[[asmSymbolicName]] constraint (cVariableName)
在以下的程式碼中,foo 必須是 writable 和 readable,所以它的 constraint modifier 是 +。
uint32_t foo = 1234;
asm("xorl %0, %0" : "+r" (foo));以下的程式碼等同於上面的程式碼。在 output operands 中,我們設定 foo 為 write-only。在 read operands 中,設定 foo 使用和 %0 的相同的 location。所以,%0 和 %1 會使用相同的 register。
uint32_t foo = 1234;
asm("xorl %1, %0" : "=r" (foo) : "0" (foo));Input Operand
一個 asm 語句有 0 個或多個 input operands,並用逗號分割。它們是用來指示該 assembly code 會讀取到的 C 變數的名稱或是 c expression。一個 input operand 可以沒有 constraint modifier。如果有的話,它們不可以是 = 和 +。
[[asmSymbolicName]] constraint (cExpression)
The code below shows how to use input operands.
uint32_t c = 1;
uint32_t d;
uint32_t *e = &c;
asm("mov %1, %0"
: "=rm" (d)
: "rm" (*e));Clobbers
Clobbers 是指一個 asm 語句除了會修改列在 output operands 裡的 locations,還會產生一些 side effects 而修改其他的 locations。我們必須要將這些沒有被列在 output operands 裡的 locations 列在 clobbers,來通知 GCC 它們會被修改。
Clobbers 不可以與 input 和 output operands 重疊。Clobbers 可以包含以下的值。
- Register names:指示該 assembly code 會修改其他的 registers,如
rax,r1,但是不可以是 stack pointer register。 - cc:指示該 assembly code 會修改 flags register。
- memory:指示該 assembly code 會對其他的 memory 做 reads 或 writes。為了確保 memory 包含正確的值,在執行該 asm 語句之前,GCC 可能需要 flush 特定的 register 的值到 memory。此外,GCC 不會假設,在該 asm 之前從 memory 讀取的任何值,在該 asm 執行之後保持不變。所以,GCC 會根據需要而重新加載它們。使用 memory clobber 有效地為 GCC 形成一個 read/write 的 memory barrier。
如下程式碼中,bsfl 指令會修改 flags register。
uint32_t mask = 1234;
uint32_t index;
asm("bsfl %1, %0"
: "=r" (index)
: "r" (mask)
: "cc");下面程式碼中,我們直接修改 eax,所以要將 eax 列在 clobber 裡。
asm("xorl %%eax, %%eax"
: /* no output */
: /* no input */
: "eax");Goto Labels
asm 的 goto qualifier 允許 assembly code 可以 jump 到一個或多個 C labels。asm 語句的 GotoLabels 包含著 assembly code 裡面會 jump 過去的 C labels。
在 assembly code 裡參照一個 label 時,使用 %l(小寫 L)加上 label 的編號。GCC 從 0 開始,對 output 和 input operands 編號,並也會接下去對 labels 編號。下面的程式碼顯示如何參照一個 label。另外,在計算編號時,記得使用 + 的 operand 要算 2 個。
asm goto("btl %1, %0\n\t"
"jc %l2"
: /* No outputs. */
: "r" (p1), "r" (p2)
: "cc"
: carry);
return 0;
carry:
return 1;結語
本文章指介紹了基本的 constraints。每一種 architecture 還有很多特定的 constraints。在開始使用 GCC inline assembly 之前,可以根據你當前的 architecture,先了解還有哪些額外的 constraints 可以使用。








