上一节提到过两种传值方式,一种是通过寄存器传值,一种是通过全局内存变量来传值,如果每个函数都使用自己的规则来进行传值的话,在进行大型项目开发时,函数的相互沟通就会是个很大的问题...

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

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

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

Passing Data Values in C Style C语言风格的传值方式:

    上一节提到过两种传值方式,一种是通过寄存器传值,一种是通过全局内存变量来传值,如果每个函数都使用自己的规则来进行传值的话,在进行大型项目开发时,函数的相互沟通就会是个很大的问题,在之前的Moving Data 汇编数据移动 (四) 结束篇 栈操作的章节里,我们介绍过栈,栈是一段特殊的内存区域,C语言编写的程式在经过编译后,生成的汇编指令里,函数之间就是通过栈来传递参数的。至于函数的返回结果,如果是32位的整数,则将其存放在EAX寄存器里,如果是64位的整数,则将其通过EDX:EAX的寄存器对来返回给主调用程式,如果结果是浮点数,则将该浮点数存储在FPU的ST0寄存器里。

    下面就具体的介绍下栈在函数传值时的工作原理。

Revisiting the stack 重温栈的工作方式:

    栈是程序内存区域底部的一段保留区域,它从高地址往低地址方向反向增长,由ESP寄存器作为指针指向栈顶位置,可以通过PUSH指令向栈里存放数据,当PUSH指令执行时,会先将ESP减去要压入数据的尺寸,然后将数据存储到ESP指向的内存位置。可以通过POP指令将数据弹出栈,POP指令会将ESP指向的数据弹出到指定的寄存器,然后将ESP加上弹出数据的尺寸,从而让其指向之前压入栈的数据。

Passing function parameters on the stack 通过栈来传递函数的参数:

    在C风格里,当主程式用CALL指令调用某个函数前,会先将函数所需的参数通过PUSH指令压入栈,至于压栈的顺序是和C语言里函数定义的原型里的参数顺序刚好相反的,例如,某个函数原型是test(a,b,c),则对应的汇编指令会先将第三个参数c压入栈,再将第二个参数b压入栈,最后将第一个参数a压入栈。

    由于CALL指令调用的函数在执行完后,需要能够返回到主调用程序继续执行,所以CALL指令执行时,在内部会自动将返回地址也压入栈,这样函数执行完后,RET指令就可以根据压入的返回地址来进行返回。所以CALL指令执行后,栈里的情况会如下图所示:


图1

    上图ESP指向的栈顶位置,存储的是CALL指令压入栈的函数返回地址,在ESP之前还依次压入了3个参数。

    参数压入栈后,该如何访问到这些参数呢?前面的章节,我们介绍过寄存器间接寻址的方式,就是通过寄存器里的值作为内存地址,再加上一个可能的偏移值来访问到内存里的数据,因此,我们可以利用ESP寄存器进行间接寻址,如下图所示:
 

图2

    由于函数内部执行时还可能会用到PUSH和POP指令,PUSH和POP执行时会修改ESP的位置,所以我们不能用ESP作为基址,来访问栈里的参数,但是我们可以将ESP的值存储到另一个寄存器,该寄存器就是EBP寄存器,同时为了不破坏EBP里原来的值,在将ESP传值给EBP之前,需要先将EBP原来的值压入栈,然后就可以通过EBP间接寻址的方式访问到这些参数了,下图就演示了这种做法:
 

图3

    上图在将EBP原来的值压入栈后,ESP就可以将当前的栈顶位置赋值给EBP,函数里的汇编指令就能通过8(%ebp)即EBP加8来访问到第一个参数,第二个和第三个参数的访问也是同理,EBP不会像ESP那样受到PUSH,POP之类的指令影响,所以栈里参数的内存位置与EBP的相对偏移值就不会发生变化。

Function prologue and epilogue 函数的开场与结束:

    从上面图3可以看出,在CALL指令进入函数后,为了能访问到栈里的参数,需要先将EBP压入栈,再将ESP传值给EBP,在函数结束时,反过来就要先将EBP传值给ESP,然后再将EBP原来的值弹出栈,所以C风格的函数有一套标准的开场和结束代码:

function:
    pushl %ebp
    movl %esp, %ebp
    .
    .
    movl %ebp, %esp
    popl %ebp
    ret

    上面的代码片段里,开头先用pushl %ebp指令将EBP原来的值压入栈,然后通过movl %esp, %ebp指令将ESP当前的值传递给EBP,在函数RET指令返回前,则先用movl %ebp, %esp指令将ESP恢复到上面图3的位置,然后popl %ebp指令就可以将ESP指向的Old EBP Value(EBP原来的值)恢复到EBP寄存器,并且将ESP加4个字节,让其指向Return Address(返回地址),这样最后一条RET指令就可以根据当前栈顶的返回地址返回到主调用程序。

    上面开场的pushl %ebp和movl %esp, %ebp两条指令还可以用ENTER一条指令来代替,ENTER指令执行时,会自动在内部完成pushl %ebp和movl %esp, %ebp的操作,同样的,可以用LEAVE指令来代替结束时的movl %ebp, %esp和popl %ebp两条指令,ENTER和LEAVE指令可以简化函数的开场和结束代码。

Defining local function data 定义函数的局部变量:

    函数的局部变量是指函数里的一些私有数据,如果将局部变量的值存放在寄存器里,则由于寄存器数量有限,能存储的局部变量数据也会受到限制,所以在C风格的函数里,局部变量也是放在栈里的,然后就可以利用EBP作为基址,加上偏移值来访问这些局部变量了,如下图所示:


图4

    如上图所示,可以通过-4(%ebp)即EBP减4来访问Local Variable 1对应的局部变量里的值,但是上图有个问题,就是当函数里执行PUSH操作时,ESP就会下移到局部变量的内存位置,从而会将这些局部变量的值给覆盖掉,为了避免这样的问题,可以先让ESP减去局部变量的总字节数,从而让ESP越过局部变量的区域,这样PUSH之类的操作就不会覆盖掉局部变量的值了,如下图所示:
 

图5

    由于一开始ESP需要减去局部变量的字节数,所以C风格函数的开场代码就需要进行类似如下的修改:

function:
    pushl %ebp
    movl %esp, %ebp
    subl $8, %esp
    .
    .

    这里,在pushl %ebp和movl %esp, %ebp之后,通过subl $8, %esp指令将ESP的值减8,从而为局部变量预留出8个字节的空间,这8个字节可以用来放置两个32位的整数类型的局部变量。

    在函数RET返回前,结束代码会用EBP来恢复ESP,从而将这些局部变量给自动丢弃掉。

Cleaning out the stack 函数返回后,清理栈中的参数:

    主调用程式在用CALL指令调用函数前,会先用PUSH指令将参数压入栈,但是函数执行完后,通过RET指令返回时,RET指令只会将返回地址弹出栈,而返回地址之前压入的参数还是留在栈里的,需要主调用程式自己来清理,一种方式是通过POP指令将参数一个一个弹出栈,还有一种更简单更常用的方式:将ESP加上栈里参数的总字节数,从而将这些参数给丢弃掉,这所以可以这么做,是因为栈是向低地址方向增长的,ESP加上参数的总字节后,之后的PUSH操作就会自动将丢弃掉的参数给覆盖掉。

    因此通过CALL指令调用函数的模板如下:

pushl %eax
pushl %ebx
call compute
addl $8, %esp

    上面代码片段里,先用PUSH指令将两个参数依次压入栈,然后通过CALL指令调用进入compute函数,在compute函数执行完,并用RET指令返回后,最后用ADD指令将ESP加8(之前压入栈的两个参数的总字节数是8),从而手动清理掉栈里的参数。

An example 完整的例子:

    下面的functest3.s程式就演示了如何通过栈来进行函数的传值:

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

.type area, @function
area:
    pushl %ebp
    movl %esp, %ebp
    subl $4, %esp
    fldpi
    filds 8(%ebp)
    fmul %st(0), %st(0)
    fmulp %st(0), %st(1)
    fstps -4(%ebp)
    movl -4(%ebp), %eax
    movl %ebp, %esp
    popl %ebp
    ret
 

    上面的functest3.s例子在前一章functest2.s的基础上做了些改动,本例是用栈代替寄存器来传递参数的,例如上面在call area指令执行函数前,会通过pushl $10将半径10压入栈作为area函数的参数,在call指令进入area函数后,先是标准的开场代码:pushl %ebp和movl %esp, %ebp,接着用subl $4, %esp将ESP减4从而预留出4个字节的空间作为函数的局部变量。在fldpi将pi值压入FPU寄存器栈后,再通过filds 8(%ebp)将第一个半径参数也压入栈,然后是fmul和fmulp两条指令根据ST0和ST1的值计算出圆面积,计算出的面积值存储在FPU的ST0里。

    在得到结果后,为了演示局部变量的用法,这里用fstps -4(%ebp)指令将ST0里的结果弹出到-4(%ebp)指向的第一个局部变量的内存位置,最后再由movl -4(%ebp), %eax指令将该局部变量里的值存储到EAX里,作为函数的返回值,这里用的是EAX作为返回结果的寄存器,但是在实际使用时,尤其是在C程序调用汇编函数时,如果汇编函数执行的结果是浮点数,则应该以ST0作为返回结果的寄存器,上面的functest3.s例子只是为了演示局部变量的传值用法,所以才改用EAX作为结果返回。

    area函数在RET指令返回前,是一段标准的结束代码:movl %ebp, %esp和popl %ebp 。

    在RET指令返回到主调用程式后,先通过addl $4, %esp将栈里残留的参数给清理掉,再由movl %eax, result将EAX里的结果存储到result全局变量里。

Watching the stack in action 在调试器里观察栈的存储情况:

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

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

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

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

(gdb) print $esp
$1 = (void *) 0xbffff390
(gdb)

    上面输出显示,一开始ESP栈顶指针指向的是0xbffff390的内存位置,继续往下调试到CALL指令执行前:

(gdb) s
13        pushl $10
(gdb) s
14        call area
(gdb) print $esp
$2 = (void *) 0xbffff38c
(gdb) x/d 0xbffff38c
0xbffff38c:    10
(gdb)

    可以看到,在pushl $10执行后,ESP的值变为0xbffff38c,比一开始的0xbffff390减少了4个字节,同时0xbffff38c这个新的栈顶位置里存储的值为10,也就是PUSH指令压入的半径参数。

    我们继续单步调试,进入CALL调用的函数:

14        call area
(gdb) s
31        pushl %ebp
(gdb) print $esp
$3 = (void *) 0xbffff388
(gdb) x/x 0xbffff388
0xbffff388:    0x08048085
(gdb) x/d 0xbffff38c
0xbffff38c:    10
(gdb)

    在CALL指令进入area函数后,ESP的值变为0xbffff388,比之前pushl $10执行后的值0xbffff38c又减少了4个字节,通过x/x 0xbffff388命令可以查看到新的ESP指向的位置里存储的值是0x08048085,该值就是CALL指令压入栈的返回地址。通过x/d 0xbffff38c命令输出的10表示,在返回地址之前压入的是半径参数10 。

    在area函数里第一条指令pushl %ebp执行后,gdb调试器里的输出情况如下:

31        pushl %ebp
(gdb) s
32        movl %esp, %ebp
(gdb) print $esp
$4 = (void *) 0xbffff384
(gdb) x/x 0xbffff384
0xbffff384:    0x00000000
(gdb)

    从上面的输出可以看到,pushl %ebp执行后,ESP的值减4变为0xbffff384,该内存里压入的值0x00000000就是原始的EBP的值,接下来就可以通过movl %esp, %ebp指令将ESP的值赋值给EBP,这样函数里就可以用EBP加偏移值来访问到函数的参数和局部变量了:

32        movl %esp, %ebp
(gdb) s
area () at functest3.s:33
33        subl $4, %esp

(gdb) x/3x $ebp
0xbffff384:    0x00000000    0x08048085    0x0000000a
(gdb)

    在movl %esp,%ebp执行后,EBP指向的内存位置就是当前的栈顶位置,通过x/3x $ebp命令就可以查看到当前栈里依次压入了10的半径参数,0x08048085的返回地址,以及0x00000000的EBP的原始值。

    继续单步调试:

33        subl $4, %esp
(gdb) s
34        fldpi
(gdb) print $esp
$5 = (void *) 0xbffff380
(gdb)

    可以看到,在subl $4, %esp指令执行后,ESP的值变为0xbffff380,减少了4个字节,这4个字节是预留给存放32位单精度浮点数的局部变量的。

    接下来,在fldpi和filds 8(%ebp)两条指令执行后,可以用info all命令来查看到FPU里压入寄存器栈的值:

34        fldpi
(gdb) s
35        filds 8(%ebp)
(gdb) s
36        fmul %st(0), %st(0)
(gdb) info all
......................
st0            10    (raw 0x4002a000000000000000)
st1            3.1415926535897932385128089594061862    (raw 0x4000c90fdaa22168c235)

st2            0    (raw 0x00000000000000000000)
st3            0    (raw 0x00000000000000000000)
st4            0    (raw 0x00000000000000000000)
st5            0    (raw 0x00000000000000000000)
......................

    在fmul %st(0), %st(0)和fmulp %st(0), %st(1)指令计算完圆面积后,FPU里的结果如下:

(gdb) info all
........................
st0            314.159271240234375    (raw 0x40079d14630000000000)
st1            0    (raw 0x00000000000000000000)
st2            0    (raw 0x00000000000000000000)
........................
 

    ST0里存储的314.159271240234375就是半径10的圆面积,接着就可以用fstps -4(%ebp)指令将ST0里的结果弹出到-4(%ebp)指向的局部变量:

38        fstps -4(%ebp)
(gdb) s
39        movl -4(%ebp), %eax
(gdb) print $esp
$6 = (void *) 0xbffff380
(gdb) x/4x $esp
0xbffff380:    0x439d1463    0x00000000    0x08048085    0x0000000a
(gdb) x/f $esp
0xbffff380:    314.159271
(gdb)

    上面0xbffff380就是局部变量的内存位置,该位置里的值314.159271就是fstps指令弹出到此处的结果。

    一路往下执行到RET指令:

39        movl -4(%ebp), %eax
(gdb) s
40        movl %ebp, %esp
(gdb) s
41        popl %ebp
(gdb) s
42        ret
(gdb) info all
eax            0x439d1463    1134367843
ecx            0x0    0
edx            0x0    0
ebx            0x0    0
esp            0xbffff388    0xbffff388
ebp            0x0    0x0

    从上面输出可以看到EAX里的值0x439d1463为浮点结果的十六进制格式,ESP恢复到CALL进入area函数时的值0xbffff388,而EBP则恢复为原来的0x0 。

    继续单步执行:

42        ret
(gdb) s
_start () at functest3.s:15
15        addl $4, %esp
(gdb) s
16        movl %eax, result
(gdb) s
17        pushl $2
(gdb) print $esp
$7 = (void *) 0xbffff390
(gdb) x/f &result
0x80490d4 <result>:    314.159271
(gdb)

    上面输出可以看到ret指令执行后,程序的执行流程切换到_start主调用程式的15行,也就是call area的下一条指令addl $4, %esp的位置,当addl $4, %esp指令将ESP加4后,ESP的值就恢复为程序最开始运行时的0xbffff390,从而丢弃掉之前压入栈的参数,在movl %eax, result指令执行后,EAX就将计算出来的面积值314.159271存储到result全局变量里。

    后面半径2和半径120的函数执行过程也是一样的。

    下一篇介绍如何将函数放置到单独的文件里,以方便模块化开发。

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

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

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

相关文章

什么是汇编语言(一) 汇编底层原理,指令字节码

什么是汇编语言(二) 高级语言与汇编

使用IA-32平台提供的高级功能 (一)

汇编数据处理 (二)

高级数学运算 (二) 基础浮点运算

Moving Data 汇编数据移动 (一)