c++ - 谜:将GNU C标签指针转换为函数指针,并使用内联asm将ret放入该 block 中。 block 被优化掉了吗?

标签 c++ c gcc clang inline-assembly

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

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

#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之后的代码是将5分配给c的值,然后进行两行汇编以将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,其中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进行比较的代码。

另一方面,带有O0的clang会产生以下噩梦,这种噩梦会崩溃:
# 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部分),不幸的是,决定在rsp之前增加ret是一个好主意,以便确保我们在需要的地方完全不同成为。

c发出的东西甚至更可疑:
# 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? c到底是怎么结束的呢?

从某种程度上说,我理解编译器认为不需要在if之后将ifelse都移到同一位置的死代码,但是在这里我的知识和见识就停止了。

所以现在,亲爱的C和C++专家,汇编迷和编译器粉碎者,这里出现了一个问题:

为什么?

您为什么认为如果我们添加了else分支,编译器便决定将这两个标签视为等效,或者为什么clang在其中添加了1,最后但并非最不重要:对C标准有深刻理解的人可能会指出这段代码与正常情况的偏差如此之大,以至于我们最终陷入了这种非常奇怪的情况。

最佳答案

someone with a deep understanding of the C standard could maybe point out where this piece of code deviated so badly from normality that we ended up in this really really weird situation.



您认为ISO C标准对此代码有什么要说的吗?到处都是UB和GNU扩展,尤其是指向本地标签的指针。

将标签指针转换为函数指针并对其进行调用显然是UB 。 GCC手册没有说您可以做到。在另一个函数中用goto标签也是UB。

您只能通过诱使编译器认为可能会到达该块以便不删除它,然后使用GNU C Basic asm语句在此处发出ret指令来使此工作生效。

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

还要注意,c=5中的hi:编译时完全禁用了优化(并且else goto bye注释了),类似于movl $5, -20(%rbp)的asm。即使用调用者的RBP修改调用者堆栈框架中的局部变量。因此,您有一个嵌套函数。

GNU C允许您define nested functions可以访问其父作用域的本地变量。 (如果您喜欢从实验中获得的asm,则您会喜欢GCC使用mov -immediate存储到堆栈的机器代码的可执行蹦床,如果您使用指向嵌套函数的指针!)
asm volatile ("movl $5, %eax");缺少EAX上的垃圾。如果正常到达此语句,则踩到编译器的脚趾,它将是UB,而不是好像它是一个单独的函数。

GNU C Basic asm的用例(无约束/无用)是诸如cli(禁用中断)之类的指令,没有涉及整数寄存器的任何内容,并且绝对不是ret

如果要使用内联asm定义可调用函数,则可以在全局范围内使用asm(""),也可以将其用作__attribute__((naked))函数的主体。

关于c++ - 谜:将GNU C标签指针转换为函数指针,并使用内联asm将ret放入该 block 中。 block 被优化掉了吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59103667/

相关文章:

c# - 无法在 C# 中使用 String.Format

c++ - Clang 跳过初始化列表构造的处理

c# - 是否有必要释放从 C# 接收到的 C++ 字符串的内存?

c++ - for_each 给出两个(或 n 个)相邻元素

c - 用 C 返回 "string"时的奇怪行为

c - 访问嵌套结构的元素

c++ - C 中的结构出现问题,处于不正确结果的边缘

c - C中语句的最大长度

c++ - 向类的每个方法添加不变检查

c++ - 如何获得堆栈排序的正确输出