ฉันได้รับค่าที่มีขนาดใหญ่กว่า 8 บิตจากจำนวนเต็ม 8 บิตได้อย่างไร


118

ฉันติดตามแมลงที่น่ารังเกียจอย่างยิ่งที่ซ่อนอยู่หลังอัญมณีชิ้นเล็ก ๆ นี้ ผมทราบว่าต่อซี ++ สเปคลงนามล้นเป็นพฤติกรรมที่ไม่ได้กำหนด sizeof(int)แต่เมื่อล้นเกิดขึ้นเมื่อค่าที่จะขยายไปยังบิตกว้าง ตามที่ผมเข้าใจมัน incrementing ไม่ควรที่เคยเป็นพฤติกรรมที่ไม่ได้กำหนดตราบเท่าที่char sizeof(char) < sizeof(int)แต่นั่นไม่ได้อธิบายว่าcการได้รับค่าที่เป็นไปไม่ได้อย่างไร ในฐานะจำนวนเต็ม 8 บิตจะcเก็บค่าที่มากกว่าความกว้างบิตได้อย่างไร

รหัส

// Compiled with gcc-4.7.2
#include <cstdio>
#include <stdint.h>
#include <climits>

int main()
{
   int8_t c = 0;
   printf("SCHAR_MIN: %i\n", SCHAR_MIN);
   printf("SCHAR_MAX: %i\n", SCHAR_MAX);

   for (int32_t i = 0; i <= 300; i++)
      printf("c: %i\n", c--);

   printf("c: %i\n", c);

   return 0;
}

เอาท์พุต

SCHAR_MIN: -128
SCHAR_MAX: 127
c: 0
c: -1
c: -2
c: -3
...
c: -127
c: -128  // <= The next value should still be an 8-bit value.
c: -129  // <= What? That's more than 8 bits!
c: -130  // <= Uh...
c: -131
...
c: -297
c: -298  // <= Getting ridiculous now.
c: -299
c: -300
c: -45   // <= ..........

ลองดูที่ ideone


61
"ฉันทราบว่าตามข้อกำหนด C ++ การเซ็นชื่อล้นนั้นไม่ได้กำหนดไว้" - ขวา เพื่อความแม่นยำไม่ใช่แค่ค่าที่กำหนดไม่ได้ แต่พฤติกรรมก็คือ การปรากฏตัวเพื่อให้ได้ผลลัพธ์ที่เป็นไปไม่ได้ทางกายภาพเป็นผลที่ถูกต้อง

@hvd ฉันแน่ใจว่ามีคนอธิบายว่าการใช้งาน C ++ ทั่วไปทำให้เกิดพฤติกรรมนี้อย่างไร บางทีอาจเกี่ยวข้องกับการจัดตำแหน่งหรือการprintf()แปลงอย่างไร
rliu

คนอื่น ๆ ได้กล่าวถึงปัญหาหลัก ความคิดเห็นของฉันเป็นเรื่องทั่วไปและเกี่ยวข้องกับแนวทางการวินิจฉัย ฉันเชื่อว่าส่วนหนึ่งของสาเหตุที่คุณพบปริศนาเช่นนี้คือความเชื่อที่เป็นไปไม่ได้ที่เป็นไปไม่ได้ เห็นได้ชัดว่ามันเป็นไปไม่ได้ดังนั้นจงยอมรับสิ่งนั้นและมองอีกครั้ง
Tim X

@TimX - ฉันสังเกตพฤติกรรมและสรุปได้ชัดเจนว่ามันเป็นไปไม่ได้ในแง่นั้น การใช้คำของฉันอ้างถึงจำนวนเต็ม 8 บิตซึ่งถือค่า 9 บิตซึ่งเป็นไปไม่ได้ตามคำจำกัดความ ความจริงที่เกิดขึ้นแสดงให้เห็นว่ามันไม่ได้รับการปฏิบัติเป็นค่า 8 บิต ตามที่คนอื่น ๆ กล่าวไว้ปัญหานี้เกิดจากข้อบกพร่องของคอมไพเลอร์ ความเป็นไปไม่ได้ที่ดูเหมือนจะเป็นไปไม่ได้เพียงอย่างเดียวในที่นี้คือค่า 9 บิตในช่องว่าง 8 บิตและความเป็นไปไม่ได้ที่ชัดเจนนี้อธิบายได้จากพื้นที่นั้น "ใหญ่" กว่าที่รายงาน
ไม่ลงนาม

ฉันเพิ่งทดสอบกับ mechine ของฉันและผลลัพธ์ก็เป็นอย่างที่ควรจะเป็น c: -120 c: -121 c: -122 c: -123 c: -124 c: -125 c: -126 c: -127 c: -128 c: 127 c: 126 c: 125 c: 124 c: 123 c: 122 c: 121 c: 120 c: 119 c: 118 c: 117 และสภาพแวดล้อมของฉันคือ: Ubuntu-12.10 gcc-4.7.2
VELVETDETH

คำตอบ:


111

นี่คือบั๊กของคอมไพเลอร์

แม้ว่าการได้รับผลลัพธ์ที่เป็นไปไม่ได้สำหรับพฤติกรรมที่ไม่ได้กำหนดจะเป็นผลลัพธ์ที่ถูกต้อง แต่ก็ไม่มีพฤติกรรมที่ไม่ได้กำหนดไว้ในโค้ดของคุณ สิ่งที่เกิดขึ้นคือคอมไพเลอร์คิดว่าพฤติกรรมนั้นไม่ได้กำหนดและปรับให้เหมาะสม

หากcมีการกำหนดเป็นint8_tและint8_tส่งเสริมการintแล้วc--ควรจะดำเนินการลบc - 1ในทางคณิตศาสตร์และแปลงผลที่ได้กลับไปint int8_tการลบในintจะไม่ล้นและการแปลงค่าอินทิกรัลนอกช่วงเป็นอินทิกรัลประเภทอื่นนั้นใช้ได้ หากลงนามประเภทปลายทางผลลัพธ์จะถูกกำหนดให้ใช้งานได้ แต่ต้องเป็นค่าที่ถูกต้องสำหรับประเภทปลายทาง (และถ้าประเภทปลายทางไม่ได้ลงนามผลลัพธ์จะถูกกำหนดไว้อย่างดี แต่จะใช้ไม่ได้ที่นี่)


ฉันจะไม่อธิบายว่ามันเป็น "ข้อบกพร่อง" เนื่องจากการโอเวอร์โฟลว์ที่ลงนามทำให้เกิดพฤติกรรมที่ไม่ได้กำหนดคอมไพลเลอร์จึงมีสิทธิ์อย่างสมบูรณ์ที่จะถือว่ามันจะไม่เกิดขึ้นและปรับลูปให้เหมาะสมเพื่อเก็บค่ากลางcในประเภทที่กว้าง สันนิษฐานว่านั่นคือสิ่งที่เกิดขึ้นที่นี่
Mike Seymour

4
@MikeSeymour: ล้นเพียงอย่างเดียวที่นี่คือการแปลง (โดยนัย) การแปลงมากเกินไปในการลงนามไม่มีพฤติกรรมที่ไม่ได้กำหนด เป็นเพียงการให้ผลลัพธ์ที่กำหนดการนำไปใช้งาน (หรือเพิ่มสัญญาณที่กำหนดการนำไปใช้งาน แต่ดูเหมือนจะไม่เกิดขึ้นที่นี่) ความแตกต่างในการกำหนดความแตกต่างระหว่างการดำเนินการทางคณิตศาสตร์และการแปลงเป็นเรื่องแปลก แต่นั่นเป็นวิธีที่มาตรฐานภาษากำหนดไว้
Keith Thompson

2
@KeithThompson นั่นคือสิ่งที่แตกต่างระหว่าง C และ C ++: C อนุญาตให้ใช้สัญญาณที่กำหนดการนำไปใช้งาน C ++ ไม่ได้ C ++ ระบุเพียงว่า "หากมีการลงนามประเภทปลายทางค่าจะไม่เปลี่ยนแปลงหากสามารถแสดงในประเภทปลายทาง (และความกว้างของบิตฟิลด์) มิฉะนั้นค่าจะถูกกำหนดโดยการนำไปใช้งาน"

เมื่อมันเกิดขึ้นฉันไม่สามารถสร้างพฤติกรรมแปลก ๆ บน g ++ 4.8.0 ได้
Daniel Landau

2
@DanielLandau ดูความคิดเห็นที่ 38 ในข้อบกพร่องนั้น: "แก้ไขแล้วสำหรับ 4.8.0" :)

15

คอมไพเลอร์สามารถมีจุดบกพร่องที่นอกเหนือจากที่ไม่เป็นไปตามมาตรฐานได้เนื่องจากมีข้อกำหนดอื่น ๆ คอมไพเลอร์ควรเข้ากันได้กับเวอร์ชันอื่น ๆ นอกจากนี้ยังอาจคาดว่าจะเข้ากันได้กับคอมไพเลอร์อื่น ๆ ในบางลักษณะและยังสอดคล้องกับความเชื่อบางประการเกี่ยวกับพฤติกรรมที่ถือครองโดยฐานผู้ใช้ส่วนใหญ่

ในกรณีนี้ดูเหมือนจะเป็นข้อบกพร่องของการปฏิบัติตามข้อกำหนด การแสดงออกc--ควรจัดการในทางที่จะคล้ายกันc c = c - 1ที่นี่ค่าcทางด้านขวาจะเลื่อนระดับเป็นประเภทintจากนั้นการลบจะเกิดขึ้น เนื่องจากcอยู่ในช่วงของint8_tการลบนี้จะไม่ล้น int8_tแต่มันอาจคุ้มค่าที่จะออกในช่วงของการ เมื่อค่านี้ถูกกำหนดให้เป็นแปลงที่เกิดขึ้นกลับไปชนิดเพื่อให้เหมาะกับผลที่ได้กลับเข้ามาint8_t cในกรณีที่ไม่อยู่ในขอบเขตการแปลงมีมูลค่าที่กำหนดโดยการนำไปใช้งาน แต่ค่าที่อยู่นอกช่วงint8_tไม่ใช่ค่าที่กำหนดในการนำไปใช้งานที่ถูกต้อง การนำไปใช้งานไม่สามารถ "กำหนด" ว่าประเภท 8 บิตมี 9 บิตขึ้นไปในทันใด สำหรับค่าที่จะกำหนดให้ใช้งานได้หมายความว่ามีการผลิตบางสิ่งในช่วงint8_tและโปรแกรมจะดำเนินต่อไป มาตรฐาน C จึงช่วยให้สามารถแสดงพฤติกรรมต่างๆเช่นเลขคณิตอิ่มตัว (โดยทั่วไปใน DSP) หรือสรุปรอบ (สถาปัตยกรรมกระแสหลัก)

คอมไพเลอร์จะใช้ประเภทเครื่องกว้างพื้นฐานเมื่อมีการจัดการค่าของชนิดจำนวนเต็มขนาดเล็กเช่นหรือint8_t charเมื่อมีการคำนวณทางคณิตศาสตร์ผลลัพธ์ที่อยู่นอกช่วงของประเภทจำนวนเต็มขนาดเล็กสามารถจับได้อย่างน่าเชื่อถือในประเภทที่กว้างขึ้นนี้ เพื่อรักษาลักษณะการทำงานที่มองเห็นได้ภายนอกว่าตัวแปรเป็นประเภท 8 บิตผลลัพธ์ที่กว้างขึ้นจะต้องถูกตัดทอนลงในช่วง 8 บิต ต้องใช้รหัสที่ชัดเจนในการทำเช่นนั้นเนื่องจากตำแหน่งที่เก็บข้อมูลของเครื่อง (รีจิสเตอร์) กว้างกว่า 8 บิตและพอใจกับค่าที่มากขึ้น ที่นี่คอมไพลเลอร์ละเลยที่จะทำให้ค่าเป็นปกติและส่งผ่านไปprintfตามที่เป็นอยู่ ตัวระบุการแปลง%iในprintfไม่รู้ว่าอาร์กิวเมนต์มาจากการint8_tคำนวณแต่เดิม มันใช้งานได้กับไฟล์int การโต้เถียง.


นี่เป็นคำอธิบายที่ชัดเจน
David Healy

คอมไพเลอร์สร้างโค้ดที่ดีโดยปิดตัวเพิ่มประสิทธิภาพ ดังนั้นคำอธิบายโดยใช้ "กฎ" และ "คำจำกัดความ" จึงไม่สามารถใช้ได้ มันเป็นจุดบกพร่องในเครื่องมือเพิ่มประสิทธิภาพ

14

ฉันใส่สิ่งนี้ในความคิดเห็นไม่ได้ฉันจึงโพสต์เป็นคำตอบ

ด้วยเหตุผลแปลก ๆ บางอย่างตัว--ดำเนินการจึงเป็นผู้ร้าย

ฉันทดสอบโค้ดที่โพสต์บน Ideone และแทนที่c--ด้วยc = c - 1และค่ายังคงอยู่ในช่วง [-128 ... 127]:

c: -123
c: -124
c: -125
c: -126
c: -127
c: -128 // about to overflow
c: 127  // woop
c: 126
c: 125
c: 124
c: 123
c: 122

ตาประหลาด? ผมไม่ทราบว่ามากเกี่ยวกับสิ่งคอมไพเลอร์ไม่ให้แสดงออกเหมือนหรือi++ i--มีแนวโน้มที่จะส่งเสริมค่าตอบแทนเป็นintและส่งผ่าน นั่นเป็นข้อสรุปเชิงตรรกะเดียวที่ฉันคิดได้เพราะคุณได้รับค่าที่ไม่สามารถรวมเป็น 8 บิตได้


4
เพราะโปรโมชั่นการหนึ่งวิธีc = c - 1 c = (int8_t) ((int)c - 1การแปลงค่านอกช่วงintเป็นint8_tพฤติกรรมที่กำหนดไว้ แต่เป็นผลลัพธ์ที่นำไปใช้งาน อันที่จริงแล้วไม่ c--ควรทำการแปลงเดียวกันด้วยหรือ

12

ฉันเดาว่าฮาร์ดแวร์พื้นฐานยังคงใช้การลงทะเบียน 32 บิตเพื่อเก็บ int8_t นั้น เนื่องจากข้อกำหนดไม่ได้กำหนดลักษณะการทำงานสำหรับโอเวอร์โฟลว์การใช้งานจึงไม่ตรวจสอบการโอเวอร์โฟลว์และอนุญาตให้จัดเก็บค่าที่มากขึ้นด้วย


หากคุณทำเครื่องหมายตัวแปรโลคัลในขณะที่volatileคุณกำลังบังคับให้ใช้หน่วยความจำและส่งผลให้ได้รับค่าที่คาดหวังภายในช่วง


1
โอ้ว้าว. ฉันลืมไปว่าแอสเซมบลีที่คอมไพล์แล้วจะเก็บตัวแปรโลคัลไว้ในรีจิสเตอร์หากทำได้ ดูเหมือนจะเป็นคำตอบที่เป็นไปprintfได้มากที่สุดพร้อมกับไม่สนใจsizeofค่ารูปแบบ
rliu

3
@roliu เรียกใช้ g ++ -O2 -S code.cpp แล้วคุณจะเห็นแอสเซมบลี ยิ่งไปกว่านั้น printf () เป็นฟังก์ชันอาร์กิวเมนต์ตัวแปรดังนั้นอาร์กิวเมนต์ที่มีอันดับน้อยกว่า int จะได้รับการเลื่อนระดับเป็น int
เลขที่

@nos ฉันต้องการที่จะ ฉันไม่สามารถติดตั้ง UEFI boot loader (โดยเฉพาะ rEFInd) เพื่อให้ archlinux ทำงานบนเครื่องของฉันดังนั้นฉันจึงไม่ได้เข้ารหัสด้วยเครื่องมือ GNU มาเป็นเวลานาน ฉันจะไปให้ถึง ... ในที่สุด ตอนนี้เป็นแค่ C # ใน VS และพยายามจำ C / เรียนรู้ C ++ :)
rliu

@rollu เรียกใช้ในเครื่องเสมือนเช่น VirtualBox
nos

@nos ไม่อยากทำให้หัวข้อตกราง แต่ใช่ฉันทำได้ ฉันสามารถติดตั้ง linux ด้วย BIOS bootloader ได้ ฉันแค่ดื้อรั้นและถ้าฉันไม่สามารถใช้งานกับ UEFI bootloader ได้ฉันก็คงไม่ทำให้มันทำงานได้เลย: P.
rliu

11

รหัสแอสเซมเบลอร์เผยปัญหา:

:loop
mov esi, ebx
xor eax, eax
mov edi, OFFSET FLAT:.LC2   ;"c: %i\n"
sub ebx, 1
call    printf
cmp ebx, -301
jne loop

mov esi, -45
mov edi, OFFSET FLAT:.LC2   ;"c: %i\n"
xor eax, eax
call    printf

EBX ควรจะขึ้นต้นด้วย FF หลังการลดหรือควรใช้เฉพาะ BL กับส่วนที่เหลือของ EBX ที่ชัดเจน อยากรู้ว่ามันใช้ sub แทน dec. -45 นั้นดูลึกลับ มันคือการกลับบิตของ 300 & 255 = 44. -45 = ~ 44 มีการเชื่อมต่ออยู่ที่ไหนสักแห่ง

ต้องผ่านการทำงานมากขึ้นโดยใช้ c = c - 1:

mov eax, ebx
mov edi, OFFSET FLAT:.LC2   ;"c: %i\n"
add ebx, 1
not eax
movsx   ebp, al                 ;uses only the lower 8 bits
xor eax, eax
mov esi, ebp

จากนั้นใช้เฉพาะส่วนต่ำของ RAX ดังนั้นจึง จำกัด ไว้ที่ -128 ถึง 127 ตัวเลือกคอมไพเลอร์ "-g -O2"

หากไม่มีการเพิ่มประสิทธิภาพจะสร้างรหัสที่ถูกต้อง:

movzx   eax, BYTE PTR [rbp-1]
sub eax, 1
mov BYTE PTR [rbp-1], al
movsx   edx, BYTE PTR [rbp-1]
mov eax, OFFSET FLAT:.LC2   ;"c: %i\n"
mov esi, edx

ดังนั้นจึงเป็นจุดบกพร่องในเครื่องมือเพิ่มประสิทธิภาพ


4

ใช้%hhdแทน%i! ควรแก้ปัญหาของคุณ.

สิ่งที่คุณเห็นคือผลลัพธ์ของการเพิ่มประสิทธิภาพคอมไพเลอร์รวมกับการที่คุณบอกให้ printf พิมพ์หมายเลข 32 บิตจากนั้นจึงกดหมายเลข (8 บิตที่คาดคะเน) ลงบนสแต็กซึ่งเป็นขนาดตัวชี้จริง ๆ เพราะนี่คือวิธีการทำงานของ push opcode ใน x86


1
ฉันสามารถจำลองพฤติกรรมดั้งเดิมในระบบของฉันโดยใช้g++ -O3. การเปลี่ยน%iเป็น%hhdไม่ได้เปลี่ยนอะไร
Keith Thompson

3

ฉันคิดว่าสิ่งนี้ทำโดยการเพิ่มประสิทธิภาพของโค้ด:

for (int32_t i = 0; i <= 300; i++)
      printf("c: %i\n", c--);

ตัวรวบรวมใช้int32_t iตัวแปรทั้งสำหรับiและc. ปิดการเพิ่มประสิทธิภาพหรือทำการแคสต์โดยตรง printf("c: %i\n", (int8_t)c--);


จากนั้นปิดการเพิ่มประสิทธิภาพ หรือทำสิ่งนี้:(int8_t)(c & 0x0000ffff)--
Vsevolod

1

cเป็นตัวกำหนดให้เป็นint8_tแต่เมื่อใช้งาน++หรือ--มากกว่าint8_tจะถูกแปลงโดยปริยายแรกที่intและผลการดำเนินงานแทนค่าภายในของคถูกพิมพ์ด้วย printf intที่เกิดขึ้นจะ

ดูค่าที่แท้จริงของcafter whole loop โดยเฉพาะหลังจากการลดลงครั้งสุดท้าย

-301 + 256 = -45 (since it revolved entire 8 bit range once)

เป็นค่าที่ถูกต้องซึ่งคล้ายกับพฤติกรรม -128 + 1 = 127

cเริ่มที่จะใช้intหน่วยความจำขนาด แต่พิมพ์เป็นเมื่อพิมพ์เป็นตัวเองโดยใช้เพียงint8_t 8 bitsใช้ประโยชน์ทั้งหมด32 bitsเมื่อใช้เป็นไฟล์int

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


0

ฉันคิดว่ามันเกิดขึ้นเพราะลูปของคุณจะดำเนินไปจนกระทั่ง int i จะกลายเป็น 300 และ c กลายเป็น -300 และค่าสุดท้ายเป็นเพราะ

printf("c: %i\n", c);

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