GCC Inline Assembly

Photo by amoon ra on Unsplash
Photo by amoon ra on Unsplash
GCC 的 inline assembly 讓我們可以在 C code 中嵌入 assembly code。比起直接撰寫 assembly code,C 程式碼的可讀性好多了。本文章將介紹如何使用 GCC inline assembly。

GCC 的 inline assembly 讓我們可以在 C code 中嵌入 assembly code。比起直接撰寫 assembly code,C 程式碼的可讀性好多了。本文章將介紹如何使用 GCC inline assembly。

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

ConstraintDescription
mOperand 可以是一個 memory address。
rOperand 可以是一個 general-purpose register。
iOperand 可以是一個 immediate integrer。
gOperand 可以是一個 memory、general-purpose register、或 immediate integer。
0, 1, …, 9Operand 可以是該編號的 operand。
Simple Constraints.

一個 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 ModifierDescription
=該 operand 會被該 instruction 寫入。
+該 operand 會被該 instruction 讀取與寫入。
Constraint Modifiers.

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 可以使用。

參考

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

You May Also Like
Photo by Timothée Geenens on Unsplash
Read More

x86 Memory Map

在 x86 PC 起動後,它會處在 real mode。此時,我們可以存取 1 MB 以下的 memory。然而,BIOS 也會使用一些 memory。因此,我們必須要知道 BIOS 佔據哪些範圍,才能夠避開它們
Read More
Photo by Lanju Fotografie on Unsplash
Read More

Makefile

Makefile 是 Linux 中最常用的編譯工具了。Stuart Feldman 在 1967 的 Bell Labs 裡創造了它。雖然它可能比你我的年紀都還大,但是它現在還是依然地活躍。本文章將介紹 Makefile 的一些基本語法。
Read More
Photo by NEOM on Unsplash
Read More

ELF

ELF file 是 Linux 使用的檔案格式。不管是要學習 linkers 或是反組譯,都需要了解 ELF file。本文章將介紹 ELF 檔案格式。
Read More