skip to Main Content

Environnment:

  • gcc version 9.4.0 (Ubuntu 9.4.0-1ubuntu1~20.04.2))

The conditions under which this occurs are:

  • The printed variable is of float type.
  • There are type conversion errors in the printf.
  • The buffer is refreshed before the printf call.

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 %pn", test, &test);
     //fflush(stdout);
    printf("second print  test data is %x test %pn", 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.

3

Answers


  1. There is no particular reason why two identical, consecutive function calls with the same undefined behavior (function call argument types do not match the expected function parameter types after default argument promotions for variadic arguments of printf) should produce the same results.

    For example, in some ABI (application binary interface) definitions, some function call arguments are passed in registers, with floating point arguments passed to the called function in completely different registers than integer arguments, so the printf call will be printing a junk register value.

    One way to print a float in hexadecimal as though it was an integer, is to use a union type containing a float member and an unsigned integer type of the same size as the float. Let us assume that uint32_t is the same size as a float, then:

    #include <stdio.h>
    #include <inttypes.h>
    
    int main()
    {
        union
        {
            float f;
            uint32_t u32;
        } test;
        test.f = 5.0;
        printf("first print   test data is %"PRIx32" test %pn", test.u32, (void *)&test);
         //fflush(stdout);
        printf("second print  test data is %"PRIx32" test %pn", test.u32, (void *)&test);
    }
    

    Example output:

    first print   test data is 40a00000 test 0x7ffec4f1683c
    second print  test data is 40a00000 test 0x7ffec4f1683c
    
    Login or Signup to reply.
  2. 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:

    • arguments of type float and double is in SSE class
    • arguments of pointer type are in INTEGER class
    • arguments of int type, that %x expects, are also in INTEGER class

    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("%xn", (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("%pn", (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.

    int main()
    {
        float test = 5.0;
        printf("first print   test data is %x test %pn", test, (void*)&test);
        printf("second print  test data is %x test %pn", test, (void*)&test);
        printf("&test = %xn", (int)(void*)&test);
        printf("environ = %pn", (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.

    Login or Signup to reply.
  3. You should verify with a debugger that the address of variable test actually matches the output from printf().

    In the disassembly, you can see 2 arguments being passed in registers (RDI and RSI), and one in xmm0. The compiler also generates mov eax, 1 instruction before calling printf(), in order to indicate to it the number of passed floating point arguments, which is 1. @KamilCuk’s answer explains this.

    The first argument is your format string, which is passed in register RDI.
    The second argument is the float variable test, which is passed in register xmm0, however, since you use an incorrect format specifier, printf() will grab this argument from register RSI.
    The third argument, which is the address that "changes", is actually supposed to be passed in register RSI, however, your format specifiers will cause printf() to load this argument from register RDX.

    If you look at the disassembly, there are no modifications done to the register RDX, meaning the address you see in the first printf() output is most likely not the address of your floating point variable test, but some other value left in this volatile register. This register is then most likely zeroed out in the first call to printf() (or other function calls, like fflush()), which is why you see the (nil) in your second output.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search