Linux的x86汇编程序设计

本质上来说, 这篇文章是把我最感兴趣的两样编程东西: Linux 操作系统和汇编语

言程序设计结合在一起. 这两个都不(或者说应该不)需要介绍; 像 Win32 的汇编,

Linux 的汇编运行在 32 位的保护模式下...但它又有一个截然不同的优势就是它允

许你调用 C 的标准库函数和 Linux 的共享库函数. 我开始给 Linux 下的汇编语言

编程来个简要介绍; 为了更好读一点, 你可能要跳过这个基本的小节.

编译和链接

---------------------

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 ======================================================================

在 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( "%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)

Jan's Linux & Assembler HomePage (http://www.bewoner.dma.be/JanW/eng.html)

我也要感谢 Jeff Weeks(http://gameprog.com/codex), 在我找到 Jan 的网页之前

给了我一些 GAS 的 hello-world 代码.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值