为了学习ASM,看了2个小例子。
程序一:helloworld
hello.s内容:
.data
msg:
.ascii "hello world asm!\n"
len = . -msg
.text
.global _start
_start:
movl $len, %edx
movl $msg, %ecx
movl $1 , %ebx #1代表 文件描述符
movl $4, %eax #4代表 调用write
int $0x80
movl $0, %ebx
movl $1, %eax # 1代表 调用 _exit
int $0x80
编译运行:
as hello.s -o hello.o
ld hello.o -o hello
./hello
输出:
pcl@V3:~/work/test/asm$ ./hello
hello world asm!
这段汇编相当于:
1 2 3 4 5 6 7 8 9 |
|
.data段有一个标号msg,代表字符串“Hello,world!\n”的首地址,相当于C程序的一个全局变量。在汇编指示.ascii定义的字符串末尾没有隐含的‘\0’。汇编程序中的len代表一个常量,它的值由当前地址减去符号msg所代表的地址得到,换句话说就是字符串“Hello,world!\n”的长度。现在解释一下这行代码中的.,汇编器总是从前到后把汇编代码转换成目标文件,在这个过程中维护一个地址计数器,当处理到每个段的开头时把地址计数器置成0,然后每处理一条汇编指示或指令就把地址计数器增加相应的字节数,在汇编程序中用.可以取出当前地址计数器的值,是一个常量。
在_start中调了两个系统调用,第一个是write系统调用,第二个是以前讲过的_exit系统调用。在调write系统调用时,eax寄存器保存着write的系统调用号4,ebx、ecx、edx寄存器分别保存着write系统调用需要三个参数。ebx保存着文件描述符,进程中每个打开的文件都用一个编号来标识,成为文件描述符,文件描述符1表示标准输出,对应的C标准I/O库的stdout。ecx保存着输出缓冲区的首地址。edx保存着输出的字节数。write系统调用把从msg开始的len个字节写到标准输出。
C代码中的write函数是系统调用的包装函数,其内部实现就是把传进来的三个参数分别赋给ebx、ecx、edx寄存器,然后执行movl $4,%eax和int $0x80两指令。这个函数不可能完全用C代码写,因为任何C代码都不会编译生成int指令,所以这个函数有可能完全用汇编写的,也有可能是C用内联汇编写的,甚至是一个宏定义。_exite函数也是如此。
程序二:计算最大值并返回
max.s内容:
#PURPOSE: This program finds the maximum number of a
# set of data items.
#
#VARIABLES: The registers have the following uses:
#
# %edi - Holds the index of the data item being examined
# %ebx - Largest data item found
# %eax - Current data item
#
# The following memory locations are used:
#
# data_items - contains the item data. A 0 is used
# to terminate the data
#
.section .data
data_items: #These are the data items
.long 3,67,34,222,45,75,54,34,44,33,22,11,66,0
.section .text
.globl _start
_start: movl $0, %edi # move 0 into the index register
movl data_items(,%edi,4), %eax # load the first byte of data
movl %eax, %ebx # since this is the first item, %eax is
# the biggest
start_loop: # start loop
cmpl $0, %eax # check to see if we've hit the end
je loop_exit
incl %edi # load next value
movl data_items(,%edi,4), %eax
cmpl %ebx, %eax # compare values
jle start_loop # jump to loop beginning if the new
# one isn't bigger
movl %eax, %ebx # move the value as the largest
jmp start_loop # jump to loop beginning
loop_exit:
# %ebx is the status code for the _exit system call
# and it already has the maximum number
movl $1, %eax #1 is the _exit() syscall
int $0x80
编译运行:
as max.s -o max.o
ld max.o -o max
./max
echo $?
输出222 即最大值
解释:
这个程序在一组数中找到一个最大的数,并把它作为程序的退出状态。这段数在.data段给出:
data_items: .long 3,67,34,222,45,75,54,34,44,33,22,11,66,0
.long指示声明一组数,每个数32位,相当于C数组。数组开头有个标号data_items,汇编器会把数组的首地址作为data_items符号所代表的地址,data_items类似于C中的数组名。data_items这个标号没有.globl声明是因为它只在这个汇编程序内部使用,链接器不需要知道这个名字的存在。除了.long之外常用的声明:
- .byte,也是声明一组数,每个数8位
- .ascii,例: .ascii "Hello World",声明了11个数,取值为相应字符的ASCII码。和C语言不同的是这样声明的字符串末尾是没有'\0'字符的。
data_items数组的最后一个数是0,我们在一个循环中依次比较每个数,碰到0的时候就终止循环。在这个循环中:
- edi寄存器保存数组中的当前位置,每次比较完一个数就把edi的值加1,指向数组中的下一个数。
- ebx寄存器保存到目前为止找打的最大值,如果发现有更大的数就更新ebx的值。
- eax寄存器保存当前要比较的数,每次更新edi之后,就把下一个数读到eax中。
_start: movl $0, %edi
初始化edi,指向数组的第0个元素。
movl data_items(,%edi,4), %eax
这条指令把数组的第0个元素传送到eax寄存器中。data_items是数组的首地址,edi的值是数组的下标,4表示数组的每个元素占4字节,那么数组中第edi个元素的地址应该是data_items+edi*4。从这个地址读数据,写成指令就是上面那样。
movl %eax, %ebx
ebx的初始值也是数组的第0个元素。
下面进入一个循环,在循环的开头用标号start_loop表示,循环的末尾之后用标号loop_exit表示。
start_loop: cmpl $0, %eax je loop_exit
比较eax的值是不是0,如果是0就说明到了数组末尾了,就要跳出循环。cmpl指令将两个操作数相减,但计算结果并不保存,只是根据计算结果改变eflags寄存器中的标志位。如果两个操作数相等,则计算结果为0,eflags中的ZF位置1。je是一个条件跳转指令,它检查eflags中的ZF位,ZF位为1则发生跳转,ZF位为0则不跳转继续执行下一条指令。(条件跳转指令和比较指令是配合使用的)je的e就表示equal。
incl %edi movl data_items(,%edi,4), %eax
将edi的值加1,把数组中的下一个数组传送到eax寄存器中。
cmpl %ebx, %eax jle start_loop
把当前数组元素eax和目前为止找到的最大值ebx做比较,如果前者小于等于后者,则最大值没有变,跳转到循环开头比较下一个数,否则继续执行下一条指令。jle也是一个条件跳转指令,le表示less than or equal。
movl %eax, %ebx jmp start_loop
更新了最大值ebx然后跳转到循环开头继续比较下一个数。jmp是一个无条件跳转指令,什么条件也不判断直接跳转。loop_exit标号后面的指令用_exit系统调用来退出程序。