2

I wrote some c code to play around with float values in memory, but ended up getting some unexpected output from printf compiling with "gcc (GCC) 12.1.1 20220730" with -std=c11 option.

I have no idea why it's behaving like this and would like to know what's happening, if I'm doing something wrong and how do I get it to printf a float value as hex if it's possible without converting it to another type first?

Here is the code used and output of different runs.

Code:

#include <stdio.h>

int main()
{
    float f3 = 1.1;
    float f4 = 1.0;
    unsigned char *t1 = &f3;

    unsigned *t2 = &f3;

    printf("P1: %x\n", t2[0]);
    printf("P2: %x %x %x %x\n", t1[0], t1[1], t1[2], t1[3]);
    printf("P3: %p\n", &f3);
    printf("P4: %lx, %lx, %llx\n", &f3, f3, f4);

    printf("T1: %f, %f, %lx, %lx\n", f3, f4, f4, f3);
    printf("T2: %x, %lx\n", f4, f3);
    printf("T3: %x, %lx\n", f3, f4);
    
    return 0;
}

The main problem seems to be with printing a float as hex:

printf("%x\n", f3);

Output 1:

P1: 3f8ccccd                            // as expected
P2: cd cc 8c 3f                         // as expected
P3: 0x7ffc667d4d40                      // as expected
P4: 7ffc667d4d40, 3ff19999a0000000, 0   // pointer value as expected, but second and third isn't. Values stay the same after each run

T1: 1.100000, 1.000000, 5556db09b2a0, 0 // first two values as expected, second and third isn't, Values do not stay the same after each run
T2: db09b2a0, 0                         // this value keeps changing after each run
T3: db09b2a0, 0                         // same as above, but should be different? Also changes after each run.

Output 2:

P1: 3f8ccccd
P2: cd cc 8c 3f
P3: 0x7ffef87ebb00
P4: 7ffef87ebb00, 3ff19999a0000000, 0

T1: 1.100000, 1.000000, 55fb6a1962a0, 0
T2: 6a1962a0, 0
T3: 6a1962a0, 0

Output 3:

P1: 3f8ccccd
P2: cd cc 8c 3f
P3: 0x7ffdb2026640
P4: 7ffdb2026640, 3ff19999a0000000, 0

T1: 1.100000, 1.000000, 564dd210c2a0, 0
T2: d210c2a0, 0
T3: d210c2a0, 0
Clifford
  • 88,407
  • 13
  • 85
  • 165
AnonZX
  • 31
  • 3
  • 4
    With `unsigned *t2 = &f3;` you break *strict aliasing*. Dereferencing `t2` leads to *undefined behavior*. – Some programmer dude Aug 14 '22 at 12:51
  • 3
    Technically you have undefined behavior with e.g. `printf("P3: %p\n", &f3);` as well. The `%p` format expects a `void *` pointer. Mismatching format specifier and argument type leads to UB. For pointers on a "normal" modern PC-like system it usually works though, but to be correct you need to cast. – Some programmer dude Aug 14 '22 at 12:53
  • 4
    What's worse is e.g. `printf("T2: %x, %lx\n", f4, f3);`. Here you have very bad UB when you treat `double` values (the `float` values are *promoted* to `double`) as `unsigned int` and `unsigned long` respectively. – Some programmer dude Aug 14 '22 at 12:56
  • 2
    Does this answer your question? [how to print float as hex bytes format in C?](https://stackoverflow.com/questions/45228925/how-to-print-float-as-hex-bytes-format-in-c) – Dan Getz Aug 14 '22 at 13:23
  • FYI, regarding "strict aliasing" and the GCC optimizer: https://www.geeksforgeeks.org/strict-aliasing-rule-in-c-with-examples/ – paulsm4 Aug 14 '22 at 15:20
  • A note to myself and anyone else wondering why the values keep changing. Looking at the assembly it seems like when calling `printf("%x\n", f1)` it reads the value from either a normal register or the stack, depending on number of arguments, while when calling `printf("%f\n", f1)` it reads the value from the floating point registers. So the value read will be from one of the normal registers or stack, which can be any value since no value was assigned to them and therefore the value can change depending on the current value in the register. This also explains the correct output of P1. – AnonZX Aug 15 '22 at 18:42
  • Using the assumption and knowledge of stack vs floating point register allocation for passing function arguments, it is possible to get the correct output form `printf("%x\n", f1)`, by passing more values than there are floating point registers to printf, to force passing the argument by normal register or stack. Testing shows some unreliability, but it does follow expectations of printing the correct hex value of the float. Results will probably vary between architectures and compilers. – AnonZX Aug 16 '22 at 13:08

2 Answers2

1

The main problem seems to be with printing a float as hex:

printf("%x\n", f3);

Yes, because the %x format specifier expects an unsigned int as an argument, but you're passing in float which is being promoted to a double.

Using the wrong format specifier triggers undefined behavior, which in this case manifests as strange output. As to what's happening under the hood, floating point values are typically passed to a function using floating point registers while integer values are typically pushed onto the stack.

This is also invalid:

printf("P1: %x\n", t2[0]);

As it causes a strict aliasing violation. This basically means you can't access the bytes of one type as if it were another type, unless the destination type is char or unsigned char.

The proper way to print the byte representation of a floating point type is to have an unsigned char * point to the first byte, then loop through the bytes and print each one.

dbush
  • 205,898
  • 23
  • 218
  • 273
  • I think I understand why it's happening after looking at the assembly, but I'm actually looking for a way to directly get the hex value of a float without converting it every time if it's possible. Also just wondering but is the way I'm converting the float to an unsigned char* and printing that incorrect? – AnonZX Aug 14 '22 at 13:47
  • 1
    @AnonZX Using an `unsigned char *` to print the bytes is fine. Anything else is undefined behavior. – dbush Aug 14 '22 at 14:01
0

In all the cases of "unexpected output", you are passing a parameter whose type does not match the format specifier. You are telling printf() to expect one thing, but passing another. That is always undefined behaviour, and it is largely pointless to speculate how a specific output came about. Moreover passing a float to printf() promotes it to a double - so the representation you are trying to inspect will have changed from that of the original float variable.
Also printf() is dealing with these types at runtime - the compiler's type checking cannot help you here, it is printf()'s rules that apply, not the compiler's - it is a variadic function, and as far as the compiler is concerned may take any number of arguments of any type. That said many compilers will check stdio format specifiers since they are a well defined part of the standard library - you may need to enable specific warnings or higher warning level to enable that checking in your compiler.

Generally to inspect the bits that represent the floating point values, you need to take the address of the float, cast that address to an integer pointer of the same width, then de-reference it.
However strict aliasing rules make that (stricty) undefined, so while *(uint32_t*)&f1 will most likely yield the expected integer value, you cannot rely upon it.

A well-defined solution is to access the bytes as unsigned char, or to copy the bytes (for example using memcpy()) to an integer object of the same size.

For example:

#include <stdio.h>
#include <inttypes.h>
#include <stdint.h>
#include <string.h>

int main()
{
    float f1 = 1.0f ;
    float f2 = 1.1f ;
    uint32_t b1 = 0u  ;
    uint32_t b2 = 0u  ;
    
    memcpy( &b1, &f1, sizeof(b1) ) ;
    memcpy( &b2, &f2, sizeof(b2) ) ;
        
    printf( "f1: %f @%p = %"PRIx32" (%02x %02x %02x %02x)\n", 
            f1, &f1, b1,
            ((unsigned char*)&f1)[0],
            ((unsigned char*)&f1)[1],
            ((unsigned char*)&f1)[2],
            ((unsigned char*)&f1)[3] ) ;
                                                                
    printf( "f2: %f @%p = %"PRIx32" (%02x %02x %02x %02x)\n", 
            f2, &f2, b2,
            ((unsigned char*)&f2)[0],
            ((unsigned char*)&f2)[1],
            ((unsigned char*)&f2)[2],
            ((unsigned char*)&f2)[3] ) ;
            
    return 0;
}

outputs:

f1: 1.000000 @0x7fffe3e06668 = 3f800000 (00 00 80 3f)
f2: 1.100000 @0x7fffe3e0666c = 3f8ccccd (cd cc 8c 3f)

(clearly the addresses will vary).

You can also achieve type-punning through a union:

typedef union 
{
    float f ;
    uint32_t u ;

} uFloatIntPun ;

Then you can for example:

uFloatIntPun pun ;
pun.f = f1 ;
printf( "%"PRIx32"\n", pun.u ) ;

Of course if all you intend is to inspect the location and internal representation and byte order of specific variables, it is far simpler and less error prone to observe them in a symbolic debugger.

Clifford
  • 88,407
  • 13
  • 85
  • 165