使用 GNU 工具轉換 C 為組合語言後再修改

簡介

GNU 的組譯器名稱為 as,該程式屬於 GNU binutil 套件的一部分,在 Linux 與 Cygwin等環境中都有此組譯器,但是,由於保護機制的影響,單純使用 as 組譯器很難在 Cygwin 之下執行。

實際上,直接使用 as 組譯的情況並不多見,較常見的情況是使用 gcc 作為組譯器,由於 gcc 會在適當的時機呼叫組譯器 as與連結器 ld,因此,使用 GNU工具的程式設計師通常將 gcc 作為編譯、組譯與連結的萬用工具。在本節中,我們也將以 gcc 作為主要的組譯工具。

為了避免直接撰寫組合語言時誤觸作業系統的保護機制,我們可以利用 gcc 將 C 程式轉換為組合語言,然後再對這些組合語言進行修改,以此種方式『撰寫』組合語言,而非直接從零開始。

將C語言編譯為組合語言後修改

首先,請讀者先撰寫如範例 4.20的 C語言程式 (swap.c),該程式宣告了x, y, t等 3 個變數,然後,利用三個指定敘述將 x 與 y 交換,這是一個在排序法中常見的動作。

範例 4.20 將 x 與 y 交換的C語言程式 (檔名:swap.c)

int main() {
  int x=5, y=3, t;
  t=x;
  x=y;
  y=t;
  printf("x=%d\n",x);
  printf("y=%d\n",y);
  return 1;
}

接著,我們利用 gcc -S swap.c -o swap.s 指令,將C語言程式 (swap.c) 編譯為組合語言檔 (swap.s),swap.s 組合語言的內容如範例 4.21 (a) 所示。其中,29行的call ___main 指令之前的程式碼,主要是進行堆疊與框架環境的設定動作。接著,30行的movl $5, -4(%ebp) 指令將常數 5 放入變數 -4(%ebp) 這個位址中,該位址即為變數a位於堆疊中的位址。然後,31行的movl $3, -8(%ebp) 指令則是將常數 3 放入變數 b 的位址 -8(%ebp) 中。接著,在32-37 行,執行了 t=x; x=y; y=t 的交換動作,其中的 t 變數位址為 -12(%ebp)。然後,在 38-39 行的兩個指令movl -4(%ebp), %eax; movl %eax, 4(%esp),將位於 -4(%ebp) 的x變數放到堆疊上,以便作為 printf 函數的第2個參數,然後在 40 行時,再度利用 movl $LC0, (%esp) 指令,將 LC0這個字串 (也就是 "x=%d\12\0" ) 放入堆疊的第一個參數中,然後用 call printf 指令呼叫 printf 函數,這就是輸出指令 printf("x=%d\n",x);的呼叫過程。接著,位於42-45 行內的是 printf("y=%d\n",y) 的組合語言程式,然後,在 46 行當中,movl $1, %eax 將傳回值 1 放入 eax 暫存器中,然後利用leave; ret兩個指令回到上一層。這就是整個C語言程式 (swap.c) 的 IA32 組合語言。

範例:指令 gcc -S swap.c -o swap.s 所產生的組合語言 (swap.s)

    .file    "swap.c"
    .def ___main; .scl 2; .type    32;    .endef
    .section .rdata,"dr"
LC0:
    .ascii "x=%d\12\0"
LC1:
    .ascii "y=%d\12\0"

    .text
.globl _main
    .def _main; .scl 2; .type 32; .endef
_main:
    pushl    %ebp
    movl    %esp, %ebp
    subl    $24, %esp
    andl    $-16, %esp
    movl    $0, %eax
    addl    $15, %eax
    addl    $15, %eax
    shrl    $4, %eax
    sall    $4, %eax
    movl    %eax, -16(%ebp)
    movl    -16(%ebp), %eax
    call    __alloca
    call    ___main
    movl    $5, -4(%ebp)
    movl    $3, -8(%ebp)
    movl    -4(%ebp), %eax
    movl    %eax, -12(%ebp)
    movl    -8(%ebp), %eax
    movl    %eax, -4(%ebp)
    movl    -12(%ebp), %eax
    movl    %eax, -8(%ebp)
    movl    -4(%ebp), %eax
    movl    %eax, 4(%esp)
    movl    $LC0, (%esp)
    call    _printf
    movl    -8(%ebp), %eax
    movl    %eax, 4(%esp)
    movl    $LC1, (%esp)
    call    _printf
    movl    $1, %eax
    leave
    ret
    .def _printf; .scl 3; .type 32; .endef

範例:將 swap.s 組合語言檔修改後儲存為 swap_asm.s

    .file    "swap.c"
    .def ___main; .scl 2; .type    32; .endef
    .section .rdata,"dr"
LC0:
    .ascii "x=%d\12\0"
LC1:
    .ascii "y=%d\12\0"
    .data
x:    .long 5
y:    .long 3
t:    .long 0
    .text
.globl _main
    .def _main; .scl 2; .type 32; .endef
_main:
    pushl    %ebp
    movl    %esp, %ebp
    subl    $24, %esp
    andl    $-16, %esp
    movl    $0, %eax
    addl    $15, %eax
    addl    $15, %eax
    shrl    $4, %eax
    sall    $4, %eax
    movl    %eax, -16(%ebp)
    movl    -16(%ebp), %eax
    call    __alloca
    call    ___main

    movl    x, %eax
    movl    %eax, t
    movl    y, %eax
    movl    %eax, x
    movl    t, %eax
    movl    %eax, y
    movl    x, %eax
    movl    %eax, 4(%esp)
    movl    $LC0, (%esp)
    call    _printf
    movl    y, %eax
    movl    %eax, 4(%esp)
    movl    $LC1, (%esp)
    call    _printf
    movl    $1, %eax
    leave
    ret
    .def _printf; .scl 3; .type 32; .endef

在讀懂了組合語言程式 (swap.s) 的意義之後,我們就可以修改該組合語言,試著在其中加上一些指令。

由於C語言編譯器使用堆疊暫存器 esp與框架暫存器 ebp 作為變數的存取點,這對我們來說並不容易理解。因此,筆者將 swap.c 編譯後的組合語言 swap.s 修改為 swap_asm.s,用自己定義的 x, y, t 變數,取代原先的版本。然後,我們再利用 gcc 組譯 swap_asm.s 檔,結果其輸出與 swap.s 一模一樣,這顯示了我們的修改方式是正確的。以下是筆者的編譯與修改過程,讀者也可自行測試看看。

範例: 將 C 程式 swap.c 編譯為組合語言並修改的過程

$ gcc -S swap.c -o swap.s

… 修改 swap.s 為 swap_asm.s

$ gcc swap.s -o swap.o

$ ./swap.o
x=3
y=5

$ gcc swap_asm.s -o swap_asm.o

$ ./swap_asm.o
x=3
y=5

從以上的過程,我們可以看到 GNU 組合語言的語法,其中,我們修改的組合語言如範例 4.23所示。其中,我們在資料段 (.data) 中宣告了 x, y, t 等三個變數,並且分別設定初值,而在程式段中,我們利用暫存器 eax 作為中介,移動記憶體變數。例如,t=x 可以由 movl x,%eax與 movl %eax,t 等兩個指令完成。這就是程式的主要部分。接著,我們利用movl x, %eax, movl%eax, 4(%esp) 兩個指令,把 x 變數推入 printf 的參數區,再用 movl $LC0, (%esp) 指令把 "x=%d\n" 字串的指標推入參數區,然後才利用 call _printf指令呼叫 printf 函數。這就是該程式的主要邏輯。

範例:組合語言檔 swap_asm.s 經過修改的片段

    .data
x:    .long 5                  ;   int x = 5, y = 3, t;
y:    .long 3                  ; …
t:    .long 0                  ; 
…                             ; 
    movl    x, %eax            ;   t=x;
    movl    %eax, t            ; 
    movl    y, %eax            ;   x=y;
    movl    %eax, x            ; 
    movl    t, %eax            ;   y=t;
    movl    %eax, y            ; 
    movl    x, %eax            ; 將 x 放入參數區
    movl    %eax, 4(%esp)      ; 
    movl    $LC0, (%esp)       ; 將 "x=%d\n" (LC0) 放入參數區
    call    _printf            ;   printf("x=%d\n",x);
    movl    y, %eax            ; 將 y 放入參數區
    movl    %eax, 4(%esp)      ; 
    movl    $LC1, (%esp)       ; 將 "y=%d\n" (LC1) 放入參數區
    call    _printf            ;   printf("y=%d\n",y);
…                             ; …

透過編譯器產生組合語言的方式,系統程式設計師可以使用如上的修改法,以設計組合語言,而不需要完全從頭開始,這種方法在嵌入式系統的開發過程當中也相當常見,例如,我們可以將效率不好的程式碼修改掉,以增進程式的效率,如此,系統程式人員就能更輕鬆的完成工作,而不需要從頭開始撰寫組合語言。

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