Re: GCC Bug

原文:GCC Bug

哈,這次老師給我的作業可是難題,有好一陣子沒有碰觸 gdb ,更何況這次還要跟 FPU 打交道,下面是我的測試程式,和原文只有小部份差異,但重點部份是一樣的:

#include <cstdio>
using namespace std;

double f(double z)
{
    return z;
}

void foo(double fraction)
{
    double z = fraction * 20.0;
    int t1 = z;
    int t2 = f(z);
    printf("t1=%d, t2=%d\n", t1, t2);
}

int main()
{
    foo(6.0/20.0);
    return 0;
}

問題

輸出:

6, 6

是我們預期的結果。但在某些情況下卻會產生

5, 6

這樣的輸出。但若是在 compile 時加上 -ffloat-store 則又能得到正確的結果。根據 gcc optimization options 的說明:

Do not store floating point variables in registers, and inhibit other options that might change whether a floating point value is taken from a register or memory.

我們懷疑是從 register 讀出時發生意想不到的進位,導致結果有所出入。

實驗

一開始在嘗試 reproduce 時就遇上問題,在和 aaa 討論後才發現,我們使用的 gcc 版本和 compile options 都不盡相同。最後確定了發生問題的環境:

  1. cygwin + gcc 3.4.4
  2. gcc –O text.c

下面是我嘗試過的平台和結果:

  -O -O3
gcc 4.4.1 No No
gcc 3.4.4 (cygwin) Yes No

有趣的開始!

O3 版本

g++ -Wall -O3 -S -masm=intel main.cpp

這個版本是最好理解的, compiler 已經最佳化到:

  • 不呼叫 foo()、f(),直接以常數取代。(沒記錯,這算是 compiler const folding 和 const propagation 的範疇)

看一下 assembly 就很清楚:

LC2:
	.ascii "t1=%d, t2=%d\12\0"
	.align 4
...
_main:
    push   ebp
    mov    eax, 16
    mov    ebp, esp
    sub    esp, 24
    and    esp, -16
    call   __alloca
    call   ___main
    mov    DWORD PTR [esp], OFFSET FLAT:LC2 ; 看一眼 LC2 ,一個取址動作 :)
    mov    edx, 6
    mov    eax, 6
    mov    DWORD PTR [esp+8], edx
    mov    DWORD PTR [esp+4], eax
    call   _printf
    leave
    xor    eax, eax
    ret

O 版本

g++ -Wall -O -S -masm=intel main.cpp

O 版本就沒那麼聰明,而是真正的去呼叫 foo() :

_main:
    push   ebp
    mov    ebp, esp
    sub    esp, 8
    and    esp, -16
    mov    eax, 16
    call   __alloca
    call   ___main
    fld    QWORD PTR LC4
    fstp   QWORD PTR [esp]
    call   __Z3food
    mov    eax, 0
    leave
    ret

而且可以發現,LC4 是 foo() 的參數:

LC4:
	.long	858993459
	.long	1070805811
	.text
	.align 2


0.3 的 IEEE 754 Double precision (64 bits) 表示式:
0 01111111101 00110011001100110011  110011001100110011001100110011
                        1070805811                       858993459

嗯嗯,O 版本先做了簡單的運算展開。不過在進入 foo() 的 floating point 計算前,得先對 x87 FPU 有簡單的概念 :

image節錄自: Intel® 64 and IA-32 Architectures Software Developer’s Manual ─ Volume 1: Basic Architecture

x87 上用來儲存資料的 registers 被組織成一塊 stack 的,並且搭配了 control, status 等 registers 可供控制、查詢之用。 Stack 以下圖的方式運做:

image節錄自: Intel® 64 and IA-32 Architectures Software Developer’s Manual ─ Volume 1: Basic Architecture

Stack 的 top 又稱為 ST(0),是許多 FPU 指令的隱藏 operand 之一,像是待會在 foo() 看見的 fld、fstp、fmul 指令都會 implicitly 操作 ST(0) 。

__Z3food:
    push    ebp
    mov     ebp, esp
    push    ebx
    sub     esp, 20
    fld     DWORD PTR LC1
    fmul    QWORD PTR [ebp+8]
    fnstcw  WORD PTR [ebp-6]
    movzx   eax, WORD PTR [ebp-6]
    or      ax, 3072
    mov     WORD PTR [ebp-8], ax
    fldcw   WORD PTR [ebp-8]
    fist    DWORD PTR [ebp-12]      ; store integer
    fldcw   WORD PTR [ebp-6]
    mov     ebx, DWORD PTR [ebp-12]
    fstp    QWORD PTR [esp]
    call    __Z1fd
    fnstcw  WORD PTR [ebp-6]
    movzx   eax, WORD PTR [ebp-6]
    or      ax, 3072
    mov     WORD PTR [ebp-8], ax
    fldcw   WORD PTR [ebp-8]
    fistp   DWORD PTR [esp+8]
    fldcw   WORD PTR [ebp-6]
    mov     DWORD PTR [esp+4], ebx
    mov     DWORD PTR [esp], OFFSET FLAT:LC2
    call    _printf
    add     esp, 20
    pop     ebx
    pop     ebp
    ret

這邊使用倒推法,來找到 t1 及其在 assembly 的表示,尋著上頭的 highlight 部份,可以發現:

  1. printf 從 ebx 拿到 t1 的值,也就是 ebx 、
  2. ebx 由 ebp-12 得到、
  3. ebp-12 則是 fist 的結果,也就是 FPU 的 ST(0)、
  4. 往上回溯,只有 L6 處有個 fld ,因此,ST(0) 指的是 fmul 的結果、

反推的過程也符合程式的邏輯,所以下一步就是用 debugger 去追蹤:

Breakpoint 1, 0x0040104f in foo ()
1: x/i $pc
0x40104f <_Z3food+7>:   flds   0x402030
(gdb) info float
  R7: Empty   0x3ffd9999999999999800
  R6: Empty   0x00e07c93022200240000
  R5: Empty   0x51c27c92fe956123a74c
  R4: Empty   0x79d37c9a06007c930460
  R3: Empty   0x0a887c9479870022bf80
  R2: Empty   0xbf9800010a886123debc
  R1: Empty   0x00006123a74c7c93043e
=>R0: Empty   0xdee46123de0c6123d8cc

Status Word:         0xffff0000
                       TOP: 0
Control Word:        0xffff037f   IM DM ZM OM UM PM
                       PC: Extended Precision (64-bits)
                       RC: Round to nearest
Tag Word:            0xffffffff
Instruction Pointer: 0x1b:0x004010c8
Operand Pointer:     0xffff0023:0x0022cd10
Opcode:              0xdd1c

我們將中斷點設在 foo() 函式,接著使用 info float 去 dump FPU 相關的 register (FPU 相關 register 似乎無法加到 gdb 的 Automatic Display!?所以每次都需手打 ),比較特別的幾個要點都被 highlight 了:

  1. R0 ~ R7 即剛剛提到的 FPU data register stack : stack 目前是空的,雖然指向 R0 ,但顯示為 Empty
  2. Control Word 裡頭的 RC (rounding-control)決定了 floating-point 的進位(round)方式。
    image 
    節錄自: Intel® 64 and IA-32 Architectures Software Developer’s Manual ─ Volume 1: Basic Architecture

認識了 gdb 對於 FPU 的輸出後,可以開始一步步逼近問題核心 ── ebp-12 了。

foo() 的一開始,是讀入 0x402030 上的值到 FPU stack 上:

Breakpoint 1, 0x0040104f in foo ()
1: x/i $pc
0x40104f <_Z3food+7>:   flds   0x402030

好奇這 magic number 是何來的,可以透過之前的 disassembly 結果:

LC1:
	.long	1101004800    ; = 0x41a00000
	.text
	.align 2

或是動態的用 debugger 觀看:

(gdb) x 0x402030
0x402030 <_data_start__+48>:    0x41a00000 ; = 1101004800

而 0x41a00000 便是 20.0 的 IEEE 754 0.3 的 IEEE 754 Double precision (64 bits) 表示式。來用 debugger 確認一下:

Breakpoint 1, 0x0040104f in foo ()
1: x/i $pc
0x40104f <_Z3food+7>:   flds   0x402030
(gdb) info float
  R7: Empty   0x3ffd9999999999999800
  R6: Empty   0x00e07c93022200240000
  R5: Empty   0x51c27c92fe956123a74c
  R4: Empty   0x79d37c9a06007c930460
  R3: Empty   0x0a887c9479870022bf80
  R2: Empty   0xbf9800010a886123debc
  R1: Empty   0x00006123a74c7c93043e
=>R0: Empty   0xdee46123de0c6123d8cc

(gdb) si
0x00401055 in foo ()
1: x/i $pc
0x401055 <_Z3food+13>:  fmull  0x8(%ebp)

(gdb) info float
=>R7: Valid   0x4003a000000000000000 +20
  R6: Empty   0x00e07c93022200240000

執行完上面的指令後, FPU 的 data stack 現在是 20 ,接著進行 x 0.3 的乘法(ebp+0x8 是 foo() 的參數,即 0.3):

0x401055 <_Z3food+13>:  fmull  0x8(%ebp)

(gdb) si
0x00401058 in foo ()
1: x/i $pc
0x401058 <_Z3food+16>:  fnstcw -0x6(%ebp)
(gdb) info float
=>R7: Valid   0x4001bffffffffffffe00 +6
  R6: Empty   0x00e07c93022200240000

得到了結果:+6 ,無誤。不過接下來的 FPU control word 卻令人意外:

(gdb) info float
...
Control Word:        0xffff037f   IM DM ZM OM UM PM
                       PC: Extended Precision (64-bits)
                       RC: Round to nearest

0x40105f <_Z3food+23>:  or     $0xc00,%ax
0x401063 <_Z3food+27>:  mov    %ax,-0x8(%ebp)
0x401067 <_Z3food+31>:  fldcw  -0x8(%ebp)

(gdb) info float
...
Control Word:        0xffff0f7f   IM DM ZM OM UM PM
                       PC: Extended Precision (64-bits)
                       RC: Round toward zero

O 版將 RC 從 Round to nearest 改變成 Round toward zero 。這會影響到 fist 的運作。在執行 fist 前,我們把預期的結果: 6.0 的 IEEE 754 Double precision 求出來:0x4018000000000000 ;而目前的 R7 值是 0x4001bffffffffffffe00 ,小於該值。表示 R7 現在是不足 6.0 ,而此時又改以 Round toward zero 來進行取整數,那結果會是 5 則是預期了。

(gdb) si
0x0040106a in foo ()
1: x/i $pc
0x40106a <_Z3food+34>:  fistl  -0xc(%ebp)
(gdb) info float
=>R7: Valid   0x4001bffffffffffffe00 +6
  R6: Empty   0x4bc000010101e259acd0
...
Control Word:        0xffff0f7f   IM DM ZM OM UM PM
                       PC: Extended Precision (64-bits)
                       RC: Round toward zero

(gdb) x 0x22ccfc              ; ebp - 0xc = 0x22ccfc
0x22ccfc:       0x61100049

(gdb) si
0x0040106d in foo ()
1: x/i $pc
0x40106d <_Z3food+37>:  fldcw  -0x6(%ebp)
(gdb) x 0x22ccfc
0x22ccfc:       0x00000005

執行結果印證了我們的預期。

-ffloat-store 版

模仿取得 O 版 disassembly 的方式來取得 -ffloat-store 的結果:

g++ -Wall -O -ffloat-store -S -masm=intel main.cpp

-ffloat-store 版和 O 版相同,會去呼叫 foo() ,不過比較兩者 foo() 可以發現: -ffloat-store 版只多了兩行,在進行 x 0.3 後,會馬上將值寫回 call stack 上,並再讀入。

image也就是,當 6.0 被寫入記憶體時,是以 Round to nearest 的方式寫進:

(gdb) si
0x00401058 in foo ()
1: x/i $pc
0x401058 <_Z3food+16>:  fstpl  -0x10(%ebp)
(gdb) info float
=>R7: Valid   0x4001bffffffffffffe00 +6
  R6: Empty   0xdbc000010101e4ada9d8
...
Control Word:        0xffff037f   IM DM ZM OM UM PM
                       PC: Extended Precision (64-bits)
                       RC: Round to nearest

而讀回 FPU stack 時,已經是個精確的 6.0 :

(gdb) si
0x0040105e in foo ()
1: x/i $pc
0x40105e <_Z3food+22>:  fnstcw -0x12(%ebp)
(gdb) info float
=>R7: Valid   0x4001c000000000000000 +6
  R6: Empty   0x10000000000400010000
...
Control Word:        0xffff037f   IM DM ZM OM UM PM
                       PC: Extended Precision (64-bits)
                       RC: Round to nearest

3 則留言:

Unknown 提到...

Keiko 大大沒有新文章了..><

Keiko 提到...

真的!而且超過半年!!!Shame on me ...

Keiko 提到...

不過我更新了退伍計時器 :)

Windows + Visual Studio + VSCode + CMake 的疑難雜症

Environment Windows 10 Visual Studio 2019 CMake 3.27.7 VSCode VSCode CMake Tools 1. CMAKE_BUILD_TYPE 是空的 參考一下 這篇 的處理。 大致上因為 Visual...