【问题标题】:Is it possible to write a zero-cost exception handling in C?是否可以用 C 编写零成本的异常处理?
【发布时间】:2013-03-06 02:02:32
【问题描述】:

g++编译器具有零成本异常处理的特性。据我了解,try 什么都不做,但是当抛出异常时,会执行异常处理程序的子例程。像这样:

void foo() {
    try {
        bar(); // throws.
    } catch (Type exc) {
        baz();
    }
}

在伪代码(c-stylish)中看起来像这样:

void foo() {
    bar();
    return;
catch1_Type:
    baz();
}

bar() 抛出。异常例程执行以下操作:

啊,返回地址在函数 foo() 中!并且返回地址在第一个 try-catch 块中,我们抛出类型 Type,所以异常处理程序的地址是 foo+catch1_Type。所以清理堆栈,这样我们就可以结束了!

现在我的问题是:有没有办法在 C 中实现它? (可以是 C99 或更新版本,虽然我对 gcc 支持的 C 方言感兴趣)。我知道我可以使用例如 libunwind 进行堆栈检查和遍历,尽管我不知道如何获取catch1_Type 标签的地址。这可能是不可能的。

异常处理程序可能是一个不同的函数,这同样可以,但是如何在另一个函数中获取堆栈帧foo 的局部变量的地址?这似乎也是不可能的。

那么……有什么办法吗?我不想用这个进入汇编程序,但如果其他一切都失败了也是可以接受的(尽管局部变量 - 伙计,如果使用不同的优化级别,你永远不知道它们在哪里)。

要明确一点 - 这个问题的目的是避免 setjmp/longjmp 方法。

编辑:我发现了一个很酷的想法,但并不完全奏效:

gcc 中的嵌套函数。他们能做什么?

  • 可以访问局部变量,
  • 可以在父函数中转到本地标签!
  • 可以被我们函数的被调用者调用,前提是我们传递一个指向嵌套函数的指针,因此它可以通过被调用者中的指针获得。

阻碍我做任何零成本的事情的缺点:

  • 如果它们未被使用,即使在 -O0 级别也会被优化。我能对此做些什么吗?如果可以的话,我可以在抛出异常时通过符号名称获取地址,它只会完成实现异常的工作,而这些异常在不抛出时不会花费任何成本......

【问题讨论】:

  • 这似乎对这个问题略有不同:stackoverflow.com/questions/307610/… ...它询问异常在 C++ 中是如何工作的;一旦您知道 C++ 如何实现异常,您就可以决定是否可以为您的 C 程序复制该实现。
  • @MartinAtkins,我只是稍微了解 g++ 中的异常是如何实现的,尽管我认为我在某处读到 g++ 中的异常支持部分在编译器和链接器方面,所以重复如果不修改工具,它在 C 中将是一个问题。不过,我会阅读您提到的主题以获得更多法力值。
  • 你完全错了,try/catch 构造是有代价的。首先try 必须以某种方式标记堆栈和所有寄存器的位置,而catch/throw 必须逐个展开try 上下文。 C setjmp/longjmp 中有一个“等效”功能,它的作用完全相同,只是它周围的胶水更少。
  • @JensGustedt,不,我没记错。展开表是在构建时生成的(我认为是链接时间?),它们足以让异常例程展开堆栈并找到异常处理程序的地址。在运行时,不会发生任何标记。 systemcall.org/blog/2010/10/zero-cost-exception-handling-in-cpp
  • 那篇文章至少误导了 setjmp/longjmp 部分。以 C 为中心的方法永远不必通过跳转表的“列表”。

标签: c exception optimization elf


【解决方案1】:

我已经找到了一些时间来尝试这个想法,并且我非常接近为我自己的问题找到解决方案。详情如下:

  • gcc 允许嵌套函数,它可以访问父函数的局部变量,并且可以转到父函数中的标签!
  • gcc 将不会发出内部函数的代码,如果它只被定义但没有被引用。您可以定义内联无操作函数,该函数获取指向本地函数的指针,并在父函数中调用它。这将强制为内部函数生成代码,并且成本为零(在更高的优化级别,内联无操作调用将被删除)。不过,在最近的 gcc 中优化内联调用可能会阻止生成内部函数的代码..
  • 不好的是,我认为没有办法强制嵌套(内部)函数成为全局符号。它们始终是本地的,因此无法使用 dlsym 获取地址。
  • 另一个坏消息,如果程序使用这样的嵌套函数,valgrind 会崩溃;)我能够验证我的简单测试程序,但无法使用 valgrind 来验证没有内存违规。

我用来检查的代码有明显的缺陷:它不是“零成本”,因为必须在函数执行期间设置指向异常处理例程的全局指针。要是我们能做这个编译时间就好了!

好吧,毕竟如果想要正确使用内部函数,比如将指向它的指针传递给被调用者,这样他们就可以在抛出异常的情况下调用它,我们可能会有非常快的异常处理,快得多,快得多比 setjmp/longjmp...

我会继续破解,也许我会找到一种方法(一些汇编代码块来强制 GAS 将该函数注册为父级的个性例程?)。

#include <stdio.h>
#include <dlfcn.h>
#include <stdlib.h>

typedef void (*catch_routine)(void*);

catch_routine g_r = NULL;

void tostr_internal(char* str, int a)
{
    int result = a + 'a';
    if (result < 'a' || result > 'z')
    {
        // handle exception
        if(g_r)
        {
            g_r(&a);
        }
        else
        {
            fprintf(stderr, "Exception not caught!");
            abort();
        }
    }
    else
    {
        str[0] = result;
        str[1] = '\0';
    }
}

char* tostring(int a)
{
    __label__ exhandler;
    char* string = (char*)malloc(2*sizeof(char));

    void personality(void* exid) {
        fprintf(stderr, "Number %d is not a character!\n", *(int*)(exid));
        free(string);
        goto exhandler;
    }
    g_r = personality;

    tostr_internal(string, a);
    return string;

exhandler:
    return NULL;
}

int main(int a, char** b)
{
    int i = 0;

    for(i = 0; i < 10000; i++)
    {
        int trythisbastard = i % 95;
        char* result = tostring(trythisbastard);
        if (result)
        {
            fprintf(stderr, "Number %d is %s\n", trythisbastard, result);
            free(result);
        }
    }

    return 0;
}

【讨论】:

  • 在 C++ 的早期,C++ 编译器 (cfront) 生成的 C 代码由 C 编译器编译为目标代码。异常处理是迫使该系统中断的功能之一。没有办法生成可以处理异常的 C 代码。有关详细信息,请参阅 Stroustrup 的“C++ 的设计和演变”(D&E)。
  • 只有一个全局变量g_r 来记录异常处理程序会遇到嵌套try 块的问题。您必须至少维护一堆异常处理程序。
  • @JonathanLeffler,我知道。这段代码只是一个小实验,我一直在尝试。 dlopen self 并通过符号名称获取内部函数的地址...说实话,我认为全局堆栈也不是一个好的选择。如果我们必须将指针存储在某处,最快的代码可能是堆栈,所以一个函数参数(访问堆栈比访问全局数据更快,我认为在 x86_64 上,参数将使用寄存器传递,而不是堆栈内存)。
  • @JonathanLeffler 通过转译为 C 来实现 C++ 异常真的完全不可能还是不切实际?如果这在某种程度上完全不可能,我很惊讶,我想就此提出一个问题。
  • @Praxeolitic:阅读前面提到的 D&E。我的理解是基于 Stroustrup 所说的。一个真正可靠的异常处理系统被认为是“不可行的”。也许人们已经制定了不同的方法,现在是可能的。但我相信它最终需要无法用 C 编写的汇编级代码。
猜你喜欢
  • 2011-06-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-12-17
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多