跳过调用者返回序列中的函数

3
在函数调用序列中,例如:
                main() --> A() --> B()-->C();

当被调用的函数完成后,通常会返回到调用函数,例如C()返回到B(),然后返回到A()等。

我想知道是否也可以直接返回到调用顺序中较早的函数,使C()返回到main()并跳过B()A()

如果可能,我该如何做?能否解释一下它是如何工作的,以及它在现实中何时使用。

这是我的代码

#include <stdio.h>
int A(void);
int B(void);
void main(void )
{
    A();
}

int A()
{ 
    printf("enter A()\n");
    B();
    printf("exit A()\n");
}

int B()
{
    printf("enter B()\n");
    printf("exit B()\n");
}

我希望能够从函数B()跳过返回到函数A(),以便printf("exit A()\n");不会被执行,并给我这个结果:
enter A()                                                                                                                  
enter B()                                                                                                                  
exit B() 

4
除非使用 setjmp / longjmp,否则无法在 C 中实现。 或者 B() 调用 exit() 代替返回。如果这是 C++,可以使用 try/catch 和异常将堆栈展开回到 main(),如果 main 已编译为支持该功能(使用 try/catch)。 - Peter Cordes
1
请注意,main() 可以编译为尾调用 A,因为您将其声明为 void。此外,您的程序具有未定义的行为:您从非 void 函数 A() 和 B() 的末尾掉落。而 void main(void) 不是 ISO C 中有效的函数签名之一:int main(void) 是合法的。void main() 在 x86 正常编译器上可以工作,但您将只获得垃圾退出状态。 - Peter Cordes
4
我认为我们可能正在讨论一个XY问题,这是指如果您回到一步并描述您想通过跳过返回路径的某个部分来实现什么目标,那么您可能会得到更好的帮助。您想使用这种机制做什么?如果您描述一下,可能有不同的方法可以实现,这些方法更容易理解和/或被广泛使用(这意味着您将学习到更好地可重用于其他问题的内容)。 - Yunnosch
@nora:我的意思是,你为什么要打汇编标签呢?你是想学习汇编中的堆栈布局和编译器如何实现吗?因为走这条路不会给你提供一种安全或可移植的方法来完成这个问题。 - Peter Cordes
这个问题的意义是什么?它类似于这个吗? - RbMm
显示剩余3条评论
1个回答

3

首先,确保 @PeterCordes 对问题进行了详细解答。

好的,我们开始吧:

这种类型的操作可以使用一种称为“长跳转”(long jump)的东西来完成,因此您编辑后的代码应该是这样的:

#include <stdio.h>
#include <setjmp.h>//c standard library header
jmp_buf env; // for saving longjmp environment
main()
{
    int r, a=100;
    printf("call setjmp to save environment\n");
    if ((r=setjmp(env)) == 0){
        A();
        printf("normal return\n");
    }
    else
        printf("back to main() via long jump, r=%d a=%d\n", r, a);
}
int A()
{ 
    printf("enter A()\n");
    B();
    printf("exit A()\n");
}
int B()
{
    printf("enter B()\n");
    printf("long jump? (y|n) ");
    if (getchar()=='y')
    longjmp(env, 1234);
    printf("exit B()\n");
}

让我们了解一下刚刚发生了什么

在上面的程序中,setjmp()将当前执行环境保存在一个jmp_buf结构中,并返回0.

程序继续调用A(),然后调用B().

当在函数B()中时,如果用户选择不通过long jump返回,则函数将显示正常的返回序列。

如果用户选择通过longjmp(env,1234)返回,则执行将返回到最后保存的环境,并带有非零值。

在这种情况下,它会导致B()直接返回到main(),跳过A()

长跳的原理非常简单。当一个函数结束时,它通过当前堆栈帧中的(caller_EIP, caller_EBP)返回,如果我们用调用序列中较早的函数的(saved_EIP, saved_EBP)来替换它,执行会直接返回到那个函数。

除了(saved_EIP, saved_EBP)之外,setjmp()还可以保存CPU的通用寄存器和原始ESP,以便longjmp()可以恢复返回函数的完整环境。

长跳可以用于中止调用序列中的函数,导致执行从之前保存的已知环境恢复。

虽然在用户模式程序中很少使用,但它是系统编程中常用的技术。

例如,它可以在信号捕获器中使用,以绕过导致异常或陷阱错误的用户模式函数。

你可以查看这个也很好。


我会尝试并让您知道结果。 - nora
3
-fomit-frame-pointer 参数默认开启,因此 longjmp 函数不能假设存在一个保存 EBP/返回地址的链表。我认为 longjmp 函数根本不需要解开整个堆栈,它只是跳转到在 jmp_buf 中保存的地址,并从 jmp_buf 恢复 ESP/RSP 和其他调用保留寄存器。所以它不像异常处理那样解开整个堆栈去查找第一个能够处理它的 catch 块。longjmp 函数有一个特定的 jmp_buf 作为其目标。参见setjmp和longjmp实现了解其实现方式。 - Peter Cordes
你错误地描述了setjump/longjump的工作方式。setjump保存非易失性寄存器(以及可能的操作系统特定数据,例如在Windows x86上的SEH帧),而longjump则恢复上下文(并可能在某些操作系统上执行一些特定于操作系统的操作,例如在x86 Windows上的RtlUnwind)。 - RbMm

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接