在 C 語言當中內嵌 GNU 的組合語言

GNU 程式設計

GNU 程式簡介

GNU 與 C 語言

gcc 編譯器

glibc 函式庫

make 建置

ld 連結器

as 組譯器

ar 函式庫

objdump

objcopy

訊息

相關網站

參考文獻

最新修改

簡體版

English

有時,為了能增進程式的效率,或者在 C 語言當中加入一些組合語言程式碼,我們會採用內嵌的方式,將組合語言內嵌在C語言當中。針對這點,gcc提供了如圖 4.10的內嵌語法,讓我們在 C 語言中可以直接撰寫組合語言。其中,assembler template 為組合語言程式、output operands 為輸入參數、input operands 為輸入參數,而 list of clobbered registers 則指定了被更改的暫存器參數列表。

asm(                                   ; 內嵌起始符號
     assembler template                ; 組合語言程式
     : output operands                 ; : 輸出參數列表
     : input operands                  ; : 輸入參數列表
     : list of clobbered registers     ; : 被更改的暫存器列表
     );                                ; 結束符號

圖 4.10GNU 的內嵌組合語言語法

舉例而言,範例 4.24的C語言程式當中,就內嵌了組合語言。其中,第一欄是內嵌組合語言的C語言程式 inlineAsm1.c,第二欄則是其說明,第三欄是我們利用gcc將該程式轉換成inlineAsm.s 組合語言程式的片段,其中有底線的文字部分對應的是內嵌組合語言的展開部分。

在GNU的內嵌組合語言 (assembler template) 中,由於 % 被用作參數 %1, %2, … 的前置字元,因此當遇到暫存器(像是 %eax) 時,必須使用兩個 % 符號 (%%) 作為暫存器的起始標記,因此,在範例 4.24中,原本的 addl %ebx, %eax 指令,就成了 addl %%ebx, %%eax 這個更冗長的指令。

在範例 4.24的內嵌參數部分,:"a"(foo), "b"(bar) 是輸入參數,代表要將 foo 變數傳遞給限制條件為 "a" 的暫存器,由於在 IA32 中限制條件為"a"的暫存器就是 eax,因此,foo 變數將會傳給 eax 暫存器。同理,"b"(bar) 參數代表將 bar 傳給 ebx 暫存器。而輸出參數 :"=a"(foo) 則是將 eax 的結果傳回到 foo 變數中。

範例 4.24 內嵌組合語言的C程式

int main(void)                   ; ...                
{                                ; movl $10, -8(%ebp)       ; foo=-8(%ebp)=10
  int foo = 10, bar = 15;        ; movl $15, -12(%ebp)      ; bar=-12(%ebp)=15
                                 ; 輸入參數:
  asm(                           ; movl -8(%ebp), %eax      ; eax = -8(%ebp)=foo
    "addl %%ebx,%%eax"           ; movl -12(%ebp), %ebx     ; ebx = -12(%ebp)=bar
    :"=a"(foo)                   ; /APP                     ; 嵌入的程式
    :"a"(foo), "b"(bar)          ; addl %ebx,%eax           ; eax = eax+ebx
  );                             ; /NO_APP                  ; 傳出參數
                                 ; movl %eax, -8(%ebp)      ; foo=eax
  printf("foo=%d\n", foo);       ; movl -8(%ebp), %eax
  return 0;                      ; movl %eax, 4(%esp)
}                                ; movl $LC0, (%esp)
                                 ; call _printf
                                 ; ...

請讀者仔細觀察範例 4.24的組合語言程式碼,其中,內嵌指令 asm() 中的輸入參數部分為 :"a"(foo), "b"(bar)。這導致代表foo參數的-8(%ebp)被傳入限制條件 "a" 所對應的 eax暫存器中;而代表bar參數的-12(%ebp)則被傳入限制條件"b"所對應的ebx 暫存器中,然後,才執行內嵌指令 addl %ebx, %eax。在執行完畢之後,輸出參數 :"=a"(foo) 指定將 eax 傳回 foo:-8(%ebp) 變數中,於是最後補上了 movl %eax, -8(%ebp) 指令,完成整個內嵌呼叫的過程。

範例 4.25顯示了該程式的編譯執行過程,首先,我們利用 gcc 加上 –S 參數,將C語言編譯為組合語言檔案。接著,再利用gcc作為組譯連結器,將inlineAsm1.s組譯並連結為執行檔,然後執行之,結果,foo果然變成 25,這代表嵌入的組合語言程式發揮了效用。

範例 4.25 <範例 4.24> 的編譯與執行過程

$ gcc -S inlineAsm1.c -o inlineAsm1.s  ; 將C程式編譯為組合語言

$ gcc inlineAsm1.s -o inlineAsm1       ; 組譯 inlineAsm1.s 組合語言檔

$ ./inlineAsm1                         ; 執行inlineAsm1 的執行檔
foo=25                                 ; 結果: foo+bar 的值為25

在嵌入式組合語言 asm() 的語法中,每個參數的語法為 "限制條件"(變數),例如範例 4.24中的 "a"(foo), "b"(bar) 等,其中的 "a", "b" 都是限制條件,而 (foo), (bar) 則是C語言中所傳入傳出的變數。

限制條件可能代表某一個暫存器、一群暫存器、記憶體變數、或者數值等。例如,限制條件m代表『記憶體運算元』、p代表『合法的位址』,而 g 則代表『通用型暫存器、記憶體位址或立即定址的數值』的集合等等。

但是,限制條件的設定與CPU的架構有關,同一個符號在不同的CPU中會有不同的意義。例如,在IA32處理器中,限制條件a代表 eax、b 代表 ebx、c代表 ecx、d代表 edx、S 代表 esi、D代表 edi、r代表『eax ebx ecx edx esi edi』的集合、q代表 『eax ebx ecx edx』的集合、g代表『eax,ebx, ecx, edx 或變數』,而 1,則代表與參數 %1 相同, 2 代表與參數 %2 相同 …。

表格 4.1 GNU嵌入式組合語言的限制條件 (針對 IA32處理器)

限制 說明 限制 說明
A eax I (instant constant) 常數 (0到31 之間)
B ebx m (memory) 記憶體位址
C ecx r (register) 暫存器eax ebx ecx edx esi edi
D edx q eax ebx ecx edx
S esi g (general) eax,ebx, ecx, edx、變數或常數
D edi 0, 1, 2, … 參數 %0 %1 %2 ….

利用表格 4.1中的限制條件,我們可以放寬暫存器的限制,改使用像 addl %2, %0 這樣的方式。這種方式可以讓編譯器自行選擇能使用的暫存器,如此,可以增進程式的效率。在範例 4.26中,我們就使用了此種參數指定的方式,利用 addl %2, %0 指令內嵌,然後,在輸出參數0 中指定 :"=r"(foo),這使得 foo 可使用的候選的暫存器放寬為 r 條件的『eax ebx ecx edx esi edi』。接著,在輸入參數 %1, %2中指定 :"0"(foo), "r"(bar),這同樣使得 bar 的候選暫存器放寬為r 條件的『eax ebx ecx edx esi edi』。但是,由於 foo 變數已經在輸出參數 :"=r"(foo)被指定了,因此,必須在第一個輸入參數的 (foo) 前加上 "0" 條件,告訴編譯器該參數所用的暫存器必須與 :"=r"(foo) 所選擇的一致,如此,就能讓編譯器有更多的選擇空間。範例 4.26顯示了我們將前述的內嵌組合語言之條件由 a, b 放寬為 r 之後的結果。

範例 4.26 將<範例 4.24> 的條件放寬為 “r” – (2)

int main(void)                    ;
{                                 ; movl    $10, -4(%ebp)     ; foo=-4(%ebp)=10
  int foo = 10, bar = 15;         ; movl    $15, -8(%ebp)     ; bar=-8(%ebp)=15
                                  ;                           ; 輸入參數:
  asm(                            ; movl    -4(%ebp), %ecx    ; ecx = -4(%ebp)=foo
    "addl %2,%0"                  ; movl    -8(%ebp), %edx    ; edx = -8(%ebp)=bar
    :"=r"(foo)                    ; /APP                      ; 嵌入的程式
    :"0"(foo), "r"(bar)           ; addl %edx,%ecx            ; eax = eax+ebx
    :"0"                          
  );                              ; /NO_APP
  printf("foo=%d\n", foo);        ; movl    %ecx, %edx        ; 傳出參數
  return 0;                       ; movl    %edx, %eax        ; foo=eax
}                                 ; movl    %eax, -4(%ebp)
                                  ; movl    -4(%ebp), %eax
                                  ; movl    %eax, 4(%esp)
                                  ; movl    $LC0, (%esp)
                                  ; call    _printf

乍看之下,範例 4.26的輸出程式碼並沒有比範例 4.24更短或更有效率,但是,仔細看範例 4.27中兩個組合語言的對照後會發現,foo 變數從 -8(%ebp) 改變為 -4(%ebp),而 bar 則從 -12(%ebp) 改變為 -8(%ebp),這是因為在範例 4.26中,由於編譯器選擇改用 addl %edx, %ecx 取代範例 4.24中的 addl %ebx, %eax,這使得範例 4.26的組合語言能少掉第4行的push %ebx 的動作,於是增進了效率,並且減少了堆疊的使用空間。

範例 4.27 <範例 4.24>與<範例 4.26>的組合語言對照

...                               ; ...
_main:                            ;  _main:
    pushl    %ebp                 ;    pushl    %ebp
    movl    %esp, %ebp            ;    movl    %esp, %ebp
    pushl    %ebx                 ;    subl    $24, %esp
    subl    $20, %esp             ;    andl    $-16, %esp
    andl    $-16, %esp            ;    movl    $0, %eax
    movl    $0, %eax              ;    addl    $15, %eax
    addl    $15, %eax             ;    addl    $15, %eax
    addl    $15, %eax             ;    shrl    $4, %eax
    shrl    $4, %eax              ;    sall    $4, %eax
    sall    $4, %eax              ;    movl    %eax, -12(%ebp)
    movl    %eax, -16(%ebp)       ;    movl    -12(%ebp), %eax
    movl    -16(%ebp), %eax       ;    call    __alloca
    call    __alloca              ;    call    ___main
    call    ___main               ;    movl    $10, -4(%ebp)
    movl    $10, -8(%ebp)         ;    movl    $15, -8(%ebp)
    movl    $15, -12(%ebp)        ;    movl    -4(%ebp), %ecx
    movl    -8(%ebp), %eax        ;    movl    -8(%ebp), %edx
    movl    -12(%ebp), %ebx       ;    /APP
/APP                              ;    addl %edx,%ecx
    addl  %ebx,%eax               ;    /NO_APP
/NO_APP                           ;    movl    %ecx, %edx
    movl    %eax, -8(%ebp)        ;    movl    %edx, %eax
    movl    -8(%ebp), %eax        ;    movl    %eax, -4(%ebp)
    movl    %eax, 4(%esp)         ;    movl    -4(%ebp), %eax
    movl    $LC0, (%esp)          ;    movl    %eax, 4(%esp)
    call    _printf               ;    movl    $LC0, (%esp)
    movl    $0, %eax              ;    call    _printf
    movl    -4(%ebp), %ebx        ;    movl    $0, %eax
    leave                         ;    leave
    ret                           ;    ret
...                               ;  ...

有關gcc 的內嵌組合語言的語法,您可以參考網路上的『GCC Inline Assembly - HOWTO』一文 。

在本節中,我們已經介紹了如何使用 gcc 進行編譯、組譯與連結的方法,由於gcc是一個編譯、組譯與連結的萬用工具,因此,通常我們不會直接使用 GNU的 as 組譯器,而會改用 gcc 作為組譯器,以便與 C 語言程式進行連結,甚至在C語言當中直接內嵌組合語言,這讓我門不需要完全從組合語言開始撰寫,對系統程式的開發有相當大的用處,特別是嵌入式系統。

Facebook

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 3.0 License