Floats are 32 bits in size, they are passed naturally on the stack.
Doubles are 64 bits in size, they are passed, on the stack, respecting the Little Endian convention1,
pushing first the upper 32 bits and than the lower ones.
//C prototype of callee
double foo(double a, float b);
foo(3.1457, 0.241);
;Assembly call
;3.1457 is 0x40092A64C2F837B5ULL
;0.241 is 0x3e76c8b4
push DWORD 3e76c8b4h ;b, is 32 bits, nothing special here
push DWORD 0c2f837b5h ;a, is 64 bits, Higher part of 3.1457
push DWORD 40092a64h ;a, is 64 bits, Lower part of 3.1457
call foo
add esp, 0ch
;Call, using the FPU
;ST(0) = a, ST(1) = b
sub esp, 0ch
fstp QWORD PTR [esp] ;Storing a as a QWORD on the stack
fstp DWORD PTR [esp+08h] ;Storing b as a DWORD on the stack
call foo
add esp, 0ch
Long doubles are 80 bits2 wide, while on the stack a TBYTE could be stored with
two 32 bits pushes and one 16 bit push (for 4 + 4 + 2 = 10), to keep the stack aligned on
4 bytes, it ends occupying 12 bytes, thus using three 32 bits pushes.
Respecting Little Endian convention, bits 79-64 are pushed first3, then bits 63-32
followed by bits 31-0.
//C prototype of the callee
void __attribute__((cdecl)) foo(long double a);
foo(3.1457);
;Call to foo in assembly
;3.1457 is 0x4000c9532617c1bda800
push DWORD 4000h ;Bits 79-64, as 32 bits push
push DWORD 0c9532617h ;Bits 63-32
push DWORD 0c1bda800h ;Bits 31-0
call foo
add esp, 0ch
;Call to foo, using the FPU
;ST(0) = a
sub esp, 0ch
fstp TBYTE PTR [esp] ;Store a as ten byte on the stack
call foo
add esp, 0ch
A floating point values, whatever its size, is returned in ST(0)
4.
//C
float one() { return 1; }
;Assembly
fld1 ;ST(0) = 1
ret
//C
double zero() { return 0; }
;Assembly
fldz ;ST(0) = 0
ret
//C
long double pi() { return PI; }
;Assembly
fldpi ;ST(0) = PI
ret
1 Lower DWORD at lower address.
2 Known as TBYTE, from Ten Bytes.
3 Using a full width push with any extension, higher WORD is not used.
4 Which is TBYE wide, note that contrary to the integers, FP are always returned with more precision that it is required.