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’ 选项)
——————————————————————–
有任何评论和建议请发往
========================================================================
你可以完全随意复制该文件, 但是保留文章的出处
原著于: