代码改变世界

如何在Linux下写汇编

2013-05-02 15:23  fingertouch  阅读(5502)  评论(0编辑  收藏  举报

本文为翻译文章,原文参见:http://docs.cs.up.ac.za/programming/asm/derick_tut/

1.NASM编译器

目前Linux下的汇编器主要有:as、as86和gas,但是本文使用的是NASM(The Netwide Assembler)。它使用Intel形式的汇编格式,和Intel形式相对的是AT&T形式的汇编格式。

2.Linux下汇编介绍

2.1DOS和Linux下汇编的主要不同

(1)DOS下的汇编,主要通过 int 21h 中断来实现各种DOS功能调用,而BIOS调用则是主要通过 int 10h 和 int 16h 中断来实现。但是在Linux中, 所有以上的功能调用都是通过内核来实现的。因此所有的功能都是通过“系统调用”来实现,而我们可以通过使用 int 80h 中断来实现系统调用。其中,Linux大约有190个左右的系统调用,比DOS下的要少。

(2)Linux是一个真正32位保护模式的操作系统,因此我们使用的是32为的汇编程序。32位汇编程序运行我们使用全部的内存(4G),这意味着我们不用在考虑段基址了,也不用在修改和操作段寄存器了,从某种程度上来说,变的更容易了。

(3)在32为汇编程序中,我们可以使用32位的寄存器 EAX、EBX、ECX等代替传统的16位寄存器 AX、BX、CX等寄存器。

2.2Linux下汇编的编写

(1)数据段 (.data section)

数据段主要用来“声明初始化数据”,换句话说,是用来定义常“变量”的,这个“变量”主要是指在程序中不会一直变化,定义后就会保持不变。通常数据段都是用来定义常用的标号,比如:文件名、缓存大小等等,当然,你也可以使用 EQU 这个指令来实现。定义常“变量”可以使用的指令有:DB、DW、DD、DQ和DT,例如:

section    .data
    message:        db    'Hello World!'    ;声明一个字节类型(byte)的字符数组
    msglength:    equ    12            ;声明字节数组的长度
    buffersiz:        dw    1024            ;声明一个1024大小(字)的缓存

(2)变量段(.bss section)

该段主要是用来定义变量的,这里可以使用的指令有:RESB、RESW、EWSD、EWSQ和REST等,这些指令可以用来预留一些内存空间给定义的变量。例如:

section    .bss
    filename:        resb    255    ;定义255个字节的内存空间
    number:        resb    1    
    bignum:        resw    1    ;定义1个字的内存空间(1字=2字节)
    realarray:        resq    10    ;定义10个reals大小的数组

(3)代码段(.text section)

这部分主要是用来写汇编代码的,通常,代码段必须以 global _start(_start为标号,自定义)开头,他的主要含义是告诉内核程序是从这里开始的,内核在看到这部分变编译后的信息,就会将相关的CS:IP指向这里,然后开始执行程序。就和C函数中的main()函数类似。例如:

section    .text
    global    _start

    _start:
        pop    ebx    ;这里是程序开始执行的入口
        .
        .
        .

2.3 Linux下的系统调用

Linux下的系统调用和DOS下的系统调用类似,主要通过以下几个步骤:

(1)将你的系统调用号放进EAX中(因为我们是在32位下,所以使用32位的EAX寄存器)。

(2)设置系统调用参数,并且依次将参数放进EBX、ECX、EDX、ESX、EDI和EBP。

(3)调用相关中断(对应Linux来说是 80h;对于DO来说是 21h)。

(4)最后的调用结果会返回到EAX中保存。

说明:第二步中的参数是按照顺序放置的,比如第一个参数放在EBX中,第二个参数放在ECX中,…第5个参数放在EDI中。但是只有Linux2.4以后的版本才至此第6个参数EBP,以上的版本只支持前5个参数。如果有多于6个参数,则EBX用来存放参数列表在内存中的位置,但是通常情况下是不会多于6个参数的。

例如:

mov    eax,1    ;exit系统调用
    mov    ebx,0    ;返回参数是0
    int    80h    ;使用80h中断,然后系统内核便开始调用函数

从上述例子我们可以看出,exit()函数的系统调用号是1,但是我们怎么知道其他的系统调用号呢?并且他们的参数是如何?首先,所有的系统调用都位于 /usr/inlucde/(asm/asm-generic)/unistd.h中,其中也包含了他们的系统调用号(比如exit对应的是1)。但是为了方便,我们可以在下面的Linxu系统调用表中查看系统调用所对应的编号。当然,我们可以通过man 2 系统调用函数(man 2 exit)来查看该系统调用的具体情况。

2.4 “Hello Word!”

还是经典的“Hello Word!”,为了将其打印出来,我们使用标准输出(STDOUT),其文件描述符为1,以下是完整的程序:

section    .data
    hello:    db    'Hello World!'    ;字符
    helloLen    equ    $-hello            ;字符的长度

section    .text
    global    _start

_start:
    mov eax,4    ;sys_write的系统调用
    mov ebx,1    ;参数1,文件描述符,stdout是1
    mov ecx,hello    ;字符的起始地址
    mov edx,helloLen    ;字符的长度

    int 80h        ;系统调用

    mov eax,1    ;sys_exit的系统调用
    mov ebx,0    ;sys_exit的返回参数0,表示无错误
    int 80h

我们在vim中书写上述代码,然后将其保存为 hello.asm。

2.5 编译与连接

(1)打开控制终端

(2)将当前目录设置成 hello.asm同一目录

(3)使用NASM编译 hello.asm程序: nasm –f elf hello.asm

(4)使用 ld –s –o hello hello.o 指令连接程序

(5)运行程序 ./hello

程序将会输出:“Hello World!”

3 进阶

3.1 命令行参数和栈

从DOS程序中获取命令行参数是一件痛苦的经理,因为这要考虑到PSP和段寄存器。但是,在Linux中,这一切都显示那么简单,但程序开始运行时,所有的参数都会被放到栈中,如果我们想要获取他们,只需pop即可。

假设现在有一个program程序,他有三个参数:

./program foo bar 42

 

则现在程序的栈是如下形式:

image

现在,我们可以写一个获取其三个参数的程序出来:

section .text
    global _start

_start:
    pop eax    ;获取参数个数:3
    pop ebx    ;获取程序名称:program
    pop ebx    ;获取第一个参数:foo
    pop ecx    ;获取第二个参数:bar
    pop edx    ;获取第三个参数:42

    mov eax,1
    mov ebx,0
    
    int 80h    ;退出

3.2 “过程”和跳转

NASM中没有TASM中的过程定义,但是可以使用标号来代替。例如:

image

其中有一点需要注意:

你可以 jump 到一个标号,但是你必须 call 一个 过程。

假设现在有如下代码:

if(AX == 'w') {
 writeFile();
} else {
 doSomethingElse();
}

那么其对应的汇编将是如下形式:

cmp    AX,'w'        ;
jne    skipWrite        ;
call    writeFile        ;
jmp    outOfThisMess    ;

skipWrite:
    call doSomethingElse
outOfThisMess:
    ...        ;

3.3 Linux和DOS汇编程序对比

3.3.1 “Hello World”程序输出对比

image

对比说明:

(1)DOS下的前三行,在Linux中是不需要的,因为Linux是一个32位的保护模式的操作系统,因此所有的寄存器和分页都已经是32位的,无需特殊处理,因此就不需要段寄存器,并且也不需要设置栈的大小。

(2)Linux下的NASM和DOS下的TASM/MASM的语法区别:

1) NASM使用SECTION .DATA而不是MASM中的.DATA。

2) NASM运行我们直接使用EQU定义常量,例如:bufferlen : equ 400,那么在程序中bufferlen就等于400,这意味着我们不必使用括号来去该地址的内容。

3) NASM使用$表示当前行偏移SECTION的地址,如:hello: db 'Hello world!',10 helloLen: equ $-hello 中的$就表示当前行的偏移地址,$-hello表示当前行和上一行相差的距离,即字符的长度。

4) Linux下可以直接使用字符长度打印出字符,不必像DOS那样采用$-terminated的形式。

5) 为了打印出字符后换行,在Linux中需要加一个linefeed character(10),但是在DOS下,必须使用一个linefeed character(10)和一个carriage return(13)。

6) Linux下的代码段叫.TEXT,而DOS下的叫.CODE

7) Linux下的代码段,必须以GLOBAL XXX开始,这样做的目的是告诉系统内核程序开始执行的地址。

8) 由于Linux下无需考虑段寄存器,所以Linux下的汇编代码没有ASSUME关键字。

9) Linux下代码段(.text section)的结束不需要想DOS那样 END XXX。

(3) DOS下汇编程序中,START标签的前两句主要表示:DS寄存器指向 data segment;CS寄存器指向code segment。Linux直接使用32位寄存器,所以不需要这么设置。

(4) 在16位的DOS汇编程序中,我们使用16位的寄存器 AX、BX、CX、DX等。但是在32位的Linux中,我们使用扩展寄存器 EAX、EBX、ECX、EDX等。其中 AX 是 EAX 的低16位,AH 是 AX的高8位,AL 是 AX 的底8位,没有EAL这种寄存器。

(5) 在DOS中,如果我们想将变量的地址放进寄存器中,我们必须使用offset关键字来将其放进正确的段寄存器中。但是在Linux中不需要如此。

(6) 在DOS中,我们使用 int 21h 中断来调用 DOS 服务来打印字符。但是在Linux中,我们使用 int 80h 中断来调用 系统调用 来打印字符。并且其参数放置位置不同(参见3.1)。

(7) 退出调用的不同之处和(6)类似。

3.3.2 命令参数及文件写入对比

image

image

image