本文由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~~