`
universsky
  • 浏览: 91291 次
文章分类
社区版块
存档分类
最新评论

Linux 汇编工具

 
阅读更多

三、Linux 汇编工具


Linux平台下的汇编工具虽然种类很多,但同 DOS/Windows一样,最基本的仍然是汇编器、连接器和调试器。

1.汇编器
汇编器(assembler)的作用是将用汇编语言编写的源程序转换成二进制形式的目标代码。Linux平台的标准汇编器是
GAS,它是 GCC所依赖的后台汇编工具,通常包含在 binutils软件包中。GAS 使用标准的 AT&T汇编语法,可以用来
汇编用 AT&T格式编写的程序:
[xiaowp@gary code]$ as -o hello.o hello.s

Linux平台上另一个经常用到的汇编器是 NASM,它提供了很好的宏指令功能,并能够支持相当多的目标代码格式,包

bin、a.out、coff、elf、rdf等。NASM采用的是人工编写的语法分析器,因而执行速度要比 GAS 快很多,更重要的是它使
用的是 Intel汇编语法,可以用来编译用 Intel语法格式编写的汇编程序:

[xiaowp@gary code]$ nasm -f elf hello.asm

2.链接器


由汇编器产生的目标代码是不能直接在计算机上运行的,它必须经过链接器的处理才能生成可执行代码。链接器通常用来
将多个目标代码连接成一个可执行代码,这样可以先将整个程序分成几个模块来单独开发,然后才将它们组合(链接)成一
个应用程序。 Linux使用 ld作为标准的链接程序,它同样也包含在 binutils软件包中。汇编程序在成功通过
GAS 或
NASM的编译并生成目标代码后,就可以使用 ld将其链接成可执行程序了:

[xiaowp@gary code]$ ld -s -o hello hello.o

3.调试器
有人说程序不是编出来而是调出来的,足见调试在软件开发中的重要作用,在用汇编语言编写程序时尤其如此。Linux下
调试汇编代码既可以用 GDB、DDD这类通用的调试器,也可以使用专门用来调试汇编代码的 ALD(Assembly Language
Debugger)。
从调试的角度来看,使用 GAS 的好处是可以在生成的目标代码中包含符号表(symbol table),这样就可以使用 GDB 和
DDD来进行源码级的调试了。要在生成的可执行程序中包含符号表,可以采用下面的方式进行编译和链接:
[xiaowp@gary code]$ as --gstabs -o hello.o hello.s
[xiaowp@gary code]$ ld -o hello hello.o

执行 as命令时带上参数 --gstabs可以告诉汇编器在生成的目标代码中加上符号表,同时需要注意的是,在用 ld命令进
行链接时不要加上 -s参数,否则目标代码中的符号表在链接时将被删去。
在 GDB 和 DDD中调试汇编代码和调试 C语言代码是一样的,你可以通过设置断点来中断程序的运行,查看变量和寄
存器的当前值,并可以对代码进行单步跟踪。图
1是在 DDD中调试汇编代码时的情景:



1用 DDD中调试汇编程序
汇编程序员通常面对的都是一些比较苛刻的软硬件环境,短小精悍的ALD可能更能符合实际的需要,因此下面主要介绍
一下如何用ALD来调试汇编程序。首先在命令行方式下执行ald命令来启动调试器,该命令的参数是将要被调试的可执行
程序:

[xiaowp@gary doc]$ ald hello
Assembly Language Debugger 0.1.3
Copyright (C) 2000-2002 Patrick Alken
hello: ELF Intel 80386 (32 bit), LSB, Executable, Version 1 (current)


Loading debugging symbols...(15 symbols loaded)
ald>

当 ALD的提示符出现之后,用 disassemble命令对代码段进行反汇编:

ald> disassemble -s .text
Disassembling section .text (0x08048074 -0x08048096)
08048074 BA0F000000 mov edx, 0xf
08048079 B998900408 mov ecx, 0x8049098
0804807E BB01000000 mov ebx, 0x1
08048083 B804000000 mov eax, 0x4
08048088 CD80 int 0x80
0804808A BB00000000 mov ebx, 0x0
0804808F B801000000 mov eax, 0x1
08048094 CD80 int 0x80

上述输出信息的第一列是指令对应的地址码,利用它可以设置在程序执行时的断点:

ald> break 0x08048088
Breakpoint 1 set for 0x08048088

断点设置好后,使用 run命令开始执行程序。ALD在遇到断点时将自动暂停程序的运行,同时会显示所有寄存器的当前值:

ald> run
Starting program: hello
Breakpoint 1 encountered at 0x08048088
eax = 0x00000004 ebx = 0x00000001 ecx = 0x08049098 edx = 0x0000000F
esp = 0xBFFFF6C0 ebp = 0x00000000 esi = 0x00000000 edi = 0x00000000
ds = 0x0000002B es = 0x0000002B fs = 0x00000000 gs = 0x00000000
ss = 0x0000002B cs = 0x00000023 eip = 0x08048088 eflags = 0x00000246
Flags: PF ZF IF
08048088 CD80 int 0x80

如果需要对汇编代码进行单步调试,可以使用 next命令:

ald> next
Hello, world!
eax = 0x0000000F ebx = 0x00000000 ecx = 0x08049098 edx = 0x0000000F
esp = 0xBFFFF6C0 ebp = 0x00000000 esi = 0x00000000 edi = 0x00000000
ds = 0x0000002B es = 0x0000002B fs = 0x00000000 gs = 0x00000000
ss = 0x0000002B cs = 0x00000023 eip = 0x0804808F eflags = 0x00000346
Flags: PF ZF TF IF
0804808F B801000000 mov eax, 0x1

若想获得
ALD支持的所有调试命令的详细列表,可以使用 help命令:

ald> help


Commands may be abbreviated.
If a blank command is entered, the last command is repeated.
Type `help <command>' for more specific information on <command>.
General commands


attach clear continue detach disassemble
enter examine file help load
next quit register run set
step unload window write
Breakpoint related commands
break delete disable enable ignore
lbreak tbreak

使用宏

清单 3演示本节讨论的概念;它接受用户名作为输入并返回一句问候语。

清单 3.读取字符串并向用户显示问候语的程序

行号 NASM GAS
001 section .data .section .data
002
003 prompt_str db 'Enter your name: ' prompt_str:
004 .ascii "Enter Your Name: "
005 ; $ is the location counter pstr_end:
006 STR_SIZE equ $ -prompt_str .set STR_SIZE, pstr_end -prompt_str
007
008 greet_str db 'Hello ' greet_str:
009 .ascii "Hello "
010
011 GSTR_SIZE equ $ -greet_str gstr_end:
012 .set GSTR_SIZE, gstr_end -greet_str
013
014 section .bss .section .bss
015
016 ; Reserve 32 bytes of memory // Reserve 32 bytes of memory
017 buff resb 32 .lcomm buff, 32
018
019 ; A macro with two parameters // A macro with two parameters
020 ; Implements the write system call // implements the write system call
021 %macro write 2 .macro write str, str_size
022 mov eax, 4 movl $4, %eax
023 mov ebx, 1 movl $1, %ebx
024 mov ecx, %1 movl \str, %ecx


025 mov edx, %2 movl \str_size, %edx
026 int 80h int $0x80
027 %endmacro .endm
028
029
030 ; Implements the read system call // Implements the read system call
031 %macro read 2 .macro read buff, buff_size
032 mov eax, 3 movl $3, %eax
033 mov ebx, 0 movl $0, %ebx
034 mov ecx, %1 movl \buff, %ecx
035 mov edx, %2 movl \buff_size, %edx
036 int 80h int $0x80
037 %endmacro .endm
038
039
040 section .text .section .text
041
042 global _start .globl _start
043
044 _start: _start:
045 write prompt_str, STR_SIZE write $prompt_str, $STR_SIZE
046 read buff, 32 read $buff, $32
047
048 ; Read returns the length in eax // Read returns the length in eax
049 push eax pushl %eax
050
051 ; Print the hello text // Print the hello text
052 write greet_str, GSTR_SIZE write $greet_str, $GSTR_SIZE
053
054 pop edx popl %edx
055
056 ; edx = length returned by read // edx = length returned by read
057 write buff, edx write $buff, %edx
058
059 _exit: _exit:
060 mov eax, 1 movl $1, %eax
061 mov ebx, 0 movl $0, %ebx
062 int 80h int $0x80

本节要讨论宏以及 NASM和 GAS 对它们的支持。但是,在讨论宏之前,先与其他几个特性做一下比较。

清单 3演示了未初始化内存的概念,这是用 .bss部分指令(第
14行)定义的。BSS 代表 “ block storage segment”
(原来是以一个符号开头的块),BSS 部分中保留的内存在程序启动时初始化为零。BSS 部分中的对象只有一个名称和
大小,没有值。与数据部分中不同,BSS 部分中声明的变量并不实际占用空间。
NASM使用 resb、resw和 resd关键字在 BSS 部分中分配字节、字和双字空间。GAS 使用 .lcomm关键字分配字
节级空间。请注意在这个程序的两个版本中声明变量名的方式。在 NASM中,变量名前面加 resb(或 resw
或 resd)关键字,后面是要保留的空间量;在 GAS 中,变量名放在 .lcomm关键字的后面,然后是一个逗号和要
保留的空间量。
NASM:varname resb size
GAS:.lcomm varname, size
清单 3还演示了位置计数器的概念(第
6行)。 NASM提供特殊的变量($和 $$变量)来操作位置计数器。在 GAS
中,无法操作位置计数器,必须使用标签计算下一个存储位置(数据、指令等等)。
例如,为了计算一个字符串的长度,在 NASM中会使用以下指令:

prompt_str db 'Enter your name: '
STR_SIZE equ $ - prompt_str ; $ is the location counter


$提供位置计数器的当前值,从这个位置计数器中减去标签的值(所有变量名都是标签),就会得出标签的声明和当前位
置之间的字节数。equ用来将变量 STR_SIZE的值设置为后面的表达式。GAS 中使用的相似指令如下:
prompt_str:

.ascii "Enter Your Name: "


pstr_end:


.set STR_SIZE, pstr_end - prompt_str
末尾标签(pstr_end)给出下一个位置地址,减去启始标签地址就得出大小。还要注意,这里使用 .set将变量
STR_SIZE 的值设置为逗号后面的表达式。也可以使用对应的 .equ。在 NASM中,没有与 GAS 的 set指令对应的指
令。
正如前面提到的,清单 3使用了宏(第
21行)。在 NASM和 GAS 中存在不同的宏技术,包括单行宏和宏重载,但是
这里只关注基本类型。宏在汇编程序中的一个常见用途是提高代码的清晰度。通过创建可重用的宏,可以避免重复输入相
同的代码段;这不但可以避免重复,而且可以减少代码量,从而提高代码的可读性。
NASM使用 %beginmacro指令声明宏,用 %endmacro指令结束声明。%beginmacro指令后面是宏的名称。宏
名称后面是一个数字,这是这个宏需要的宏参数数量。在 NASM中,宏参数是从 1开始连续编号的。也就是说,宏的第
一个参数是 %1,第二个是 %2,第三个是 %3,以此类推。例如:

%beginmacro macroname 2
mov eax, %1
mov ebx, %2


%endmacro
这创建一个有两个参数的宏,第一个参数是 %1,第二个参数是 %2。因此,对上面的宏的调用如下所示:
macroname 5, 6

还可以创建没有参数的宏,在这种情况下不指定任何数字。
现在看看
GAS 如何使用宏。GAS 提供
.macro和 .endm指令来创建宏。.macro指令后面跟着宏名称,后面可以有
参数,也可以没有参数。在 GAS 中,宏参数是按名称指定的。例如:

.macro macroname arg1, arg2
movl \arg1, %eax
movl \arg2, %ebx


.endm

当在宏中使用宏参数名称时,在名称前面加上一个反斜线。如果不这么做,链接器会把名称当作标签而不是参数,因此会
报告错误。

回页首

函数、外部例程和堆栈
本节的示例程序在一个整数数组上实现选择排序。

清单 4.在整数数组上实现选择排序

行号 NASM GAS
001 section .data .section .data
002
003 array db array:
004 89, 10, 67, 1, 4, 27, 12, 34, .byte 89, 10, 67, 1, 4, 27, 12,
005 86, 3 34, 86, 3
006
007 ARRAY_SIZE equ $ -array array_end:
008 .equ ARRAY_SIZE, array_end -array
009
010 array_fmt db " %d", 0 array_fmt:
011 .asciz " %d"
012
013 usort_str db "unsorted array:", 0 usort_str:
014 .asciz "unsorted array:"
015
016 sort_str db "sorted array:", 0 sort_str:
017 .asciz "sorted array:"
018
019 newline db 10, 0 newline:
020 .asciz "\n"
021
022
023 section .text .section .text
024 extern puts
025
026 global _start .globl _start
027
028 _start: _start:
029
030 push usort_str pushl $usort_str
031 call puts call puts
032 add esp, 4 addl $4, %esp
033
034 push ARRAY_SIZE pushl $ARRAY_SIZE

035 push array pushl $array
036 push array_fmt pushl $array_fmt
037 call print_array10 call print_array10
038 add esp, 12 addl $12, %esp
039
040 push ARRAY_SIZE pushl $ARRAY_SIZE
041 push array pushl $array
042 call sort_routine20 call sort_routine20
043
044 ; Adjust the stack pointer # Adjust the stack pointer
045 add esp, 8 addl $8, %esp
046
047 push sort_str pushl $sort_str
048 call puts call puts
049 add esp, 4 addl $4, %esp
050
051 push ARRAY_SIZE pushl $ARRAY_SIZE
052 push array pushl $array
053 push array_fmt pushl $array_fmt
054 call print_array10 call print_array10
055 add esp, 12 addl $12, %esp
056 jmp _exit jmp _exit
057
058 extern printf
059
060 print_array10: print_array10:
061 push ebp pushl %ebp
062 mov ebp, esp movl %esp, %ebp
063 sub esp, 4 subl $4, %esp
064 mov edx, [ebp + 8] movl 8(%ebp), %edx
065 mov ebx, [ebp + 12] movl 12(%ebp), %ebx
066 mov ecx, [ebp + 16] movl 16(%ebp), %ecx
067
068 mov esi, 0 movl $0, %esi
069
070 push_loop: push_loop:
071 mov [ebp -4], ecx movl %ecx, -4(%ebp)
072 mov edx, [ebp + 8] movl 8(%ebp), %edx
073 xor eax, eax xorl %eax, %eax
074 mov al, byte [ebx + esi] movb (%ebx, %esi, 1), %al
075 push eax pushl %eax
076 push edx pushl %edx


077
078 call printf call printf
079 add esp, 8 addl $8, %esp
080 mov ecx, [ebp -4] movl -4(%ebp), %ecx
081 inc esi incl %esi
082 loop push_loop loop push_loop
083
084 push newline pushl $newline
085 call printf call printf
086 add esp, 4 addl $4, %esp
087 mov esp, ebp movl %ebp, %esp
088 pop ebp popl %ebp
089 ret ret
090
091 sort_routine20: sort_routine20:
092 push ebp pushl %ebp
093 mov ebp, esp movl %esp, %ebp
094
095 ; Allocate a word of space in stack # Allocate a word of space in stack
096 sub esp, 4 subl $4, %esp
097
098 ; Get the address of the array # Get the address of the array
099 mov ebx, [ebp + 8] movl 8(%ebp), %ebx
100
101 ; Store array size # Store array size
102 mov ecx, [ebp + 12] movl 12(%ebp), %ecx
103 dec ecx decl %ecx
104
105 ; Prepare for outer loop here # Prepare for outer loop here
106 xor esi, esi xorl %esi, %esi
107
108 outer_loop: outer_loop:
109 ; This stores the min index # This stores the min index
110 mov [ebp -4], esi movl %esi, -4(%ebp)
111 mov edi, esi movl %esi, %edi
112 inc edi incl %edi
113
114 inner_loop: inner_loop:
115 cmp edi, ARRAY_SIZE cmpl $ARRAY_SIZE, %edi
116 jge swap_vars jge swap_vars
117 xor al, al xorb %al, %al
118 mov edx, [ebp -4] movl -4(%ebp), %edx


mov al, byte [ebx + edx]
cmp byte [ebx + edi], al
jge check_next
mov [ebp -4], edi
check_next:
inc edi
jmp inner_loop
swap_vars:
mov edi, [ebp -4]
mov dl, byte [ebx + edi]
mov al, byte [ebx + esi]
mov byte [ebx + esi], dl
mov byte [ebx + edi], al
inc esi
loop outer_loop
mov esp, ebp
pop ebp
ret
_exit:
mov eax, 1
mov ebx, 0
int 80h
movb (%ebx, %edx, 1), %al
cmpb %al, (%ebx, %edi, 1)
jge check_next
movl %edi, -4(%ebp)
check_next:
incl %edi
jmp inner_loop
swap_vars:
movl -4(%ebp), %edi
movb (%ebx, %edi, 1), %dl
movb (%ebx, %esi, 1), %al
movb %dl, (%ebx, %esi, 1)
movb %al, (%ebx, %edi, 1)
incl %esi
loop outer_loop
movl %ebp, %esp
popl %ebp
ret
_exit:
movl $1, %eax
movl 0, %ebx
int $0x80

初看起来清单 4似乎非常复杂,实际上它是非常简单的。这个清单演示了函数、各种内存寻址方案、堆栈和库函数的使用方
法。这个程序对包含 10个数字的数组进行排序,并使用外部 C库函数 puts和 printf输出未排序数组和已排序数
组的完整内容。为了实现模块化和介绍函数的概念,排序例程本身实现为一个单独的过程,数组输出例程也是这样。我们
来逐一分析一下。
在声明数据之后,这个程序首先执行对 puts的调用(第
31行)。puts函数在控制台上显示一个字符串。它惟一的参
数是要显示的字符串的地址,通过将字符串的地址压入堆栈(第
30行),将这个参数传递给它。
在 NASM中,任何不属于我们的程序但是需要在链接时解析的标签都必须预先定义,这就是 extern关键字的作用
(第
24行)。GAS 没有这样的要求。在此之后,字符串的地址 usort_str被压入堆栈(第
30行)。在 NASM中,
内存变量(比如 usort_str)代表内存位置本身,所以 push usort_str这样的调用实际上是将地址压入堆栈
的顶部。但是在 GAS 中,变量 usort_str必须加上前缀$,这样它才会被当作地址。如果不加前缀 $,那么会将内存
变量代表的实际字节压入堆栈,而不是地址。
因为在堆栈中压入一个变量会让堆栈指针移动一个双字,所以给堆栈指针加 4(双字的大小)(第
32行)。
现在将三个参数压入堆栈,并调用 print_array10函数(第
37行)。在 NASM和 GAS 中声明函数的方法是相同
的。它们仅仅是通过
call指令调用的标签。


在调用函数之后,ESP代表堆栈的顶部。esp + 4代表返回地址,esp + 8代表函数的第一个参数。在堆栈指针上加
上双字变量的大小(即 esp + 12、esp + 16等等),就可以访问所有后续参数。
在函数内部,通过将 esp复制到 ebp(第
62行)创建一个局部堆栈框架。和程序中的处理一样,还可以为局部变量
分配空间(第
63行)。方法是从 esp中减去所需的字节数。esp – 4表示为一个局部变量分配
4字节的空间,只要
堆栈中有足够的空间容纳局部变量,就可以继续分配。
清单 4演示了基间接寻址模式(第
64行),也就是首先取得一个基地址,然后在它上面加一个偏移量,从而到达最终
的地址。在清单的 NASM部分中,[ebp + 8]和 [ebp – 4](第
71行)就是基间接寻址模式的示例。在 GAS 中,
寻址方法更简单一些:4(%ebp)和 -4(%ebp)。
在 print_array10例程中,在 push_loop标签后面可以看到另一种寻址模式(第
74行)。在 NASM和 GAS
中的表示方法如下:
NASM:mov al, byte [ebx + esi]
GAS:movb (%ebx, %esi, 1), %al
这种寻址模式称为基索引寻址模式。这里有三项数据:一个是基地址,第二个是索引寄存器,第三个是乘数。因为不可能
决定从一个内存位置开始访问的字节数,所以需要用一个方法计算访问的内存量。NASM使用字节操作符告诉汇编器要移
动一个字节的数据。在 GAS 中,用一个乘数和助记符中的 b、w或 l后缀(例如 movb)来解决这个问题。初看上去
GAS 的语法似乎有点儿复杂。
GAS 中基索引寻址模式的一般形式如下:

%segment:ADDRESS (, index, multiplier)


%segment:(offset, index, multiplier)


%segment:ADDRESS(base, index, multiplier)


使用这个公式计算最终的地址:

ADDRESS or offset + base + index * multiplier.


因此,要想访问一个字节,就使用乘数 1;对于字,乘数是 2;对于双字,乘数是 4。当然,NASM使用的语法比较简单。
上面的公式在 NASM中表示为:

Segment:[ADDRESS or offset + index * multiplier]
为了访问
1、2或 4字节的内存,在这个内存地址前面分别加上 byte、word或 dword。
其他方面
清单 5读取命令行参数的列表,将它们存储在内存中,然后输出它们。

清单 5.读取命令行参数,将它们存储在内存中,然后输出它们

行号 NASM GAS
001 section .data .section .data
002
003 ; Command table to store at most // Command table to store at most
004 ; 10 command line arguments // 10 command line arguments
005 cmd_tbl: cmd_tbl:
006 %rep 10 .rept 10
007 dd 0 .long 0
008 %endrep .endr
009
010 section .text .section .text


011
012 global _start .globl _start
013
014 _start: _start:
015 ; Set up the stack frame // Set up the stack frame
016 mov ebp, esp movl %esp, %ebp
017 ; Top of stack contains the // Top of stack contains the
018 ; number of command line arguments. // number of command line arguments.
019 ; The default value is 1 // The default value is 1
020 mov ecx, [ebp] movl (%ebp), %ecx
021
022 ; Exit if arguments are more than 10 // Exit if arguments are more than 10
023 cmp ecx, 10 cmpl $10, %ecx
024 jg _exit jg _exit
025
026 mov esi, 1 movl $1, %esi
027 mov edi, 0 movl $0, %edi
028
029 ; Store the command line arguments // Store the command line arguments
030 ; in the command table // in the command table
031 store_loop: store_loop:
032 mov eax, [ebp + esi * 4] movl (%ebp, %esi, 4), %eax
033 mov [cmd_tbl + edi * 4], eax movl %eax, cmd_tbl( , %edi, 4)
034 inc esi incl %esi
035 inc edi incl %edi
036 loop store_loop loop store_loop
037
038 mov ecx, edi movl %edi, %ecx
039 mov esi, 0 movl $0, %esi
040
041 extern puts
042
043 print_loop: print_loop:
044 ; Make some local space // Make some local space
045 sub esp, 4 subl $4, %esp
046 ; puts function corrupts ecx // puts functions corrupts ecx
047 mov [ebp -4], ecx movl %ecx, -4(%ebp)
048 mov eax, [cmd_tbl + esi * 4] movl cmd_tbl( , %esi, 4), %eax
049 push eax pushl %eax
050 call puts call puts
051 add esp, 4 addl $4, %esp
052 mov ecx, [ebp -4] movl -4(%ebp), %ecx


053 inc esi incl %esi
054 loop print_loop loop print_loop
055
056 jmp _exit jmp _exit
057
058 _exit: _exit:
059 mov eax, 1 movl $1, %eax
060 mov ebx, 0 movl $0, %ebx
061 int 80h int $0x80

清单 5演示在汇编程序中重复执行指令的方法。很自然,这种结构称为重复结构。在 GAS 中,重复结构以 .rept指令
开头(第
6行)。用一个 .endr指令结束这个指令(第
8行)。.rept后面是一个数字,它指定 .rept/.endr结
构中表达式重复执行的次数。这个结构中的任何指令都相当于编写这个指令 count次,每次重复占据单独的一行。
例如,如果次数是 3:

.rept 3
movl $2, %eax
.endr


就相当于:

movl $2, %eax
movl $2, %eax
movl $2, %eax
在 NASM中,在预处理器级使用相似的结构。它以 %rep指令开头,以 %endrep结尾。%rep指令后面是一个表达式
(在 GAS 中 .rept指令后面是一个数字):
%rep <expression>

nop%endrep
在 NASM中还有另一种结构,times指令。与 %rep相似,它也在汇编级起作用,后面也是一个表达式。例如,上面
的 %rep结构相当于:
times <expression> nop

以下代码:

%rep 3
mov eax, 2
%endrep


相当于:

times 3 mov eax, 2


它们都相当于:

mov eax, 2
mov eax, 2
mov eax, 2
在清单 5中,使用 .rept(或 %rep)指令为 10个双字创建内存数据区。然后,从堆栈一个个地访问命令行参数,并
将它们存储在内存区中,直到命令表填满。


在这两种汇编器中,访问命令行参数的方法是相似的。ESP(堆栈顶部)存储传递给程序的命令行参数数量,默认值是
1(表示没有命令行参数)。esp + 4存储第一个命令行参数,这总是从命令行调用的程序的名称。esp + 8、esp +
12等存储后续命令行参数。
还要注意清单 5中从两边访问内存命令表的方法。这里使用内存间接寻址模式(第
31行)访问命令表,还使用了 ESI(和 EDI)中的偏移量和一个乘数。因此,NASM中的 [cmd_tbl + esi * 4]相当于 GAS 中的 cmd_tbl(,
%esi, 4)。

四、系统调用

即便是最简单的汇编程序,也难免要用到诸如输入、输出以及退出等操作,而要进行这些操作则需要调用操作系统所提供
的服务,也就是系统调用。除非你的程序只完成加减乘除等数学运算,否则将很难避免使用系统调用,事实上除了系统调
用不同之外,各种操作系统的汇编编程往往都是很类似的。
在 Linux平台下有两种方式来使用系统调用:利用封装后的 C库(libc)或者通过汇编直接调用。其中通过汇编语言来直
接调用系统调用,是最高效地使用 Linux内核服务的方法,因为最终生成的程序不需要与任何库进行链接,而是直接和
内核通信。
和 DOS 一样,Linux下的系统调用也是通过中断(int 0x80)来实现的。在执行 int 80指令时,寄存器 eax中存放的是
系统调用的功能号,而传给系统调用的参数则必须按顺序放到寄存器 ebx,ecx,edx,esi,edi中,当系统调用完成之
后,返回值可以在寄存器 eax中获得。
所有的系统调用功能号都可以在文件
/usr/include/bits/syscall.h中找到,为了便于使用,它们是用 SYS_<name>这样的
宏来定义的,如 SYS_write、SYS_exit等。例如,经常用到的 write函数是如下定义的:

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

该函数的功能最终是通过
SYS_write这一系统调用来实现的。根据上面的约定,参数 fb、buf和 count分别存在寄存器
ebx、ecx和 edx中,而系统调用号 SYS_write则放在寄存器 eax中,当 int 0x80指令执行完毕后,返回值可以从寄存
器 eax中获得。
或许你已经发现,在进行系统调用时至多只有 5个寄存器能够用来保存参数,难道所有系统调用的参数个数都不超过
5
吗?当然不是,例如 mmap函数就有 6个参数,这些参数最后都需要传递给系统调用 SYS_mmap:

void * mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset);

当一个系统调用所需的参数个数大于 5时,执行int 0x80指令时仍需将系统调用功能号保存在寄存器 eax中,所不同的
只是全部参数应该依次放在一块连续的内存区域里,同时在寄存器 ebx中保存指向该内存区域的指针。系统调用完成之后,
返回值仍将保存在寄存器 eax中。
由于只是需要一块连续的内存区域来保存系统调用的参数,因此完全可以像普通的函数调用一样使用栈(stack)来传递系
统调用所需的参数。但要注意一点, Linux采用的是 C语言的调用模式,这就意味着所有参数必须以相反的顺序进栈,即
最后一个参数先入栈,而第一个参数则最后入栈。如果采用栈来传递系统调用所需的参数,在执行int 0x80指令时还应该
将栈指针的当前值复制到寄存器 ebx中。

五、命令行参数

在 Linux操作系统中,当一个可执行程序通过命令行启动时,其所需的参数将被保存到栈中:首先是 argc,然后是指向
各个命令行参数的指针数组
argv,最后是指向环境变量的指针数据
envp。在编写汇编语言程序时,很多时候需要对这些
参数进行处理,下面的代码示范了如何在汇编代码中进行命令行参数的处理:
例3.处理命令行参数

# args.s


.text
.globl _start
_start:
popl %ecx # argc
vnext:
popl %ecx # argv
test %ecx, %ecx # 空指针表明结束
jz exit
movl %ecx, %ebx
xorl %edx, %edx
strlen:
movb (%ebx), %al
inc%edx
inc%ebx
test %al, %al
jnz strlen
movb $10, -1(%ebx)
movl $4, %eax # 系统调用号(sys_write)
movl $1, %ebx # 文件描述符(stdout)
int $0x80
jmp vnext
exit:
movl $1,%eax # 系统调用号(sys_exit)
xorl %ebx, %ebx # 退出代码
int $0x80
ret

六、GCC内联汇编

用汇编编写的程序虽然运行速度快,但开发速度非常慢,效率也很低。如果只是想对关键代码段进行优化,或许更好的办
法是将汇编指令嵌入到 C语言程序中,从而充分利用高级语言和汇编语言各自的特点。但一般来讲,在 C代码中嵌入汇
编语句要比"纯粹"的汇编语言代码复杂得多,因为需要解决如何分配寄存器,以及如何与C代码中的变量相结合等问题。
GCC提供了很好的内联汇编支持,最基本的格式是:

__asm__("asm statements");

例如:

__asm__("nop");

如果需要同时执行多条汇编语句,则应该用"\\n\\t"将各个语句分隔开,例如:

__asm__( "pushl %%eax \\n\\t"
"movl $0, %%eax \\n\\t"


"popl %eax");

通常嵌入到 C代码中的汇编语句很难做到与其它部分没有任何关系,因此更多时候需要用到完整的内联汇编格式:

__asm__("asm statements" : outputs : inputs : registers-modified);

插入到 C代码中的汇编语句是以":"分隔的四个部分,其中第一部分就是汇编代码本身,通常称为指令部,其格式和在汇
编语言中使用的格式基本相同。指令部分是必须的,而其它部分则可以根据实际情况而省略。
在将汇编语句嵌入到C代码中时,操作数如何与C代码中的变量相结合是个很大的问题。GCC采用如下方法来解决这个
问题:程序员提供具体的指令,而对寄存器的使用则只需给出"样板"和约束条件就可以了,具体如何将寄存器与变量结合
起来完全由GCC和GAS来负责。
在GCC内联汇编语句的指令部中,加上前缀'%'的数字(如%0,%1)表示的就是需要使用寄存器的"样板"操作数。指令部中
使用了几个样板操作数,就表明有几个变量需要与寄存器相结合,这样GCC和GAS在编译和汇编时会根据后面给定的
约束条件进行恰当的处理。由于样板操作数也使用'%'作为前缀,因此在涉及到具体的寄存器时,寄存器名前面应该加上两
个'%',以免产生混淆。
紧跟在指令部后面的是输出部,是规定输出变量如何与样板操作数进行结合的条件,每个条件称为一个"约束",必要时可
以包含多个约束,相互之间用逗号分隔开就可以了。每个输出约束都以'='号开始,然后紧跟一个对操作数类型进行说明的
字后,最后是如何与变量相结合的约束。凡是与输出部中说明的操作数相结合的寄存器或操作数本身,在执行完嵌入的汇
编代码后均不保留执行之前的内容,这是GCC在调度寄存器时所使用的依据。
输出部后面是输入部,输入约束的格式和输出约束相似,但不带'='号。如果一个输入约束要求使用寄存器,则GCC在预
处理时就会为之分配一个寄存器,并插入必要的指令将操作数装入该寄存器。与输入部中说明的操作数结合的寄存器或操
作数本身,在执行完嵌入的汇编代码后也不保留执行之前的内容。
有时在进行某些操作时,除了要用到进行数据输入和输出的寄存器外,还要使用多个寄存器来保存中间计算结果,这样
就难免会破坏原有寄存器的内容。在GCC内联汇编格式中的最后一个部分中,可以对将产生副作用的寄存器进行说明,
以便
GCC能够采用相应的措施。
下面是一个内联汇编的简单例子:
例4.内联汇编

/* inline.c */
int main()
{
int a = 10, b = 0;
__asm__ __volatile__("movl %1, %%eax;\\n\\r"
"movl %%eax, %0;"
:"=r"(b) /* 输出
*/
:"r"(a) /*输入 */
:"%eax"); /* 不受影响的寄存器 */

printf("Result: %d, %d\\n", a, b);
}

上面的程序完成将变量a的值赋予变量b,有几点需要说明:

.
变量b是输出操作数,通过%0来引用,而变量a是输入操作数,通过%1来引用。
.
输入操作数和输出操作数都使用r进行约束,表示将变量a和变量b存储在寄存器中。输入约束和输出约束的不同
点在于输出约束多一个约束修饰符'='。

.
在内联汇编语句中使用寄存器eax时,寄存器名前应该加两个'%',即%%eax。内联汇编中使用%0、%1等来标识
变量,任何只带一个'%'的标识符都看成是操作数,而不是寄存器。
.
内联汇编语句的最后一个部分告诉
GCC它将改变寄存器eax中的值,GCC在处理时不应使用该寄存器来存储任
何其它的值。
·由于变量b被指定成输出操作数,当内联汇编语句执行完毕后,它所保存的值将被更新。
在内联汇编中用到的操作数从输出部的第一个约束开始编号,序号从0开始,每个约束记数一次,指令部要引用这些操作
数时,只需在序号前加上'%'作为前缀就可以了。需要注意的是,内联汇编语句的指令部在引用一个操作数时总是将其作为
32位的长字使用,但实际情况可能需要的是字或字节,因此应该在约束中指明正确的限定符:
限定符意义
"m"、"v"、"o"内存单元
"r"任何寄存器
"q"寄存器eax、ebx、ecx、edx之一
"i"、"h"直接操作数
"E"和"F"浮点数
"g"任意
"a"、"b"、"c"、"d"分别表示寄存器eax、ebx、ecx和edx
"S"和"D"寄存器esi、edi
"I"常数(0至
31)

分享到:
评论

相关推荐

    AT&T汇编(linux汇编)

    如果我们选择的汇编开发工具是GCC/GAS的话,就必须了解AT&T汇编语法,因为GCC/GAS只支持这种汇编语法。

    Linux__汇编语言开发指南

    一、简介 二、LINUX汇编语法格式(AT&T与INTEL汇编的区别) 三、简单程序 四、LINUX汇编工具 五、系统调用 六、命令行参数 七、GCC内联汇编 八、小结

    nasm--linux下著名的汇编语言工具

    nasm--linux下著名的汇编语言工具

    Linux下汇编开发

    Linux下汇编开发环境、汇编工具及实例说明,供入门者参考。

    Linux 汇编语言开发指南

    汇编语言的优点是速度快,可以直接对硬件进行操作,这对...本文为那些在Linux 平台上编写汇编代码的程序员提供指南,介绍 Linux 汇编语言的语法格式和开发工具,并辅以具体的例子讲述如何开发实用的Linux 汇编程序。

    linux 汇编

    主要讲linux内核汇编的使用

    LINUX汇编开发指南

    介绍linux汇编语言的语法格式和开发工具,并辅以具体的例子讲述如何开发实用的linux汇编程序。

    linux分区工具介绍[汇编].pdf

    linux分区工具介绍[汇编].pdf

    linux-gdb调试汇编

    NULL 博文链接:https://deepfuture.iteye.com/blog/758349

    汇编编译连接调试工具

    学习汇编必备的工具,包括汇编代码的编译 连接 调试工具。主要是针对linux系统下学习汇编。

    经典UNIX系统书籍汇编

    经典UNIX系统书籍汇编,linux书籍工具书也是比较好的资料

    ARM、MIPS、X86、PowerPC反汇编工具V2.0.3

    反汇编Linux/Windows OS运行的32位/64位程序/动态库文件,CPU类型:ARM PowerPC MIPS X86 操作菜单选择:文件解析 Alx+P ELF文件解析 Alt+E 另有CORE文件解调用栈、文本比较等功能。V2.0.3相对上一版本,完善ARM64、...

    汇编工具nasm中文手册

    NASM是一个为可移植性与模块化而设计的一个80x86的汇编器。它支持相当多 的目标文件格式,包括Linux和'NetBSD/FreeBSD','a.out','ELF','COFF',微软16 位的'OBJ'和'Win32'。它还可以输出纯二进制文件。它的语法设计...

    Atmel(爱特梅尔)提供的51单片机的宏汇编器工具包

    Atmel(爱特梅尔)提供的51单片机的宏汇编器工具包,含win版和linux版,及Atmel(爱特梅尔)51单片机的数据手册和汇编器的用户手册。

    Fresh汇编开发工具

    FreshFresh IDE 2.1.4 提升了在 Linux 下通过 Wine 使用的体验,修复了很多 bug,代码完成窗口的性能提升,升级 FreshLib 到最新版本,强烈建议使用 Wine 的用户升级。 Fresh IDE 是一个内置的 Flat 汇编编译器...

    Hopper Disassembler v3 for Mac 3.6.10 二进制反编译反汇编工具

    帮助您汇编,反编译和调试Mac,iOS,Linux和Windows上的二进制文件和可执行文件 使用Hopper Disassembler,你可以分析函数的序言,并提取像基本块和局部变量的过程数据。 在检测的过程,Hopper Disassembler自动...

    arm-linux-gcc4.4交叉工具链之汇编器as文档

    最新版armlinux交叉工具链之汇编器as文档,E文,了解了解

Global site tag (gtag.js) - Google Analytics