cgcc

Why used printf the same variable addr twice result in different print values?


Environnment:

The conditions under which this occurs are:

Build command:

gcc floatTest.c -o floatTest -std=c99(arg c17 c11 c99 the result same as)

Code (also available on Godbolt):

#include <stdio.h>

int main()
{
    float test = 5.0;
    printf("first print   test data is %x test %p\n", test, &test);
     //fflush(stdout);
    printf("second print  test data is %x test %p\n", test, &test);
}

Output:

first print   test data is 37df33f4 test 0x7ffd37df3508
second print  test data is 37df33f4 test (nil)

I tried printing an integer type, and none of the mentioned issues occurred. The problem only arises when printing a floating-point type, and when the three conditions mentioned above are met, abnormal values are printed. I also tried using different compiler versions, but the results were consistently abnormal. Switching between different C standards yielded the same result. However, note that when I compiled and tested using Visual Studio 2022, the issue did not occur.


Solution

  • Why used printf the same variable addr twice result in different print values?

    From X86-64 ABI (that I guess that you are using) https://refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf section 3.2.3 Parameter passing we know that:

    Pointer types and int types use the same class. From page 20 of the document the class INTEGER types use the next available register of the sequence %rdi, %rsi, %rdx, %rcx, %r8 and %r9 is used and SSE class use different registers.

            movss   xmm0, dword ptr [rbp - 4]
            cvtss2sd        xmm0, xmm0
            lea     rdi, [rip + .L.str.2]
            lea     rsi, [rbp - 4]
            /* note - rdx not assigned */
            mov     al, 1
            call    printf@PLT
    

    The format string passed to printf is a pointer. When printf starts, it reads the format string from %rdi register that points to the format string.

    Then to output the %x printf reads the content of the next register in INTEGER class %rsi and prints it formatted as an hex. The content of this register is populated with the next argument in INTEGER class, so the first printed value 37df33f4 is actually the address of &test reinterpreted as an int. You can confirm this by adding for example printf("%x\n", (int)(void*)&test); to your code.

    Then the %p format specifier makes printf read a value from the next INTEGER class register %rdx. That register is not touched before the call to printf, so %rdx contains any garbage value that was there before the call to printf.

    On the beginning of your program, the %rdx register happens to contain the value 0x7ffd37df3508. That value most probably comes from the 3rd argument to main. On POSIX systems, the C standard library calls the main with 3 arguments main(argv, argc, environ), and all these arguments happen to be in INTEGER class. The address to environ is placed in the %rdx register, and your main code does not override it, so the first printf call happens to print it. You can confirm it by printing extern char **environ; printf("%p\n", (void*)environ); and comparing the result.

    After the call to printf, the %rdx register happens to contain the value 0. That value is assigned somewhere inside printf source code. You would have to go with a disassembler to get the exact location where it is assigned.

    Note that this answer is solely very specific to X86-64 ABI. Also the compiler can detect that the code is invalid and decide to spawn nasal demons instead of producing any sane output.

    #include <stdio.h>
    extern char **environ;
    int main() {
        float test = 5.0;
        printf("first print   test data is %x test %p\n", test, (void*)&test);
        printf("second print  test data is %x test %p\n", test, (void*)&test);
        printf("&test = %x\n", (int)(void*)&test);
        printf("environ = %p\n", (void*)environ);
    }
    
    first print   test data is 978a72ec test 0x7ffd978a7418
    second print  test data is 978a72ec test (nil)
    &test = 978a72ec
    environ = 0x7ffd978a7418
    

    why this issue does not occur if the buffer is not flushed

    Because fflush happens to assign some different value to %rdx register. You would have to go through a disassembler or debugger to find the location where this register happens to be assigned.