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 可以使用。
參考
- How to Use Inline Assembly Language in C Code, Using the GNU Complier Collection.
- GCC’s assembler syntax.