My code works, I don’t know why.

國王的耳朵是驢耳朵

關於GNU Inline Assembly

| Comments

以前稍微接觸過GNU Inline Assembly,對於那些奇怪的符號總是覺得匪夷所思。這次找時間把他整理一下。雖然釐清了一些觀念,不過卻產生更多的疑惑,也許以後有機會看到範例會慢慢有感覺吧。

目錄

前言

我自己對於GNU Inline Assembly的看法。

  • 編譯器 夠聰明,所以暫存器分配可以安心交給編譯器處理。也就是說語法上面要處理這塊。
  • 暫存器、變數有些資訊仍然要讓編譯器知道,讓編譯器產生object binary遵守這樣的規則,如
    • 這個operand是一個暫存器
    • 這個operand是一塊記憶體
    • 這個operand是浮點常數
  • 不想讓編譯器幫你安排暫存器,而是在Inline Assembly指定暫存器的話,就要明確的列出來。讓編譯器知道這些暫存器有被改過資料,進而針對這些暫存器做適當的處理。

測試環境

我使用ARMv7為主的Banana Pi開發版加上Lubuntu 14.04作為測試環境。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ lsb_release -a
No LSB modules are available.
Distributor ID:   Ubuntu
Description:  Ubuntu 14.04.3 LTS
Release:  14.04
Codename: trusty

$ dmesg
...
[    0.000000] Linux version 3.4.90 (bananapi@lemaker) (gcc version 4.6.3 (Ubuntu/Linaro 4.6.3-1ubuntu5) ) #2 SMP PREEMPT Tue Aug 5 14:11:40 CST 2014
[    0.000000] CPU: ARMv7 Processor [410fc074] revision 4 (ARMv7), cr=10c5387d
...

$ gcc -v
...
Target: arm-linux-gnueabihf
...
gcc version 4.8.4 (Ubuntu/Linaro 4.8.4-2ubuntu1~14.04) 

語法

inline assembler關鍵字是asm,不過__asm__也可以使用()。

根據目前(Dec/2015)的gcc手冊,inline assembler有分為basicextended兩種。雖然我使用的平台是gcc 4.8.4,而且gcc 4.8.5手冊(官方網站上沒有4.8.4手冊)並沒有提到這個部份。但是目前語法上測試的確沒有問題,但是有些說明上面卻很難驗證是否可以套用到4.8.5上(例如最佳化的說明、需要注意常犯的錯誤),請自行斟酌。

以下是整理自最新的手冊說明,請自行斟酌您使用的gcc版本是否有符合。

Basic inline assembler

1
 [ volatile ] asm("Assembler Template");

以下是整理自最新(Dec/2015)的手冊說明節錄,請自行斟酌您使用的gcc版本是否有符合。

  • basic inline assembler 預設就是volatile
  • 基本上編譯器只是把引號內的東西抄錄,所以只要組譯器支援的語法,就可以寫入Assembler Template內
  • 和extended inline assembler的差異
    • extended inline assembler 只允許在函數內使用
    • naked屬性的函數必須使用basic inline assembler(見註解)
    • basic inline assembler就是把template內的字串作為組合語言組譯。而%字元在extended inline assembler有特別意義,然而有些組合語言如x86中%是暫存器語法的一部份。以至於%字元要在extended inline assembler中改為%%才是真正的意思,舉個例子%eax->%%eax
  • 有要使用C 語言的資料,使用extended inline assembler比較妥當
  • GCC 最佳化時是有可能把你的inline assembler幹掉或是和你想的不一樣,請注意
  • 你不可以從一個asm(..)裏面跳到另外一個asm(..)的label

最簡單的廢話範例如下

1
asm("nop"); /* 啥事都不要做 */

在沒有使用C 語言的變數下,就和一般的組合語言沒有差太多。 更複雜一點的例子可以看rtenv裏面的使用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
size_t strlen(const char *s) __attribute__ ((naked));
size_t strlen(const char *s)
{
    asm(
        "    sub  r3, r0, #1            \n"
        "strlen_loop:               \n"
        "    ldrb r2, [r3, #1]!        \n"
        "    cmp  r2, #0                \n"
        "   bne  strlen_loop        \n"
        "    sub  r0, r3, r0            \n"
        "    bx   lr                    \n"
        :::
    );
}

要注意__attribute__ ((naked));是有意義的。這是為何這段範例沒有直接指名用到C 語言函式變數名稱的關鍵點。有興趣請看這邊,請直接找字串naked

Extended inline assembler

1
2
3
4
5
 asm [volatile] ( AssemblerTemplate
                    : OutputOperands   // optional
                  [ : InputOperands    // optional
                  [ : Clobbers ]       // optional
                  ])

Assembler Template基本上就是你要寫的組語加上 Inline Assembler 專用的符號。要注意的是,在編譯的過程中,你寫的inline assembler可能由於最佳化考慮不會被組譯。如果你確認你inline assembler一定要被組譯,請加上volatile keyword。

Assembler 專用的符號節錄如下:

符號 說明
%% 單一%字元
%{ 單一{字元
%} 單一}字元
|{ 單一|字元
%= 只知道並驗證過會產生唯一的數字。用途部份看不懂,英文真是奧妙的東西啊。

AssemblerTemplate

由於前言提到的三項個人猜測,造成inline assembler要使用C 語言變數時語法會出現很多令人眼花撩亂的符號。

由於編譯器提供協助分配暫存器和記憶體,也就是說需要有對應的語法指定目前指令的operand是什麼。GCC 有兩種方式指定,分別是

  • 編號指定,從零開始編號
  • Symbolic name指定: GCC 3.1以後支援出處

分別給個範例讓各位感受一下

編號指定,從零開始編號

這邊%0, %1就是編號。後面operand可以看到就是指定變數、以及變數的限制。這邊簡單解釋一下=表示這是一個輸出、而r表示變數要放在暫存器中、m表示變數是放在記憶體中。有興趣比對編譯出來的binary反組譯時的組合語言請看這邊。不過編號和指令中的operand似乎很隨意,我沒有看到特殊規範。只能交叉比對assembler template和input/output operands才能看出端倪。我猜更複雜的情況你還要比對反組譯出來的結果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

int main(void)
{
    int var1 = 12;
    int var2 = 10;

    asm("mov %0, %1 \n  \
         add %1, %0, $1" : "=r"(var1), "=r"(var2) : "r"(var2), "r"(var1):);
    printf("var1 = %d, var2 = %d\n", var1, var2);

    asm("ldr r5, %0 \n":           : "m"(var1): "r5");
    asm("str r4, %0"   : "=m"(var2):          : "r4");
    return 0;
}

Symbolic name指定

編號的缺點就是可讀性比較差,所以gcc 3.1出現使用symbolic name的方式。至於那一個比較好,看你自己習慣。

直接把上面的範例更改一下。GCC 4.8.5手冊上面說symbolic name隨便取,甚至和變數同名稱都可以,只要單一asm(…)內的 symbolic name不要重複就好。有興趣比對編譯出來的binary反組譯時的組合語言請看這邊

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int main(void)
{
    int var1 = 12;
    int var2 = 10;

    asm("mov %[my_var1], %[my_var2] \n  \
         add %[my_var3], %[my_var4], $1" :
            [my_var1] "=r" (var1), [my_var3] "=r" (var2) :
            [my_var2] "r"  (var2), [my_var4] "r"  (var1) :);
    printf("var1 = %d, var2 = %d\n", var1, var2);

    asm("ldr r5, %[my_var1] \n":: [my_var1] "m"(var1): "r5");
    asm("str r4, %[my_var1]": [my_var1] "=m" (var2):: "r4");
    return 0;
}

接下來來看每個欄位吧。

Output operands

1
[ [asmSymbolicName] ] constraint (cvariablename)

[asmSymbolicName] 是GCC 3.1以後支援語法,如前所述,不用Symbolic Name就用編號方式對應assembler template operand。

指定結果要存在C 語言中的那個變數。要注意的除了要設定對的資訊(constraints,下面會節錄) 以外,operand的prefix一定要是=+這兩個constraint。

隨便舉幾個範例

  • =r(var1):變數請寫入並放在暫存器中
  • =m(var1):變數請寫入並存到記憶體中

Input operands

1
[ [asmSymbolicName] ] constraint (cvariablename)

[asmSymbolicName] 是GCC 3.1以後支援語法,如前所述,不用Symbolic Name就用編號方式對應assembler template operand。

指定要從在C 語言中的那個變數取出資料。主要是要設定對的資訊(constraints,下面會節錄) 。

  • r(var1):變數請放在暫存器中
  • m(var1):變數是在記憶體中

Clobbered registers list

先講結論,在asm("語法")中明確地指定暫存器名稱的話,要在這邊列出。

現在我會習慣查單字。Clobbered查英文單字會發現就是把東西用力地砸毀。所以翻譯成中文就是「砸爛的暫存器列表」。什麼是爛掉的暫存器?就是本節前面的結論囉。

另外從Dec/2015的gcc 手冊還有找到下面語法,一樣請注意版本問題

符號 說明
“cc” 和狀態有關的flag暫存器會被修改
“memory” 這段組合語言會讀寫列出operand以外的記憶體內容,因此編譯器會視情況備份暫存器或讀寫記憶體

Constraints

1
2
3
4
<Constraints>       ::= <Constraint Modifier> <Other Constraints> | <Other Constraints>
<Other Constraints> ::= <Simple Constraints> | <Machine Constraints>

; /* 以上BNF是我整理的,terminal symbol請自行看手冊 */

節錄整理我看得懂感興趣的部份。

Simple Constraints
符號 說明
空白字元 會被忽略,排版用
m operand 存放在記憶體中
r operand 將被放在暫存器中
i operand 是一個整數常數,該常數包含下面的情形(symbolic name):`#define MAX_LINE (32)`
n operand 是一個整數常數,只允許填入數字
E operand 是一個浮點數常數,不清楚和`F`的差異
F operand 是一個浮點數常數,不清楚和`E`的差異
g operand 存在暫存器(r)或是記憶體內(m),或是這是一個整數常數
X 不用檢查operand

  

你可以使用組合技如"rim",如果這樣寫的話,意思是要編譯器幫你挑一個最適合的方式處理對應於assembler template內的operand。   

Constraint Modifier
符號 說明
= 表示這是一個write only的operand,必須為contraint開始字元。
+ 表示這個 operand 在指令中是同時被讀寫的,必須為contraint開始字元。
& 該operand 為earlyclobber。earlyclobber就是在instruction讀取該operand前,該operand會被寫入。雖然如此,到底是多久前?是和data hazard有關嘛?還是跟資料一致性有關?或者是和編譯器 最佳化造成非預期結果有關?真是一團謎完全搞不懂做啥用,也不清楚使用時機。這邊有範例,一樣搞不懂為什麼要有+, &的modifier
% 該operand 可以讓編譯器 決定這個operand是否和後面的operand交換(commutative),完全搞不懂做啥用

  

ARM 專用的Constraint

我參考的是gcc 4.8.5手冊(因為和測試環境的gcc版本最接近),可能有版本的問題,這些我都沒有做實驗測試,請自行斟酌。

符號 說明(一般模式)
w VFP 浮點運算
G 浮點運算的0.0
I 8 bit正整數
K I contraint 的invert (一的補數),Wen: 不知道為什麼要扯到I constraint?
L I contraint 的負數 (二的補數),Wen: 不知道為什麼要扯到I constraint?
M 0 ~ 32的正整數
Q 要參考的記憶體位址存放在一個暫存器內
R operand是一個const pool內的東西,不要問我const pool是啥,估狗到都和Java有關
S operand 目前檔案中.text內的一個symbol
Uv VFP load/store 指令可存取的記憶體
Uy iWMMXt load/store 指令可存取的記憶體
Uq ARMv4 ldrsb 指令可存取的記憶體

完整列表在這邊,要注意的是2015年12月的手冊又多了一些新的contstraint。請自行參考。

參考資料

附錄

  • C 語言標準有提到編譯器可以使用asm keyword,而且沒有定義語法。有興趣可以找C11C99C89的標準,直接搜尋asm就可以看到了。

  • naked使用basic inline assembler和extended inline assembler比較

下面兩個函數,strcmp1沒有任何extended inline assembler而strcmp2硬塞了一個下去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
int strcmp1(const char *a, const char *b) __attribute__ ((naked));
int strcmp1(const char *a, const char *b)
{
    asm(
        "strcmp_lop1:                \n"
        "   ldrb    r2, [r0],#1     \n"
        "   ldrb    r3, [r1],#1     \n"
        "   cmp     r2, #1          \n"
        "   it      hi              \n"
        "   cmphi   r2, r3          \n"
        "   beq     strcmp_lop1      \n"
        "    sub     r0, r2, r3      \n"
        "   bx      lr              \n"
        :::
    );
}

int strcmp2(const char *a, const char *b) __attribute__ ((naked));
int strcmp2(const char *a, const char *b)
{
        int i;
    asm(
        "strcmp_lop2:                \n"
        "   ldrb    r2, [r0],#1     \n"
        "   ldrb    r3, [r1],#1     \n"
        "   cmp     r2, #1          \n"
        "   it      hi              \n"
        "   cmphi   r2, r3          \n"
        "   mov     %1, $1 \n"
        "   beq     strcmp_lop2      \n"
        "    sub     r0, r2, r3      \n"
        "   bx      lr              \n"
        :"=r"(i)::
    );
}

我們可以比較一下下面兩個函數最後編譯出來的指令,strcmp2顯然和我們預期的差很多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
000083f4 <strcmp1>:


int strcmp1(const char *a, const char *b) __attribute__ ((naked));
int strcmp1(const char *a, const char *b)
{
    asm(
    83f4:  f810 2b01     ldrb.w r2, [r0], #1
    83f8:  f811 3b01     ldrb.w r3, [r1], #1
    83fc:  2a01         cmp  r2, #1
    83fe:  bf88         it   hi
    8400:  429a         cmphi    r2, r3
    8402:  d0f7         beq.n  83f4 <strcmp1>
    8404:  eba2 0003    sub.w  r0, r2, r3
    8408:  4770        bx   lr
        "   beq     strcmp_lop1      \n"
        "    sub     r0, r2, r3      \n"
        "   bx      lr              \n"
        :::
    );
}
    840a:  4618        mov  r0, r3

0000840c <strcmp2>:
        "   beq     strcmp_lop2      \n"
        "    sub     r0, r2, r3      \n"
        "   bx      lr              \n"
        :"=r"(i)::
    );
}

  • 範例一的反組譯節錄
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
$ objdump -d -S asm
...
000083f4 <main>:
#include <stdio.h>

int main(void)
{
...
    int var1 = 12;
    83fa:  230c         movs r3, #12
    83fc:  603b         str  r3, [r7, #0]

    int var2 = 10;
    83fe:  230a         movs r3, #10
    8400:  607b         str  r3, [r7, #4]

    asm("mov %0, %1 \n  \
         add %1, %0, $1" : "=r"(var1), "=r"(var2) : "r"(var2), "r"(var1):);
    8402:  687b         ldr  r3, [r7, #4]
    8404:  683a         ldr  r2, [r7, #0]
    8406:  461a         mov  r2, r3
    8408:  f102 0301    add.w  r3, r2, #1
    840c:  603a         str  r2, [r7, #0]
    840e:  607b         str  r3, [r7, #4]
...
    asm("ldr r5, %0 \n":           : "m"(var1): "r5");
    8424:  683d         ldr  r5, [r7, #0]

    asm("str r4, %0"   : "=m"(var2):          : "r4");
    8426:  607c         str  r4, [r7, #4]
...
}
...

  • 範例二的反組譯節錄
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
$ objdump -d -S asm
...
000083f4 <main>:
#include <stdio.h>

int main(void)
{
...
    int var1 = 12;
    83fa:  230c         movs r3, #12
    83fc:  603b         str  r3, [r7, #0]

    int var2 = 10;
    83fe:  230a         movs r3, #10
    8400:  607b         str  r3, [r7, #4]

    asm("mov %[my_var1], %[my_var2] \n  \
         add %[my_var3], %[my_var4], $1" :
            [my_var1] "=r" (var1), [my_var3] "=r" (var2) :
            [my_var2] "r"  (var2), [my_var4] "r"  (var1) :);
    8402:  687b         ldr  r3, [r7, #4]
    8404:  683a         ldr  r2, [r7, #0]
    8406:  461a         mov  r2, r3
    8408:  f102 0301    add.w  r3, r2, #1
    840c:  603a         str  r2, [r7, #0]
    840e:  607b         str  r3, [r7, #4]
...
    asm("ldr r5, %[my_var1] \n":: [my_var1] "m"(var1): "r5");
    8424:  683d         ldr  r5, [r7, #0]

    asm("str r4, %[my_var1]": [my_var1] "=m" (var2):: "r4");
    8426:  607c         str  r4, [r7, #4]
...
}

Comments