1. 程式人生 > >C表示式中的彙編指令

C表示式中的彙編指令

C 表示式中的彙編指令

asm 為 gcc 中的關鍵字,asm 表示式為在 C程式碼中嵌套匯編指令,該表示式只是單純的替換出彙編程式碼,並不對彙編程式碼的含義進行解析。

asm 表示式有兩種形式,第二種 asm-qualifiers 包含了 goto 語句。
第一種形式為常見的用法,AssemblerTemplate 和 OutputOperands 必須存在, 其中 Clobbers 存在需要 InputOperands 也出現。

asm asm-qualifiers ( AssemblerTemplate 
                 : OutputOperands 
                 [ : InputOperands
                 [ : Clobbers ] ])

asm asm-qualifiers ( AssemblerTemplate 
                      : 
                      : InputOperands
                      : Clobbers
                      : GotoLabels)

Qualifiers 的型別

  • volatile, 避免編譯器的優化
  • inline, 內斂限定符,最小的體積
  • goto, 包含跳轉指令

引數

  • AssemblerTemplate
    - 彙編指令模板是包含彙編器指令的文字字串,編輯器替換引用輸入,編譯器不會解析該指令的含義。
  • OutputOperands
    - 由 AssemblerTemplate 中的指令修改的C變數的逗號分隔列表,允許使用空列表。
  • InputOperands
    - 由 AssemblerTemplate 中的指令讀取的C變數的逗號分隔列表,允許使用空列表。
  • Clobbers
    - 用逗號分隔的暫存器列表或由 AssemblerTemplate 修改的值,不能出現在 OutputOperands 和 InputOperands 中被提及,允許使用空列表。
  • GotoLabels
    - 當使用asm的goto形式時,此部分包含 AssemblerTemplate 中的程式碼可能跳轉到的所有C標籤的列表。

AssemblerTemplate

彙編指令由一個字串給出,多條彙編指令結合在一起使用的時候,中間以 \r\t 隔開,如

asm("inc %0\n\tinc %0" : "=r"(res) : "0"(res));

/APP
# 11 "asm.c" 1
        inc %rax
        inc %rax
# 0 "" 2
/NO_APPs

需要轉義的字元:%, =, {, }, |

故在ATT彙編中,對暫存器進行操作的需要雙 %%, 如 inc %%rax

.

OutputOperands

運算元之間用逗號分隔。 每個運算元具有以下格式:

[ [asmSymbolicName] ] constraint (cvariablename)
  • asmSymbolicName
    - 為運算元指定名稱,格式為 %[name]
    c // res = num asm("movq %[num], %[res]" : [res] "=r"(res) : [num] "m"(num));
    - 如果未指定名稱使用數字, 從 output 域開始,第一個引數為 %0, 一次類推, 這裡的 res 為 %0, num 為 %1
    c // res = num asm("movq %1, %0" : "=r"(res) : "m"(num));
  • constraint
    - 一個字串常量,用於指定對運算元的儲存的 約束, 需要以 "=" 或 "+" 開頭
  • cvariablename
    - 指定一個C左值表示式來儲存輸出,通常是一個變數名。 括號是語法的必需部分

第一個引數為增加可讀性使用的,現在我們有程式碼如下

int64_t res;
int64_t num = 1;

asm("movq %[num], %[res]" : [res] "=r"(res) : [num] "m"(num));
asm("movq %1, %0" : "=r"(res) : "m"(num));
asm("movq %1, %0" : "=m"(res) : "m"(num));
asm("movq %1, %0" : "=r"(res) : "r"(num));

// 對應的彙編程式碼, 只保留asm表示式中的程式碼
# 13 "asm.c" 1
        movq -16(%rbp), %rax  // asm-1
 # 0 "" 2
/NO_APP

/APP
 # 15 "asm.c" 1
        movq -16(%rbp), %rax  // asm-2
 # 0 "" 2
/NO_APP

/APP
 # 17 "asm.c" 1
        movq -16(%rbp), -8(%rbp)  // asm-3
 # 0 "" 2
/NO_APP

/APP
 # 19 "asm.c" 1
        movq %rax, %rax  // asm-4
 # 0 "" 2
/NO_APP
  1. 使用名稱替換和數字替換效果一樣,見 asm-1 和 asm-2
  2. 約束的用法,這裡使用比較簡單通用的的兩種情況,r 為通過暫存器定址操作,m 通過記憶體定址操作,所以看到當約束了 r 就對應暫存器的操作。
  3. 結果儲存在 res 也就是 cvariablename 中

InputOperands

輸入運算元使C變數和表示式中的值可用於彙編程式碼。

[ [asmSymbolicName] ] constraint (cexpression)
  • asmSymbolicName 和輸出列表的用法完全一致
  • constraint 約束不能使用 =+. 可以使用 "0", 這表明在輸出約束列表中(從零開始)的條目,指定的輸入必須與輸出約束位於同一位置。
int64_t res = 3;
int64_t num = 1;
asm("addq %1, %0" : "=g"(res) : "0"(num));

// 輸入輸出位置相同
        movq    $3, -8(%rbp)
        movq    $1, -16(%rbp)
        movq    -16(%rbp), %rax
/APP
# 32 "asm.c" 1
        addq %rax, %rax
# 0 "" 2
/NO_APP
  • cexpression 可以不為左值,作為彙編表示式的輸入值即可

Clobbers

破壞列表,主要用於指示編譯器生成的彙編指令。

從asm表示式中看到輸出運算元中列出條目的更改編譯器是可以確定的,但內聯彙編程式碼可能不僅對輸出進行了修改。 例如,計算可能需要其他暫存器,或者處理器可能會由於特定彙編程式指令而破壞暫存器的值。 為了將這些更改通知編譯器,在Clobber列表中列出這些會產生副作用的條目。 破壞列表條目可以是暫存器名稱,也可以是特殊的破壞列表項(在下面列出)。 每個內容列表條目都是一個字串常量,用雙引號引起來並用逗號分隔。

  • 暫存器

      ```c
      asm volatile("movc3 %0, %1, %2"
              : /* No outputs. */
              : "r"(from), "r"(to), "g"(count)
              : "%rbx", "%rcx", "%rdx", "memory");
    
      /APP
      # 25 "asm.c" 1
              movc3 %rax, %r8, -72(%rbp)
      # 0 "" 2
      /NO_APP
      ```
    
      可以看到使用到了 rax 暫存器,然後修改程式在 Clobbers 增加 %rax, 結果如下
    
      ```c
      asm volatile("movc3 %0, %1, %2"
              : /* No outputs. */
              : "r"(from), "r"(to), "g"(count)
              : "%rax", "%rbx", "%rcx", "%rdx", "memory");
    
      /APP
      # 25 "asm.c" 1
              movc3 %r8, %r9, -72(%rbp)
      # 0 "" 2
      /NO_APP
      ```
      編譯器在產生的彙編程式碼中就未使用 %rax 暫存器了。
  • 特殊破壞列表項
    - "cc", 表示彙編程式碼修改了標誌暫存器
    - "memory", 為了確保記憶體中包含正確的值,編譯器可能需要在執行asm之前將特定的暫存器值重新整理到記憶體中

編譯器為了破壞列表項的值受到破壞,當這些條目是暫存器時,不對其進行使用;為特殊引數時,重新重新整理得到最新的值。

約束

  • 一些基礎的約束
約束名 說明
whitespace 空白字元被忽略
m 允許使用記憶體運算元,以及機器通常支援的任何型別的地址
o 允許使用記憶體運算元,但前提是地址是可偏移的
V 允許使用記憶體運算元,不可偏移的記憶體地址,與 "o'互斥
r 允許在通用暫存器中使用的暫存器運算元,其中可以指定暫存器,如 a(%rax), b(%rbx)
i 允許使用立即整數運算元
n 允許使用具有已知數值的立即整數運算元, ‘I’, ‘J’, ‘K’, … ‘P’ 更應該使用 n
F 允許使用浮點立即數
g 允許使用任何暫存器,記憶體或立即數整數運算元,但非通用暫存器除外
X 允許任何運算元, ‘0’, ‘1’, ‘2’, … ‘9’
p 允許使用有效記憶體地址的運算元
  • 識別符號約束
識別符號 說明
= 表示此運算元是由該指令寫入的:先前的值將被丟棄並由新資料替換
+ 表示該運算元由指令讀取和寫入
& 表示(在特定替代方法中)此運算元是早期指令運算元,它是在使用輸入運算元完成指令之前寫入的,故輸入運算元部分不能分配與輸出運算元相同的暫存器
% 表示該運算元與後續運算元的可交換指令

核心示例

  1. x86 的記憶體屏障指令。
// 避免編譯器的優化,宣告此處記憶體可能發生破壞
#define barrier() asm volatile("" ::: "memory")
// 在32位的CPU下,lock 指令為鎖匯流排,加上一條記憶體操作指令就達到了記憶體屏障的作用,64位的cpu已經有新增的 *fence 指令可以使用
// mb() 執行一個記憶體屏障作用的指令,為指定CPU操作;破壞列表宣告 cc memory 指示避免編譯器進行優化
#ifdef CONFIG_X86_32
#define mb() asm volatile(ALTERNATIVE("lock; addl $0,-4(%%esp)", "mfence", \
                                X86_FEATURE_XMM2) ::: "memory", "cc")
#define rmb() asm volatile(ALTERNATIVE("lock; addl $0,-4(%%esp)", "lfence", \
                                X86_FEATURE_XMM2) ::: "memory", "cc")
#define wmb() asm volatile(ALTERNATIVE("lock; addl $0,-4(%%esp)", "sfence", \
                                X86_FEATURE_XMM2) ::: "memory", "cc")
#else
#define mb()    asm volatile("mfence":::"memory")
#define rmb()   asm volatile("lfence":::"memory")
#define wmb()   asm volatile("sfence" ::: "memory")
#endif
  1. x86 下獲取 current 的值
DECLARE_PER_CPU(struct task_struct *, current_task);

#define this_cpu_read_stable(var)   percpu_stable_op("mov", var)

static __always_inline struct task_struct *get_current(void)
{
        return this_cpu_read_stable(current_task);
}

#define percpu_stable_op(op, var)           \
({                          \
        typeof(var) pfo_ret__;              \
        switch (sizeof(var)) {              \
        case 8:                     \
                asm(op "q "__percpu_arg(P1)",%0"    \
                : "=r" (pfo_ret__)          \
                : "p" (&(var)));            \
                break;                  \
        }                       \
        pfo_ret__;                  \
})

current_task 為一個 struct task_struct 型別的指標,追蹤巨集呼叫,在x86-64 下命中了 case 8: 的彙編程式碼, 展開的程式碼為

asm("mov" "q ""%%""gs" ":" "%" "P1"",%0" : "=r" (pfo_ret__) : "p" (&(current_task)));
// 變換一下為
asm("movq %%gs:%P1, %0" : "=r"(pfo_ret__) : "p"(&(current_task)));

這行程式碼的含義為將 約束輸入部分必須為有效的地址(p約束), 將CPU id(通過段暫存器gs和偏移通過GDT得到,這裡後文分析了)通過暫存器(r約束)賦值給 pfo_ret__.

參考

GCC文件
C語言ASM彙編內嵌語