如何从_calling_函数返回值?

 一线天24_226 发布于 2023-02-07 11:02

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

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

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并使用固定的函数签名而不是模板:

template 
T 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语言.

1 个回答
  • 问题是这是否可以相对干净和便携地完成.

    答案是它不能.

    除了调用堆栈如何在不同系统上实现的所有非可移植细节之外,假设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语言.

    2023-02-07 11:04 回答
撰写答案
今天,你开发时遇到什么问题呢?
立即提问
热门标签
PHP1.CN | 中国最专业的PHP中文社区 | PNG素材下载 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有