【问题标题】:How to effect a return of a value from the _calling_ function?如何从 _calling_ 函数中返回一个值?
【发布时间】:2014-01-19 04:04:49
【问题描述】:

我希望能够强制“双重返回”,即拥有一个强制从其调用函数返回的函数(是的,我知道并不总是调用函数等)显然我希望能够通过操作堆栈来做到这一点,并且我认为至少以某种不可移植的机器语言方式是可能的。问题是这是否可以相对干净和便携地完成。

给出一段具体的代码来填写,我要写函数

void foo(int x) {
    /* magic */
}

使下面的函数

int bar(int x) {
    foo(x);
    /* long computation here */
    return 0;
}

返回,比如说,1;并且不执行长计算。假设 foo() 可以假设它只被具有 bar 签名的函数调用,即 int(int)(因此明确知道它的调用者返回类型是什么)。

注意事项:

  • 请不要跟我说这是不好的做法,我是出于好奇而问的。
  • 调用函数(在示例中为bar()不得修改。它不会知道被调用的函数在做什么。 (同样在示例中,只能修改 /* magic */ 位)。
  • 如果有帮助,您可以假设没有发生内联(可能是不切实际的假设)。

【问题讨论】:

  • 这不仅仅是不好的做法;如果不修改bar 或者做一些带有例外的愚蠢技巧,这是不可能的。这种事情完全是结构化编程运动所反对的(我应该补充一下,这是合理且成功的)。
  • 内部函数应该如何知道调用者的返回类型是什么?!
  • “相对干净和便携”是非常主观的。投票以基于意见的方式结束。
  • @KerrekSB:这是根据关于调用者类型的假设得出的。我现在说得更清楚了。

标签: c++ c stack return stack-smash


【解决方案1】:

修改您的 void 函数 foo() 以返回布尔值是/否,然后将其包装在同名宏中:

    #define foo(x) do {if (!foo(x)) return 1;} while (0)

do .. while (0) 是,我相信你知道,标准的吞下分号技巧。

您可能还需要在声明foo() 的头文件中添加额外的括号,如:

    extern bool (foo)(int);

这可以防止使用宏(如果已定义)。 foo() 实现也是如此。

【讨论】:

    【解决方案2】:

    问题是这是否可以相对干净和便携地完成。

    答案是不能。

    除了调用堆栈如何在不同系统上实现的所有不可移植的细节之外,假设foo 被内联到bar。然后(通常)它不会有自己的堆栈帧。您不能干净或可移植地谈论对“双”或“n 次”返回进行逆向工程,因为实际调用堆栈不一定看起来像您根据 C 或 C++ 抽象进行的调用所期望的那样机器。

    破解此问题所需的信息可能(不保证)与调试信息一起提供。如果调试器要向其用户呈现“逻辑”调用堆栈,包括内联调用,那么必须有足够的信息来定位“向上两层”调用者。然后你需要模仿平台特定的函数退出代码来避免破坏任何东西。这需要恢复中间函数通常会恢复的任何内容,即使使用调试信息也可能不容易弄清楚,因为执行此操作的代码在某处的bar 中。但我怀疑既然调试器可以显示调用函数的状态,那么至少原则上调试信息可能包含足够的信息来恢复它。然后返回到原始调用者的位置(这可以通过显式跳转来实现,或者通过操纵平台所在的位置来保持其返回地址并进行正常返回)。所有这些都非常肮脏且非常不便携,因此我的回答是“不”。

    我假设您已经知道可以便携地使用异常或setjmp / longjmpbarbar 的调用者(或两者)都需要与之合作,并同意foo 如何存储“返回值”。所以我认为这不是你想要的。但是如果修改bar 的调用者是可以接受的,你可以这样做。它不漂亮,但它只是工作(在 C++11 中,使用异常)。我会让你弄清楚如何在 C 中使用 setjmp / longjmp 并使用固定的函数签名而不是模板:

    template <typename T, typename FUNC, typename ...ARGS>
    T callstub(FUNC f, ARGS ...args) {
        try {
            return f(args...);
        }
        catch (EarlyReturnException<T> &e) {
            return e.value;
        }
    }
    
    void foo(int x) {
        // to return early
        throw EarlyReturnException<int>(1);
        // to return normally through `bar`
        return;
    }
    
    // bar is unchanged
    int bar(int x) {
        foo(x);
        /* long computation here */
        return 0;
    }
    
    // caller of `bar` does this
    int a = callstub<int>(bar, 0);
    

    最后,不是一个“糟糕的实践讲座”,而是一个实用的警告——使用 any 技巧提前返回通常不适用于用 C 编写的代码或用 C++ 编写的代码不希望出现异常离开foo。原因是bar 可能已经分配了一些资源,或者在调用foo 之前将一些结构置于违反其不变量的状态,目的是释放该资源或在调用之后恢复代码中的不变量。所以对于通用函数bar,如果你跳过bar 中的代码,那么你可能会导致内存泄漏或无效数据状态。无论bar 中有什么,通常避免这种情况的唯一方法是让bar 的其余部分运行。当然,如果 bar 是用 C++ 编写的,并且期望 foo 可能会抛出,那么它将使用 RAII 来清理代码,并且它会在您抛出时运行。不过,longjmping over adestructor 具有未定义的行为,因此您必须在开始之前决定是使用 C++ 还是使用 C。

    【讨论】:

      【解决方案3】:

      有两种可移植的方法可以做到这一点,但都需要调用者函数的帮助。对于 C,它是 setjmp + longjmp。对于 C++,它是异常用法(try + catch + throw)。两者在实现上非常相似(本质上,一些早期的异常实现是基于 setjmp 的)。而且,如果没有调用者功能意识,绝对没有可移植的方式来做到这一点......

      【讨论】:

        【解决方案4】:

        唯一干净的方法是修改你的函数:

        bool foo(int x) {
            if (skip) return true;
            return false;
        }
        
        int bar(int x) {
            if (foo(x)) return 1;
            /* long computation here */
            return 0;
        }
        

        也可以使用setjmp() / longjmp() 来完成,但你也必须修改你的调用者,然后你也可以干净地完成它。

        【讨论】:

        • @einpoklum:那么显然,你搞砸了。 :) 子程序不能以这种精度可靠且可移植地将控制权从其调用者手中夺走。 (少数看起来,要么在某个时候在调用者处返回,要么终止进程,因此混乱是没有意义的。)你必须知道调用者是如何工作的,如果@ 987654324@ 可以是任意函数,它接受并返回一个 int,那么你不知道堆栈是如何布局的。
        猜你喜欢
        • 2021-12-11
        • 2014-06-28
        • 1970-01-01
        • 2011-08-26
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-11-05
        • 1970-01-01
        相关资源
        最近更新 更多