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 程序集,通常谁负责清理堆栈?
- 调用的函数(A)
- 或者被调用的函数(B)
我知道在开发时我们可以就任何一个约定达成一致。但是在这种特殊情况下通常使用什么作为默认值(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 关心这些东西。
如果我从 C 调用 ARM 汇编函数,有时我需要传入很多参数。如果它们不适合寄存器 r0、r1、r2、r3,通常期望第 5 个、第 6 个...第 x 个参数被压入堆栈,以便 ARM 程序集可以从中读取它们。
所以在 ARM 函数中,我收到了一些在堆栈上的参数。完成汇编函数后,我可以从堆栈中删除这些参数或将它们留在那里并期望 C 程序稍后处理它们。
如果我们谈论的是 GCC C 和 ARM 程序集,通常谁负责清理堆栈?
- 调用的函数(A)
- 或者被调用的函数(B)
我知道在开发时我们可以就任何一个约定达成一致。但是在这种特殊情况下通常使用什么作为默认值(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 关心这些东西。