Why does my program not check the value of a bitfield member even though there is an "if" statement?

Viewed 92

I wrote this program as a test case for the behavior of bit field member comparisons in C++ (I suppose the same behavior would be exhibited in C as well):

#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");
}

After I compile and run it with 1 as its input, I get the following output:

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

One would expect that the foo.baz == 1 check would be evaluated as true since baz is always the least significant bit in the anonymous bit field. However, the opposite seems to happen, as can be seen from the program output (which is, somewhat comfortingly, consistently the same across each program invocation).

Even more weird to me is the fact that the generated AMD64 assembly code for the program (using the GCC 10.2 compiler) does not contain even a single comparison or jump instruction!

.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

It seems that the C++ code for the if statement somehow gets optimized out (or something like that), even though I compiled the program with the default settings (i.e. I did not turn on any level of optimization or anything like that).

Interestingly enough, Clang 10.0.1 (when run without optimizations) seems to generate code with a cmp instruction (as well as a jne and a jmp one):

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"

Both of the printf strings also seem to be present in the data segment (unlike in the GCC case when only the second one is present). I cannot tell for sure (because I'm not very proficient in assembly) but this seems to be properly generated code (unlike the one which GCC generates).

However, as soon as I try compile with any kind of optimizations (even -O1) using Clang, the comparisons/jumps are gone (as well as the foo.baz == 1 string), and the generated code seems to be very similar to the one which GCC generates:

(with -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.)

I'm totally perplexed by this kind of unintuitive behavior. The only thing which comes to mind as an explanation is the interaction of some weird undefined behavior of bitfields containing signed integral types and unions. What makes me think so is that after I replace the signed integer types with their unsigned counterparts, the output of the program becomes exactly as one would expect (with 1 as input):

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

Naturally, the program crashing because of a stack smashing (just like before) is something which is not supposed to happen, which leads to my second question: why does this occur?

Here's the modified program:

#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
2 Answers

The stack smashing has nothing to do with member access.

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

The %d format conversion specifier is for an int. Which is, typically, 4 bytes. But your bar is:

int8_t bar;

just one byte.

So, scanf ends up writing a 4 bytes worth of an int value into a one byte bar, and clobbering three additional bytes in the immediate vicinity.

There's your stack smash.

The answer is trivial.

your baz struct member is 1 bit long and it is signed. So it will never be 1. The only possibe values are 0 and -1.

Compiler knows that so the condition foo.baz == 1 will never be the truth. No conditional code has to be generated.

So I afraid it is not the compiler bug, only the programmer bug :)

So if we change the code to:

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

Compiler starts to generate the conditional instructions.

https://godbolt.org/z/fzKMo5

BTW your endianess check does not make any sense here as endianess defines the byte order not the bit order

Not related to the code generation problem is use of the wrong scanf conversion specifier.

Related