在 ARMv8 中嵌套使用 RET 指令

Nested use of RET instruction in ARMv8

提问人:lafinur 提问时间:10/6/2023 最后编辑:Peter Cordeslafinur 更新时间:10/12/2023 访问量:62

问:

设 ,是两组指令,都以指令结尾,并且以链接分支。换句话说,我们有一个看起来像这样的代码(为了清楚起见,我将在下面列举一些位置):label1label2RETlabel2label1


label1:
    # Some operations...
    # (1)
    RET

label2:
    #Some operations
    BL label1
    #(2)
    RET

我想打电话进来:label2main

main:
    # Some operations...
    BL label2
    #(3)

我希望的行为是,在分支到 in 后,执行从 .然而,事实并非如此。当 I 调用时,链路寄存器保存所需返回点的地址,并被执行。但是,在里面,我调用 ,将链路寄存器替换为 。这使得 after 的指令将执行 带到 ,而 after 的指令再次指向 。你可以看到问题所在。label2main#(3)mainBL label2#(3)label2label2BL label1#(2)#RET#(1)#(2)RET#(2)#(2)

高级编程语言允许嵌套使用函数。我可以定义一个函数,我在其中调用一个函数,每个函数都有语句。因此,我想要的功能必须以某种方式实现。我怎样才能进行顺序或嵌套的调用,并返回到被调用的第一个位置?fgreturnBL some_labelRETBL

我是组装的新手,所以如果问题有点微不足道,请原谅我。

函数 汇编 x86 arm 函数调用

评论

1赞 artless noise 10/7/2023
stackoverflow.com/questions/15752188/......对于 ARM,“链路寄存器”是一种叶调用优化。它不适用于所有功能。您需要为编译器安排一个堆栈槽(帧)。因此,假设“被调用者保存的寄存器”也可以保存,或者如果是尾部调用,则可以在“label2”中使用。lrb label1
0赞 Peter Cordes 10/7/2023
为什么它被标记为 [x86]?x86 的指令将返回地址推送到堆栈上以及跳转,x86 的指令就像 ARM 一样。所以 x86 非叶函数不需要做任何特殊的事情,它们的返回地址已经堆叠好了。此外,32 位 ARM 没有名为 .您的意思是询问 AArch64 [arm64] 在哪里作为真实指令的别名存在?callretpop {pc}retret
0赞 Peter Cordes 10/7/2023
我刚刚注意到你的标题中有 ARMv8。但这包括与 [arm] 标签匹配的 AArch32 状态,并且是@old_timer回答的问题。OTOH,您对指令的使用指示 AArch64 状态,[arm64]。我现在要保持原样;ARM32 和 ARM64 的情况基本相同,除了 / 指令的语法(ARM64 始终使用 / 作为 16 字节对,并且不能直接弹出到 PC 中,因为它没有使用寄存器号作为“通用”寄存器之一。retpushpopstpldp

答:

1赞 old_timer 10/7/2023 #1

您基本上标记了多个体系结构。所以我会选择一个架构。

简短的回答是将返回地址保存到堆栈中。

unsigned int more_fun ( unsigned int x );

unsigned int fun0 ( unsigned int x )
{
    return(x+1);
}

unsigned int fun1 ( unsigned int x )
{
    return(more_fun(x));
}

unsigned int fun2 ( unsigned int x )
{
    return(more_fun(x)+1);
}

Disassembly of section .text:

00000000 <fun0>:
   0:   e2800001    add r0, r0, #1
   4:   e12fff1e    bx  lr

00000008 <fun1>:
   8:   e92d4010    push    {r4, lr}
   c:   ebfffffe    bl  0 <more_fun>
  10:   e8bd4010    pop {r4, lr}
  14:   e12fff1e    bx  lr

00000018 <fun2>:
  18:   e92d4010    push    {r4, lr}
  1c:   ebfffffe    bl  0 <more_fun>
  20:   e8bd4010    pop {r4, lr}
  24:   e2800001    add r0, r0, #1
  28:   e12fff1e    bx  lr

如果没有嵌套调用,则无需保存返回地址(在此汇编语言中显示为 lr 或链接寄存器)。额外的寄存器 r4 不是因为 r4 在这里很特殊,而是因为编译器使用的调用约定规定了一个 64 位对齐的堆栈,因此它们会加入一些其他寄存器,这不会影响代码/约定以使其对齐。

第二个调用函数,所以有两件事之一,在这种情况下,它可以做一个尾巴吗?优化并做到了这一点

fun1:
b more_fun

但事实并非如此。也许对少数人来说很明显,但我使用的是较旧的 gcc(不是最前沿的)并明显保留了默认值 armv4t。因此,也许出于这个原因,工具链不愿意处理手臂/拇指模式切换,但使用 BL,他们会在链接器中为您放置单板/蹦床。

第三个,我强迫它无法进行尾部优化,这类似于您通常看到的那种东西,如果手动完成,大多数人会编写。

实际上,这就是您期望编译器生成的内容:

fun2:
push {r4, lr}
bl  more_fun
add r0, r0, #1
pop {r4, lr}
bx  lr

在函数的开头和结尾处使用此堆栈帧

fun2:
push {r4, lr}
...
pop {r4, lr}
bx  lr

然后填写函数的内脏。

相同的 gcc 但不同的手臂结构,随着时间的推移,增加了更多的拇指互通支持。

Disassembly of section .text:

00000000 <fun0>:
   0:   e2800001    add r0, r0, #1
   4:   e12fff1e    bx  lr

00000008 <fun1>:
   8:   eafffffe    b   0 <more_fun>

0000000c <fun2>:
   c:   e92d4010    push    {r4, lr}
  10:   ebfffffe    bl  0 <more_fun>
  14:   e2800001    add r0, r0, #1
  18:   e8bd8010    pop {r4, pc}

如果手动编写可以在没有堆栈帧的情况下完成,那么在嵌套分支保留您需要保留的内容之前,然后在还原需要还原的内容之后,因此,在函数边缘使用堆栈帧时,您可以在代码中保存和还原。这没有错,这是一种非常手工的汇编方式,而不是一种高级语言编译的方式。

其他架构(例如,不包括 arm)具有不同的架构;例如,调用和返回可以使用堆栈而不是返回寄存器。因此,在这种情况下,就回邮地址而言,您只需继续拨打电话(例如 8088/86)。您最终将需要保留其他内容来进行嵌套调用,以免丢弃返回地址以外的内容。这是编译语言制定/选择调用约定的地方,该约定作为一组规则允许以通用方式构造函数,以便您可以无限期地嵌套或递归。

评论

0赞 old_timer 10/7/2023
对于 armv8 或其他体系结构,您可能已标记。阅读该体系结构的文档,找到调用和返回解决方案,以及堆栈解决方案和堆栈指针等,并从中确定是否需要保留返回地址,如果需要,如何保留。然后就去做吧。
0赞 old_timer 10/7/2023
armv8 部分源自 armv7 和 armv1,但是一个全新的指令集架构......因此,请再次查找所讨论的架构。