การร่ายสองครั้งไปยัง int ที่ไม่ได้ลงนามบน Win32 ถูกตัดทอนเป็น 2,147,483,648


85

รวบรวมรหัสต่อไปนี้:

เอาต์พุต (MSVC x86):

INT_MAX: 2147483647
UINT_MAX: 4294967295
Double value: 2147483649.000000
Direct cast value: 2147483648
Indirect cast value: 2147483649

เอาต์พุต (MSVC x64):

INT_MAX: 2147483647
UINT_MAX: 4294967295
Double value: 2147483649.000000
Direct cast value: 2147483649
Indirect cast value: 2147483649

ในเอกสารไมโครซอฟท์มีการกล่าวถึงการลงนามจำนวนเต็มค่าสูงสุดในการแปลงจากไม่มีการdoubleunsigned int

ค่าทั้งหมดข้างต้นINT_MAXจะถูกตัดทอน2147483648เมื่อเป็นการกลับมาของฟังก์ชัน

ฉันใช้Visual Studio 2019เพื่อสร้างโปรแกรม นี้ไม่ได้เกิดขึ้นในGCC

ฉันทำอะไรผิดหรือเปล่า? มีวิธีที่ปลอดภัยในการแปลงdoubleไปunsigned int?


24
และไม่คุณไม่ได้ทำอะไรผิด (บางทีนอกจากพยายามใช้คอมไพเลอร์ "C" ของ Microsoft)
Antti Haapala

5
ทำงานบนเครื่อง™ของฉันทดสอบกับ VS2017 v15.9.18 และ VS2019 v16.4.1 ใช้ความช่วยเหลือ> ส่งข้อเสนอแนะ> รายงานข้อบกพร่องเพื่อแจ้งให้ทราบเกี่ยวกับเวอร์ชันของคุณ
Hans Passant

5
ฉันสามารถทำซ้ำได้ฉันมีผลลัพธ์เช่นเดียวกับ OP VS2019 16.7.3.
anastaciu

2
@EricPostpischil แน่นอนมันเป็นรูปแบบบิตของINT_MIN
Antti Haapala

คำตอบ:


70

บั๊กคอมไพเลอร์ ...

จากการจัดให้มีการชุมนุมโดย @anastaciu, โดยตรงโทรรหัสหล่อ__ftol2_sseซึ่งดูเหมือนว่าจะแปลงจำนวนที่จะเซ็นสัญญายาว ชื่อรูทีนเป็นftol2_sseเพราะนี่คือเครื่องที่เปิดใช้งาน sse - แต่ float อยู่ในการลงทะเบียนจุดลอยตัว x87

; Line 17
    call    _getDouble
    call    __ftol2_sse
    push    eax
    push    OFFSET ??_C@_0BH@GDLBDFEH@Direct?5cast?5value?3?5?$CFu?6@
    call    _printf
    add esp, 8

โยนทางอ้อมในทางกลับกันทำ

; Line 18
    call    _getDouble
    fstp    QWORD PTR _d$[ebp]
; Line 19
    movsd   xmm0, QWORD PTR _d$[ebp]
    call    __dtoui3
    push    eax
    push    OFFSET ??_C@_0BJ@HCKMOBHF@Indirect?5cast?5value?3?5?$CFu?6@
    call    _printf
    add esp, 8

ซึ่งจะปรากฏและจัดเก็บค่าสองเท่าให้กับตัวแปรท้องถิ่นจากนั้นโหลดลงในทะเบียน SSE และการเรียก__dtoui3ซึ่งเป็นรูทีนการแปลง int แบบไม่ได้ลงนามสองครั้ง ...

พฤติกรรมของการโยนโดยตรงไม่เป็นไปตาม C89; และไม่สอดคล้องกับการแก้ไขใด ๆ ในภายหลังแม้แต่ C89 ก็บอกอย่างชัดเจนว่า:

การดำเนินการส่วนที่เหลือเสร็จสิ้นเมื่อค่าของชนิดอินทิกรัลถูกแปลงเป็นชนิดที่ไม่ได้ลงนามไม่จำเป็นต้องทำเมื่อค่าของชนิดลอยถูกแปลงเป็นชนิดที่ไม่ได้ลงนาม ดังนั้นช่วงของค่าแบบพกพาเป็น[0, Utype_MAX + 1)


ฉันเชื่อว่าปัญหาอาจเกิดขึ้นต่อเนื่องจากปี 2548 - เคยมีฟังก์ชันการแปลงที่เรียกว่า__ftol2ซึ่งอาจใช้งานได้กับรหัสนี้กล่าวคือจะแปลงค่าเป็นหมายเลขที่ลงนาม -2147483647 ซึ่งจะให้ผลลัพธ์ที่ถูกต้อง ผลลัพธ์เมื่อตีความตัวเลขที่ไม่ได้ลงชื่อ

น่าเสียดายที่__ftol2_sseไม่ใช่การแทนที่แบบดร็อปอิน__ftol2อย่างที่ควรจะเป็นแทนที่จะใช้บิตที่มีค่าน้อยที่สุดตามที่เป็นอยู่ - ส่งสัญญาณข้อผิดพลาดนอกช่วงโดยการส่งคืนLONG_MIN/ 0x80000000ซึ่งตีความว่าไม่ได้ลงนามนานที่นี่ไม่ได้อยู่ที่ ทุกสิ่งที่คาดหวัง พฤติกรรมของ__ftol2_sseจะใช้ได้สำหรับsigned longเนื่องจากการแปลงค่า double a> LONG_MAXเป็นsigned longจะมีพฤติกรรมที่ไม่ได้กำหนด


23

ต่อไปนี้@ คำตอบ AnttiHaapala ของผมทดสอบใช้รหัสการเพิ่มประสิทธิภาพ/Oxและพบว่านี้จะลบข้อผิดพลาดในขณะที่__ftol2_sseไม่ได้ใช้:

การเพิ่มประสิทธิภาพแบบอินไลน์getdouble()และเพิ่มการประเมินนิพจน์คงที่ทำให้ไม่จำเป็นต้องมีการแปลงที่รันไทม์ทำให้จุดบกพร่องหายไป

ด้วยความอยากรู้อยากเห็นฉันได้ทำการทดสอบเพิ่มเติมคือการเปลี่ยนรหัสเพื่อบังคับให้แปลง float-to-int ที่รันไทม์ ในกรณีนี้ผลลัพธ์ยังคงถูกต้องคอมไพลเลอร์ที่มีการเพิ่มประสิทธิภาพจะใช้__dtoui3ในการแปลงทั้งสอง:

อย่างไรก็ตามการป้องกันการซับใน__declspec(noinline) double getDouble(){...}จะทำให้จุดบกพร่องกลับมา:

__ftol2_sseถูกเรียกในการแปลงทั้งสองทำให้ผลลัพธ์2147483648ในทั้งสองสถานการณ์ความสงสัย @zwolถูกต้อง


รายละเอียดการรวบรวม:

  • ใช้บรรทัดคำสั่ง:
cl /permissive- /GS /analyze- /W3 /Gm- /Ox /sdl /D "WIN32" program.c        
  • ใน Visual Studio:

    • ปิดการใช้งานRTCในProject -> Properties -> Code Generationและการตั้งค่าRuntime พื้นฐานการตรวจสอบที่จะเริ่มต้น

    • การเปิดใช้งานการเพิ่มประสิทธิภาพในProject -> Properties -> Optimizationและการตั้งค่าการเพิ่มประสิทธิภาพการ / ปีวัว

    • ด้วยดีบักเกอร์ในx86โหมด


5
ตลกดีที่พวกเขาชอบ "ตกลงเมื่อเปิดใช้การเพิ่มประสิทธิภาพพฤติกรรมที่ไม่ได้กำหนดจะไม่ได้กำหนดจริงๆ" => โค้ดทำงานได้จริง: F
Antti Haapala

3
@AnttiHaapala ใช่ใช่ Microsoft ที่ดีที่สุด
anastaciu

1
การเพิ่มประสิทธิภาพที่นำไปใช้คือการอินไลน์และการประเมินนิพจน์คงที่ มันไม่ได้ทำการแปลง float-to-int ที่รันไทม์อีกต่อไป ฉันสงสัยว่าบั๊กจะกลับมาถ้าคุณบังคับgetDoubleไม่อยู่ในบรรทัดและ / หรือเปลี่ยนเพื่อส่งคืนค่าที่คอมไพเลอร์ไม่สามารถพิสูจน์ได้ว่าคงที่
zwol

1
@zwol คุณคิดถูกแล้วการบังคับให้ออกนอกระบบและการป้องกันการประเมินอย่างต่อเนื่องจะทำให้จุดบกพร่องกลับมา แต่คราวนี้ในทั้งสอง Conversion
anastaciu

7

ไม่มีใครได้มองที่ asm สำหรับของ __ftol2_sseMS

ผลจากการที่เราสามารถอนุมานได้ว่ามันอาจเปลี่ยนจาก x87 ลงนามint/ long(ทั้งสองชนิด 32 บิตบน Windows) uint32_tแทนได้อย่างปลอดภัย

x86 FP -> คำสั่งจำนวนเต็มที่ทำให้ผลลัพธ์จำนวนเต็มมากเกินไปไม่เพียงแค่ตัด / ตัดทอน: พวกมันสร้างสิ่งที่ Intel เรียกว่า "จำนวนเต็มไม่ จำกัด "เมื่อค่าที่แน่นอนไม่สามารถแสดงได้ในปลายทาง: ตั้งบิตสูงบิตอื่น ๆ จะชัดเจน กล่าวคือ0x80000000 .

(หรือหากไม่ได้มาสก์ข้อยกเว้นที่ไม่ถูกต้องของ FP ก็จะเริ่มทำงานและไม่มีการจัดเก็บค่าใด ๆ แต่ในสภาพแวดล้อม FP เริ่มต้นข้อยกเว้น FP ทั้งหมดจะถูกปิดบังด้วยเหตุนี้สำหรับการคำนวณ FP คุณจะได้รับ NaN แทนข้อผิดพลาด)

ซึ่งรวมถึงคำสั่ง x87 เช่นfistp(โดยใช้โหมดการปัดเศษปัจจุบัน) และคำสั่ง SSE2 เช่นcvttsd2si eax, xmm0(โดยใช้การตัดทอนไปที่ 0 นั่นคือความtหมายพิเศษ)

ดังนั้นจึงเป็นข้อผิดพลาดในการรวบรวมdouble-> แปลงเป็นโทรไปunsigned__ftol2_sse


บันทึกด้านข้าง / แทนเจนต์:

บน x86-64 สามารถคอมไพล์ FP -> uint32_t cvttsd2si rax, xmm0แปลงเป็นปลายทางที่เซ็นชื่อ 64 บิตสร้าง uint32_t ที่คุณต้องการในครึ่งล่าง (EAX) ของปลายทางจำนวนเต็ม

คือ C และ C ++ UB หากผลลัพธ์อยู่นอกช่วง 0..2 ^ 32-1 ดังนั้นจึงเป็นเรื่องปกติที่ค่าบวกหรือค่าลบจำนวนมากจะปล่อยให้ครึ่งต่ำของ RAX (EAX) เป็นศูนย์จากรูปแบบบิตจำนวนเต็มไม่ จำกัด (ไม่เหมือนกับการแปลงจำนวนเต็ม -> จำนวนเต็มไม่รับประกัน การลดโมดูโลของค่าพฤติกรรมของการแคสต์ค่าลบสองเท่าเป็น int ที่ไม่ได้ลงนามที่กำหนดไว้ในมาตรฐาน C หรือไม่พฤติกรรมที่แตกต่างกันบน ARM เทียบกับ x86เพื่อให้ชัดเจนไม่มีอะไรในคำถามคือพฤติกรรมที่ไม่ได้กำหนดหรือแม้แต่การนำไปใช้งานฉันแค่ชี้ให้เห็นว่าหากคุณมี FP-> int64_t คุณสามารถใช้มันเพื่อใช้งาน FP-> uint32_t ได้อย่างมีประสิทธิภาพซึ่งรวมถึง x87fistp ซึ่งสามารถเขียนปลายทางจำนวนเต็ม 64 บิตแม้ในโหมด 32 บิตและ 16 บิตซึ่งแตกต่างจากคำสั่ง SSE2 ซึ่งสามารถจัดการได้เฉพาะจำนวนเต็ม 64 บิตในโหมด 64 บิตเท่านั้น


1
ฉันอยากจะดูรหัสนั้น แต่โชคดีที่ฉันไม่มี MSVC ... : D
Antti Haapala

@AnttiHaapala: ใช่ฉันไม่ทำเช่นกัน
ปีเตอร์คอร์เดส
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.