7

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

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

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

使以下功能

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

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

笔记:

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

4 回答 4

8

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

答案是它不能。

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

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

我假设您已经知道可以便携式使用异常或setjmp/ longjmp。任何一个bar或调用者bar(或两者)都需要与之合作,并同意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);

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

于 2014-01-01T16:28:41.653 回答
4

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

于 2014-01-01T16:33:51.473 回答
2

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

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(),但你也必须修改你的调用者,然后你也可以干净地完成它。

于 2014-01-01T16:33:06.540 回答
1

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

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

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

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

    extern bool (foo)(int);

这可以防止使用宏(如果已定义)。同上foo()执行。

于 2014-01-01T19:53:18.733 回答