GCC“标签作为值”——预期用途

问题描述 投票:0回答:1

我最近听说 GCC 和其他编译器中对 C/C++ 的 Labels as Values 扩展。我正在考虑如何使用它来编写线程解释器(其中虚拟机程序是通过将真实机器代码块的地址链接在一起来定义的)。

当然,我只能在定义标签的函数内部使用

goto
标签,但我仍然想在该范围之外使用标签 values;例如,当我编译或存储虚拟机代码而不是执行它时。我把口译员想象成这样:

#include <stdio.h>

void interpret (void **prog) {
    void **pc = prog;
    goto **pc;
    foo: printf("Foo"); goto nextword;
    bar: printf("Bar"); goto nextword;
    nextword:
        if (*(++pc) != STOP) {
            printf(" ");
            goto **pc;
        } else printf (".\n");
    stop: return;
}

// definition of VM instructions:
void *FOO=&&foo, *BAR=&&bar, *STOP=&&stop;

int main (int argc, char **argv) {
    // a real interpreter would generate this from source code:
    void *prog[] = {FOO, BAR, FOO, BAR, BAR, FOO, STOP};
    interpret(prog);
    return 0;
}

即,我希望标签地址作为全局常量。但上面的示例无法编译,因为扩展不允许在函数体之外使用标签作为值,而且 C 中的标签无论如何都有函数作用域。

我可以使用特殊的“设置”模式来制作

interpret()
功能,例如如果我通过
NULL
,它会初始化全局变量并立即返回。我想我可以忍受这一点,但这似乎很糟糕。

我见过的几个示例将标签地址存储在函数内的静态数组中,然后将程序作为整数索引列表传递到该数组中。但这会在运行时为每个 VM 指令添加额外的获取操作;这不是与此扩展的全部目的背道而驰吗?

我有什么遗漏的吗?或者,如果这是一个功能请求,是否有原因导致像我的示例这样的东西无法实现?

c++ c gcc clang goto
1个回答
0
投票

由于内联,实现起来会很困难。

如果在静态初始化器中获取标签的地址,GCC 会完全禁用该函数的内联。这是因为当函数内联时,代码的位置发生了物理变化,因此静态变量的值会出现错误,例如:

void f() {
    goto *&&x;
    x: return;
}

void g() {
    f();  // If f is inlined: `&&x` would be somewhere in `g()`
}

void h() {
    f();  // `&&x` if `f()` is inlined here would be different
}

但是如果你允许它在函数的静态初始化器外部中使用,你永远不知道该地址是否被引入另一个翻译单元:

// TU 1
inline int f(void* p) {
    goto *p;
    a: return 1;
    b: return 2;
}

int h(void* p) { return f(p); }  // f is inlined into h

// TU 2
inline int f(void* p) {
    goto *p;
    a: return 1;
    b: return 2;
}
static void* A = &&a;
int h(void* p);
int g() {
    h(A);  // This would be the address inside `f`, which doesn't exist in TU1 and not an expected value of the inlined function.
}

在 C 中这甚至更加困难,因为内联函数在 C 中的工作方式。我确信 GCC 也做了一系列可达性优化,因此允许这样做意味着它必须假设所有具有未知值的计算 goto 可以转到任何标签,因此它根本无法优化任何标签。


如果切换到带索引的计算转到:

enum instruction {
    FOO, BAR, STOP
};

void interpret_computed_goto(enum instruction *pc) {
    static void* indices[] = { &&foo, &&bar, &&stop };
    goto *indices[*pc];
    foo: printf("Foo"); goto nextword;
    bar: printf("Bar"); goto nextword;
    nextword:
        if (*(++pc) != STOP) {
            printf(" ");
            goto *indices[*pc];
        } else printf (".\n");
    stop: return;
}

int main (int argc, char **argv) {
    enum instruction prog[] = {FOO, BAR, FOO, BAR, BAR, FOO, STOP};
    interpret_computed_goto(prog);
}

...这通常可以很容易地转换成

switch
:

void interpret_switch_direct(enum instruction *pc) {
    // goto *indices[*pc] -> switch (pc)
    start:
    switch (*pc) {
    // labels -> cases
    case FOO: printf("Foo"); goto nextword;
    case BAR: printf("Bar"); goto nextword;
    nextword:
        if (*(++pc) != STOP) {
            printf(" ");
            // Second computed goto: reroute to original switch
            goto start;
        } else printf (".\n");
    case STOP: return;
    }
}

这也许可以机械地完成,但是从头开始重写它会变得更具可读性:

void interpret_switch(enum instruction *pc) {
    if (*pc == STOP) return;
    while (1) {
        switch (*pc++) {
        case FOO: printf("Foo"); break;
        case BAR: printf("Bar"); break;
        case STOP: printf (".\n"); return;
        }
        if (*pc != STOP) printf(" ");
    }
}

您会发现像这样的 switch-case 的实现就像有一个大的标签地址表,然后使用适当的地址计算出 goto。 (此示例不会发生这种情况,因为没有足够的案例,但如果您添加更多案例,您可以在生成的程序集中看到该表)。

担心表的索引可能是不成熟的优化。如果这确实成为一个问题,至少在 x86 上,

goto *p
return ((return_type(*)(void)) p)()
基本相同,因此您可能需要切换到函数而不是标签。

© www.soinside.com 2019 - 2024. All rights reserved.