在程序集中有效地调用具有相同参数的多个函数

时间:2011-07-04 06:53:48

标签: assembly

我在一个带有一些参数的函数中。让我们说:

  

__ cdecl函数(int a,int b,long c)

(我们暂时忘记__stdcall)

现在,在程序集中我想调用带有a,b,c参数的多个函数。 我知道的第一件事我需要注意的是,如果我进行“调用”,那么堆栈将与我如何获得它的位置错位,因为它将首先获取当前的EIP和EBP。我假设我可以存储旧的EIP和旧的EBP并计算当前的EIP和EBP,在堆栈中替换它们然后只是跳转。如果这个假设是错误的,可能会带来更多问题(随意指出),这是可以的,但这是无关紧要的,因为我可以再次推动这些论点。现在真正的问题是:我可以通过一次推送参数(或者使用堆栈,我得到它)多次调用多个具有相同堆栈的函数吗?还是会引起一些问题?

示例:

push 2
push 3
push 4
call X
call Y
Call Z
add esp, 12

所以,我希望X,Y,Z具有相同的参数和这种方式,特别是对于多个函数,它比使用“普通代码”更有效,它会为每个函数调用推送一次所有参数。

3 个答案:

答案 0 :(得分:3)

在基于完全堆栈的调用约定中,实现调用的汇编方面没有任何问题,如:

void myfunction(void)
{
    call_some_func(1, 2, 3);
    call_another_func(1, 2, 3);
    call_more_stuff(1, 2, 3);
    call_even_more_stuff(0, 1, 2, 3);
    call_yet_more(2, 3);
    ...
}

作为一个序列(AT& T语法,32位x86,我是UN * X家伙):

myfunction:
    pushl   $3
    pushl   $2
    pushl   $1
    call    call_some_func
    call    call_another_func
    call    call_more_stuff
    pushl   $0
    call    call_even_more_stuff
    addl    $8, %esp
    call    call_yet_more_stuff
    ...
    addl    $8, %esp
    ret

调用约定(对于微软称之为cdecl样式,因为在UN * X i386 ABI上也用于基于堆栈的参数传递)都具有 stackpointer的属性call返回后未更改。这意味着如果您将一系列参数推送到堆栈并执行call,那么在您调用的函数返回后,它们仍然在堆栈中。所以没有什么可以阻止你重新使用这些;如图所示,在某些情况下,如果您使用比以前调用中使用的更多/更少参数调用func,您甚至可以重新使用堆栈中已有的内容。

函数返回后,堆栈又是你的;你没有来清理(在addl $..., %esp之后直接执行call ),如果那里已有的东西对你有用的话保持它。

对于基于寄存器的函数调用,这显然不起作用。虽然,如果您的CPU架构允许多寄存器加载/存储各种类型,您仍然可以使用该东西。例如,在ARM上,可以将上述内容改为:

myfunction:
    stmfd   sp!, {lr}
    mov    r0, #1
    mov    r1, #2
    mov    r2, #3
    stmfd  sp!, {r0-r2}
    bl     call_some_func
    ldmfd  sp, {r0-r2}
    bl     call_another_func
    ldmfd  sp, {r0-r2}
    bl     call_more_stuff
    ldmfd  sp!, {r1-r3}
    mov    r0, #0
    stmfd  sp!, {r2, r3}
    bl     call_even_more_stuff
    ldmfd  sp!, {r0, r1, lr}
    b      call_yet_more_stuff

即。你把东西放在堆栈上并从那里加载它而不用更改负载的堆栈指针(ARM上的sp!在改变和只使用堆栈寄存器之间做出区别。)

最后,创建代码的C版本,通过针对您的平台/ CPU /调用约定的高度优化的编译器运行它并检查生成的代码将是一个好主意。如今,编制者已经非常善于找到重用这些东西的机会。

修改
如果您正在考虑以下事项:

void myfunc(void *a1, void *a2, void *a3)
{
    func1(a1, a2, a3);
    func2(a1, a2, a3);
    func3(a1, a2, a3);
}

然后你可以做的就是用堆叠“玩河内塔”并重新订购它;进入myfunc调用者的返回地址位于堆栈的最顶层,参数如下;因此,在将返回地址移动到堆栈的最底部时,使用clobber寄存器(UN {X上的%eax%ecx%edx)临时存储值。只需要一个“hanoi”就可以轻松实现三个args:

myfunc:
    popl    %eax          ; return address now in EAX
    popl    %ecx          ; arg[1]
    popl    %edx          ; arg[2]
    xchgl   %eax, (%esp)  ; swap return address and arg[3]
    pushl   %eax          ; re-push arg[3]
    pushl   %edx          ; and arg[2]
    pushl   %ecx          ; and arg[1]
    call    func1
    call    func2
    call    func3
    popl    %ecx          ; pop of dummy, gets %esp to pre-call
    jmpl    0xc(%esp)     ; use jmpl to return - address at "bottom"

Edit2 :我最初在这里使用非易失性寄存器(%ebx)来保存返回地址;正如评论员正确评论的那样,它会破坏寄存器中的值并给我们的调用者带来问题。为了防止这种情况,可以使用上面在堆栈上重新排序的方法。

答案 1 :(得分:1)

使用堆栈内存来保存临时变量没有问题,但请记住,这是不是标准调用对流,所以所有调用函数都必须在汇编程序中进行。

更常规的方法是使用分配的内存长3 * 4字节(x,y,z变量),并使用指针引用作为调用函数的输入。

答案 2 :(得分:1)

技术上没有,但通常是

允许该函数更改堆栈参数;它们就像本地框架对象一样。

然而,通常他们没有。因此,如果您调用自己的函数,则应该是安全的,特别是如果您在函数定义中添加注释。

调用库函数时,您会很快发现参数是否已更改。如果没有,除非未来的某些库修订版开始浏览参数,否则您是安全的。这不太可能,但由于您违反了API,您实际上无法抱怨。这是一个风险,尽管风险很小。