Reputation: 69
Why does The following code work totally different on IA-32 and x86-64?
#include <stdio.h>
int main() {
double a = 10;
printf("a = %d\n", a);
return 0;
}
On IA-32, the result is always 0. However, on x86-64 the result can be anything between MAX_INT and MIN_INT. What is the reason behind this?
Upvotes: -6
Views: 281
Reputation: 13
Dmitri's answer gives a rough understanding of this undefined behavior, I'll give an explanation in detail.
For 32-bit case, here is the disassembly:
0000118d <main>:
118d: 8d 4c 24 04 lea ecx,[esp+0x4]
1191: 83 e4 f0 and esp,0xfffffff0
1194: ff 71 fc push DWORD PTR [ecx-0x4]
1197: 55 push ebp
1198: 89 e5 mov ebp,esp
119a: 53 push ebx
119b: 51 push ecx
119c: 83 ec 10 sub esp,0x10
119f: e8 37 00 00 00 call 11db <__x86.get_pc_thunk.ax>
11a4: 05 50 2e 00 00 add eax,0x2e50
11a9: dd 80 1c e0 ff ff fld QWORD PTR [eax-0x1fe4]
11af: dd 5d f0 fstp QWORD PTR [ebp-0x10]
11b2: 83 ec 04 sub esp,0x4
11b5: ff 75 f4 push DWORD PTR [ebp-0xc]
11b8: ff 75 f0 push DWORD PTR [ebp-0x10]
11bb: 8d 90 14 e0 ff ff lea edx,[eax-0x1fec]
11c1: 52 push edx
11c2: 89 c3 mov ebx,eax
11c4: e8 77 fe ff ff call 1040 <printf@plt>
11c9: 83 c4 10 add esp,0x10
11cc: b8 00 00 00 00 mov eax,0x0
11d1: 8d 65 f8 lea esp,[ebp-0x8]
11d4: 59 pop ecx
11d5: 5b pop ebx
11d6: 5d pop ebp
11d7: 8d 61 fc lea esp,[ecx-0x4]
11da: c3 ret
The program fetches address eax-0x1fe4
, which is likely where the IEEE754 format of number 10
located, and store the number in ebp-0x10
(see addr 11a9
and 11af
). We can use a debugger to get the value from this address:
(gdb) info r
eax 0x56558ff4 1448447988
ecx 0xffffcea0 -12640
edx 0xffffcec0 -12608
ebx 0xf7fa7e14 -134578668
esp 0xffffce70 0xffffce70
ebp 0xffffce88 0xffffce88
esi 0x56558eec 1448447724
edi 0xf7ffcb60 -134231200
eip 0x565561b2 0x565561b2 <main+37>
eflags 0x202 [ IF ]
cs 0x23 35
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x0 0
gs 0x63 99
(gdb) x/g 0xffffce88 - 0x10 # ebp - 0x10
0xffffce78: 4621819117588971520
(gdb) x/gx 0xffffce88 - 0x10
0xffffce78: 0x4024000000000000
After using an IEEE 754 converter, we know the value from ebp-0x10
is just the double representation of 10
.
Since 32-bit (x86) ABI uses the stack to pass function parameters, we can figure out [eax-0x1fec]
is the format string %d
. [ebp-0x10]
and [ebp-0xc]
are the lower and higher parts of double representation of 10
(0x4024000000000000
), respectively.
Well, since %d
only accepts a DWORD parameter, and the number 0x4024000000000000
is stored in little endian, the output will be the lower 32-bits of it, which is 0
.
Here is the disassembly for 64-bit case:
0000000000001139 <main>:
1139: 55 push rbp
113a: 48 89 e5 mov rbp,rsp
113d: 48 83 ec 10 sub rsp,0x10
1141: f2 0f 10 05 c7 0e 00 movsd xmm0,QWORD PTR [rip+0xec7] # 2010 <_IO_stdin_used+0x10>
1148: 00
1149: f2 0f 11 45 f8 movsd QWORD PTR [rbp-0x8],xmm0
114e: 48 8b 45 f8 mov rax,QWORD PTR [rbp-0x8]
1152: 66 48 0f 6e c0 movq xmm0,rax
1157: 48 8d 05 aa 0e 00 00 lea rax,[rip+0xeaa] # 2008 <_IO_stdin_used+0x8>
115e: 48 89 c7 mov rdi,rax
1161: b8 01 00 00 00 mov eax,0x1
1166: e8 c5 fe ff ff call 1030 <printf@plt>
116b: b8 00 00 00 00 mov eax,0x0
1170: c9 leave
1171: c3 ret
It is easy to see that xmm0
register gets its value from rip+0xec7
and store it in rbp-0x8
(see addr 1141
and 1149
). We can use a debugger to check it again:
(gdb) disass
Dump of assembler code for function main:
0x0000555555555139 <+0>: push %rbp
0x000055555555513a <+1>: mov %rsp,%rbp
0x000055555555513d <+4>: sub $0x10,%rsp
=> 0x0000555555555141 <+8>: movsd 0xec7(%rip),%xmm0 # 0x555555556010
0x0000555555555149 <+16>: movsd %xmm0,-0x8(%rbp)
0x000055555555514e <+21>: mov -0x8(%rbp),%rax
0x0000555555555152 <+25>: movq %rax,%xmm0
0x0000555555555157 <+30>: lea 0xeaa(%rip),%rax # 0x555555556008
0x000055555555515e <+37>: mov %rax,%rdi
0x0000555555555161 <+40>: mov $0x1,%eax
0x0000555555555166 <+45>: call 0x555555555030 <printf@plt>
0x000055555555516b <+50>: mov $0x0,%eax
0x0000555555555170 <+55>: leave
0x0000555555555171 <+56>: ret
End of assembler dump.
(gdb) x/gx 0x555555556010
0x555555556010: 0x4024000000000000
(gdb) ni 2
5 printf("a = %d\n", a);
(gdb) info r
rax 0x555555555139 93824992235833
rbx 0x7fffffffde18 140737488346648
rcx 0x555555557dd8 93824992247256
rdx 0x7fffffffde28 140737488346664
rsi 0x7fffffffde18 140737488346648
rdi 0x1 1
rbp 0x7fffffffdd00 0x7fffffffdd00
rsp 0x7fffffffdcf0 0x7fffffffdcf0
r8 0x0 0
r9 0x7ffff7fcbf40 140737353924416
r10 0x7fffffffda40 140737488345664
r11 0x206 518
r12 0x0 0
r13 0x7fffffffde28 140737488346664
r14 0x7ffff7ffd000 140737354125312
r15 0x555555557dd8 93824992247256
rip 0x55555555514e 0x55555555514e <main+21>
eflags 0x206 [ PF IF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
fs_base 0x7ffff7dba740 140737351755584
gs_base 0x0 0
(gdb) x/gx 0x7fffffffdd00-0x8 # rbp - 0x8
0x7fffffffdcf8: 0x4024000000000000
(gdb)
After this, rax
register gets the value again and passes to xmm0
. However, since the ABI in 64-bit uses registers to pass function parameters, and the first 6 registers are rdi
, rsi
, rdx
, rcx
, r8
and r9
(see 2).
Register rdi
gets the format string from address 0x555555556008
, however rsi
is uninitialized and passed as a parameter to substitute a
in the C source. Therefore, the value of rsi
will be printed and displayed as a "random garbage" value.
(gdb) c
Continuing.
a = -8680
[Inferior 1 (process 25194) exited normally]
Since %d
only cares the last 32-bits, rsi
's value 0x7fffffffde18
will be truncated to 0xffffde18
, which is -8680
in decimal.
Upvotes: 0
Reputation: 9385
Passing an argument to printf()
that doesn't match the format specifiers in the format string is undefined behaviour... and with undefined behaviour, anything could happen and the results aren't necessarily consistent from one instance to another -- so it should be avoided.
As for why you see a difference between x86 (32-bit) and x86-64, it's likely because of differences in the way parameters are passed in each case.
In the x86 case, the arguments to printf()
are likely being passed on the stack, aligned on 4-byte boundaries -- so when printf()
processes the %d
specifier it reads a 4-byte int
from the stack, which is actually the lower 4 bytes from a
. Since a
was 10 those bytes have no bits set, so they're interpreted as an int
value of 0.
In the x86-64 case, the arguments to printf()
are all passed in registers (though some would be on the stack if there were enough of them)... but double
arguments are passed in different registers than int
arguments (such as %xmm0 instead of %rsi). So when printf()
tries to process an int
argument to match the %d
specifier, it takes it from a different register that the one a
was passed in, and uses whatever garbage value was left in the register instead of the lower bytes of a
, and interprets that as some garbage int
value.
Upvotes: 1
Reputation: 141648
%d
actually is used for printing int
. Historically the d
stood for "decimal", to contrast with o
for octal and x
for hexadecimal.
For printing double
you should use %e
, %f
or %g
.
Using the wrong format specifier causes undefined behaviour which means anything may happen, including unexpected results.
Upvotes: 2