还在苦苦敲代码开发APP?你out啦! 试试积木搭建APP吧~

NASM x86汇编入门指南

来源:     2015-08-18 15:59:04    人气:     我有话说( 0 人参与)

NASM x86汇编入门指南原文链接:http: docs.cs.up.ac.za programming asm derick_tut #helloworld内容1.介绍2.为什么写这篇文章3.NASM(The...

NASM x86汇编入门指南
原文链接:http://docs.cs.up.ac.za/programming/asm/derick_tut/#helloworld
 
内容
1.   介绍
2.   为什么写这篇文章
3.   NASM(The Netwide Assembler)汇编编译工具
  3.1   为什么使用NASM?
  3.2   如何安装NASM?
4.       Linux汇编介绍
  4.1   DOS和Linux汇编主要不同的地方
  4.2   一个汇编程序的组成
  4.3   linux系统调用
    4.3.1   阅读参考手册
  4.4   “Hello World!”汇编程序
  4.5   编译和链接汇编代码
5.       更多的高级概念
  5.1   命令行参数和栈
  5.2   过程调用和跳转
附录 参考
 
一、介绍
本教程是介绍如何在linux环境下编写汇编代码的入门文章,为了适应不同的人,这里包含了两个版本。
1.       一步一步学习指导:这个版本详细的进行了解释,它假设你没有DOS基础,也没有使用过linux,并教给你一些基本技能,比如如何使用终端和DOS命令.
2.       快速开始:如果你急于想体验linux汇编程序,编译并运行它,如果你有一些DOS汇编基础并能使用linux终端软件,你可以先看这篇教程。它简单讲解了linux和DOS汇编的不同,以至于不会让你混淆它们。
这里,我们使用NASM作为汇编编译工具,关于它的细节可以看附录C:参考资料,来获取更多信息。

二、为什么写这篇文章?
最主要的原因是为了使得在linux下编写汇编程序比DOS下变得更容易、更好更实用,并且,还将教给你一些linux方面知识(除非你已经对它很熟悉)
用汇编编程看起来相当受虐待(并且用它写整个代码也很荒谬),尤其是在如今,拥有很多功能强大的编译器甚至是图形界面的集成开发环境,生成的汇编代码甚至超过了一些专业级的汇编程序员。但是,使用汇编有一个优点就是有助于你更加熟悉处理器和内核的内部工作原理,特别是有时候在C/C++中内嵌汇编尤其有用。如果你想让你的代码执行得更快,你可以调整并优化你的编译器生成的汇编代码(前提是你比现代编译器的编写者更能处理好生成的代码。

三、NASM(The Netwide Assembler)汇编编译工具
3.1为什么使用NASM?
linux几乎总是默认安装as和gas作为默认的汇编程序编译器,然而,我们这里使用的NASM,采用intel语法,类似于TASM、MASM和其它的DOS汇编工具。(as和gas采用AT&T语法,与intel语法有些不同,例如AT&T语法中,寄存器前面必须加上%前缀,并且源源操作数在目的操作数之前,详细请看附录C:参考:用AS和AT&T语法,或者看我另外一篇关于AT&T的汇编入门文章)
3.2 如何安装NASM?
         下载地址:http://www.nasm.us/
         可以下载源码包或者rpm包,rpm –iUh *.rpm

四、Linux汇编介绍
4.1 DOS和Linux汇编主要不同的地方
DOS汇编中,大部分工作依靠21号中断(int 21h)来完成,并且BIOS服务中断用int 10h和int 16h,在linux中,所有的函数通过linux系统调用最终被内核处理,并且通过int 80h陷入内核代替用户空间执行,这称为linux的软中断(关于软中断,这里不细讲,我以后会专门写篇文章来结合x86的流水线和地址空间来讲解linux的异常中断的细节,软中断是用户合法进入内核的唯一方式,流水线通过执行int指令,跳转到中断向量表,查找中断号80h,执行中断服务程序ISR,来陷入内核空间开始)。一件更令人高兴的事,linux的系统调用比DOS更少但更实用。
linux是一个32位保护模式编程系统,因此使得我们能处理真正的现代的32位汇编,32位代码运行在flat(平板)内存模型,其基本意思就是你根本不用再担心段寄存器的处理,因为你不必用段地址来重写或者修改段寄存器,它的每个地址都是32位长,并包含一个偏移量(这里暂时不必去深入理解,只需要记住它就行了)
x86的32位汇编代码中,你可以使用32位寄存器如eax,ebx,ecx,edx等等,来代替16寄存器ax,bx,cx,dx等等。
DOS的16编程时代已经过时了,只有一些不舍得扔下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,RESQ和REST指令来为你的变量申请为初始化空间。
section .bss
         filename: resb 255                                        ;255字节
         number: resb 1
         bignum: resw 1
         realarray: resq 10
.text section      ;代码section
这个section用于存放用户代码,.text section必须从global _start开始,来告诉内核程序从什么地方开始执行(类似于C或JAVA中的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中
对于系统调用,x86有6个寄存器可以使用,分别是是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),4、34、1表示对应的系统调用的系统调用号。
4.3.1阅读参考手册
         首先,打开一个终端程序(用CTRL+ALT+F1到F6切换第一个console到第6个console,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 参数2,argv[2]
 
42 参数3,argv[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”),你不能用proc和endp这样的指令,相反,你应该用一个符号标志,例如fileWrite: ,像我们的_start:一样,好比我们调用main函数,下面是一个linux和DOS的例子:

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之类的跳转指令!如果这样,将导致一个段错误,而终止你的进程。记住一个规则就是:
         可以跳转到符号标志处,但必须是一个过程调用。
旁注:PSP:program segment prefix,就是程序段的前缀,当输入一个外部命令加载一子程序时,COMMAND(类似于linux的bash 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,+6CH和80H字段。
 
附录 参考
Writing a useful program with NASM
Introduction to UNIX assembly programming
Linux Assembler Tutorial by Robin Miyagi

NASM x86 汇编 入门 指南

本文源自互联网,采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可,
版权归原作者,如有问题请联系service@tsingfun.com (编辑:admin)
分享到: