如何从_calling_函数返回值?

时间:2014-01-01 16:19:55

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

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

为了填写一段具体的代码,我想编写函数

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

以便以下功能

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

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

注意:

  • 请不要告诉我这是不好的做法,我要求出于好奇心。
  • 不得修改调用函数(在示例中,bar() 。它不会知道所调用的函数是什么。 (再次在示例中,只能修改/* magic */位)。
  • 如果有帮助,你可能会认为没有内联(也许是一个不切实际的假设)。

4 个答案:

答案 0 :(得分:8)

  

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

答案是它不能。

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

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

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

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的行为有不明确的行为,所以你必须先决定是在处理C ++还是使用C语言。

答案 1 :(得分:4)

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

答案 2 :(得分: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()来完成,但您也必须修改您的来电者,然后您也可以干净利落地完成。

答案 3 :(得分:1)

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

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

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

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

    extern bool (foo)(int);

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