很多高级编程语言都有函数的概念,函数的出现提高了代码的重利用率,相同功能的代码可以独立出来,在需要的时候直接调用就可以了,不需要再重新写一遍。汇编语言里同样可以定义和使用函数...

    本文由zengl.com站长对
    http://pan.baidu.com/share/link?shareid=3717576860&uk=940392313 汇编教程英文版相应章节进行翻译得来。

    另外再附加一个英特尔英文手册的共享链接地址:
    http://pan.baidu.com/share/link?shareid=2345340326&uk=940392313 (在某些例子中会用到)

    本篇翻译对应汇编教程英文原著的第328页到第336页,对应原著第11章 (注意这里的页数不是页脚的页数,而是pdf电子档顶部,分页输入框中的页数,也就是包含了目录,前言部分的总页数,pdf电子档总页数是577,当前已翻译完的页数为336 / 577)。

    很多高级编程语言都有函数的概念,函数的出现提高了代码的重利用率,相同功能的代码可以独立出来,在需要的时候直接调用就可以了,不需要再重新写一遍。汇编语言里同样可以定义和使用函数,函数除了可以和主程序写在同一个文件里,也可以作为模块写在其他的文件里,在需要时再链接到主程序就可以使用了,下面就具体的介绍下汇编函数的定义和使用方法。

Defining Functions 函数的定义:

    当程序里某个功能会被重复使用时,那么就可以将该功能的代码定义成函数,在主程式需要使用时,只需传递相关的数据给该函数,然后通过特殊的指令将处理器的执行流程切换到函数里,当函数针对传递过来的数据进行相关处理并生成对应的结果后,再通过汇编指令返回到主程式即可,下图就演示了函数的调用与返回的过程:


图1

    上图演示了主程式从_start开始运行,当遇到call指令时,就将执行流程切换到function1函数内,在执行完function1函数里的代码后,最后通过ret指令返回主程式。

    在进一步研究汇编函数之前,我们先来看下高级语言里是如何定义和使用函数的,因为高级语言的代码结构可读性比较强,有助于理解函数的概念。下面的ctest.c程式就演示了如何用C语言定义一个简单的计算圆面积的函数,以及该函数的调用方式:

/* ctest.c - An example of using functions in C */
#include <stdio.h>
float function1(int radius)
{
    float result = radius * radius * 3.14159;
    return result;
}
int main()
{
    int i;
    float result;
    i = 10;
    result = function1(i);
    printf("Radius: %d, Area: %f\n", i, result);
    i = 2;
    result = function1(i);
    printf("Radius: %d, Area: %f\n", i, result);
    i = 120;
    result = function1(i);
    printf("Radius: %d, Area: %f\n", i, result);
    return 0;
}
 

    上面代码里定义了一个function1的函数:

float function1(int radius)

    括号里的是该函数的参数,也就是当主程式要调用function1函数时,需要给他传递一个int整数类型的参数作为计算圆面积的半径。最左侧的float是该函数会返回的值的类型,这里表示function1函数在计算完圆面积后,会将结果以float浮点类型返回。

    在main主程式里要调用function1函数时,直接传递给它一个整数类型的半径参数即可:

result = function1(i);

    上面的function1执行后,返回的结果会通过赋值语句存储到result里,result在之前已经被定义为float浮点类型了,所以可以用来存储该函数的返回结果。

    可以用gcc来编译该程式,运行结果如下所示:

$ gcc -o ctest ctest.c
$ ./ctest

Radius: 10, Area: 314.158997
Radius: 2, Area: 12.566360
Radius: 120, Area: 45238.894531

$

    在对函数有了一个大致的了解后,接下来我们看下汇编里如何创建一个类似上面的函数。

Assembly Functions 汇编里的函数:

    汇编里的函数定义和C语言里的类似,你需要定义该函数需要接受的输入参数,然后定义该函数如何处理输入数据,以及函数执行完后,如何生成对应的结果。

Writing functions 编写自己的汇编函数:

    在汇编里编写函数有如下三个步骤:
  • Define what input values are required
    定义函数的输入参数
  • Define the processes performed on the input values
    定义函数如何处理输入数据的
  • Define how the output values are produced and passed to the calling program
    定义如何生成结果,以及该结果如何传递回调用程式
    下面对这三个步骤依次进行介绍。

Defining input values 定义函数的输入参数:

    很多函数都会定义输入参数(当然也可以根据需要,不定义输入参数),在汇编里可以使用如下三种方式来将参数传递给函数:
  • Using registers
    使用寄存器
  • Using global variables
    使用全局变量
  • Using the stack
    使用内存栈
    使用寄存器传递参数是最简便的方式,不过使用寄存器时,需要根据传递数据的尺寸大小,选择合适的寄存器。

Defining function processes 定义函数的处理过程:

    不同的汇编器,函数定义的方式也不一样,在GNU汇编器里,是用.type伪指令来定义函数的:

.type func1, @function
func1:

    上面代码片段里,用.type伪指令将func1标签名定义为函数名,这样func1标签的内存位置就是函数的入口地址,调用该函数时只需使用CALL指令,后面再跟个func1的函数名即可。

    函数里的处理过程和主程式里的代码没有什么区别,主程式可以使用的指令和寄存器资源,在函数里同样可以使用。

    当函数执行完后,就可以通过RET指令返回到调用该函数的主程式。

Defining output values 定义函数的返回结果:

    你可以通过以下两种方式将结果返回:
  • Place the result in one or more registers
    将结果存储到一个或多个寄存器里
  • Place the result in a global variable memory location
    将结果存储到一个全局变量的内存位置里
Creating the function 函数定义的具体代码:

    知道了函数定义的大体结构,下面就写一个计算圆面积的函数:

.type area, @function
area:
    fldpi
    imull %ebx, %ebx
    movl %ebx, value
    filds value
    fmulp %st(0), %st(1)
    ret

    上面的代码片段里,定义了一个area函数,该函数使用FPU来计算圆的面积。EBX寄存器里存储的是主程式传递过来的半径值,area函数里先通过fldpi指令将pi即3.14159...的常量值加载到FPU寄存器栈,再通过imull %ebx, %ebx指令将EBX乘以EBX即计算半径的平方,平方的结果存储在EBX里,并由movl %ebx, value指令将EBX里的平方结果存储到value全局变量(该全局变量定义在主程式里),接着就可以用filds value指令将value里的平方值加载到ST0,之前加载进栈的pi值则移入ST1,最后由fmulp %st(0), %st(1)指令将ST0里的平方值和ST1里的pi值相乘,就得到圆面积,并存储到ST1里,又由于fmulp指令会弹出ST0的值,所以最终的结果就会位于ST0 。

    根据area函数的定义,在主程式里要调用该函数的话,就必须遵守以下三条规则:
  • The input value must be placed in the EBX register as an integer value.
    输入参数必须以整数形式存放在EBX寄存器里
  • A 4-byte memory location called value must be created in the main program.
    主程式里必须定义一个名为value的全局变量,并为其分配4个字节的内存空间
  • The output value is located in the FPU ST(0) register.
    由于输出结果存储在FPU的ST0栈顶寄存器里,所以需要从ST0里来获取到函数的结果
    主程式要调用area函数,就必须遵守以上三条规则,例如,假设主程式以浮点的形式将半径值存储到EBX里,那么整个计算结果就会不正确。

    可以看出来,如果某个程序里有很多的函数,而每个函数都有一套自己的规则的话,程序实现起来就会很困难,在后面的章节里会提到一种"C语言的传值风格",就是让所有函数都遵守这种风格来传递数据,就可以有效解决函数之间相互调用时的传值问题。

Accessing functions 函数的调用

    在函数定义好后,就可以在其他地方通过CALL指令来调用该函数,CALL指令的格式如下:

call function

    上面的function操作数是要调用的函数名称,要注意的是:在调用函数之前,需要将所有输入参数放置到适当的位置。

    下面的functest1.s程式就完整的演示了汇编里定义和调用计算圆面积的函数的方法:

# functest1.s - An example of using functions
.section .data
precision:
    .byte 0x7f, 0x00
.section .bss
    .lcomm value, 4
.section .text
.globl _start
_start:
    nop
    finit
    fldcw precision
    movl $10, %ebx
    call area
    movl $2, %ebx
    call area
    movl $120, %ebx
    call area
    movl $1, %eax
    movl $0, %ebx
    int $0x80

.type area, @function
area:
    fldpi
    imull %ebx, %ebx
    movl %ebx, value
    filds value
    fmulp %st(0), %st(1)
    ret
 

    上面代码的_start主入口程式里,先通过finit指令初始化FPU,然后用fldcw precision指令将FPU内部数学计算时的浮点精度设为单精度模式(详情可以参考之前的"高级数学运算 (一) FPU寄存器介绍"里的内容),接着通过movl $10, %ebx将10作为半径值存储到EBX里,准备好输入参数,就可以用CALL指令调用area函数,area函数里的代码在之前讲解过,它会根据EBX里的半径计算出圆面积,并将该结果存储到ST0栈顶寄存器,主程式里一共用CALL调用了三次area函数,输入参数分别为10,2,120 。

    在经过汇编链接后,gdb里的调试输出情况如下:

$ as -gstabs -o functest1.o functest1.s
$ ld -o functest1 functest1.o
$ gdb -q functest1

Reading symbols from /home/zengl/Downloads/asm_example/func/functest1...done.
(gdb) break *_start+1
Breakpoint 1 at 0x8048075: file functest1.s, line 11.
(gdb) r
Starting program: /home/zengl/Downloads/asm_example/func/functest1

Breakpoint 1, _start () at functest1.s:11
11        finit

(gdb) s
12        fldcw precision
(gdb) s
13        movl $10, %ebx
(gdb) s
14        call area
(gdb) s
25        fldpi
(gdb)

    可以看到在call area指令执行后,执行流程就切换到area函数对应标签名下的第一条指令了,继续往下执行:

..................................
(gdb) s

area () at functest1.s:30
30        ret

(gdb) s
_start () at functest1.s:15
15        movl $2, %ebx

(gdb) info all
...............................
ebx            0x64    100
...............................
st0            314.159271240234375    (raw 0x40079d14630000000000)
...............................

    从上面输出可以看到,在area函数执行完后,会通过ret指令返回到_start主程式,并且下一条将要执行的指令就是call area下面的指令:movl $2, %ebx,area计算完圆面积后,EBX里的值100是半径10的平方,ST0里的314.159...的值是计算出来的面积,在整个程式执行完毕后,FPU寄存器栈里的结果如下:

(gdb) info all
...............................
st0            45238.93359375    (raw 0x400eb0b6ef0000000000)
st1            12.56637096405029296875    (raw 0x4002c90fdb0000000000)
st2            314.159271240234375    (raw 0x40079d14630000000000)

...............................
(gdb)

    st0里的值为半径120的圆面积,ST1里的值为半径2的圆面积,ST2里的值为半径10的圆面积,这些结果和之前C语言写的ctest.c程式的计算结果一致。

Function placement 汇编函数的位置布局:

    上面的functest1.s例子里函数定义在_start主入口程式的后面,其实将函数定义在_start前面也是可以的,汇编链接器会自动根据函数定义的位置来设定CALL指令的跳转偏移值。

    此外,你也可以不使用.type area , @function来进行函数声明,只要定义了一个area的标签名,call area时就会自动进入area标签所在的内存位置去执行代码,你可以试着将functest1.s里的.type area , @function给去掉,效果是一样的,说明CALL指令是根据标签名来进行查找和跳转的。

Using registers 汇编函数里使用寄存器时的注意事项:

    当主程式通过CALL指令进入某个函数时,由于主程式里可以使用的寄存器,在该函数里也可以使用,所以这些函数有可能会将主程式里一些需要使用的寄存器的值给修改掉,从而会影响程序的正常执行,所以在CALL调用函数前,可以通过PUSH指令将主程式需要使用的寄存器的值保存到栈,也可以用PUSHA保存所有的寄存器,在函数执行完后,再用POP指令恢复指定的寄存器,或用POPA来恢复所有的寄存器。

    此外,如果函数将返回的结果保存在一个寄存器里,则在用POP指令恢复寄存器前,需要先将该寄存器里的结果保存到一个安全的地方。

Using global data 使用全局变量来进行函数传值:

    前面的例子演示了如何使用寄存器来传值,下面通过functest2.s的例子演示如何使用全局变量来传值:

# functest2.s - An example of using global variables in functions
.section .data
precision:
    .byte 0x7f, 0x00
.section .bss
    .lcomm radius, 4
    .lcomm result, 4
    .lcomm trash, 4
.section .text
.globl _start
_start:
    nop
    finit
    fldcw precision
    movl $10, radius
    call area
    movl $2, radius
    call area
    movl $120, radius
    call area
    movl $1, %eax
    movl $0, %ebx
    int $0x80

.type area, @function
area:
    fldpi
    filds radius
    fmul %st(0), %st(0)
    fmulp %st(0), %st(1)
    fstps result
    ret
 

    和之前functest1.s不同的是,functest2.s将半径值保存在radius全局变量里,这样在call area进入area函数后,就可以直接通过filds radius指令将radius里的半径值加载到FPU的ST0栈顶寄存器,然后通过fmul %st(0), %st(0)浮点指令计算出半径的平方,在fmulp %st(0), %st(1)计算出圆面积后,最后由fstps result指令将ST0里的面积值弹出到result全局变量里,函数在ret指令返回后,_start主入口程式里就可以通过result全局变量来获取到结果。

    所以functest2.s的输入参数和返回结果都是通过全局变量来完成的。

    在汇编链接后,该程式的调试输出结果如下:

(gdb) x/d &radius
0x80490d0 <radius>:    10
(gdb) x/f &result
0x80490d4 <result>:    314.159271
(gdb)

    从上面输出可以看到,在第一次call area指令执行后,radius里的半径值为10,对应result里存储的面积值为314.159271,和预期的一致。

    使用全局变量来传值并不是通用的编程方法,下一节介绍如何使用C语言的风格来传值,也就是通过内存栈来进行函数间的传值,这种方法更加通用一些。

    OK,就到这里,休息,休息一下 o(∩_∩)o~~
上下篇

下一篇: 汇编函数的定义和使用 (二)

上一篇: 汇编字符串操作 (二) 字符串操作结束篇

相关文章

基本数学运算 (一)

优化汇编指令 (三)

基本数学运算 (四) 基本运算结束篇

Moving Data 汇编数据移动 (二)

汇编开发相关工具 (二)

使用IA-32平台提供的高级功能 (三) SSE相关指令