For ARM assembly
我在子程序中做了以下操作:
SubRoutine:
PUSH {r1,r2,lr}
//code that changes r1 and r2
POP {r1,r2,lr}
bx lr
这是从子程序返回并继续在主函数中执行代码的正确方法吗?我看到周围的人在做以下事情:
SubRoutine:
PUSH {r1,r2,lr}
//code that changes r1 and r2
POP {r1,r2,pc}
bx lr
但是我不知道为什么你在推LR的时候会弹出PC。哪一种方法是正确的,为什么?
另外,如果在子程序中调用子程序,您是否执行以下操作:
SubRoutine:
PUSH {r1,r2,lr}
//code that changes r1 and r2
PUSH {lr}
bl AnotherRoutine (where bx lr will be used to return from it)
POP {lr}
POP {r1,r2,pc}
bx lr
或者你这样做:
SubRoutine:
PUSH {r1,r2,lr}
//code that changes r1 and r2
PUSH {lr}
bl AnotherRoutine(where bx lr will be used to return from it)
POP {pc}
POP {r1,r2,pc}
bx lr
你应该注意以下三种情况:
- 叶片:
void foo(void) {};
- 尾呼:
int foo(void) { return bar(); };
- 中间体:
int foo(void) { int i; i = bar() + 4; return i; };
有很多方法可以实现这些调用。下面是一些示例,并不是在ARM汇编器中实现epilogue和prologue的唯一方法。
叶职能之一
许多函数是叶类型,不需要保存lr
。您只需使用bx lr
来返回。例如,
SubRoutine:
PUSH {r1,r2}
//code that changes r1 and r2
POP {r1,r2}
bx lr
同样,通常使用r1和r2传递参数,并且子程序可以自由地使用/销毁它们。ARM调用约定如果您从汇编程序调用'C'函数,则会出现这种情况。因此,通常情况下,没有人会保存r1和r2,但由于它是汇编程序,您可以做任何您喜欢的事情(即使这是一个坏主意)。因此,如果您遵循标准,实际上示例只有bx lr
。
尾部调用
如果你的函数是叶子,除了最后调用另一个函数,你可以使用下面的快捷方式,
Sub_w_tail:
// Save callee-saved regs (for whatever calling convention you need)
// Leave LR as is.
// ... do stuff
B tail_call
LR
由调用者保存到Sub_w_tail
,您只需直接跳转到tail_call
,后者返回到原始调用者。
中间函数
这是最复杂的。这是一个可能的序列,
SubRoutine:
PUSH {r1,r2,lr}
//code that changes r1 and r2
bl AnotherRoutine (where bx lr will be used to return from it)
// more code
POP {r1,r2,pc} // returns to caller of 'SubRoutine'
旧调用约定的一些细节在ARM Link和frame寄存器问题中。你可以使用这个约定。在ARM汇编程序中,有许多不同的方法来执行尾声和序幕。
最后一个相当复杂;或者至少编码起来很乏味。让编译器来决定使用什么寄存器以及在堆栈上放置什么寄存器要好得多。然而,在编写汇编程序时,通常只需要知道如何编写第一个(LEAF函数)。用汇编语言编写从高级语言调用的优化子例程是最有效率的。了解它们是如何工作的对于理解编译后的代码是很有用的。您还应该考虑内联汇编器,这样您就不必处理这些细微差别了。