前些天有个年轻的同事问我:杰伦杰伦,为什么相同的code,同一个变量在Windows和Linux上打印出来的结果不一样啊?我没搞明白为什么有这个问题,那不就说明实际上这个变量的值在Windows/Linux上不一样啊!于是找来相关code看了一眼,简化后如下:

union MyUnion
{
    int i;
    float f;
};

int main()
{
    MyUnion u;
    u.i = 0xdeadbeef;
    printf("i %x\nf %x\n", u.i, u.f);
}

“打印结果i的值是相同的,但是f的值不一样。”
啊?这是个union,i的值一样一样当然意味着f的值实际上也是一样的。再细看,我大惊失色,怎么能用%x输出float类型呢?不应该用%f吗?这是undefined behavior啊!
“那为什么两边打印的f结果不一样呢。”
“那就是不同系统的调用约定不一样了。”

MSVC

我们首先得知道Windows上x86-64的调用约定。根据Wikipedia上的x86 calling conventions,前四个整型参数依次使用寄存器%rcx%rdx%r8%r9,前四个浮点参数依次使用%xmm0%xmm1%xmm2%xmm3寄存器。如果参数表没有指明原型,则调用者应当在整型的寄存器和浮点的寄存器各存一次。
再来看下Windows上的对应的汇编代码:

    MyUnion u;
    u.i = 0xdeadbeef;
00007FF788A94CDD  mov         dword ptr [u],0DEADBEEFh  
    printf("i %x\nf %x\n", u.i, u.f);
00007FF788A94CE4  cvtss2sd    xmm0,dword ptr [u]  
00007FF788A94CE9  movaps      xmm2,xmm0  
00007FF788A94CEC  movq        r8,xmm2  
00007FF788A94CF1  mov         edx,dword ptr [u]  
00007FF788A94CF4  lea         rcx,[string "i %x\nf %x\n" (07FF788A99CA8h)]  
00007FF788A94CFB  call        printf (07FF788A91190h)  

可以看到,printf第一个参数字符串"i %x\nf %x\n"的地址被保存到了%rcx里;u.i的值被直接保存到了%edx寄存器里,%edx是调用约定的第二个整数寄存器;对于u.f,在调用printf前,先通过cvtss2sd指令将整型转换成了浮点型保存到浮点寄存器%xmm0中,再把该值分别保存到%xmm2%r8里。此时%r8里的数据为0xc3d5b7dde0000000。由于需要的%x是个32位整型,printf会去查找对应的整型寄存器,即输出%r8的低32位,所以最后看到的结果是e0000000:

i deadbeef
f e0000000

GCC

Linux调用约定的寄存器依次为%rdi%rsi%rdx%rcx%r8%r9,浮点寄存器依次为%xmm0%xmm7
再来看下用gcc得到的汇编:

    11c4:       c7 45 f4 ef be ad de    movl   $0xdeadbeef,-0xc(%rbp)
    11cb:       f3 0f 10 45 f4          movss  -0xc(%rbp),%xmm0
    11d0:       f3 0f 5a c0             cvtss2sd %xmm0,%xmm0
    11d4:       8b 45 f4                mov    -0xc(%rbp),%eax
    11d7:       89 c6                   mov    %eax,%esi
    11d9:       48 8d 3d 25 0e 00 00    lea    0xe25(%rip),%rdi        # 2005 <_ZStL19piecewise_construct+0x1>
    11e0:       b8 01 00 00 00          mov    $0x1,%eax

字面值0xdeadbeef被保存到了地址为-0xc(%rbp)的内存中,然后又被复制到了%esi寄存器作为参数u.i。该值也被复制到了xmm0中,随后原地做了一次类型转换。但是print需要的仍然是%x,所以在打印u.f时会去查找用来保存第三个参数的寄存器%rdx,而%rdx里的内容完全是随机的,所以得到结果也是随机的。

小结

如果你对float类型的二进制表达感兴趣,可以将对它取地址再将它转成int类型,而不是直接通过整型去打印它。

printf("i %x\nf %x\n", u.i, *(int*)&u.f);

讲了半天,其实MSVC和GCC在编译时都会检测这个UB并给出warning。所以大家还是不要对编译器的warning熟视无睹啊,千万别去踩UB的累,要是出了问题就是“代码不规范,你我两行泪”,浪费很多不必要的时间。