c++ - 为什么我的程序不检查位域成员的值,即使有 "if"语句?

标签 c++ c unions signed bit-fields

我编写这个程序作为 C++ 中位域成员比较行为的测试用例(我想在 C 中也会表现出相同的行为):

#include <cstdint>
#include <cstdio>

union Foo
{
    int8_t bar;
    struct
    {
#if __BYTE_ORDER == __LITTLE_ENDIAN
        int8_t baz : 1;
        int8_t quux : 7;
#elif __BYTE_ORDER == __BIG_ENDIAN
        int8_t quux : 7;
        int8_t baz : 1;
#endif
    };
};

int main()
{
    Foo foo;
    scanf("%d", &foo.bar);
    if (foo.baz == 1)
        printf("foo.baz == 1\n");
    else
        printf("foo.baz != 1\n");
}

在我以 1 作为输入编译并运行它之后,我得到以下输出:

foo.baz != 1
*** stack smashing detected ***: terminated
fish: “./a.out” terminated by signal SIGABRT (Abort)

人们会期望 foo.baz == 1 检查会被评估为真,因为 baz 始终是匿名位字段中的最低有效位。然而,相反的情况似乎发生了,从程序输出中可以看出(令人欣慰的是,在每次程序调用中始终相同)。

对我来说更奇怪的是,为程序生成的 AMD64 汇编代码(使用 GCC 10.2 编译器)包含甚至一个比较或跳转指令!

.LC0:
        .string "%d"
.LC1:
        .string "foo.baz != 1"
main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        lea     rax, [rbp-1]
        mov     rsi, rax
        mov     edi, OFFSET FLAT:.LC0
        mov     eax, 0
        call    scanf
        mov     edi, OFFSET FLAT:.LC1
        call    puts
        mov     eax, 0
        leave
        ret

似乎 if 语句的 C++ 代码不知何故 被优化掉了(或类似的东西),即使我用默认设置编译程序(即我没有打开任何级别的优化或类似的东西)。

有趣的是,Clang 10.0.1(在没有优化的情况下运行时)似乎生成代码带有 cmp 指令(以及 jne 和一个 jmp 一个):

main:                                   # @main
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     dword ptr [rbp - 4], 0
        lea     rax, [rbp - 8]
        movabs  rdi, offset .L.str
        mov     rsi, rax
        mov     al, 0
        call    scanf
        mov     cl, byte ptr [rbp - 8]
        shl     cl, 7
        sar     cl, 7
        movsx   edx, cl
        cmp     edx, 1
        jne     .LBB0_2
        movabs  rdi, offset .L.str.1
        mov     al, 0
        call    printf
        jmp     .LBB0_3
.LBB0_2:
        movabs  rdi, offset .L.str.2
        mov     al, 0
        call    printf
.LBB0_3:
        mov     eax, dword ptr [rbp - 4]
        add     rsp, 16
        pop     rbp
        ret
.L.str:
        .asciz  "%d"

.L.str.1:
        .asciz  "foo.baz == 1\n"

.L.str.2:
        .asciz  "foo.baz != 1\n"

这两个 printf 字符串似乎也出现在数据段中(与 GCC 中只有第二个出现的情况不同)。我不能确定(因为我不是很精通汇编)但这似乎是正确生成的代码(与 GCC 生成的代码不同)。

但是,一旦我尝试使用 Clang 进行任何类型的优化(甚至 -O1)编译,比较/跳转就消失了(以及 foo.baz == 1 字符串),生成的代码似乎与 GCC 生成的代码非常相似:

(使用-O1)

main:                                   # @main
        push    rax
        mov     rsi, rsp
        mov     edi, offset .L.str
        xor     eax, eax
        call    scanf
        mov     edi, offset .Lstr
        call    puts
        xor     eax, eax
        pop     rcx
        ret
.L.str:
        .asciz  "%d"

.Lstr:
        .asciz  "foo.baz != 1"

( You may want to check the generated assembly code by different compiler versions yourself using Compiler Explorer. )

我对这种不符合直觉的行为感到非常困惑。唯一想到的解释是包含有符号整数类型和 union 的位域的一些奇怪的未定义行为的交互。让我这么认为的是,在我用无符号整数类型替换有符号整数类型后,程序的输出完全符合预期(使用 1 作为输入):

foo.baz == 1
*** stack smashing detected ***: terminated
fish: “./a.out” terminated by signal SIGABRT (Abort)

自然地,程序因堆栈崩溃(就像以前一样)而崩溃是应该发生的事情,这引出了我的第二个问题:为什么会发生发生这种情况?

修改后的程序如下:

#include <cstdint>
#include <cstdio>

union Foo
{
    uint8_t bar;
    struct
    {
#if __BYTE_ORDER == __LITTLE_ENDIAN
        uint8_t baz : 1;
        uint8_t quux : 7;
#elif __BYTE_ORDER == __BIG_ENDIAN
        uint8_t quux : 7;
        uint8_t baz : 1;
#endif
    };
};

int main()
{
    Foo foo;
    scanf("%d", &foo.bar);
    if (foo.baz == 1)
        printf("foo.baz == 1\n");
    else
        printf("foo.baz != 1\n");
}

... and the generated assembly code by GCC :

.LC0:
        .string "%d"
.LC1:
        .string "foo.baz == 1"
.LC2:
        .string "foo.baz != 1"
main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        lea     rax, [rbp-1]
        mov     rsi, rax
        mov     edi, OFFSET FLAT:.LC0
        mov     eax, 0
        call    scanf
        movzx   eax, BYTE PTR [rbp-1]
        and     eax, 1
        test    al, al
        je      .L2
        mov     edi, OFFSET FLAT:.LC1
        call    puts
        jmp     .L3
.L2:
        mov     edi, OFFSET FLAT:.LC2
        call    puts
.L3:
        mov     eax, 0
        leave
        ret

最佳答案

堆栈粉碎与成员访问无关。

scanf("%d", &foo.bar);

%d 格式转换说明符用于 int。通常是 4 个字节。但是你的 bar 是:

int8_t bar;

只有 一个 字节。

因此,scanf 最终将一个 4 个字节的 int 值写入一个字节的 bar,并破坏了紧邻的三个额外字节。

这是你的堆栈粉碎。

关于c++ - 为什么我的程序不检查位域成员的值,即使有 "if"语句?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/63431731/

相关文章:

c++ - 为什么我不能使用 istream_view 和 std::accumulate 来总结我的输入?

C "Error: Invalid initializer"

c - 如何用语法理解指向数组的指针

recursion - F# - 在运行时创建递归可区分联合

c++ - 放置新的 STL 容器并在之后安全地销毁它

c++ - union 、类和结构在哪里使用?

c++ - 在 Boost MultiIndex 容器中移动 equal_range 时迭代器失败

c++ - 使用另一个类成员的类成员函数指针

c++ - 为什么默认情况下数据实例不为 NULL?

C++ 子模式匹配