其實,直接以 assembly 開發所有程式是很不容易的,應該也很少人會這麼做;不過,了解 assembly 還是有好處的,像是:
- 一般寫的 assembly code 會比由 compiler 所產生的更小更快
- assembly 可以直接存取硬體,一般來說這是高階語言不允許的
- 學習如何寫 assembly 有助於更深入了解電腦是如何運作的
- 學習如何寫 assembly 可以更了解 compiler 與高階語言是如何運作的
第一支 NASM 程式
之後的程式,將會以 C 語言為起點,中間進入 assembly code,最後再回到 C 以結束程式。
這樣做的好處有以下兩點:
- 讓 C 確保程式可以正確的在 protected mode 中運作,所有的 segment 與對應的 segment register 都交由 C 負責初始化,寫 assembly code 時完全不用擔心這個部分。
- 可以在 assembly code 中使用 C standard library
首先,先說明所使用的 C 程式,程式如下:
再來是 assembly code:int main() {
int ret_status;
ret_status = asm_main();
return ret_status;
}
在開發程式中,有幾點是必須注意的:;檔案: 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
- 預先初始化的資料要擺在 .data segment 中 (預先定義好的字串)
- 未初始化的資料要擺在 .bss(block started by symbol) segment 中 (儲存使用者輸入的變數)
- 程式碼的部分則則是擺在 .text segment 中
- 若是在 DOS/Windows 的環境下,asm_main 的前方必須加上底線(_)變成 _asm_main,稱為 C calling convention (但在 Linux 的環境下則不用)
- 程式中指定了 asm_main 為 global,表示同一個模組中的程式碼都可以存取 asm_main (在 asm_io 模組中定義的 print_int 也是 global,因此才可以在程式中使用)
編譯與執行
程式開發完成後,當然要進行編譯與執行了,有以下步驟要分別進行:
- 組譯 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 - 編譯 C code
# 僅編譯,不進行 link
shell> gcc -c driver.c - 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;因此這是必須要注意的。
而這個部分,大概只有以下情況需要注意:
- 在不同的電腦中傳遞二進位資料時(可能透過檔案或是網路)
- 當二進位資料以 multi-byte 形式寫入記憶體,並進行單一 byte 讀取的時候,反之亦然
沒有留言:
張貼留言