2008年1月27日 星期日

PC Assembly Language 學習筆記(3) - Creating a Program

前言

其實,直接以 assembly 開發所有程式是很不容易的,應該也很少人會這麼做;不過,了解 assembly 還是有好處的,像是:
  1. 一般寫的 assembly code 會比由 compiler 所產生的更小更快
  2. assembly 可以直接存取硬體,一般來說這是高階語言不允許的
  3. 學習如何寫 assembly 有助於更深入了解電腦是如何運作的
  4. 學習如何寫 assembly 可以更了解 compiler 與高階語言是如何運作的


第一支 NASM 程式

之後的程式,將會以 C 語言為起點,中間進入 assembly code,最後再回到 C 以結束程式。

這樣做的好處有以下兩點:
  1. 讓 C 確保程式可以正確的在 protected mode 中運作,所有的 segment 與對應的 segment register 都交由 C 負責初始化,寫 assembly code 時完全不用擔心這個部分。
  2. 可以在 assembly code 中使用 C standard library

首先,先說明所使用的 C 程式,程式如下:
int main() {
int ret_status;
ret_status = asm_main();

return ret_status;
}
再來是 assembly code:
;檔案: first.asm
;描述:第一支 NASM 程式
;目的:詢問兩個整數,並印出兩個整數之和
;
;建立執行檔的語法如下:(in Ubuntu Linux)
; nasm -f elf first.asm
; gcc -o first first.o driver.c asm_io.o

%include "asm_io.inc"

;初始化資料置於 .data segment
segment .data
; 這些 label 指向儲存顯示用字串的記憶體位址
prompt1 db "Enter a number : ", 0 ;記得要加 null terminator(\0)
prompt2 db "Enter another number : ", 0
outmsg1 db "You entered ", 0
outmsg2 db " and ", 0
outmsg3 db ", the sum of these is ", 0

;未初始化的資料置於 .bss(block started by symbol) segment
segment .bss
;這些 label 指向儲存輸入的 double words 記憶體位址
input1 resd 1 ;指定一個未初始化的 double word 空間
input2 resd 1

;程式碼置於 .text segment
segment .text
global asm_main
asm_main:
enter 0, 0 ;程式開始
pusha

mov eax, prompt1 ;將 Label "prompt1" 的 memory address 置於 EAX register 中
call print_string ;作者提供的 library,可印出 EAX register 所指向的字串

call read_int ;作者提供的 library,可讀取使用者輸入的 integer,並存到 EAX register 中
mov [input1], eax ;將 EAX register 中的 interger 存入 Label "input1"

mov eax, prompt2 ;輸出 prompt2 字串
call print_string

call read_int ;讀取使用者輸入的 interger 並置於 Label "input2"
mov [input2], eax

mov eax, [input1] ;將輸入的兩個 integer 相加,並將結果置於 EBX register 中
add eax, [input2]
mov ebx, eax

dump_regs 1 ;將 register 的內容印出
dump_mem 2, outmsg1, 1 ;將 memory 的內容印出


mov eax, outmsg1 ;輸出 outmsg1 (string)
call print_string
mov eax, [input1] ;輸出 input1 (integer)
call print_int
mov eax, outmsg2 ;輸出 outmsg2 (string)
call print_string
mov eax, [input2] ;輸出 input2 (integer)
call print_int
mov eax, outmsg3 ;輸出 outmsg3 (string)
call print_string
mov eax, ebx ;輸出加總後的結果 (integer)
call print_int
call print_nl ;輸出 new line

popa
mov eax, 0 ; 回到 C 程式
leave
ret
在開發程式中,有幾點是必須注意的:
  1. 預先初始化的資料要擺在 .data segment 中 (預先定義好的字串)
  2. 未初始化的資料要擺在 .bss(block started by symbol) segment 中 (儲存使用者輸入的變數)
  3. 程式碼的部分則則是擺在 .text segment
  4. 若是在 DOS/Windows 的環境下,asm_main 的前方必須加上底線(_)變成 _asm_main,稱為 C calling convention (但在 Linux 的環境下則不用)
  5. 程式中指定了 asm_main 為 global,表示同一個模組中的程式碼都可以存取 asm_main (在 asm_io 模組中定義的 print_int 也是 global,因此才可以在程式中使用)


編譯與執行

程式開發完成後,當然要進行編譯與執行了,有以下步驟要分別進行:
  1. 組譯 assembly code
    # 使用 ELF(Executable and Linkable Format) 格式 (GNU C Compiler)
    # 若使用 DJGPP,則格式要改為 COFF(Common Object File Format)
    # 若使用 Borland compiler,則格式為 OBJ

    shell> nasm -f elf first.asm

    # 若是使用 -l 參數可以產生 listing file,檔案中描述程式碼是如何被組譯的
    shell> nasm -f elf -l first.list first.asm
  2. 編譯 C code
    # 僅編譯,不進行 link
    shell> gcc -c driver.c
  3. link 所有 object file
    # first 為執行檔名稱
    shell> gcc -o first driver.o first.o asm_io.o

    # 或是省略第二個步驟,直接進行 compile 與 link
    shell> gcc -o first driver.c first.o asm_io.o


Big & Little Endian Representation

不同的 CPU 中,資料表示的方法是有差別的,以 double word 大小(4 bytes)儲存十進位 20 為例,兩種表示法如下:
  • Big Endian (最大的 byte 擺在最前面):00 00 00 14
  • Little Endian (最小的 byte 擺在最前面):14 00 00 00

使用 big endian 大多為 RISC 的處理器,或是 Motorola 的處理器;而一般我們電腦用的 Intel-based CPU,則是使用 little endian;因此這是必須要注意的。

而這個部分,大概只有以下情況需要注意:
  1. 在不同的電腦中傳遞二進位資料時(可能透過檔案或是網路)
  2. 當二進位資料以 multi-byte 形式寫入記憶體,並進行單一 byte 讀取的時候,反之亦然

沒有留言:

張貼留言