GCC C 和 ARM 程序集堆栈清理

GCC C and ARM Assembly Stack Cleanup

如果我从 C 调用 ARM 汇编函数,有时我需要传入很多参数。如果它们不适合寄存器 r0、r1、r2、r3,通常期望第 5 个、第 6 个...第 x 个参数被压入堆栈,以便 ARM 程序集可以从中读取它们。

所以在 ARM 函数中,我收到了一些在堆栈上的参数。完成汇编函数后,我可以从堆栈中删除这些参数或将它们留在那里并期望 C 程序稍后处理它们。

如果我们谈论的是 GCC C 和 ARM 程序集,通常谁负责清理堆栈?

我知道在开发时我们可以就任何一个约定达成一致。但是在这种特殊情况下通常使用什么作为默认值(ARM 程序集和 GCC C)?

低级代码通常如何描述它实现的行为?似乎应该对此有某种标准的描述。如果没有,那么您似乎只需要同时尝试这两种方法,看看哪一种不会崩溃。

如果有人对代码的外观感兴趣:

arm_function:    
    stmfd sp, {r4-r12, lr}     # Save registers that are not the first three registers, SP->PASSED ARGUMENTS
    ldmfd sp, {r4-r6}          # Load 3 arguments that were passed through the stack, SP->PASSED ARGUMENTS 
    sub sp, sp, #40            # Adjust the stack pointer so it points to saved registers, STACK POINTER->SAVED REGISTERS->PASSED ARGUMENTS

    #The main function body.

    ldmfd sp!, {r4-r12, lr},  # Load saved registers STACK POINTER->PASSED ARGUMENTS
    add sp, sp, #12           # Increment stack pointer to remove passed arguments, SP->NOTHING

    # If the last code line would not be there, the caller would need to remove the arguments from stack.

更新: 看来 C/C++ 选择 A. 是相当标准的。编译器通常使用像 cdecl 这样的调用约定,其工作方式与下面答案中的代码非常相似。可以在 link about calling conventions 中找到更多信息。改变函数的 C/C++ 调用约定似乎不是这样 common/easy。对于较旧的 C 标准,我无法更改它,因此看起来使用 A 应该是一个不错的默认选择。

如果调用函数(参数传递)在堆栈上分配了一些空间,则在调用函数内完成堆栈清除。你可能会问它是如何发生的。在ARM中@Olaf已经完全清除,在x86中通常是这样的:

sub     esp, 8      ; make some room 
...                 ; move arguments on stack
call    func
add     esp, 8      ; clean the stack

push    eax          ; push the arguments
push    ebx          ; or pusha, then after call, popa

call func

add     esp, 8       ; assuming registers are 4 bytes each

ABI(应用程序二进制接口)中还解释了系统中调用者和被调用者之间的交互是如何发生的。您可能会发现它很有用。

当前的 ARM 过程调用标准是 AAPCS

可以找到特定于语言的 ABI here。相关的将是关于C的文档,但其他的应该类似(为什么要重新发明轮子?)。

AAPCS 的第 14 页可能是阅读的良好开端。

它基本上需要调用者清理堆栈,因为这是最简单的方法:将额外的参数压入堆栈,调用函数并在 return 之后通过添加偏移量简单地调整堆栈指针(压入堆栈的字节数;这始终是 4 的倍数(“自然 32 位 ARM 字大小”)。

但是如果你使用 gcc,你可以通过使用内联汇编器避免自己处理堆栈。这提供了将 C 变量(等)传递给汇编代码的功能。如果需要,这还将自动将参数加载到寄存器中。只需查看 gcc 文档即可。有点难以弄清楚细节,但我更喜欢这个而不是在某个地方有原始的汇编存根。

好的,我添加了这个,因为理解原理可能有问题:

caller:
    ...
    push  r5    // argument which does not fit into r0..r3 anymore
    bl    callee
    add   sp,4  // adjust SP

callee:
    push r5-r7,lr  // temp, variables, return address
    sub  sp,8   // local variables
    // processing
    add   sp, 8     // restore previous stack frame
    pop   r5-r7,pc  // restore temp. variables and return (replaces bx)

您可以通过反汇编一些示例 C 函数来验证这一点。请注意,如果不使用临时寄存器或函数不调用另一个函数(不需要为此堆栈 lr),前导码和后导码可能会有所不同。

此外,调用者可能必须在调用之前堆栈 r0..r3。但这是编译器优化的问题。

例如,可以使用 gdb 和 objdump 进行反汇编。 我使用 -mabi=aapcs 进行 gcc 调用;不确定 gcc 是否会使用不同的标准。请注意,所有目标文件必须使用相同的标准。

编辑: 刚刚看了一下 AAPCS,它指出 SP 只需要 4 字节对齐。我可能将其与 Cortex-M 中断处理系统混淆了(无论出于何种原因,可能对于具有 64 位总线的 M7)默认情况下将 SP 对齐到 8 字节(软件配置选项)。 但是,SP 在 public 接口处必须是 8 字节对齐的。好的,标准实际上 比我记忆中的要复杂。这就是为什么我更喜欢 gcc 关心这些东西。