NASM x86汇编入门指南

http://blog.csdn.net/flickedball/archive/2009/11/15/4812051.aspx

 

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    过程调用和跳转

附录 A 如何使用 linux 终端

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

附录 C 参考

 

一、             介绍

本教程是介绍如何在 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 字段。

 

附录 C 参考

Writing a useful program with NASM

Introduction to UNIX assembly programming

Linux Assembler Tutorial by Robin Miyagi

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值