Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to print a single-precision float with printf

I'm trying to print a floating point number in x86_64 assembly but it just prints the value as zero.

There's a few questions about this already. One appeared to be resolved by ensuring that you set the number of vector registers you're using in %al. Another showed that you need to have a stack alignment of 16 bytes. But, I'm doing both those things and still not getting correct output.

This is my program:

# prints a floating point value
.section .rodata
.fmt: .string "num: %f\n"
.num: .float 123.4

.section .text
.global main
.type   main, @function
main:
  subq $8, %rsp     # 16-byte alignment

  # print my number
  movss .num, %xmm0 # load float value
  movq $.fmt, %rdi  # load format string
  movb $1, %al      # use 1 vector register
  call printf

  # exit
  addq $8, %rsp     # undo alignment
  movq $0, %rax     # return 0
  ret
like image 439
cgmb Avatar asked May 06 '16 23:05

cgmb


1 Answers

printf(3)'s %f format specifier wants a double.
There is no way to get printf to accept a float, only double or long double.


C's default argument promotions specify that calls to variadic functions like foo(char *fmt, ...) promote float to double, and perform the usual integer promotions of narrow integer types to int, for trailing args that match the ... part of the prototype. (The same applies to all args for calling functions with no prototype.) N1570 6.5.2.2 Function calls, subsections 6 and 7.

Thus C provides no way for a caller to pass a float to printf, so it has no conversion for it. %f means double. %lf also works for double in modern printf implementations, C99/C11 and C++11. You can safely use the same %lf format string with a double for printf and scanf.

Note that scanf is different. float * and double * aren't affected by those promotions, so you can actually scan into a float with %f.


Load with CVTSS2SD .num(%rip), %xmm0

If you look at compiler output, you'll see gcc do everything you did. It uses RIP-relative addressing for static storage as usual.

GCC also uses pxor to zero the register first to break the false dependency on the old value of %xmm0. (cvtss2sd's poor design leaves the upper 64 bits of the destination unchanged.) GCC errs on the side of caution, and inserts xor-zeroing instructions to break false dependencies in many cases.


You're probably getting 0 because the upper bits of xmm0 happen to be zero. When printf looks at the low 64 bits of xmm0 as a double (IEEE binary64 on x86), it finds the bit pattern for 123.4f in the low 32 bits of the mantissa, and the rest zero. As a 64-bit double, this bit-pattern represents a very small (subnormal) number, so it comes out as zero with %f.

You can try the equivalent with a float, (e.g. on http://www.h-schmidt.net/FloatConverter/IEEE754.html), setting some bits in the low half to see what you get.

If you used %g (scientific notation) or %a (hex representation of the double bit-pattern), the non-zero bits would show up. (Unless maybe if you had Denormals Are Zero mode enabled in the MXCSR, although glibc might use purely integer stuff to pick apart FP bit-patterns when converting to base-10 strings; it's a hard problem.)

like image 115
Peter Cordes Avatar answered Nov 15 '22 10:11

Peter Cordes