本文为翻译文章,原文参见: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
则现在程序的栈是如下形式:
现在,我们可以写一个获取其三个参数的程序出来:
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中的过程定义,但是可以使用标号来代替。例如:
其中有一点需要注意:
你可以 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”程序输出对比
对比说明:
(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 命令参数及文件写入对比
来源:https://www.cnblogs.com/fingertouch/archive/2013/05/02/3054721.html