Martin Gao
Martin Gao

Reputation: 69

Using "%d" to print double variable outputs differently in IA32 and IA32-64

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

Answers (3)

junyu33
junyu33

Reputation: 13

Dmitri's answer gives a rough understanding of this undefined behavior, I'll give an explanation in detail.

32-bit

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.

64-bit

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

Dmitri
Dmitri

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

M.M
M.M

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

Related Questions