Linux的x86 匯編程序設計
更新時間: 2007-05-30 10:35:15來源: 粵嵌教育瀏覽量:1257
本質上來說, 這篇文章是把我感興趣的兩樣編程東西: Linux 操作系統和匯編語言程序設計結合在一起. 這兩個都不(或者說應該不)需要介紹; 像 Win32 的匯編,Linux 的匯編運行在 32 位的保護模式下...但它又有一個截然不同的優勢就是它允許你調用 C 的標準庫函數和 Linux 的共享庫函數. 我開始給 Linux 下的匯編語言編程來個簡要介紹; 為了更好讀一點, 你可能要跳過這個基本的小節.
編譯和鏈接
---------------------
Linux 下兩個主要的匯編器是 Nasm(free, Netwide Assembler)和 GAS(free, Gnu Assembler),
后一個和 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 的前四個字節). 因為 printf 需要一個指向字符串的指針, 我們應該指明 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 ======================================================================
在 Linux 下使用中斷
-------------------------
Linux 是一個運行在保護模式下的共享庫的環境, 意味著沒有中斷服務, Right? 錯了. 我注意到在 GAS 的例子源碼中用了 INT 80, 注釋是 "sys_write(ebx, ecx, edx)". 這個函數是 Linux 系統調用接口的一部分, 意思是 INT 80 必須是到達系統調用服務的門戶. 在 Linux 源碼中到處看時(忽略從不要使用 INT 80 接口的警告, 因為函數號可能隨時改變), 我發現 "系統調用號(system call numbers)" -- 就是說, 傳給 INT80 的 # 對應著一個系統調用子程序 -- 在 UNISTD.H 中. 一共有 189 個, 所以我不會在這里列出來...但如果你在 Linux 做匯編, 給自己做個好事, 打印出來吧. 當調用 INT 80 時, eax 設為用調用的功能號. 傳給系統調用則程序的參數必須按順序放在下列寄存器中:
ebx, ecx, edx, esi, edi
這樣, 個參數就在 ebx 里, 第二個在 ecx 里... 注意在一個系統調用程序里, 不是用棧來傳遞參數. 調用的返回值在 eax 里. 還有, INT 80 接口和一般的調用一樣. 下面的這個程序就演示了 INT 80h 的使用. 這個程序檢查并顯示了它自己的 PID. 注意 使用 printf() 格式化字符串 -- 這個調用的C 結構是:
printf( "%d\n", curr_PID);
也要注意結束符在匯編里不一定可靠, 我常用十六進制(0Ah, 0Dh)代表 CR\LF.
;pid.asm====================================================================
BITS 32
GLOBAL main
EXTERN printf
SECTION .data
szText1 db Getting Current Process ID...,0Ah,0Dh,0
szDone db Done!,0Ah,0Dh,0
szError db Error in int 80!,0Ah,0Dh,0
szOutput db \%d,0Ah,0Dh,0 ;printf() 的格式字符串
SECTION .text
main:
push dword szText1 ;開始信息
call printf
pop ecx
GetPID:
mov eax, dword 20 ; getpid() 系統調用
int 80h ; 系統調用中斷
cmp eax, 0 ; 沒有 PID 0 ! :)
jb Error
push eax ; 把返回值傳遞給 printf
push dword szOutput ; 把格式字符串傳遞給 printf
call printf
pop ecx ; 清除棧
pop ecx
push dword szDone ; 結束信息
call printf
pop ecx
jmp Exit
Error:
push dword szError
call printf
pop ecx
Exit:
ret
; EOF =====================================================================
的話
-----------
大多數的麻煩來自對 Nasm 的習慣上. 而 nasm 帶有手冊, 但缺省是不安裝的, 所以你必須把它從
/user/local/bin/nasm-0.97/nasm.man
移(cp 或 mv)到
/usr/local/man/man1/nasm.man.
格式有點亂, 可以很簡單的用 nroff 指示符來解決. 但它不會給你 Nasm 的整個文檔; 要解決這個問題, 把 nasmdoc.txt 從
/usr/local/bin/nasm-0.97/doc/nasmdoc.txt
拷貝到
/usr/local/man/man1/nasmdoc.man
現在你可以用 man nasm, man nasmdoc 來看 nasm 的手冊和文檔了
想得到更多的信息, 查查這里:
Linux Assembly Language HOWTO (Linux 匯編語言 HOWTO)
Linux I/O Port Programming Mini-HOWTO (Linux I/O 端口編程 Mini-HOWTO)
Jans Linux & Assembler HomePage (http://www.bewoner.dma.be/JanW/eng.html)
我也要感謝 Jeff Weeks(http://gameprog.com/codex), 在我找到 Jan 的網頁之前
給了我一些 GAS 的 hello-world 代碼.