我希望能够强制'双回',即有一个强制从其调用函数返回的函数(是的,我知道并不总是有一个真正的调用函数等).显然我希望是能够通过操纵堆栈来做到这一点,并且我认为至少在一些非便携式机器语言方式中它是可能的.问题是这是否可以相对干净和便携地完成.
为了填写一段具体的代码,我想写一下这个函数
void foo(int x) { /* magic */ }
这样以下功能
int bar(int x) { foo(x); /* long computation here */ return 0; }
回来,比方说1
; 并且不执行长计算.假设它foo()
可以假设它只被具有条形签名的函数调用,即int(int)
(因此具体知道它的调用者返回类型是什么).
笔记:
请不要告诉我这是不好的做法,我是出于好奇而问.
不得修改调用函数(在示例中bar()
).它不会知道所调用的函数是什么.(再次在示例中,只能修改位)./* magic */
如果它有帮助,你可能会假设没有内联(也许是一个不切实际的假设).
Steve Jessop.. 8
问题是这是否可以相对干净和便携地完成.
答案是它不能.
除了调用堆栈如何在不同系统上实现的所有非可移植细节之外,假设foo
内联到bar
.然后(通常)它将没有自己的堆栈帧.您不能干净利落地或可移植地谈论逆向工程"双"或"n次"返回,因为实际的调用堆栈不一定看起来像您期望的基于C或C++抽象的调用机.
您需要解决此问题的信息可能(无保证)可用于调试信息.如果调试器要向其用户显示"逻辑"调用堆栈,包括内联调用,则必须有足够的信息来定位"两级调高"调用者.然后,您需要模仿特定于平台的函数退出代码,以避免破坏任何内容.这需要恢复中间函数通常会恢复的任何东西,即使使用调试信息也可能不容易弄清楚,因为执行它的代码在bar
某处.但我怀疑,因为调试器可以显示该调用函数的状态,所以至少在原理上调试信息可能包含足够的信息来恢复它.然后回到原始调用者的位置(可以通过显式跳转实现,或者通过操纵平台保存其返回地址并进行正常返回的任何位置).所有这些都非常脏,非常不便携,因此我的"不"答案.
我假设您已经知道可以移植使用异常或setjmp
/ longjmp
.或者(或两者)bar
的调用者bar
需要与之合作,并同意foo
如何存储"返回值".所以我认为这不是你想要的.但是如果修改调用者bar
是可以接受的,你可以做这样的事情.它不漂亮,但它只是工作(在C++ 11中,使用异常).我会留下它,你要弄清楚如何在C中使用setjmp
/ longjmp
并使用固定的函数签名而不是模板:
templateT callstub(FUNC f, ARGS ...args) { try { return f(args...); } catch (EarlyReturnException &e) { return e.value; } } void foo(int x) { // to return early throw EarlyReturnException (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 (bar, 0);
最后,不是一个"糟糕的练习讲座",而是一个实际的警告 - 使用任何技巧提前返回通常不能很好地用C编写的代码或用C++编写的代码不会出现异常foo
.原因是bar
可能已经分配了一些资源,或者在调用之前将一些结构放入违反其不变量的状态,foo
目的是释放该资源或在调用之后恢复代码中的不变量.因此,对于一般函数bar
,如果您跳过代码,bar
则可能导致内存泄漏或无效的数据状态.一般来说,避免这种情况的唯一方法bar
是允许其余部分bar
运行.当然,如果bar
是用C++编写的,foo
可能会抛出,那么它将使用RAII作为清理代码,它将在你抛出时运行.longjmp
但是,在adestructor上有不确定的行为,因此你必须在开始之前决定是处理C++还是C语言.
问题是这是否可以相对干净和便携地完成.
答案是它不能.
除了调用堆栈如何在不同系统上实现的所有非可移植细节之外,假设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上有不确定的行为,因此你必须在开始之前决定是处理C++还是C语言.