繁体   English   中英

谜团:将 GNU C 标签指针转换为函数指针,使用内联 asm 在该块中放置一个 ret。 阻止被优化掉?

[英]Mystery: casting a GNU C label pointer to a function pointer, with inline asm to put a ret in that block. Block being optimized away?

首先:这段代码被认为是纯粹的乐趣,请不要在生产中做这样的事情。 在任何环境下编译和执行这段代码后,对您、您的公司或您的驯鹿造成的任何伤害,我们概不负责。 下面的代码不安全,不可移植,显然很危险。 被警告。 长帖如下。 你被警告了。

现在,在免责声明之后:让我们考虑以下代码:

#include <stdio.h>

int fun()
{
    return 5;
}

typedef int(*F)(void) ;

int main(int argc, char const *argv[])
{

    void *ptr = &&hi;

    F f = (F)ptr;

    int  c = f();
    printf("TT: %d\n", c);

    if(c == 5) goto bye;
    //else goto bye;     /*  <---- This is the most important line. Pay attention to it */

hi:
    c = 5;
    asm volatile ("movl $5, %eax");
    asm volatile ("retq");

bye:
    return 66;
}

一开始我们有我创建的函数fun纯粹是为了获取生成的汇编代码的参考。

然后我们声明一个函数指针F指向不带参数并返回 int 的函数。

然后我们使用不太知名的 GCC 扩展https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html来获取标签hi的地址,这也适用于 clang。 然后我们做了一些邪恶的事情,我们创建了一个名为 f 的函数指针F并将其初始化为上面的标签。

然后最糟糕的是,我们实际上调用了这个函数,并将它的返回值赋给了一个名为C的局部变量,然后我们将它打印出来。

下面是一个if检查分配给c的值是否确实是我们需要的值,如果是,则转到bye以便他的应用程序正常退出,退出代码为 66。如果这可以认为是正常的退出代码。

下一行被注释掉了,但我可以说这是整个应用程序中最重要的一行。

标签hi后面的hi代码就是给c的值赋值为5,然后两行汇编把eax的值初始化为5,并真正从“函数”调用中返回。 如前所述,有一个参考函数fun可以生成相同的代码。

现在我们编译这个应用程序,并在我们的在线平台上运行它: https : //gcc.godbolt.org/z/K6z5Yc

生成下面的组件(具有-O1导通,并且O0给出了类似的结果,虽然有点多更长):

# else goto bye  is COMMENTED OUT
fun:
        mov     eax, 5
        ret
.LC0:
        .string "TT: %d\n"
main:
        push    rbx
        mov     eax, OFFSET FLAT:.L3
        call    rax
        mov     ebx, eax
        mov     esi, eax
        mov     edi, OFFSET FLAT:.LC0
        mov     eax, 0
        call    printf
        cmp     ebx, 5
        je      .L4
.L3:
        movl $5, %eax
        retq
.L4:
        mov     eax, 66
        pop     rbx
        ret

重要的mov eax, OFFSET FLAT:.L3行是mov eax, OFFSET FLAT:.L3 ,其中L3对应于我们的hi标签,以及之后的行: call rax实际调用它。

并运行如下:

ASM generation compiler returned: 0
Execution build compiler returned: 0
Program returned: 66
    TT: 5

现在,让我们重新审视应用程序中最重要的一行并取消注释。

使用-O0我们得到以下程序集,由 gcc 生成:

# else goto bye  is UNCOMMENTED
# even gcc -O0  "knows" hi: is unreachable.
fun:
        push    rbp
        mov     rbp, rsp
        mov     eax, 5
        pop     rbp
        ret
.LC0:
        .string "TT: %d\n"
main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 48
        mov     DWORD PTR [rbp-36], edi
        mov     QWORD PTR [rbp-48], rsi
        mov     QWORD PTR [rbp-8], OFFSET FLAT:.L4
        mov     rax, QWORD PTR [rbp-8]
        mov     QWORD PTR [rbp-16], rax
        mov     rax, QWORD PTR [rbp-16]
        call    rax
        mov     DWORD PTR [rbp-20], eax
        mov     eax, DWORD PTR [rbp-20]
        mov     esi, eax
        mov     edi, OFFSET FLAT:.LC0
        mov     eax, 0
        call    printf
        cmp     DWORD PTR [rbp-20], 5
        nop
.L4:
        mov     eax, 66
        leave
        ret

和以下输出:

ASM generation compiler returned: 0
Execution build compiler returned: 0
Program returned: 66

因此,正如您所看到的,我们的printf从未被调用过,罪魁祸首是mov QWORD PTR [rbp-8], OFFSET FLAT:.L4 ,其中L4实际上对应于我们的bye标签。

而且从我从生成的程序集中看到的,不是在生成的代码中添加了hi之后的部分的一段代码。

但至少应用程序可以运行并且至少有一些代码可以将c与 5 进行比较。

另一方面,clang 和O0生成以下噩梦,顺便说一下崩溃了:

# else goto bye  is UNCOMMENTED
# clang -O0 also doesn't emit any instructions for the hi: block
fun:                                    # @fun
        push    rbp
        mov     rbp, rsp
        mov     eax, 5
        pop     rbp
        ret
main:                                   # @main
        push    rbp
        mov     rbp, rsp
        sub     rsp, 48
        mov     dword ptr [rbp - 4], 0
        mov     dword ptr [rbp - 8], edi
        mov     qword ptr [rbp - 16], rsi
        mov     qword ptr [rbp - 24], 1
        mov     rax, qword ptr [rbp - 24]
        mov     qword ptr [rbp - 32], rax
        call    qword ptr [rbp - 32]
        mov     dword ptr [rbp - 36], eax
        mov     esi, dword ptr [rbp - 36]
        movabs  rdi, offset .L.str
        mov     al, 0
        call    printf
        cmp     dword ptr [rbp - 36], 5
        jne     .LBB1_2
        jmp     .LBB1_3
.LBB1_2:
        jmp     .LBB1_3
.LBB1_3:
        mov     eax, 66
        add     rsp, 48
        pop     rbp
        ret
.L.str:
        .asciz  "TT: %d\n"

如果我们打开一些优化,例如O1 ,我们会从 gcc 得到:

# else goto bye  is UNCOMMENTED
# gcc -O1
fun:
        mov     eax, 5
        ret
.LC0:
        .string "TT: %d\n"
main:
        sub     rsp, 8
        mov     eax, OFFSET FLAT:.L3
        call    rax
        mov     esi, eax
        mov     edi, OFFSET FLAT:.LC0
        mov     eax, 0
        call    printf
.L3:
        mov     eax, 66
        add     rsp, 8
        ret

并且应用程序崩溃,这是可以理解的。 同样,编译器完全删除了我们的hi部分( mov eax, OFFSET FLAT:.L3脚尖到L3对应于我们的bye部分),不幸的是,在ret之前增加rsp是个好主意,以确保我们结束在我们需要去的完全不同的地方。

clang 提供了一些更可疑的东西:

# else goto bye  is UNCOMMENTED
# clang -O1
fun:                                    # @fun
        mov     eax, 5
        ret
main:                                   # @main
        push    rax
        mov     eax, 1
        call    rax
        mov     edi, offset .L.str
        mov     esi, eax
        xor     eax, eax
        call    printf
        mov     eax, 66
        pop     rcx
        ret
.L.str:
        .asciz  "TT: %d\n"

1 ? 铿锵到底是怎么做到的?

到一定程度我明白,编译器决定,死码后if其中两个ifelse去到相同的位置是不需要的,但在这里,我的知识和洞察力停止。

所以现在,亲爱的 C 和 C++ 大师、汇编爱好者和编译器粉碎者,问题来了:

为什么?

如果我们添加了else分支,你认为编译器为什么认为这两个标签应该被认为是等效的,或者为什么 clang 把 1 放在那里,最后但并非最不重要的:对 C 标准有深刻理解的人可能会指出这段代码严重偏离正常的地方,以至于我们最终陷入了这种非常奇怪的情况。

对 C 标准有深刻理解的人可能会指出这段代码哪里偏离正常情况如此严重,以至于我们最终陷入了这种非常奇怪的情况。

您认为 ISO C 标准对此代码有什么要说的吗? 它充满了 UB 和 GNU 扩展,特别是指向本地标签的指针。

将标签指针转换为函数指针并通过它调用显然是 UB GCC 手册没有说你可以这样做。 在另一个函数中goto标签也是 UB。

您只能通过诱使编译器认为该块可能已到达所以它不会被删除,然后使用 GNU C Basic asm语句在那里发出ret指令来完成这项工作。

即使禁用优化,GCC 和 clang 也会删除死代码; 例如if(0) { ... }不发出任何指令来实现...

还要注意, hi:中的c=5编译时优化完全禁用( else goto bye注释)到 asm 就像movl $5, -20(%rbp) 即使用调用者的 RBP 来修改调用者堆栈帧中的局部变量。 所以你有一个嵌套函数。

GNU C 允许您定义可以访问其父作用域的局部变量的嵌套函数 (如果你喜欢你从实验中得到的 asm,你会喜欢 GCC 使用mov -immediate 存储到堆栈的机器代码的可执行蹦床,如果你使用指向嵌套函数的指针!)


asm volatile ("movl $5, %eax"); 在 EAX 上缺少一个破坏者。 你踩到了编译器的脚趾,如果这个语句被正常到达,那将是 UB,而不是好像它是一个单独的函数。

GNU C Basic asm(无约束/clobbers)的用例是cli (禁用中断)之类的指令,不涉及整数寄存器,绝对不是ret

如果您想使用内联 asm 定义可调用函数,您可以在全局范围内使用asm("") ,或者作为__attribute__((naked))函数的主体。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM