Chinaunix首页 | 论坛 | 博客
  • 博客访问: 101971745
  • 博文数量: 19283
  • 博客积分: 9968
  • 博客等级: 上将
  • 技术积分: 196062
  • 用 户 组: 普通用户
  • 注册时间: 2007-02-07 14:28
文章分类

全部博文(19283)

文章存档

2011年(1)

2009年(125)

2008年(19094)

2007年(63)

分类: LINUX

2008-05-09 17:24:47

原文:

Shellex:  得到一篇At&t的初级教程, 考虑到国内at&t汇编的资料比较少, 所以做了翻译.
翻译得不好还请大家见谅了.原文请看文末的链接, 我做了少许修正.

========================================================================
Linux 汇编指南

Robin Miyagi著
@

ShellEx 译/修正
@
http://shellex.cublog.cn
========================================================================

start@: Thu Feb 03 02:14:37 UTC 2000

update: Fri Jul 30 23:52:23 UTC 2000

update: Fri Sep 15 22:39:17 UTC 2000 :

- 这篇指南展示了在GNU汇编器as下的Linux汇编

- 还提供了有关实用工具集的信息, 例如Objdump和ld.
添加了有关调试和gdb的用法的一些讨论

update: Thu Jan 11 20:13:06 UTC 2001 :

翻译: 2007年 8月 8日

========================================================================

* 介绍
————————————————————————

当在Linux(或者是其他的类Unix系统)下使用汇编编程时, 很重要的一点就是必
须记住: Linux是一个保护模式的操作系统(在386的机器上, Linux在保护模式
下对CPU进行操作).这就意味着平常的用户模式下的进程是不允许做某些事情,
比如说访问DMA, 或者是访问IO端口.编写Linux内核模块(在内核模式下操作)
允许直接地访问硬件(在我的汇编页阅读Assembler-HOWTO获取更多这方面的信
息).用户模式下的进程可以通过使用设备文件的方式访问硬件. 设备文件实际
上是访问了内核模块,而内核模块是可以直接访问硬件的. 这个文件对于用户模
式下的操作来说是受限的.看看我的关于内核模块编程的页面吧.
有任何意见和建议,请用Email联系我: penguin@dccnet.com .

* 系统调用
————————————————————————

在DOS下的汇编你可能会用到软中断, 特别是Dos调用的0×21中断. 在Linux中,
系统调用的通过中断0×80来实现.系统调用号通过eax寄存器来传递, 对应的参
数则通过其他寄存器来传递. 当然了, 这仅仅是对于5个及5个以下的参数的系
统调用才是这么说. 如果参数数量大于了5, 那么参数必须在内存中定位(比如
在栈中), 并且由ebx存储第一个参数的首地址.

如果你想查看系统调用的列表, 它们在  /usr/include/asm/unistd.h.
如果你想了解一些特殊系统调用(例如 write())的信息, 在提示符输入`man
2 write’ . Linux man的第2节包含了这些系统调用.

如果你查看  /usr/include/asm/unistd.h, 你将会在文件头部看到有这么几行
#define __NR_write        4

这指出, 为了的使用write()系统调用,EAX 必须被设置成 4.
现在如果你运行下文的命令;
$ man 2 write

你将得到如下的函数描述(under the  SYNOPSIS heading).

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

这指出, ebx必须为你的要写入的文件的文件描述符, ecx寄存器是你想要写入的
串所在缓冲区的指针, edx寄存器则包含了你要写入的串的长度. 如果系统调用
有更多的两个参数, 那么它们必须放在esi和edi寄存器中

那么我们怎么知道标准输出文件的文件描述符是1呢? 如果你看看/dev目录,
你会发现/dev/stdio是一个指向/proc/self/fd/1的符号链接.因此, stdio的文件
描述符是1

我留下 _exit系统调用给大家作为练习
在linux中, 系统调用在内核中处理.

* GNU 汇编
————————————————————————

在大多数的Linux系统上, 你将常常会看到GNU C 编译器(gcc). gcc使用了一个
叫做’as’的汇编器作为编译后端.这意味着gcc将C代码翻译成汇编代码, 由as汇
编成一个object文件(*.o)

‘As’  使用AT&T语法. 熟练的intel syntax汇编开发者会觉得 AT&T语法真是
“怪异之至”. 但是, 它其实一点都不比intel 语法困难. 我选择’as’是因为它
表现起来更少地出现二义性, 而且和标准的GNU/Linux程序协作得很好, 例如
gdb (支持gstabs格式), objdump(它反汇编的结果是 at&t语法的).

至少, 它是一个安装好开发工具的Linux的标准组件.之后, 我将会在这个指南
中解释调试和objdump的方法

如果你想在as的文档中了解更多关于as的信息, 在shell提示符下键入:
`info as’
也可以在Binutils包中查阅相关信息(这个包包括了一些开发工具, 比如
objdump, ld等等)

** GNU 汇编 vs Intel 汇编语法
————————————————————————

因为大多数i386平台的汇编文档都使用intel语法, 所以在这两种格式之间提供
一些对照是必须的. 这里是一个关于二者不同点的总结列表:

- AT&T汇编中, 源操作数在目标操作数之后, 和intel语法刚好相反.
- 操作指令添加一个字母后缀指定操作数的大小(例如’l'就是 dword, ‘w’就是
word, ‘b’ 就是byte).
- 立即数必须添加前缀’$', 寄存器必须添加前缀’%’.

- 寻址使用的一般语法是DISP(BASE,INDEX,SCALE). 一个具体的例子可以是:
movl mem_location(%ebx,%ecx,4), %eax

它相当于如下的intel语法:
mov eax, [eax + ecx*4 + mem_location]

现在我们来看看我们的一个小小的例子:
————————————————————————

## hello-world.s

## by Robin Miyagi

##

## Compile Instructions:

## ————————————————————-

## as -o hello-world.o hello-world.s

## ld -o hello-world -O0 hello-world.o

## This file is a basic demonstration of the GNU assembler,

## `as’

## This program displays a friendly string on the screen using

## the write () system call

########################################################################

.section .data
hello:
.ascii     “Hello, world!\n”
hello_len:
.long     . - hello
########################################################################

.section .text
.globl _start

_start:
## display string using write () system call

xorl %ebx, %ebx        # %ebx = 0

movl $4, %eax        # write () system call

xorl %ebx, %ebx        # %ebx = 0

incl %ebx         # %ebx = 1, fd = stdout

leal hello, %ecx     # %ecx —> hello

movl hello_len, %edx    # %edx = count

int $0×80         # execute write () system call

## terminate program via _exit () system call

xorl %eax, %eax         # %eax = 0

incl %eax         # %eax = 1 system call _exit ()

xorl %ebx, %ebx         # %ebx = 0 normal program return code

int $0×80         # execute system call _exit ()

在上面的程序中, 注意到使用’#'来开始注释.as也支持C语言的注释风格/*.*/.
如果你使用C注释风格, 那么它就和C中的效果一样(多行, 也可以单行). 我总
是使用’#'语法, 因为这样在emacs的asm着色下显示效果更加好.两个’#', 比如
‘##’也是允许使用的, 但不是必要的
(this is only because of a quirk of emacs asm-mode).

注意下这些节名.text和.data. 这些被用于ELF文件中来告诉连接器代码段和
数据段在哪儿. 还有 .bss节, 用来存储没有初始化的数据. 它是唯一可以在
程序运行期占据内存的节.

* 访问 命令行参数和环境变量
* Accessing Command Line Arguments and Environment Variables

当一个ELF文件被执行, 命令行参数和环境变量都在栈中. 在汇编中这意味着
当程序开始执行时,你可以通过储存在esp中的指针来访问他们. 在我的汇编页
面可以看到ELF二进制格式的相关文档

那么, 这些数据怎样在栈中组合起来? 真的非常简单.命令行参数的数目(包括
程序自己的文件名)作为一个整数被储存在esp寄存器里. 然和, 在[esp+4]中
有一个指针指向第一个命令行参数(也就是程序自己的文件名).如果有更多的
命令行参数, 指向它们的指针将被储存在[esp+8], [esp+12]…这样的地方.
在所有命令行参数指针的末尾是一个NULL指针, 在这NULL指针后面全是指向环
境变量的指针, 并且由一个NULL指针结束

下面显示了一个初始的ELF栈的大概情况:

(%esp)     argc,参数的数目(integer)
4(%esp)     char *argv (第一个命令行参数的指针)
…     余下的命令行参数的指针
?(%esp) NULL pointer
…     余下的环境变量的指针
??(%esp)     NULL pointer

现在来看一个小例子:
————————————————————————

## stack-param.s ###############################################

## Robin Miyagi ################################################

## ##########

## This file shows how one can access command line parameters

## via the stack at process start up. This behavior is defined

## in the ELF specification.

## Compile Instructions:

## ————————————————————-

## as -o stack-param.o stack-param.s

## ld -O0 -o stack-param stack-param.o

########################################################################

.section .data

new_line_char:
.byte 0×0a
########################################################################

.section .text

.globl _start

.align 4
_start:
movl %esp, %ebp        # 把%esp 储存在 %ebp

again:
addl $4, %esp        # %esp —> 在栈中的下一个参数

movl (%esp), %eax    # 把下一个参数放到eax

testl %eax, %eax    # 如果%eax (parameter) 为 NULL pointer?

jz end_again        # 那么就退出循环

call putstring        # 打印出这个参数.

jmp again         # 循环

end_again:
xorl %eax, %eax        # %eax = 0

incl %eax         # %eax = 1, system call _exit ()

xorl %ebx, %ebx        # %ebx = 0, normal program exit.

int $0×80         # execute _exit () system call

## 把字符串打印到stdout

putstring:    .type @function
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %ecx
xorl %edx, %edx
count_chars:
movb (%ecx,%edx,1), %al # 取ecx * 1 + edx位置

testb %al, %al # 如果是’\0′

jz done_count_chars # 跳出循环, 此时, edx存放字符串长

# 度, 但是不包括’\0′

incl %edx # 否则就edx = edx + 1

jmp count_chars # 循环

done_count_chars:
movl $4, %eax # write()系统调用

xorl %ebx, %ebx # 清空 ebx

incl %ebx # 多加1, 因为有’\0′

int $0×80 # write()系统调用

movl $4, %eax # 再次调用

leal new_line_char, %ecx # 装载换行符

xorl %edx, %edx # 清空edx

incl %edx # 长度是1

int $0×80 # 打印换行

movl %ebp, %esp
popl %ebp
ret

————————————————————————

*  Binutils包
————————————————————————

里面有一些二进制实用程序, 并且包含一大堆度开发者很有用的工具, 特别是
在调试期间
我现在将介绍一下其中的几个.

** Objdump
————————————————————————

Objdump 能显示一个或者多个Object文件的信息. 例如, 当你想看刚才的程序
param-stack中的信息, 只需要在命令提示符键入如下命令 (情确保工作目录
中有param-stack这个程序):

objdump -x param-stack | less

尽管显示的信息的长度超过了一个屏幕高, 但是objdump的输出已经被重定向到
less程序. 选项 ‘-x’ 告诉objdump以十六进制的形式显示数字类型的信息.下
面是上面命令的输出

—————————————————————-
stack-param:     file format elf32-i386
stack-param
architecture: i386, flags 0×00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0×08048074

Program Header:
LOAD off    0×00000000 vaddr 0×08048000 paddr 0×08048000 align 2**12
filesz 0×000000be memsz 0×000000be flags r-x
LOAD off    0×000000c0 vaddr 0×080490c0 paddr 0×080490c0 align 2**12
filesz 0×00000001 memsz 0×00000004 flags rw-

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
0 .text         0000004a  08048074  08048074  00000074  2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data         00000001  080490c0  080490c0  000000c0  2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss          00000000  080490c4  080490c4  000000c4  2**2
ALLOC
SYMBOL TABLE:
08048074 l    d  .text    00000000
080490c0 l    d  .data    00000000
080490c4 l    d  .bss    00000000
00000000 l    d  *ABS*    00000000
00000000 l    d  *ABS*    00000000
00000000 l    d  *ABS*    00000000
080490c0 l       .data    00000000 new_line_char
08048076 l       .text    00000000 again
08048087 l       .text    00000000 end_again
0804808e l       .text    00000000 putstring
08048096 l       .text    00000000 count_chars
080480a0 l       .text    00000000 done_count_chars
00000000       F *UND*    00000000
080480be g     O *ABS*    00000000 _etext
08048074 g       .text    00000000 _start
080490c1 g     O *ABS*    00000000 __bss_start
080490c1 g     O *ABS*    00000000 _edata
080490c4 g     O *ABS*    00000000 _end

—————————————————————-

注意一下, 这些信息来自于程序的首部, (ELF文件在文件首部有header
information, 是为了告诉内核怎么样把该文件加载到内存等等的操作)

ELF 文件还包含了节信息(在section table, 即节表中). 可以发现,在.text
节中包含了0×4a的信息, 在文件偏移0×74的地方, 按照4 byte的方式对齐
(4 == 2 ** 2), 有 mamory allocate属性, 是只读的, 并且还包含代码.(段
选择器cs将指向这个节(这个由操作系统控制)).

objdump还提供了程序的符号信息. 这些信息被调试器和其他开发工具用来检
查这个二进制文件.

Objdump 也能反汇编二进制可执行文件. 键入如下命令将会在标准输出中得到
文件的反汇编结果(这个操作不会对当前文件有任何修改, 因为此时Objdump仅
仅是对文件进行了读操作):

objdump -d stack-param | less

这是上面命令的输出:

—————————————————————-
stack-param:     file format elf32-i386

Disassembly of section .text:

08048074 <_start>:
8048074:    89 e5                    movl   %esp,%ebp

08048076 :
8048076:    83 c4 04                 addl   $0×4,%esp
8048079:    8b 04 24                 movl   (%esp,1),%eax
804807c:    85 c0                    testl  %eax,%eax
804807e:    74 07                    je     8048087
8048080:    e8 09 00 00 00           call   804808e
8048085:    eb ef                    jmp    8048076

08048087 :
8048087:    31 c0                    xorl   %eax,%eax
8048089:    40                       incl   %eax
804808a:    31 db                    xorl   %ebx,%ebx
804808c:    cd 80                    int    $0×80

0804808e :
804808e:    55                       pushl  %ebp
804808f:    89 e5                    movl   %esp,%ebp
8048091:    8b 4d 08                 movl   0×8(%ebp),%ecx
8048094:    31 d2                    xorl   %edx,%edx

08048096 :
8048096:    8a 04 11                 movb   (%ecx,%edx,1),%al
8048099:    84 c0                    testb  %al,%al
804809b:    74 03                    je     80480a0
804809d:    42                       incl   %edx
804809e:    eb f6                    jmp    8048096

080480a0 :
80480a0:    b8 04 00 00 00           movl   $0×4,%eax
80480a5:    31 db                    xorl   %ebx,%ebx
80480a7:    43                       incl   %ebx
80480a8:    cd 80                    int    $0×80
80480aa:    b8 04 00 00 00           movl   $0×4,%eax
80480af:    8d 0d c0 90 04 08        leal   0×80490c0,%ecx
80480b5:    31 d2                    xorl   %edx,%edx
80480b7:    42                       incl   %edx
80480b8:    cd 80                    int    $0×80
80480ba:    89 ec                    movl   %ebp,%esp
80480bc:    5d                       popl   %ebp
80480bd:    c3                       ret
—————————————————————-

选项’-d’告诉objdump将包含代码的节反汇编(一般都是.text节).使用’-D’选项
则将会反汇编所有节. Objdump之所以能够给出代码中的标号名字是因为这些信
息以及包含在了符号表(symbols table)中

第一列显示了每行代码的内存地址, 第二列显示每行汇编代码对应的机器码,第
三列则是汇编代码.

想要了解更多信息, 请查阅info 文档

** 使用size命令文件获取占用的内存空间
————————————————————————
如果你使用 `ls -l stack-param’命令将会得到这样的输出
-rwxrwxr-x    1 robin    robin         932 Sep 15 18:21 stack-param

这告诉你这个文件的长度是932字节. 但是这个文件也包含头表(header tables),
节表(section tables), 符号表(symbols tables)等等. 程序在真正运行时占有的
内存要比上面所说的少. 所以要得到真正的内存占用量, 需要输入:

size stack-param

上面的命令将得到这样的输出:

text       data        bss        dec        hex    filename
74          1          0         75         4b    stack-param

这些信息告诉你.text占用74字节, .data占用1字节, 总共占有75字节的内存

** 用strip清除符号信息.
** Getting rid of symbol information with strip
————————————————————————
strip命令能够清除程序中包含的符号信息. 不加参数的话, 它只清除掉除了
调试符号以外的符号信息. 使用`–stip-all’选项, 则会清除所有的信息, 包
括调试信息. 我建议不要这样做, 因为这样将会加大用标准开发工具对文件进
行分析的难度. 只有当文件大小成为首要的考虑因素再使用该命令.

* 用gdb调试
————————————————————————

貌似编程开发中最抓狂的就是调试了. 常常出现这样的情况, 那就是导致程序
发生崩溃的错误并非发生在程序崩溃的那一行代码.(接下来的例子将表明这一
点)

程序接到SIG_SEGV后退出.
————————————————————————

## stack-param-error.s #########################################

## Robin Miyagi ################################################

## ##########

## This file shows how one can access command line parameters

## via the stack at process start up. This behavior is defined

## in the ELF specification.

## Compile Instructions:

## ————————————————————-

## as –gstabs -o stack-param-error.o stack-param-error.s

## ld -O0 -o stack-param-error stack-param-error.o

########################################################################

.section .data

new_line_char:
.byte 0×0a
########################################################################

.section .text

.globl _start

.align 4
_start:
movl %esp, %ebp        # store %esp in %ebp

again:
addl $4, %esp        # %esp —> next parameter on stack

leal (%esp), %eax    # 注意! 这里和刚才不同.

testl %eax, %eax    # %eax (parameter) == NULL pointer?

jz end_again        # get out of loop if yes

call putstring        # output parameter to stdout.

jmp again         # repeat loop

end_again:
xorl %eax, %eax        # %eax = 0

incl %eax         # %eax = 1, system call _exit ()

xorl %ebx, %ebx        # %ebx = 0, normal program exit.

int $0×80        # execute _exit () system call

## prints string to stdout

putstring:    .type @function
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %ecx
xorl %edx, %edx
count_chars:
movb (%ecx,%edx,$1), %al
testb %al, %al
jz done_count_chars
incl %edx
jmp count_chars
done_count_chars:
movl $4, %eax
xorl %ebx, %ebx
incl %ebx
int $0×80
movl $4, %eax
leal new_line_char, %ecx
xorl %edx, %edx
incl %edx
int $0×80
movl %ebp, %esp
popl %ebp
ret

————————————————————————

注意到上面的程序使用as的 `–gstabs’选项进行编译. 这将在输出文件中写入
调试信息, 例如源代码文件, 调试符号等等.
使用`objdump  -x stack-param-error  |  less’将会展示出包含的调试信息

现在来找找是我们的错误在哪儿发生了.输入如下命令
gdb stack-param-error

这样将进入gdb提示符 `(gdb)’;

(gdb) run eat my shorts
/home/robin/programming/asm-tut/stack-param-error
eat
my
shorts
Program recieved SIGSEGV, segmentation fault
count_chars () at stack-param-error.s:47

47     movb (%ecx,%edx,$1), %al
Current language: auto; currently asm
(gdb) q
[~]$ _

(gdb输出的信息更多些, 我只是截取重要的)

它告诉我们在第47行发生了段错误. 但是这个错误实际上归咎与第29行. 如果
你看看第29行, 你会发现是`movl (%esp), %eax’. 按照intel i386操作码这将
lea一个空指针. eax永远都将是指向 0 的空指针(这是一些不可用指针), 他
将导致在第47行访问一块非法内存(因此导致段错误). 在_start中的循环没法
正常地停止, 因为中断条件需要eax为0(NULL), 但是这没法发生.

调试是一门来自时间的艺术. 想了解更多关于gdb的信息, 请查阅info(例如.
`info  gdb’).你也可以在gdb提示符下输入`help’.

gdb能够告诉你错误发生在源代码中哪一具体行的原因只有一个, 就是调试符
号和源代码已经包含在输出文件中了(回忆一下,我们使用了`–gstabs’ 选项)

——————————————————————–
有任何评论和建议请发往

========================================================================
你可以完全随意复制该文件, 但是保留文章的出处
原著于:

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