Chinaunix首页 | 论坛 | 博客
  • 博客访问: 539733
  • 博文数量: 252
  • 博客积分: 6057
  • 博客等级: 准将
  • 技术积分: 1635
  • 用 户 组: 普通用户
  • 注册时间: 2009-12-21 10:17
文章分类

全部博文(252)

文章存档

2013年(1)

2012年(1)

2011年(32)

2010年(212)

2009年(6)

分类:

2010-09-11 01:27:21

NASM x86汇编入门指南

原文链接:

 

内容

1.       介绍

2.       为什么写这篇文章

3.       NASM(The Netwide Assembler)汇编编译工具

3.1   为什么使用NASM

3.2   如何安装NASM

4.       Linux汇编介绍

Linux汇编主要不同的地方

4.2   一个汇编程序的组成

4.3   linux系统调用

4.4   “Hello World!”汇编程序

4.5   编译和链接汇编代码

5.2   过程调用和跳转

附录A 如何使用linux终端

附录B linux安装NASM或其它汇编工具

附录C 参考

 

本教程是介绍如何在linux环境下编写汇编代码的入门文章,为了适应不同的人,这里包含了两个版本。

1.       一步一步学习指导:这个版本详细的进行了解释,它假设你没有DOS基础,也没有使用过linux,并教给你一些基本技能,比如如何使用终端和DOS命令.

2.       快速开始:如果你急于想体验linux汇编程序,编译并运行它,如果你有一些DOS汇编基础并能使用linux终端软件,你可以先看这篇教程。它简单讲解了linuxDOS汇编的不同,以至于不会让你混淆它们。

这里,我们使用NASM作为汇编编译工具,关于它的细节可以看附录C:参考资料,来获取更多信息。

二、            为什么写这篇文章?

最主要的原因是为了使得在linux下编写汇编程序比DOS下变得更容易、更好更实用,并且,还将教给你一些linux方面知识(除非你已经对它很熟悉)

用汇编编程看起来相当受虐待(并且用它写整个代码也很荒谬),尤其是在如今,拥有很多功能强大的编译器甚至是图形界面的集成开发环境,生成的汇编代码甚至超过了一些专业级的汇编程序员。但是,使用汇编有一个优点就是有助于你更加熟悉处理器和内核的内部工作原理,特别是有时候在C/C++中内嵌汇编尤其有用。如果你想让你的代码执行得更快,你可以调整并优化你的编译器生成的汇编代码(前提是你比现代编译器的编写者更能处理好生成的代码。

三、            NASM(The Netwide Assembler)汇编编译工具

3.1为什么使用NASM

linux几乎总是默认安装asgas作为默认的汇编程序编译器,然而,我们这里使用的NASM,采用intel语法,类似于TASMMASM和其它的DOS汇编工具。(asgas采用AT&T语法,与intel语法有些不同,例如AT&T语法中,寄存器前面必须加上%前缀,并且源源操作数在目的操作数之前,详细请看附录C:参考:用ASAT&T语法,或者看我另外一篇关于AT&T的汇编入门文章)

3.2 如何安装NASM

         下载地址:

         可以下载源码包或者rpm包,rpm –iUh *.rpm

四、            Linux汇编介绍

4.1 DOSLinux汇编主要不同的地方

DOS汇编中,大部分工作依靠21号中断(int 21h)来完成,并且BIOS服务中断用int 10hint 16h,在linux中,所有的函数通过linux系统调用最终被内核处理,并且通过int 80h陷入内核代替用户空间执行,这称为linux的软中断(关于软中断,这里不细讲,我以后会专门写篇文章来结合x86的流水线和地址空间来讲解linux的异常中断的细节,软中断是用户合法进入内核的唯一方式,流水线通过执行int指令,跳转到中断向量表,查找中断号80h,执行中断服务程序ISR,来陷入内核空间开始)。一件更令人高兴的事,linux的系统调用比DOS更少但更实用。

linux是一个32位保护模式编程系统,因此使得我们能处理真正的现代的32位汇编,32位代码运行在flat(平板)内存模型,其基本意思就是你根本不用再担心段寄存器的处理,因为你不必用段地址来重写或者修改段寄存器,它的每个地址都是32位长,并包含一个偏移量(这里暂时不必去深入理解,只需要记住它就行了)

x8632位汇编代码中,你可以使用32位寄存器如eax,ebx,ecx,edx等等,来代替16寄存器ax,bx,cx,dx等等。

DOS16编程时代已经过时了,只有一些不舍得扔下386编程的一些老的黑客仍在用它,linux汇编更实用。(linux操作系统一部分由汇编代码编写,并且硬件驱动也常常离不开汇编代码,因为他是最靠近硬件的语言)

4.2一个汇编程序的组成

一个简单的汇编代码通常分成下面三个段:

旁注:在编译器编译并链接生成可执行文件的过程中,会出现两个section的概念,一个是在生成目标文件,通常是我们所说的.o文件,目标文件也是由多个section组成,我们通常叫这个section为节,这里的每个section的地址是静态偏移地址,是基于0的偏移地址,而在我们链接多个目标文件(.o)及库(静态库和动态库,关于这两者,详细请看ld手册,我也会在后面的文章讲解ld的一些基础知识)时,实际上是经过ld链接脚本的处理并进行重定位之后,把每个目标文件中的各个section 放到可执行文件的一个section中,这个section我们通常叫它段(例如.text节重定位之后生成.text,.data节重定位生成.data段等等),详细请参考ld manual

.data section()

这个section主要存放初始化的数据,.data section包含利用像文件名、缓冲大小,并且还可以用EQU定义常量(constant),可以使用的一些指令如:DB,DW,DD,DQ,DT

例:

section .data

         message:                   db ‘Hello world!’      ;相当于char/unsigned char* Hello world!

         msglength:       equ 12          ; 字符串长度12字节

         buffersize:        dw 1024                     ;缓冲区大小1024个字长(相当于short类型)    

.bss section              ;未初始化section

;这个section存放未初始化数据,可以用RESB,RESW,RESD,RESQREST指令来为你的变量申请为初始化空间。

section .bss

         filename: resb 255                                        ;255字节

         number: resb 1

         bignum: resw 1

         realarray: resq 10

.text section      ;代码section

这个section用于存放用户代码,.text section必须从global _start开始,来告诉内核程序从什么地方开始执行(类似于CJAVA中的main函数,这里指一个开始位置)

section .text

         global _start

_start:

         pop ebx                       ;这里是程序实际开始的地方

                   .

                   .

                   .

正如你所看到的,到目前为止,或者多或少都有一点DOS的味道,下面我们通过讲解linux系统调用之后,便可以完成你的第一个linux汇编程序了。

4.3 linux系统调用

linux系统调用和DOS系统调用并不完全一样:

1. 放系统调用号到eax

2. 设置系统调用参数到ebx,ecx

3. 调用相关中断(DOS:21h;linux:80h)

4. 返回结果通常保存在eax

对于系统调用,x866个寄存器可以使用,分别是是ebx,ecx,edx,esi,edi,ebp,如果参数多于6个,ebx必须包含一个参数存放的地址,但我们通常不必担心,因为系统调用不大可能超过6个参数,更为激动的是,linux系统调用设计一贯都遵守这个原则。

下面是一些可能有帮助的例子:

move ax,1                           ;sys_exit系统调用号

mov ebx,0                           ;exit参数0,相当于exit(0)

int 80h                                  ;80中断,通常中软中断,调用它意思就是告诉内核,你处理它

接下来,你需要知道的是如何知道系统调用是什么,它们什么功能,有几个参数等等?首先,所有的系统调用和对应的系统调用号都可以在/usr/include/asm/unistd.h中找到,在调用int 80h之前,你需要将它们存入eax中。看一看系统调用表,可以看到比如sys_write(4)sys_nice(34)sys_exit(1),4341表示对应的系统调用的系统调用号。

4.3.1阅读参考手册

         首先,打开一个终端程序(用CTRL+ALT+F1F6切换第一个console到第6console,CTRL+ALT+F7切换到图形界面),现在我们来看看”write”系统调用做了些什么,输入 man 2 write并按回车,将显示write帮助手册,2表示从手册的第二段开始查找

NAME段下面是函数的名称和功能-例如:

write – write to a file descriptor

你可能会感到意外,为什么会这样?没错,在linux中一切都是文件,像显示屏、鼠标、打印机等等,都是一个叫做设备文件的特殊文件,你可以像操作一个文本文件那样对它进行读和写,实际上应该意识到,因为在程序中读或者写一个文件是一件最简单的事情,因些,为什么不用同一种简单的方法来处理所有的事情呢,--呵呵,有点跑题了!

下面,是关于write函数的原型:

ssize_t write(int fd,const void *buf,size_t count);

如果你懂得C语言,这很好理解,因为它正是一个C语言定义的系统调用,正如你看到的,它有三个参数:文件描述符、缓冲区buf(是一个指向缓冲区首地址的指针)、需要写入的字节数,size_t类型,实际上被定义为一个整形。这里,我们应该知道,我们把这三个参数分别放在ebx,ecx,edx中。最终,write调用返回值存放在eax中。

 

接下来,我们开始我们的第一个linux汇编程序

4.4 “Hello World!”汇编程序

         通过打印”Hello World!”语句到屏幕上,似乎这总是我们开始介绍一门编程语言时所采用的适当的方法。下面我们调用write函数,指定文件描述符为STDOUT,其值为1,下面是完整代码:

 

section .data

         hello:        db ‘Hello World!\n’,10     ;’Hello World!’,加换行符

         helloLen:  equ $-hello                          ;’Hello World!’字符串长度

 

section .text

         global _start

_start:

         move ax,4                  ;4:sys_write系统调用号

         mov ebx,1                  ;1:标准输出文件描述符

         mov ecx,hello            ;hello字符串的首地址

         mov edx,helloLen     ;hello字符串长度

        

         int 80h                        ;软中断,陷入内核

        

         move ax,1                  ;sys_exit系统调用号

         mov ebx,0                  ;返回值,0表示没有错误.exit(0)

         int 80h                         ;这里有必要解释下,int 80h实际上是执行一个中断,叫做软中断,int 80h执行之后,中断会返回到原来发生中断的那条指令的下一条指令的地址开始取指,可以阅读我的另一篇关于ARM流水线的文章, 所以,mov ax,1这条指令之后的又需要再次产生一个软中断陷入内核来执行exit操作。即需要再调用一次int 80h,你只需要记住,每执行一个系统调用,都需要跟一条int 80h 来陷入内核执行。

4.5 编译和链接

         1.打开终端并保存你的代码比如hello.s

         2.输入nasm –f elf hello.s

         3.输入ld –s –o hello hello.o

          它将链接目标文件也许还有库文件一起生成可执行文件

         4.运行程序,先改变权限:chmod +x hello ,然后输入./hello,如果一切正常,你将会看到屏幕上打印出的Hello World!

        

五、            更多的高级概念

在往下继续之前,你可能想知道上面例子中equ $-hello语句的作用是什么,你可能还记得equ用来声明一个变量,它实际上是声明一个常量(constant),定义字符串的长度以确保它不会在以后被改变,但是,$-hello又是怎么算出字符串的长度的呢?这里,当NASM遇到’$’的时候,它用这行的开始的位置来取代它,也就是上一行结束时的位置,然后再减去hello的起始位置,便得到了hello字符串的实际长度了。然后将这个长度通过equ赋给helloLen,如果清楚也不要担心,你只要记得这是一种声明一个字符串长度的简洁且容易的方法就是了。

5.1命令行参数和栈

         linux中得到命令行参数并不像DOS那样麻烦,因为DOS通过PSP的内容来加载程序,因此,每次都需要从PSP中获取相关信息来实现与被加载程序的通信,在linux中要简单得多,因为当程序开始执行的时候,它的所有参数直接放在栈中,如果要得到它们,只需要简单的pop指令就行了。

下面是一个例子,说明运行一个有三个参数的程序时,它的工作原理:

./program foo bar 42     栈结构如下:

4

参数数目(argc),包含程序名称

 

program

程序名称(argv[0])

 

foo

参数1,第一个实际参数argv[1]

 

bar

参数2argv[2]

 

42

参数3argv[3],注意,这是字符串”42”而不是数字42

 

 

下面我们来写这个程序,并传入三个参数:

section .text

         global _start

_start:

         pop eax                       ;得到参数个数

         pop ebx                       ;得到argv[0],即程序名称

         pop ebx                       ;得到第一个参数 argv[1],”foo”

         pop ecx                       ;argv[2],”bar”

         pop edx                       ;argv[3],”42”

 

         mov eax,1

         mov ebx,0

         int 80h                        ;exit

上面的代码完成了函数返回时的出栈和退出操作,这显然比DOS更优雅。

5.2过程调用和跳转

         提示:NASM并不存在比如TASM中那样的过程调用的说法,所有的过程调用都是一个符号标志(lable),因此,如果你想要实现一个过程调用(”procedure”),你不能用procendp这样的指令,相反,你应该用一个符号标志,例如fileWrite: ,像我们的_start:一样,好比我们调用main函数,下面是一个linuxDOS的例子:

Linux

DOS

;proc fileWrite – write a string to a file

fileWrite:

  mov eax,4  ;write system call

  mov ebx,[filedesc] ;File descriptor

  mov ecx,stuffToWrite

  mov edx,[stuffLen]

  int 80h

  ret

;endp fileWrite

 

proc fileWrite

  mov ah,40h  ;write DOS service

  mov bx,[filehandle] ;File handle

  mov cl,[stuffLen]

  mov dx,offset,stuffToWrite

  int 21h

  ret

endp fileWrite

 

提示2:如果你熟悉了linux下的跳转指令,并可以通过它来跳转到某一符号标志处,但,请记住很重要的一点就是,如果你想从过程中返回时,用RET指令,而切记不能使用像JMP之类的跳转指令!如果这样,将导致一个段错误,而终止你的进程。记住一个规则就是:

         可以跳转到符号标志处,但必须是一个过程调用。

旁注:PSPprogram segment prefix,就是程序段的前缀,当输入一个外部命令加载一子程序时,COMMAND(类似于linuxbash shell)确定当时内存可用空间的最低端作为程序段起点。在程序所占内存空间的前256个字节中,系统会为程序创建程序的前缀(PSP)的数据区,DOS要利用PSP来和被加载程序进行通信;PSP内有程序返回、程序文件名等信息,可以通过研究psp定位文件名信息,进而获取文件名。

从这段内存区的256字节处开始(在PSP的后面),将程序装入,程序的地址被设为SA+10H:0 (其中SA为系统为程序分配内存的起始位置的段地址即当前寄存器DS的内容);

(注意:PSP区和程序区虽然物理地址连续,却有不同的段地址。)

PSP中包含以下三部分信息:

1)供被加载程序使用的DOS入口,如PSP+0+2+5+2CH字段;

2)供DOS本身使用的DOS入口,如PSP+0AH+0EH+12H+2CH字段;

3)供被加载程序使用传递参数,如PSP+5CH+6CH80H字段。

 

附录C 参考



阅读(874) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~