【问题标题】:skipping functions in return sequence of callers在调用者的返回序列中跳过函数
【发布时间】:2018-08-19 04:54:03
【问题描述】:

在一系列函数调用中,比如

                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() 

【问题讨论】:

  • 除非你使用 setjmp / longjmp,否则你不能在 C 中使用。或者你可以让B() 调用exit() 而不是返回。如果这是 C++,您可以使用 try/catch 和异常将堆栈展开回 main(),如果已编译 main 以支持它(通过使用 try / catch
  • 为什么要标记这个 x86 程序集?您是否想让B() jmp 直接到main 的末尾,而不管它是从哪里调用的。我猜理论上对于特定平台,您可以展开堆栈并获取您父母的返回地址,但这对于 Linux 与 Windows 的工作方式不同,例如在 Linux 上使用 .eh_frame 元数据。对于简单的示例,它可能会在某些时候起作用。
  • 请注意,main() 可能已编译为使用 jmp A 尾调用 A,因为您将其声明为 void。此外,您的程序具有未定义的行为:您脱离了非 void 函数 A() 和 B() 的末尾。而void main(void) 不是有效的函数签名之一是 ISO C:int main(void) 是合法的。 void main() 可以在 x86 的普通编译器上工作,你只会得到一个垃圾退出状态。
  • 我使用intel pc这就是x86标签的原因
  • 我想我们可能会在这里讨论一个 XY 问题meta.stackexchange.com/questions/66377/what-is-the-xy-problem 我的意思是,如果您退后一步并通过跳过部分回报来描述您想要实现的目标,您可能会得到更好的帮助小路。你想用这个机制做什么?如果您对此进行描述,它可能可以通过不同的方式实现,这些方式更容易理解和/或更广泛使用(这意味着您将学到一些可以更好地用于其他问题的东西)。

标签: c function assembly x86


【解决方案1】:

在检查所有内容之前,@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中 结构和returns 0

程序继续调用A(),后者又调用B()

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

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

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

原理 long jump 非常简单。当一个函数完成时,它由

返回

(caller_EIP, caller_EBP) 在当前堆栈帧中, 如果我们将(caller_EIP, caller_EBP) 替换为

(saved_EIP, saved_EBP) 早期函数 在调用序列中,执行将直接返回到该函数。

另外 到(saved_EIP, saved_EBP)setjmp() 还可以节省 CPU 的通用寄存器和 原来ESP,这样longjmp()就可以恢复返回的完整环境了 功能。

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

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

例如, 它可用于信号捕获器以绕过导致导致 异常或陷阱错误。

你可以查this也不错

【讨论】:

  • 我会尝试并告诉你结果
  • -fomit-frame-pointer 默认是打开的,所以longjmp 不能假设有一个保存的EBP/返回地址的链表。我认为longjmp 根本不需要真正展开堆栈,它只是跳转到保存在jmp_buf 中的地址,并从jmp_buf 恢复ESP/RSP 和其他调用保留寄存器.因此,这不像是展开堆栈以寻找可以处理它的第一个 catch 块的异常。 longjmp 有一个特定的 jmp_buf 作为其目的地。请参阅setjmp and longjmp implementation 了解如何实施。
  • 您当然错误地描述了 setjump/longjump 的工作方式。 setjum 保存非易失性寄存器(以及可能的特定于操作系统的数据,例如 windows x86 上的 SEH 帧)和 longjump 恢复上下文(并且可能做一些特定于操作系统的操作,RtlUnwind 在 x86 windows 上)
猜你喜欢
  • 2012-04-10
  • 2013-09-17
  • 1970-01-01
  • 2011-07-26
  • 1970-01-01
  • 2023-01-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多