【版权声明:尊重原创,转载请保留出处:blog.csdn.net/shallnet,文章仅供学习交流,请勿用于商业用途】
和高级语言一样,汇编语言在多个项目之间可能会编写相同的过程和处理,如果使用函数的话就可以不必每次需要时都重复编写实用程序代码,从而在需要它的时候调用它。
函数包含完成特定功能所需的代码,数据从主程序传递给函数,然后结果返回给主程序。调用函数时,程序执行路径被改变,切换到函数代码的第一条指令。处理器从这个位置开始执行指令,直到函数表明它可以把控制返回到主程序中的原始位置。
在汇编语言中,创建函数需要3个步骤:
1.定义输入值
函数都需要某种形式的输入数据,程序员必须定义程序如何把这些信息传递给函数,基本有三种技术:使用寄存器、使用全局变量、使用堆栈。类似于C语言的使用全局变量和形式参数。
2.定义函数处理
为了在GNU汇编器中定义函数,必须在程序中把函数名称声明为标签。为汇编器声明函数名称,使用.type命令:
.type func, @function
func:
.type 命令通知GNU汇编器,func标签定义在汇编语言程序中使用的函数的开始。函数的结束由ret指令定义,执行到ret指令时,程序控制返回主程序。
3.定义函数输出值
函数完成对数据的处理时,很可能希望把结果传递回发出调用的程序的区域。能够把结果传递回主程序,以便主程序能够利用此数据做进一步的处理或者显示。完成传送结果方式有以下常见两种:
函数完成对数据的处理时,很可能希望把结果传递回发出调用的程序的区域。能够把结果传递回主程序,以便主程序能够利用此数据做进一步的处理或者显示。完成传送结果方式有以下常见两种:
把结果存放在寄存器中、把结果存在在全局变量内存位置中。
函数的访问使用call指令,call指令使用单一操作数:
call function
在执行call指令之前,要把所有的输入值放在正确的位置中。
在汇编语言中,函数可以放在主程序的后面。和C语言不同,函数不必在主程序中调用函数之前定义。
程序调用函数时,程序员应该知道函数使用那些寄存器进行它到的内部处理。当执行返回主程序时,函数中使用用的任何寄存器或内存有可能不是原来的值了。如果掉调用的函数修改主程序使用的寄存器,那么在调用函数之前保存寄存器的当前状态,并且在函数返回之后恢复寄存器的状态。在调用之前,可以使用push指令单独保存特定寄存器,也可使用pusha指令保存所有寄存器。类似地,可以使用pop指令单独恢复寄存器的原始I状态,也可使用popa指令同时恢复所有寄存器的状态。
#func.s
.section .data
base:
.int 100
.section .bss
.lcomm result, 4
.section .text
.globl _start
_start:
nop
movl $8, %eax
call cal_func
movl $1, %eax
movl result, %ebx
int $0x80
cal_func:
addl $10, %eax
addl base, %eax
movl %eax, result
ret
make之后输出结果:
$ ./func
$ echo $?
118
实际上使用全局变量传递参数和返回结果并不是常见的程序设计方法,即使在C语言中也不常这么做。在C语言中一般使用向函数传入参数的方式向函数传入值,参数其实也就是使用的堆栈,程序中任何函数都可以访问堆栈,所以汇编语言在堆栈之中传递函数参数就可以不用担心使用寄存器和全局变量会造成的混乱的情况。
在调用函数之前,程序把函数所需的输入参数存放到堆栈的顶部,执行call指令时,把发出调用的程序返回地址也存放到堆栈顶部,堆栈指针(esp)就指向堆栈的顶部。函数就可以根据esp寄存器使用间接寻址的方式访问输入参数,并且不必弹出堆栈,以防止返回地址的丢失。一般的做法是进入函数时,把esp寄存器复制到ebp寄存器,这样就可以确保有一个寄存器永远保存指向调用函数时堆栈顶部的正确指针,在复制esp寄存器的值之前,ebp寄存器的值也被放到堆栈中。现在ebp寄存器包含堆栈开始位置,主程序的第一个输入参数位于间接寻址位置8(%ebp),第二个参数位于12(%ebp)等等。
在前面讲call指令时曾讲过汇编语言函数模板,如下:
func_lable:
pushl %ebp #函数开头把ebp的原始值保存到堆栈的顶部
movl %esp, %ebp #把当前esp堆栈指针复制到ebp寄存器
...
movl %ebp, %esp #获取存储在ebp寄存器中原始的esp寄存器值。
popl %ebp
ret
可以使用enter和leave指令建立函数开头和结尾,可以使用它们替代手工创建开头和结尾。
当在函数代码中处理运行时,很可能需要在某个位置存储数据元素,就是类似于C语言的局部变量,这个时候我们也可以使用堆栈。ebp寄存器被设置为执行堆栈指针的顶部之后,这个时候函数的局部变量可以放在堆栈中这个指针之后,此时也可以使用而不ebp寄存器引用它们,比如对于一个4字节的变量,可以通过-4(%ebp)访问第一个局部变量,用-8(%ebp)访问第二个局部变量。
如下示例是通过向函数传入两个参数并计算其和,然后再将其和作为程序的返回码退出。
.section .data
base:
.int 100
plus_no:
.int 8
.section .text
.globl _start
_start:
nop
pushl base #将函数所需参数压入堆栈顶部
pushl plus_no
call cal_func
movl $1, %eax
int $0x80
cal_func:
pushl %ebp
movl %esp, %ebp
movl $0, %eax
addl 8(%ebp), %eax # 第一个输入参数位于间接寻址位置8(%ebp)
addl 12(%ebp), %eax # 第二个参数位于12(%ebp)
movl %eax, %ebx # 将和值放在%ebx寄存器中,最后作为程序返回值返回
movl %ebp, %esp
popl %ebp
ret
make之后编译输出如下:
$ ./func
$ echo $?
108
汇编语言也像C语言一样,函数可以定义在一个单独的文件中,最后在把它们和主程序文件连接在一起。单独函数文件和通常创建的主程序文件类似,唯一区别在于不使用_start段,必须把函数名称声明为全局标签,以便其他程序能够访问它。如下:
.section .text
.type addfunc, @function
.globle addfunc
addfunc:
type命令声明addfunc标签指向 函数的开始。我们将上一个示例中两个数想加的函数卸载单独的文件中,修改后如下:
#main.s
.section .data
base:
.int 100
plus_no:
.int 8
.section .bss
.lcomm result, 4
.section .text
.globl _start
_start:
nop
pushl base
pushl plus_no
call addfunc
movl $1, %eax
int $0x80
#func.s
.section .text
.type addfunc, @function
.globl addfunc
addfunc:
pushl %ebp
movl %esp, %ebp
movl $0, %eax
addl 8(%ebp), %eax
addl 12(%ebp), %eax
movl %eax, %ebx
movl %ebp, %esp
popl %ebp
ret
make及输出结果如下:
$ make
as -o add.o add.s
as -o main.o main.s
ld -o func add.o main.o
$ ./func
$ echo $?
108
$