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
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 aunion
type containing afloat
member and an unsigned integer type of the same size as thefloat
. Let us assume thatuint32_t
is the same size as afloat
, then:Example output:
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:
int
type, that%x
expects, are also in INTEGER classPointer types and
int
types use the same class. From page 20 of the document the class INTEGER types usethe next available register of the sequence %rdi, %rsi, %rdx, %rcx, %r8 and %r9 is used
and SSE class use different registers.The format string passed to
printf
is a pointer. Whenprintf
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 value37df33f4
is actually the address of&test
reinterpreted as anint
. You can confirm this by adding for exampleprintf("%xn", (int)(void*)&test);
to your code.Then the
%p
format specifier makesprintf
read a value from the next INTEGER class register%rdx
. That register is not touched before the call toprintf
, so%rdx
contains any garbage value that was there before the call toprintf
.On the beginning of your program, the
%rdx
register happens to contain the value0x7ffd37df3508
. That value most probably comes from the 3rd argument tomain
. On POSIX systems, the C standard library calls the main with 3 argumentsmain(argv, argc, environ)
, and all these arguments happen to be in INTEGER class. The address toenviron
is placed in the%rdx
register, and yourmain
code does not override it, so the firstprintf
call happens to print it. You can confirm it by printingextern 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 insideprintf
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.
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.You should verify with a debugger that the address of variable
test
actually matches the output fromprintf()
.In the disassembly, you can see 2 arguments being passed in registers (
RDI
andRSI
), and one inxmm0
. The compiler also generatesmov eax, 1
instruction before callingprintf()
, in order to indicate to it the number of passed floating point arguments, which is1
. @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 registerxmm0
, however, since you use an incorrect format specifier,printf()
will grab this argument from registerRSI
.The third argument, which is the address that "changes", is actually supposed to be passed in register
RSI
, however, your format specifiers will causeprintf()
to load this argument from registerRDX
.If you look at the disassembly, there are no modifications done to the register
RDX
, meaning the address you see in the firstprintf()
output is most likely not the address of your floating point variabletest
, but some other value left in this volatile register. This register is then most likely zeroed out in the first call toprintf()
(or other function calls, likefflush()
), which is why you see the(nil)
in your second output.