Linux 下兩個最主要的匯編器是 Nasm(free, Netwide Assembler)和 GAS(free, Gnu A ssembler), 後一個和 GCC 結合在一起. 在這篇文章裡我將集中在 Nasm 上, 把 GAS 放在後面, 因為它使用 AT&T 的語法, 需要一個長的介紹. Nasm 調用時應該帶上 ELF 格式選項("nasm -f elf hello.asm"); 產生的目標文件用 GCC 來鏈接("gcc hello.o"), 產生最終的 ELF 二進制代碼. 下面的這個腳本可用來 編譯 ASM 的模塊; 我盡量把它寫得簡單, 所以所有它做的就是接受傳給它的第一個 文件名, 用 Nasm 編譯, 用 GCC 來鏈接. #!/bin/sh # assemble.sh ========================================================= outfile=${1%%.*} tempfile=asmtemp.o nasm -o $tempfile -f elf $1 gcc $tempfile -o $outfile rm $tempfile -f #EOF ================================================================== 基本知識: ---------- 當然最好的就是在了解系統細節之前從一個例子開始. 這裡是一個最基本的 "hello-Word" 形式的程序: ; asmhello.asm ======================================================== global main extern printf section .data msg db "Helloooooo, nurse!",0Dh,0Ah,0 section .text main: push dword msg call printf pop eax ret ; EOF ================================================================= 綱要: "global main" 必須聲明為全局的(global) -- 並且既然我們用 GCC 來鏈接, 進入點必須以 "main" 來命名 -- 從而裝入系統. "extern printf" 只是一個聲明, 為以後在程序中調用; 注意這是必須的; 參數的大小不需要聲明. 我已經把這個 例子用標准的 .data, .text 分節, 但這不是嚴格必須的 -- 可能只需要一個 .text 段, 就像在 DOS 下一樣. 在代碼的主體部分, 你必須把參數壓棧來傳遞給調用. 在 Nasm 裡, 你必須聲明 所有不明確數據的大小; 因此就有 "dword" 這個限定詞. 注意和其他匯編器一樣, Nasm 假設所有的內存/標號的引用都指的是內存地址或者標號, 而不是它的內容. 因而, 指明字符串 'msg' 的地址, 你應該使用 'push dword msg', 指明字符串 'msg' 的內容, 應該用 'push dword [msg]' (這只能包含 'msg' 的前四個字節). 因為 prin tf 需要一個指向字符串的指針, 我們應該指明 'msg' 的地址. 調用 printf 非常的直接. 注意每一次調用後你必須把棧清除(見下); 所以 PUSH 了一 個 dword 後, 我從棧裡把一個 dword POP 進一個無用的寄存器. Linux 程序只簡單的用一 個 RET 來返回系統, 由於每個進程都是 shell(或者是 PID)的產物, 所以程序結束後把 控制權還給它. 注意到在 Linux 下, 你是在 "API" 或中斷服務的場所裡使用系統帶來的標准共享庫. 所有 的外部引用由 GCC 管理, 它給 asm 程序員節省了大部分的工作. 一旦你習慣了基本的 技 巧, Linux 下的匯編編程實際上要比 DOS 簡單的多. C 調用的語法 -------------------- Linux 使用 C 的調用模式 -- 意味著參數以相反的順序進棧(最後一個最先), 調用者必 須清 除棧. 你可以從棧裡把值 pop 出來: push dword szText call puts pop ecx 或者直接修改 ESP: push dword szText call puts add esp, 4 調用的返回值在 eax 或 edx:eax 如果值大於 32 位的話. EBP, ESI, EDI, EBX 由調用 者 保存和恢復. 你必須保存你要使用的寄存器, 像下面這樣: ; loop.asm ================================================================= global main extern printf section .text msg db "HoodooVoodoo WeedooVoodoo",0Dh,0Ah,0 main: mov ecx, 0Ah push dword msg looper: call printf loop looper pop eax ret ; EOF ====================================================================== 粗一看, 非常簡單: 因為你在 10 個 printf() 調用用的是同一個字符串, 你不需要清 除棧. 但當你編譯以後, 循環不會停止. 為什麼? 因為 printf() 裡什麼地方用了 ECX 但沒有保存. 使你的循環正確的工作, 你必須在調用之前保存 ECX 的值, 調用之後 恢復它, 像這樣: ; loop.asm ================================================================ global main extern printf section .text msg db "HoodooVoodoo WeedooVoodoo",0Dh,0Ah,0 main: mov ecx, 0Ah looper: push ecx ;save Count push dword msg call printf pop eax ;cleanup stack pop ecx ;restore Count loop looper ret ; EOF ====================================================================== I/O 端口編程 -------------------- 但直接訪問硬件會怎麼樣呢? 在 Linux 下你需要一個核心模式的驅動程序來做這些 工作... 這意味著你的程序必須分成兩個部分, 一個核心模式提供硬件直接操作的功 能, 其他的用戶模式提供接口. 一個好消息就是你仍然可以在用戶模式的程序中使用 IN/OUT 來訪問端口. 要訪問端口你的程序必須取得系統的同意; 要做這個, 你必須調用 ioperm(). 這個函 數只能被有 root 權限的用戶使用, 所以你必須用 setuid() 使程序到 root 或者直接 運行在 root 下. ioperm() 的語法是這樣: ioperm( long StartingPort#, long #Ports, BOOL ToggleOn-Off) 'StartingPort#' 指明要訪問的第一個端口值(0 是端口 0h, 40h 是端口 40h, 等等), '#Ports' 指明要訪問多少個端口(也就是說, 'StartingPort# = 30h', '#Port = 10', 可以訪問 端口 30h - 39h), 'ToggleOn-Off' 如果是 TRUE(1) 就能夠訪問, 是 FALSE(0) 就不能訪問 . 一旦調用了 ioperm(), 要求的端口就和平常一樣訪問. 程序可以調用 ioperm() 任意多 次, 而不需要在後來調用 ioperm()(但下面的例子這樣做了), 因為系統會處理這些. ; io.asm =================================================================== = BITS 32 GLOBAL szHello GLOBAL main EXTERN printf EXTERN ioperm SECTION .data szText1 db 'Enabling I/O Port Access',0Ah,0Dh,0 szText2 db 'Disabling I/O Port Acess',0Ah,0Dh,0 szDone db 'Done!',0Ah,0Dh,0 szError db 'Error in ioperm() call!',0Ah,0Dh,0 szEqual db 'Output/Input bytes are equal.',0Ah,0Dh,0 szChange db 'Output/Input bytes changed.',0Ah,0Dh,0 SECTION .text main: push dword szText1 call printf pop ecx enable_IO: push word 1 ; enable mode push dword 04h ; four ports push dword 40h ; start with port 40 call ioperm ; Must be SUID "root" for this call! add ESP, 10 ; cleanup stack (method 1) cmp eax, 0 ; check ioperm() results jne Error ;---------------------------------------Port Programming Part-------------- SetControl: mov al, 96 ; R/W low byte of Counter2, mode 3 out 43h, al ; port 43h = control register WritePort: mov bl, 0EEh ; value to send to speaker timer mov al, bl out 42h, al ; port 42h = speaker timer ReadPort: in al, 42h cmp al, bl ; byte should have changed--this IS a timer jne ByteChanged BytesEqual: push dword szEqual call printf pop ecx jmp disable_IO ByteChanged: push dword szChange call printf pop ecx ;---------------------------------------End Port Programming Part---------- disable_IO: push dword szText2 call printf pop ecx push word 0 ; disable mode push dword 04h ; four ports push dword 40h ; start with port 40h call ioperm pop ecx ;cleanup stack (method 2) pop ecx pop cx cmp eax, 0 ; check ioperm() results jne Error jmp Exit Error: push dword szError call printf pop ecx Exit: ret ; EOF ===