曲径通幽论坛

 找回密码
 立即注册
搜索
查看: 6296|回复: 0
打印 上一主题 下一主题

汇编函数与 C 样式传递数据

[复制链接]

4917

主题

5879

帖子

3万

积分

GROAD

曲径通幽,安觅芳踪。

Rank: 6Rank: 6

积分
34382
跳转到指定楼层
楼主
发表于 2010-8-18 22:12:07 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
在大多数 C 语言编译器中,对于函数参数的传递,局部变量的存放,函数的返回等处理都使用标准方法处理,这种方法也适用于汇编语言。

在 C 语言中,传递函数参数的方法是使用堆栈。主程序和任何函数都可以使用堆栈。堆栈就是主程序和函数之间传递数据的桥梁。同样,对于函数返回给主程序,通用的方法是将结果存入 EAX 中(32位),EDX:EAX 用于 64 位整数,FPU 的 ST(0) 用于浮点值。

1. 在堆栈中传递函数参数
在调用函数之前,主程序把函数所需的参数存放到堆栈的顶部。按照 C 样式,要求参数存放到堆栈中的顺序和函数的原型中的顺序相反。比如函数原型中的参数顺序从左到右是 int a, int b ,那么存放到堆栈是 b 先入栈,然后 a 再入栈。下面是一个简单的 C 程序:
#include <stdio.h>
int test (int a, int b)
{
     return (a + b);
}
int main()
{
     int a = 10;
     int b = 20;
     
     test (10, 20);

     return (0);
}

这个程序只是为了演示主程序中的 a 和 b 在堆栈中的存放顺序是怎样的。在 gdb 里使用 layout split 命令打开源码和反汇编窗口,从而观察到:
(图片损坏)
在上图中,白色高亮的语句是先让 20 这个参数入栈,然后下一条让 10 这个参数入栈保存。

最容易想到,在 CALL 指令执行时,堆栈中一定会存入 CALL 指令的下一条指令地址(函数返回地址),这时堆栈的情况如下所示:
[table=197px][tr][td]参数2(20)
[/td][/tr][tr][td]参数1(10)
[/td][/tr][tr][td]函数返回地址(0x80483ea)
[/td][/tr][/table]从上面的堆栈可以看到,函数的所有输入参数都位于返回地址的“下面“(X86里堆栈递减存放数据)。为了获得这些输入参数,我们可能会想到将这些值弹出堆栈,但是这么一来导致的问题是返回地址可能在处理过程中丢失。实际上,我们获取这些输入参数也不会用“弹“的方法。

2. 函数的开头和结尾
如果不“弹“,那么可以考虑通过使用 ESP 的间接寻址的方法来访问每个参数。但是这种方法还是有问题。因为在函数中,函数处理的某个部分可能包含对数据压栈的动作。如果发生这种情况,就会改变 ESP 的位置,这样就造成用于访问堆栈中的参数的间接寻址值的丢失(你需要重新确定这个寻址值,但是已经很难)。

所以,实际中的做法是,当进入函数时,把 ESP 寄存器复制到 EBP 寄存器中。这样就确保有一个寄存器永远包含指向调用函数时的堆栈顶部的正确指针。为了避免破坏原始的 EBP 中的值,在将 ESP 复制到 EBP 之前,应当在函数的开头将 EBP 压入栈中,如:
(图片损坏)
在上图中,test() 函数的第一条指令 push %ebp 就是将旧有的 EBP 进行保存,然后接下来的指令 mov %esp, %ebp 将 ESP 复制到 EBP 中。此时堆栈结构为:
参数2(20)
参数1(10)
函数返回地址(0x80483ea)
旧有的 EBP

当函数处理完后,函数一般会执行这样两条指令:
movl %ebp, %esp
popl %ebp
第 1 条恢复 ESP 寄存器;第 2 条恢复旧有的 EBP 的值。到这里,在函数里被压栈的局部变量都会被弹出。
另外,ENTER 和 LEAVE 指令被设计为专门用于建立函数开头(ENTER)和结尾(LEAVE)。也就是说,在这里 LEAVE 指令相当于上面两条指令功能。

3. 局部变量
当 EBP 寄存器被设置为指向堆栈的顶部之后,函数中使用的任何附加的数据都可以存放在堆栈中这个指针之后,这不会影响函数参数的访问。有局部变量的函数的堆栈结构如:

这里,如果函数具有局部变量,那么编译器会根据局部变量所占用的空间大小,为局部变量开辟一片堆栈空间。这里假设函数有 3 个整型局部变量,共占用 12 字节空间大小,那么在函数开始添加一行,如 subl $12, %esp。这样我们就拥有了 12 字节的用来存放局部变量的堆栈空间了。

4. 清空堆栈
最后,还要考虑一个问题。当函数执行 RET 指令返回时,传递给函数的参数并没有出栈。如果主程序使用堆栈进行其他操作,它很可能希望从堆栈中删除旧有的输入值,以便使堆栈恢复到函数调用前的状态。

使用 POP 指令可以完成这个工作,但也可以把 ESP 堆栈指针移动回函数调用之前的原始位置。使用 ADD 指令把压入堆栈的数据元素的长度加上去,这就完成了清理堆栈的动作。
(这里主要针对汇编程序而言,在 C 语言的反汇编中,函数处理完成时清理堆栈的动作基本由 LEAVE 指令完成)

测试程序
.section .data
precision:
     .byte 0x7f, 0x00

.section .bss
     .lcomm result, 4

.section .text
.global _start
_start:
     nop
     finit
     fldcw precision
     
     pushl $10
     call area
     addl $4, %esp
     movl %eax, result
     
     movl $1, %eax
     movl $0, %ebx
     int $0x80

area:
     pushl %ebp             #旧有EBP保存
     movl %esp, %ebp        #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             #恢复旧有EBP
     ret                   #子函数返回到主函数中
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|曲径通幽 ( 琼ICP备11001422号-1|公安备案:46900502000207 )

GMT+8, 2024-4-29 15:09 , Processed in 0.083846 second(s), 22 queries .

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表