iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 3
1
自我挑戰組

Linux Inside - Synchronization & Interrupt系列 第 3

Inline Assembly & Memory Barrier

如何撰寫 Inline Assembly

相信很多人都有寫過組合語言,但應該沒想過可以在 C 語言中也撰寫組合語言吧,總之這是辦得到的,那麼以下會針對這相關的語法作一些解釋,並不會去詳細介紹每種組語的語法,會以 x86/x64 為主,並且介紹其中與 Synchronization 較相關的部份

參考 Is there a way to insert assembly code into C?

  • GCC
__asm__("movl %edx, %eax\n\t"
        "addl $2, %eax\n\t")
asm("" ::: "memory")
  • VC++
__asm {
  mov eax, edx
  add eax, 2
}

不同編譯器,使用的語法有些微差異

參考 OSDev: Inline_Assembly

那麼來講述一個 asm 的語法該如何閱讀,參考以下程式碼,參數使用冒號作為分割符

asm ( assembler template
    : output operands                   (optional)
    : input operands                    (optional)
    : clobbered registers list          (optional)
    );
  • code 使用到的組合語言
  • output 輸出的變數
  • input 輸入的變數
  • clobbered register 哪些 register 會被這段程式碼修改

Inline Assembly 範例

參考 OSDev: Inline_Assembly

範例一 Assign

#include <stdio.h>

int main() {
    int a=10, b, c;
    asm("movl %1, %0\n\t"
        :"=r"(b)         /* output */
        :"r"(a)          /* input */
    );

    printf("a = %d, b = %d\n", a, b);

    return 0;
}
  • =r(b)= 表示 write-only,r 表示 general register,(b) 則是指變數 b
  • %0 表示 output operand
  • %1 表示 input operand
  • =m(b) 除了存放在 register ,也可以放在記憶體
  • "0"(a) 如果指定數字代表與第幾個 operand 共用 register

範例二 multiple ouput, input

參考 What is the correct use of multiple input and output operands in extended GCC asm?

#include <stdio.h>

int main() {
    int input0 = 10;
    int input1 = 15;
    int output0 = 0;
    int output1 = 1;

    asm volatile("mov %[input0], %[output0]\t\n"
                 "mov %[input1], %[output1]\t\n"
                 : [output0] "=r" (output0), [output1] "=r" (output1)
                 : [input0] "r" (input0), [input1] "r" (input1)
                 :);

    printf("output0: %d\n", output0);
    printf("output1: %d\n", output1);

    return 0;
}
  • 可以使用 [output0] 這樣的語法,幫變數命名

  • 若成功執行會發現結果會是錯誤的,output0, output1 都被修改成 10,因為 GCC 認為只有在最後才會去對 output operand 作寫入的動作,所以他們可能共用同一個 register,這樣的話會導致錯誤

  • volatile 也可以搭配 asm 作使用,反正就是用來阻止編譯器幫你優化

  • 正確版本

asm volatile("mov %[input0], %[output0]\t\n"
             "mov %[input1], %[output1]\t\n"
             : [output0] "=&r" (output0), [output1] "=r" (output1)
             : [input0] "r" (input0), [input1] "r" (input1)
             :);

我們在 [output0] "=&r" (output0) 這個地方新增了 &,這是一個 early clobber 的符號,表示該 operand 在執行 instruction 讀取之前就會先被寫入

範例三 early clobber

參考 When to use earlyclobber constraint in extended GCC inline assembly?

#include <stdio.h>

int main(void) {
    int in = 1;
    int out;
    asm (
        "mov %[in], %[out];" /* out = in */
        "inc %[out];"        /* out++ */
        "mov %[in], %[out];" /* out = in */
        "inc %[out];"        /* out++ */
        : [out] "=&r" (out)
        : [in] "r" (in)
        :
    );
    printf("in = %d, out = %d\n", in, out);
}
  • 如果有使用 early clobber,out 數值變成 2,是符合預期的結果
  • 如果沒使用 early clobber,out 數值變成 3,是不符合預期的行為,因為他直到最後才執行寫入的動作
  • early clobber 是用來提示 compiler 要及早作寫入的動作
  • 注意 early clobber 不能在 input operand 中使用

範例四 Trick

#include <stdio.h>

int main() {
    int EAX;
    asm( "movl $0, %0"
        :"=a" (EAX)
    );

    return 0;
}
  • a 代表 register eax,希望 int EAX 這個變數,多被放在 register 的意思,b, c, d 等也都有對應的 register ,這可以自行參閱手冊

範例五 CAS(Compare-And-Swap)

前一天所提到的 CAS ,這其實早就有硬體實作的版本了,這是一小段在 Kernel 中會使用到的程式碼

#define __raw_cmpxchg(ptr, old, new, size, lock)        \
({                                \
    __typeof__(*(ptr)) __ret;                \
    __typeof__(*(ptr)) __old = (old);            \
    __typeof__(*(ptr)) __new = (new);            \
                                \
    volatile u32 *__ptr = (volatile u32 *)(ptr);        \
    asm volatile(lock "cmpxchgl %2,%1"            \
             : "=a" (__ret), "+m" (*__ptr)        \
             : "r" (__new), "0" (__old)            \
             : "memory");                \
                                \
    __ret;                            \
})
  • lock 並不是一個 instruction,他是一個 instruction prefix,用來告訴 CPU 在執行接下來的 instruction 時,要注意什麼,lock 指的是要鎖住 bus,想知道更多的 instruction prefix 可以參閱 這份資料 21.5. Instruction Prefixes
  • __ret__old 共用同一個 register eax
  • memory 代表 compiler barrier,稍後會解釋
  • cmpxchgl 就是 CAS 的指令,看以下 C 語言程式碼來解釋,其中的 ZF 我搞不太清楚在幹麻
// cmpxchgl __new, *__ptr
// cmpxchgl ebx, ecx
if(eax == ebx) {
    ebx = ecx
    ZF = 1
} else {// 舊的值被改動過,只好塞新的值進去
    eax = ebx
    ZF = 0;
}

Memory Barrier

在昨天又或者是前一個範例,我們都有提到 memory barrier,但究竟什麼是 memory barrier?要理解memory barrier 就必須知道這不僅僅是語法的學習,要對 cpu 是如何執行以及自己的程式碼邏輯有非常深刻的了解

我們可以從 memory order 開始了解,memory order 指的是 cpu 從記憶體抓資料的順序,現代處理器通常都是亂序執行,也就是沒有什麼硬性的規定,但大家可能會很疑惑,他既然是亂序執行,那程式執行結果不就完全亂掉了嗎?對,會亂掉。但亂掉只會在多核平行化的程式產生問題,因為亂序執行也是有遵循一些規則,並不是單純的亂序

遵循什麼規則?遵循 as-if-serial 的限制,這保證了 single thread 的程式,在執行時的正確性

比如以下程式, a = 10 b = 5 誰先執行都不重要,只要在 c = a*b 之前執行即可

#include <stdio.h>

int main() {
    int a, b, c;
    
    a = 10;// write
    b = 5;// write
    c = a*b;
    
    return 0;
}

那麼什麼時候會發生亂序執行呢?這其實只有在 lock-free 相關的程式會發生,如果每個要修改的變數,在改動前都有作 lock,是不會發生這種事情的,但這麼作的話在很多情況是很沒效率的

到目前為止可以發現,影響程式執行正確性的點在於「什麼時候寫入/載入」,也就是「load/store 的順序」,到這個部份就明天再說,因為我還在研讀資料 ......

正在閱讀


上一篇
Spinlock & MCS Lock
下一篇
Memory Barrier
系列文
Linux Inside - Synchronization & Interrupt18

1 則留言

0
davidhu0903ex3
iT邦新手 5 級 ‧ 2021-01-26 05:51:52

好詳細!!

我要留言

立即登入留言