1

我一直在尝试了解什么是跳转表,但我无法理解某些内容。从我看到的许多示例中,它们似乎几乎可以归结为这一点,或者至少这是它的一个版本:

void func1() {};
void func2() {};
void func3() {};

int main()
{
    void(*jumpTo[3])(void) = { func1, func2, func3 };
    jumpTo[1]();

    return 0;
}

这些似乎只是一个函数指针数组,由某个值/位置索引。那么跳转表是否只是索引函数指针数组?我对此非常好奇,因为我看到很多人说 switch 语句经常被编译到跳转表中作为性能指标。据我了解,通过以这种方式跳转到函数,它涉及指针取消引用和函数调用。我认为这两个对性能都不是很好。

该站点上的另一个答案说,通过这种方式“您正在添加一个 switch 语句不一定具有的函数调用开销。” 编译为跳转表的开关如何避免函数调用?

此外,这里有一个高度投票的答案说“跳转表可以是指向函数的指针数组或机器代码跳转指令数组。” 您将如何跳转到机器代码指令而不是取消引用指针?这更快吗?

两者之间的区别是在我上面的示例中,指针不必被取消引用,因为它可以被静态绑定?与在运行时传入随机数作为索引相反?

谢谢。

4

3 回答 3

3

跳转表和你的函数表基本上是一样的——一个地址数组。跳转表包含goto- 目标的地址。两者之间的唯一区别是跳跃的方式。当一个函数被调用时,返回地址被压入堆栈,所以当函数终止时它可以返回。

这里是一个跳转表的例子:

#include <stdio.h>
int main(int argc, char *argv[])
{
    switch (argc)
    {
        case 1: 
             printf("You provided no arguments.");
             break;
        case 2: 
             printf("You provided one argument.");
             break;
        case 3: 
             printf("You provided two arguments.");
             break;
        case 4: 
             printf("You provided three arguments.");
             break;
        case 5: 
             printf("You provided four arguments.");
             break;
        case 6: 
             printf("You provided five arguments.");
             break;
        default: 
             printf("You provided %d arguments.", argc-1);
             break;
    }
    return 0;
}

这编译为:

    cmp edi, 6 ;Bounds check
    ja  .L2    ;jump to default branch
    mov eax, edi
    jmp [QWORD PTR .L4[0+rax*8]]
.L4:
    .quad   .L2 ;case 0 (same as default!!!)
    .quad   .L3 ;case 1
    .quad   .L5 ;case 2
    .quad   .L6 ;case 3
    .quad   .L7 ;case 4
    .quad   .L8 ;case 5
    .quad   .L9 ;case 6
于 2017-07-22T12:56:16.820 回答
1

通常,术语跳转表是指一种技术,其中有两个以上的分支/跳转目标,并且跳转/分支目标是通过计算表中的位置以一种或另一种方式由变量选择的。本质上,您提供的示例:

void(*jumpTo[3])(void) = { func1, func2, func3 };
jumpTo[1]();

整体来说就是使用了跳转表——不仅仅是函数指针的解引用。

C 也提供了其他机制——例如 switch-case 经常被编译成一个跳转表,特别是如果 case 值的范围很窄,并且它们之间的差距很小。GCC 作为非标准扩展提供的另一种机制是使用goto标签作为带计算的指针值goto

于 2017-07-22T13:35:34.263 回答
1

主要区别在于,对于跳转表,您通常可以使用相对于程序计数器的寻址,以便表不需要任何重定位并且可以存在于.text节(或其他不可写和共享的节)中。这是因为典型的跳转表仅在同一目标文件中的极少数地方使用,并且所有偏移量对于汇编器都是已知的。

如果你有一个函数指针数组,那么你需要以某种方式产生真正的指针,这需要某种形式的重定位。

第二种可能性,即跳转指令数组,并不仅限于跳转指令本身。重要的部分是所有目标指令序列(除了最后一个)长度相同,因此可以轻松计算跳转到的偏移量。这样,不需要跳转表,但它确实需要准确的指令宽度(和计数)信息,这在大多数目标上很难保证(RISC 架构在加载常量时可能具有难以预测的有效指令计数)。这意味着在实践中,这种方法仅限于针对目标的非常特定形式的跳转指令。

于 2017-07-22T13:01:04.870 回答