本质上来说, 这篇文章是把我最感兴趣的两样编程东西: 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, ed
x)"".
这个函数是 Linux 系统调用接口的一部分, 意思是 INT 80 必须是到达系统调用服务
的门户. 在 Linux 源码中到处看时(忽略从不要使用 INT 80 接口的警告, 因为函数号
可能随时改变), 我发现 ""系统调用号(system call numbers)"" -- 就是说, 传给 INT
80
的 # 对应着一个系统调用子程序 -- 在 UNISTD.H 中. 一共有 189 个, 所以我不会在
这里列出来...但如果你在 Linux 做汇编, 给自己做个好事, 打印出来吧.
当调用 INT 80 时, eax 设为用调用的功能号. 传给系统调用则程序的参数必须按顺序
放在下列寄存器中:
ebx, ecx, edx, esi, edi
这样, 第一个参数就在 ebx 里, 第二个在 ecx 里... 注意在一个系统调用程序里, 不
是
用栈来传递参数. 调用的返回值在 eax 里.
还有, INT 80 接口和一般的调用一样. 下面的这个程序就演示了 INT 80h 的使用. 这
个
程序检查并显示了它自己的 PID. 注意 使用 printf() 格式化字符串 -- 这个调用的
C 结构
是:
printf( ""%dn"", curr_PID);
也要注意结束符在汇编里不一定可靠, 我常用十六进制(0Ah, 0Dh)代表 CRLF.
;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 代码.