จุดใดในลูปที่ทำให้จำนวนเต็มล้นกลายเป็นพฤติกรรมที่ไม่ได้กำหนด


86

นี่เป็นตัวอย่างเพื่ออธิบายคำถามของฉันซึ่งเกี่ยวข้องกับโค้ดที่ซับซ้อนกว่านี้ซึ่งฉันไม่สามารถโพสต์ได้ที่นี่

#include <stdio.h>
int main()
{
    int a = 0;
    for (int i = 0; i < 3; i++)
    {
        printf("Hello\n");
        a = a + 1000000000;
    }
}

โปรแกรมนี้มีพฤติกรรมที่ไม่ได้กำหนดบนแพลตฟอร์มของฉันเพราะaจะล้นในลูปที่ 3

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

(ติดแท็ก C และ C ++ แม้ว่าจะแตกต่างกันเพราะฉันสนใจคำตอบสำหรับทั้งสองภาษาหากแตกต่างกัน)


7
สงสัยว่าคอมไพเลอร์สามารถทำงานออกที่aไม่ได้ใช้ (ยกเว้นสำหรับการคำนวณเอง) และเพียงลบa
4386427

12
คุณอาจสนุกกับMy Little Optimizer: Undefined Behavior is Magicจาก CppCon ในปีนี้ ทั้งหมดนี้เกี่ยวกับสิ่งที่คอมไพเลอร์การเพิ่มประสิทธิภาพสามารถดำเนินการได้ตามพฤติกรรมที่ไม่ได้กำหนด
TartanLlama



คำตอบ:


108

หากคุณสนใจคำตอบเชิงทฤษฎีล้วนๆมาตรฐาน C ++ ช่วยให้พฤติกรรมที่ไม่ได้กำหนดเป็น "การเดินทางข้ามเวลา":

[intro.execution]/5: การดำเนินการที่สอดคล้องกันซึ่งเรียกใช้โปรแกรมที่มีรูปแบบที่ดีจะก่อให้เกิดพฤติกรรมที่สังเกตได้เช่นเดียวกับหนึ่งในการดำเนินการที่เป็นไปได้ของอินสแตนซ์ที่เกี่ยวข้องของเครื่องนามธรรมที่มีโปรแกรมเดียวกันและอินพุตเดียวกัน อย่างไรก็ตามหากการดำเนินการใด ๆ ดังกล่าวมีการดำเนินการที่ไม่ได้กำหนดมาตรฐานสากลฉบับนี้ไม่ได้กำหนดข้อกำหนดใด ๆ เกี่ยวกับการดำเนินการตามโปรแกรมนั้นด้วยอินพุตนั้น (ไม่ได้เกี่ยวข้องกับการดำเนินการก่อนการดำเนินการที่ไม่ได้กำหนดไว้เป็นครั้งแรก)

ดังนั้นหากโปรแกรมของคุณมีพฤติกรรมที่ไม่ได้กำหนดพฤติกรรมของโปรแกรมทั้งหมดของคุณจะไม่ถูกกำหนด


4
@KeithThompson: แต่sneeze()ฟังก์ชั่นนั้นไม่ได้กำหนดไว้ในคลาสใด ๆDemon(ซึ่งความหลากหลายของจมูกเป็นคลาสย่อย) ทำให้สิ่งทั้งหมดเป็นวงกลมไปเรื่อย ๆ
Sebastian Lenartowicz

1
แต่ printf อาจไม่กลับมาดังนั้นจึงมีการกำหนดสองรอบแรกเนื่องจากจนกว่าจะเสร็จสิ้นจะไม่ชัดเจนว่าจะมี UB หรือไม่ ดูstackoverflow.com/questions/23153445/…
usr

1
นี่คือเหตุผลที่ในทางเทคนิคคอมไพเลอร์มีสิทธิ์ในการปล่อย "nop" สำหรับเคอร์เนล Linux (เนื่องจากโค้ด bootstrap อาศัยพฤติกรรมที่ไม่ได้กำหนด): blog.regehr.org/archives/761
Crashworks

3
@Crashworks และนั่นคือเหตุผลที่ Linux เขียนและคอมไพล์เป็นC. ที่ไม่สามารถพกพาได้ (เช่น superset ของ C ที่ต้องใช้คอมไพเลอร์เฉพาะที่มีตัวเลือกเฉพาะเช่น -fno-tight-aliasing)
user253751

3
@usr ฉันคาดหวังว่าจะมีการกำหนดไว้หากprintfไม่กลับมา แต่ถ้าprintfกำลังจะกลับมาพฤติกรรมที่ไม่ได้กำหนดอาจทำให้เกิดปัญหาก่อนที่จะprintfถูกเรียก ดังนั้นการเดินทางข้ามเวลา printf("Hello\n");จากนั้นบรรทัดถัดไปจะรวบรวมเป็นundoPrintf(); launchNuclearMissiles();
user253751

31

ก่อนอื่นให้ฉันแก้ไขชื่อของคำถามนี้:

พฤติกรรมที่ไม่ได้กำหนดไม่ใช่ (โดยเฉพาะ) ของขอบเขตของการดำเนินการ

พฤติกรรมที่ไม่ได้กำหนดมีผลต่อขั้นตอนทั้งหมด: การคอมไพล์การเชื่อมโยงการโหลดและการดำเนินการ

ตัวอย่างบางส่วนเพื่อประสานสิ่งนี้โปรดจำไว้ว่าไม่มีส่วนใดที่ครบถ้วนสมบูรณ์:

  • คอมไพเลอร์สามารถสันนิษฐานได้ว่าส่วนของโค้ดที่มี Undefined Behavior จะไม่ถูกเรียกใช้งานดังนั้นจึงถือว่าพา ธ การดำเนินการที่จะนำไปสู่โค้ดนั้นเป็นโค้ดที่ตายแล้ว ดูสิ่งที่โปรแกรมเมอร์ C ทุกคนควรรู้เกี่ยวกับพฤติกรรมที่ไม่ได้กำหนดโดยไม่มีใครอื่นนอกจาก Chris Lattner
  • ผู้เชื่อมโยงสามารถสันนิษฐานได้ว่าในการมีอยู่ของคำจำกัดความหลายคำของสัญลักษณ์ที่อ่อนแอ (ที่รู้จักโดยชื่อ) คำจำกัดความทั้งหมดจะเหมือนกันโดยใช้กฎคำจำกัดความเดียว
  • ตัวโหลด (ในกรณีที่คุณใช้ไลบรารีแบบไดนามิก) สามารถถือว่าเหมือนกันดังนั้นจึงเลือกสัญลักษณ์แรกที่พบ โดยปกติจะใช้ (ab) เพื่อดักฟังการโทรโดยใช้LD_PRELOADเทคนิคบน Unixes
  • การดำเนินการอาจล้มเหลว (SIGSEV) หากคุณใช้ตัวชี้ห้อย

นี่คือสิ่งที่น่ากลัวมากเกี่ยวกับพฤติกรรมที่ไม่ได้กำหนด: เป็นไปไม่ได้ที่จะคาดการณ์ล่วงหน้าว่าพฤติกรรมใดจะเกิดขึ้นแน่นอนและการคาดการณ์นี้จะต้องได้รับการทบทวนในการอัปเดต toolchain แต่ละครั้งระบบปฏิบัติการพื้นฐาน ...


ผมขอแนะนำให้ดูวิดีโอนี้โดยไมเคิลสเปนเซอร์ (LLVM Developer): CppCon 2016 My Little เพิ่มประสิทธิภาพ: พฤติกรรมที่ไม่ได้กำหนดเป็นเวทมนตร์


3
นี่คือสิ่งที่ฉันกังวล ในรหัสจริงของฉันมันซับซ้อน แต่ฉันอาจมีกรณีที่มันมักจะล้น และฉันไม่สนใจเรื่องนั้นมากนัก แต่ฉันกังวลว่าโค้ดที่ "ถูกต้อง" จะได้รับผลกระทบจากสิ่งนี้ด้วย เห็นได้ชัดว่าฉันต้องแก้ไข แต่การแก้ไขต้องใช้ความเข้าใจ :)
jcoder

8
@jcoder: มีทางหนีที่สำคัญอยู่ที่นี่ ไม่อนุญาตให้คอมไพเลอร์คาดเดาข้อมูลอินพุต ตราบเท่าที่มีอย่างน้อยหนึ่งอินพุตที่ไม่เกิดพฤติกรรมที่ไม่ได้กำหนดคอมไพลเลอร์จะต้องตรวจสอบให้แน่ใจว่าอินพุตเฉพาะนี้ยังคงสร้างเอาต์พุตที่ถูกต้อง การพูดคุยที่น่ากลัวทั้งหมดเกี่ยวกับการเพิ่มประสิทธิภาพที่เป็นอันตรายจะใช้กับUB ที่หลีกเลี่ยงไม่ได้เท่านั้น ในทางปฏิบัติถ้าคุณจะใช้argcเป็นจำนวนลูปเคสargc=1จะไม่สร้าง UB และคอมไพเลอร์จะถูกบังคับให้จัดการสิ่งนั้น
MSalters

@jcoder: ในกรณีนี้นี่ไม่ใช่รหัสตาย อย่างไรก็ตามคอมไพเลอร์อาจฉลาดพอที่จะอนุมานได้ว่าiไม่สามารถเพิ่มขึ้นได้มากกว่าNหลายครั้งดังนั้นค่าของมันจึงถูก จำกัด ไว้
Matthieu M.

4
@jcoder: ถ้าf(good);ทำบางอย่าง X และf(bad);เรียกใช้พฤติกรรมที่ไม่ได้กำหนดโปรแกรมที่เพิ่งเรียกใช้f(good);จะรับประกันว่าจะทำ X แต่f(good); f(bad);ไม่รับประกันว่าจะทำ X

4
@Hurkyl อื่น ๆ ที่น่าสนใจถ้ารหัสของคุณเป็นคอมไพเลอร์มาร์ทจะทิ้งเปรียบเทียบและการผลิตและไม่มีเงื่อนไขif(foo) f(good); else f(bad); foo(good)
John Dvorak

28

อุกอาจเพิ่มประสิทธิภาพ C หรือ C ++ กำหนดเป้าหมาย 16 บิตคอมไพเลอร์intจะรู้ว่าพฤติกรรมในการเพิ่ม1000000000ไปยังintประเภทจะไม่ได้กำหนด

ได้รับอนุญาตโดยมาตรฐานใด ๆ ที่จะทำสิ่งที่ต้องการซึ่งอาจรวมถึงการลบโปรแกรมทั้งหมดออกint main(){}ไป

แต่สิ่งที่ใหญ่กว่าint? ฉันไม่รู้จักคอมไพเลอร์ที่ทำสิ่งนี้ (และฉันไม่ใช่ผู้เชี่ยวชาญในการออกแบบคอมไพเลอร์ C และ C ++ ไม่ว่าจะด้วยวิธีใดก็ตาม) แต่ฉันคิดว่าบางครั้งคอมไพเลอร์ที่กำหนดเป้าหมายเป็น 32 บิตintขึ้นไปจะเข้าใจว่าลูป จะไม่มีที่สิ้นสุด ( iไม่เปลี่ยนแปลง) และเพื่อให้aในที่สุดจะล้น อีกครั้งมันสามารถเพิ่มประสิทธิภาพผลลัพธ์เป็นint main(){}. ประเด็นที่ฉันพยายามจะทำให้ที่นี่คือเมื่อการเพิ่มประสิทธิภาพคอมไพเลอร์มีความก้าวร้าวมากขึ้นเรื่อย ๆ โครงสร้างพฤติกรรมที่ไม่ได้กำหนดมากขึ้นกำลังแสดงตัวเองในรูปแบบที่ไม่คาดคิด

ความจริงที่ว่าลูปของคุณไม่มีที่สิ้นสุดนั้นไม่ได้อยู่ในตัวมันเองเนื่องจากคุณกำลังเขียนไปยังเอาต์พุตมาตรฐานในเนื้อหาของลูป


3
มาตรฐานอนุญาตให้ทำทุกอย่างที่ต้องการได้ก่อนที่พฤติกรรมที่ไม่ได้กำหนดจะปรากฏหรือไม่? สิ่งนี้ระบุไว้ที่ไหน?
jimifiki

4
ทำไมต้อง 16 บิต? ฉันเดาว่า OP กำลังมองหาการเซ็นชื่อแบบ 32 บิตล้น
4386427

8
@jimifiki ในมาตรฐาน. C ++ 14 (N4140) 1.3.24 "udnefined behavior = พฤติกรรมที่มาตรฐานสากลนี้ไม่ได้กำหนดข้อกำหนด" บวกกับข้อความที่มีความยาวซึ่งอธิบายอย่างละเอียด แต่ประเด็นก็คือมันไม่ใช่พฤติกรรมของ "คำสั่ง" ที่ไม่ได้กำหนด แต่เป็นพฤติกรรมของโปรแกรม นั่นหมายความว่าตราบใดที่ UB ถูกเรียกใช้โดยกฎในมาตรฐาน (หรือโดยไม่มีกฎ) มาตรฐานจะหยุดใช้สำหรับโปรแกรมโดยรวม ดังนั้นส่วนใดส่วนหนึ่งของโปรแกรมสามารถทำงานได้ตามที่ต้องการ
Angew ไม่ภูมิใจใน SO

5
คำสั่งแรกผิด หากintเป็นแบบ 16 บิตการเพิ่มจะเกิดขึ้นในlong(เนื่องจากตัวถูกดำเนินการตามตัวอักษรมีประเภทlong) ซึ่งมีการกำหนดไว้อย่างดีจากนั้นจะถูกแปลงโดยการแปลงที่กำหนดให้ใช้งานกลับไปintเป็น
R .. GitHub STOP HELPING ICE

2
@usr พฤติกรรมของprintfถูกกำหนดโดยมาตรฐานที่จะกลับมาเสมอ
MM

11

ในทางเทคนิคภายใต้มาตรฐาน C ++ หากโปรแกรมมีพฤติกรรมที่ไม่ได้กำหนดลักษณะการทำงานของโปรแกรมทั้งหมดแม้ในเวลาคอมไพล์ (ก่อนที่โปรแกรมจะทำงานด้วยซ้ำ) จะไม่ถูกกำหนด

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

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


จะเกิดอะไรขึ้นถ้าส่วน UB อยู่ในif(false) {}ขอบเขต? นั่นเป็นพิษต่อโปรแกรมทั้งหมดหรือไม่เนื่องจากคอมไพเลอร์สมมติว่าทุกสาขามี ~ ส่วนที่กำหนดไว้อย่างดีของตรรกะดังนั้นจึงดำเนินการบนสมมติฐานที่ผิดหรือไม่?
mlvljr

1
มาตรฐานไม่ได้กำหนดข้อกำหนดใด ๆ เกี่ยวกับพฤติกรรมที่ไม่ได้กำหนดไว้ดังนั้นในทางทฤษฎีแล้วใช่มันทำให้โปรแกรมทั้งหมดเป็นพิษ อย่างไรก็ตามในทางปฏิบัติคอมไพเลอร์การเพิ่มประสิทธิภาพใด ๆ มีแนวโน้มที่จะลบโค้ดที่ตายแล้วดังนั้นจึงอาจไม่มีผลต่อการเรียกใช้งาน คุณยังไม่ควรพึ่งพาพฤติกรรมนี้
bwDraco

สิ่งที่ควรทราบขอบคุณ :)
mlvljr

9

เพื่อให้เข้าใจว่าเหตุใดพฤติกรรมที่ไม่ได้กำหนดจึงสามารถ'เดินทางข้ามเวลา' ได้ตามที่ @TartanLlama ระบุไว้อย่างเพียงพอลองมาดูกฎ 'as-if':

1.9 การทำงานของโปรแกรม

1คำอธิบายเชิงความหมายในมาตรฐานสากลนี้กำหนดเครื่องจักรนามธรรมที่ไม่กำหนดพารามิเตอร์ มาตรฐานสากลฉบับนี้ไม่มีข้อกำหนดเกี่ยวกับโครงสร้างของการปฏิบัติตามการนำไปใช้ โดยเฉพาะอย่างยิ่งพวกเขาไม่จำเป็นต้องคัดลอกหรือเลียนแบบโครงสร้างของเครื่องจักรที่เป็นนามธรรม แต่การใช้งานที่สอดคล้องกันนั้นจำเป็นต้องเลียนแบบ (เท่านั้น) พฤติกรรมที่สังเกตได้ของเครื่องจักรนามธรรมตามที่อธิบายไว้ด้านล่าง

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

มาตรฐานกำหนดเฉพาะการแมประหว่างอินพุตและเอาต์พุตเท่านั้นไม่มีอะไรอื่น ทำได้โดยการอธิบาย 'กล่องดำตัวอย่าง' แต่บอกอย่างชัดเจนว่ากล่องดำอื่น ๆ ที่มีการแมปเดียวกันนั้นใช้ได้เท่าเทียมกัน ซึ่งหมายความว่าเนื้อหาของกล่องดำไม่เกี่ยวข้อง

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

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


6

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


6

คำตอบของ TartanLlama ถูกต้อง พฤติกรรมที่ไม่ได้กำหนดสามารถเกิดขึ้นได้ตลอดเวลาแม้ในช่วงเวลาคอมไพล์ สิ่งนี้อาจดูไร้สาระ แต่เป็นคุณสมบัติหลักในการอนุญาตให้คอมไพเลอร์ทำในสิ่งที่ต้องทำ ไม่ใช่เรื่องง่ายเสมอไปที่จะเป็นคอมไพเลอร์ คุณต้องทำสิ่งที่สเป็คบอกทุกครั้ง อย่างไรก็ตามบางครั้งอาจเป็นเรื่องยากอย่างมากที่จะพิสูจน์ว่ามีพฤติกรรมบางอย่างเกิดขึ้น หากคุณจำปัญหาการหยุดชะงักได้การพัฒนาซอฟต์แวร์นั้นค่อนข้างเป็นเรื่องเล็กน้อยซึ่งคุณไม่สามารถพิสูจน์ได้ว่ามันเสร็จสมบูรณ์หรือเข้าสู่ลูปที่ไม่มีที่สิ้นสุดเมื่อป้อนอินพุตเฉพาะ

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

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

if (currentPtr + numberOfNewChars > endOfBufferPtr) { doOverflowLogic(); }

มันดูมีสติพอ อย่างไรก็ตามจะเกิดอะไรขึ้นถ้า numberOfNewChars ใหญ่มากและล้น? จากนั้นมันจะล้อมรอบและกลายเป็นตัวชี้ที่เล็กกว่าendOfBufferPtrดังนั้นตรรกะล้นจะไม่ถูกเรียก ดังนั้นพวกเขาจึงเพิ่มการตรวจสอบครั้งที่สองก่อนหน้านั้น:

if (currentPtr + numberOfNewChars < currentPtr) { detectWrapAround(); }

ดูเหมือนว่าคุณดูแลข้อผิดพลาดของบัฟเฟอร์ล้นใช่ไหม? อย่างไรก็ตามมีการส่งข้อบกพร่องโดยระบุว่าบัฟเฟอร์นี้ล้นใน Debian เวอร์ชันใดเวอร์ชันหนึ่ง การตรวจสอบอย่างรอบคอบพบว่า Debian เวอร์ชันนี้เป็นรุ่นแรกที่ใช้ gcc เวอร์ชันที่มีเลือดออกโดยเฉพาะ ใน gcc เวอร์ชันนี้คอมไพลเลอร์ยอมรับว่า currentPtr + numberOfNewChars ไม่สามารถเป็นตัวชี้ที่เล็กกว่า currentPtr ได้เนื่องจาก overflow สำหรับพอยน์เตอร์เป็นพฤติกรรมที่ไม่ได้กำหนด! นั่นเพียงพอสำหรับ gcc ในการเพิ่มประสิทธิภาพการตรวจสอบทั้งหมดและทันใดนั้นคุณก็ไม่ได้รับการป้องกันจากบัฟเฟอร์ล้นแม้ว่าคุณจะเขียนโค้ดเพื่อตรวจสอบก็ตาม!

นี่คือพฤติกรรมเฉพาะ ทุกอย่างถูกต้องตามกฎหมาย (แม้ว่าจากที่ฉันได้ยินมา gcc ย้อนกลับการเปลี่ยนแปลงนี้ในเวอร์ชันถัดไป) ไม่ใช่สิ่งที่ฉันคิดว่าเป็นพฤติกรรมที่เข้าใจง่าย แต่ถ้าคุณยืดจินตนาการของคุณออกไปอีกหน่อยก็ง่ายที่จะเห็นว่าสถานการณ์ที่แปรปรวนเล็กน้อยนี้จะกลายเป็นปัญหาที่หยุดชะงักสำหรับคอมไพเลอร์ได้อย่างไร ด้วยเหตุนี้ผู้เขียน spec จึงทำให้มันเป็น "Undefined Behavior" และระบุว่าคอมไพเลอร์สามารถทำอะไรก็ได้ที่ถูกใจ


ฉันไม่ได้พิจารณาคอมไพเลอร์ที่น่าอัศจรรย์เป็นพิเศษซึ่งบางครั้งก็ทำงานราวกับว่าการคำนวณทางคณิตศาสตร์ที่เซ็นชื่อนั้นดำเนินการในประเภทที่มีช่วงขยายเกินกว่า "int" โดยเฉพาะอย่างยิ่งเมื่อพิจารณาว่าแม้จะสร้างโค้ดแบบตรงไปตรงมาบน x86 แต่ก็มีบางครั้งที่การทำเช่นนั้นมีประสิทธิภาพมากกว่าการตัดทอนตัวกลาง ผล. สิ่งที่น่าประหลาดใจกว่านั้นคือเมื่อการล้นมีผลต่อการคำนวณอื่น ๆซึ่งสามารถเกิดขึ้นได้ใน gcc แม้ว่าโค้ดจะเก็บผลคูณของค่า uint16_t สองค่าไว้ใน uint32_t ซึ่งเป็นการดำเนินการที่ไม่ควรมีเหตุผลที่เป็นไปได้ที่จะทำให้เกิดความประหลาดใจในโครงสร้างที่ไม่ฆ่าเชื้อ
supercat

แน่นอนว่าการตรวจสอบที่ถูกต้องจะเป็นif(numberOfNewChars > endOfBufferPtr - currentPtr)เพราะ numberOfNewChars ไม่สามารถเป็นค่าลบได้และ currentPtr จะชี้ไปที่ที่ใดที่หนึ่งภายในบัฟเฟอร์ที่คุณไม่จำเป็นต้องใช้การตรวจสอบแบบ "สรุป" ที่ไร้สาระ (ฉันไม่คิดว่ารหัสที่คุณให้มาจะมีความหวังในการทำงานในบัฟเฟอร์แบบวงกลม - คุณได้ทิ้งสิ่งที่จำเป็นสำหรับสิ่งนั้นไว้ในการถอดความดังนั้นฉันจึงเพิกเฉยต่อกรณีนั้นเช่นกัน)
Random832

@ Random832 ฉันทิ้งตันไปแล้ว ฉันพยายามอ้างถึงบริบทที่กว้างขึ้น แต่เนื่องจากฉันสูญเสียแหล่งที่มาฉันพบว่าการถอดความบริบททำให้ฉันมีปัญหามากขึ้นดังนั้นฉันจึงปล่อยมันออกไป ฉันต้องการค้นหารายงานข้อผิดพลาดดังกล่าวจริงๆเพื่อที่ฉันจะได้พูดได้อย่างถูกต้อง เป็นตัวอย่างที่มีประสิทธิภาพในการคิดว่าคุณเขียนโค้ดทางเดียวและรวบรวมได้แตกต่างไปจากเดิมอย่างสิ้นเชิง
Cort Ammon

นี่เป็นปัญหาใหญ่ที่สุดของฉันเกี่ยวกับพฤติกรรมที่ไม่ได้กำหนด ทำให้บางครั้งไม่สามารถเขียนโค้ดที่ถูกต้องได้และเมื่อคอมไพลเลอร์ตรวจพบโดยค่าเริ่มต้นจะไม่บอกคุณว่ามันถูกทริกเกอร์พฤติกรรมที่ไม่ได้กำหนด ในกรณีนี้ผู้ใช้เพียงแค่ต้องการทำเลขคณิต - ตัวชี้หรือไม่ - และการทำงานอย่างหนักทั้งหมดในการเขียนรหัสความปลอดภัยก็ถูกยกเลิก อย่างน้อยก็ควรมีวิธีการใส่คำอธิบายประกอบส่วนของโค้ดที่จะพูด - ไม่มีการเพิ่มประสิทธิภาพแบบแฟนซีที่นี่ C / C ++ ถูกใช้ในพื้นที่วิกฤตมากเกินไปเพื่อให้สถานการณ์ที่เป็นอันตรายนี้ดำเนินต่อไปเพื่อสนับสนุนการเพิ่มประสิทธิภาพ
John McGrath

4

นอกเหนือจากคำตอบทางทฤษฎีแล้วข้อสังเกตในทางปฏิบัติก็คือเป็นเวลานานที่คอมไพเลอร์ได้ใช้การแปลงต่างๆบนลูปเพื่อลดปริมาณงานที่ทำภายในพวกเขา ตัวอย่างเช่นกำหนด:

for (int i=0; i<n; i++)
  foo[i] = i*scale;

คอมไพเลอร์อาจแปลงเป็น:

int temp = 0;
for (int i=0; i<n; i++)
{
  foo[i] = temp;
  temp+=scale;
}

จึงประหยัดการคูณด้วยการวนซ้ำทุกครั้ง รูปแบบการเพิ่มประสิทธิภาพเพิ่มเติมซึ่งคอมไพเลอร์ปรับตามระดับความก้าวร้าวที่แตกต่างกันจะเปลี่ยนเป็น:

if (n > 0)
{
  int temp1 = n*scale;
  int *temp2 = foo;
  do
  {
    temp1 -= scale;
    *temp2++ = temp1;
  } while(temp1);
}

แม้ในเครื่องที่มีการปิดเสียงแบบเงียบเมื่อมีการโอเวอร์โฟลว์ซึ่งอาจทำงานผิดพลาดได้หากมีจำนวนน้อยกว่า n ซึ่งเมื่อคูณด้วยสเกลจะให้ผลเป็น 0 นอกจากนี้ยังสามารถเปลี่ยนเป็นลูปที่ไม่มีที่สิ้นสุดหากมีการอ่านมาตราส่วนจากหน่วยความจำมากกว่าหนึ่งครั้งและบางอย่าง เปลี่ยนค่าโดยไม่คาดคิด (ในกรณีใดก็ตามที่ "สเกล" สามารถเปลี่ยนลูปกลางโดยไม่ต้องเรียกใช้ UB คอมไพเลอร์จะไม่ได้รับอนุญาตให้ทำการเพิ่มประสิทธิภาพ)

แม้ว่าการเพิ่มประสิทธิภาพดังกล่าวส่วนใหญ่จะไม่มีปัญหาใด ๆ ในกรณีที่มีการคูณสองประเภทที่ไม่ได้ลงนามสั้น ๆ เพื่อให้ได้ค่าที่อยู่ระหว่าง INT_MAX + 1 และ UINT_MAX แต่ gcc มีบางกรณีที่การคูณดังกล่าวภายในลูปอาจทำให้ลูปออกก่อนกำหนด . ฉันไม่ได้สังเกตเห็นพฤติกรรมดังกล่าวเกิดจากคำแนะนำการเปรียบเทียบในโค้ดที่สร้างขึ้น แต่เป็นที่สังเกตได้ในกรณีที่คอมไพเลอร์ใช้โอเวอร์โฟลว์เพื่ออนุมานว่าลูปสามารถทำงานได้สูงสุด 4 ครั้งหรือน้อยกว่านั้น โดยค่าเริ่มต้นจะไม่สร้างคำเตือนในกรณีที่อินพุตบางอย่างอาจทำให้เกิด UB และอื่น ๆ จะไม่แม้ว่าการอนุมานจะทำให้ขอบเขตด้านบนของลูปถูกละเว้น


4

พฤติกรรมที่ไม่ได้กำหนดคือตามนิยามพื้นที่สีเทา คุณก็ไม่สามารถคาดการณ์สิ่งที่จะหรือจะไม่ทำ - นั่นคือสิ่งที่ "ไม่ได้กำหนดพฤติกรรม" หมายถึง

ตั้งแต่นั้นเป็นต้นมาโปรแกรมเมอร์ก็พยายามกอบกู้ส่วนที่เหลือของการกำหนดไว้เสมอจากสถานการณ์ที่ไม่ได้กำหนด พวกเขามีรหัสบางอย่างที่พวกเขาต้องการใช้จริง ๆ แต่กลับกลายเป็นว่าไม่มีการกำหนดดังนั้นพวกเขาจึงพยายามโต้แย้ง: "ฉันรู้ว่ามันไม่ได้กำหนด แต่แน่นอนว่ามันจะเลวร้ายที่สุดที่จะทำสิ่งนี้หรือสิ่งนี้มันจะไม่มีวันทำอย่างนั้น .” และบางครั้งข้อโต้แย้งเหล่านี้ก็ถูกมากหรือน้อย แต่มักจะผิด และในขณะที่คอมไพเลอร์ฉลาดขึ้นและฉลาดขึ้น (หรือบางคนอาจพูดว่าแอบชอบและแอบดู) ขอบเขตของคำถามก็เปลี่ยนไปเรื่อย ๆ

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


และนี่คือสิ่งที่ ... คอมไพเลอร์สามารถใช้พฤติกรรมที่ไม่ได้กำหนดเพื่อเพิ่มประสิทธิภาพ แต่โดยทั่วไปแล้วพวกเขาจะไม่บอกคุณ ดังนั้นหากเรามีเครื่องมือที่ยอดเยี่ยมนี้ที่คุณต้องหลีกเลี่ยงการทำ X โดยไม่มีค่าใช้จ่ายทำไมคอมไพเลอร์ไม่สามารถแจ้งเตือนคุณเพื่อให้คุณแก้ไขได้
Jason S

1

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

อย่างไรก็ตามแน่นอนว่าสิ่งนี้ไม่ได้กำหนดไว้เนื่องจากไม่ได้กำหนดการเพิ่มประสิทธิภาพ :)


1
ไม่มีเหตุผลที่จะพิจารณาการเพิ่มประสิทธิภาพเมื่อพิจารณาว่าพฤติกรรมนั้นไม่ได้กำหนดไว้หรือไม่
Keith Thompson

2
ความจริงที่ว่าโปรแกรมทำงานตามที่ใคร ๆ คิดว่ามันไม่ควรหมายความว่าพฤติกรรมที่ไม่ได้กำหนด "หายไป" พฤติกรรมยังไม่ได้กำหนดและคุณพึ่งโชค ความจริงที่ว่าพฤติกรรมของโปรแกรมสามารถเปลี่ยนแปลงได้โดยอาศัยตัวเลือกคอมไพเลอร์เป็นตัวบ่งชี้ที่ชัดเจนว่าพฤติกรรมนั้นไม่ได้กำหนดไว้
Jordan Melo

@JordanMelo เนื่องจากคำตอบก่อนหน้านี้กล่าวถึงการเพิ่มประสิทธิภาพ (และ OP ถามเป็นพิเศษเกี่ยวกับเรื่องนี้) ฉันได้กล่าวถึงคุณลักษณะของการเพิ่มประสิทธิภาพซึ่งไม่มีคำตอบก่อนหน้านี้ ฉันยังชี้ให้เห็นว่าแม้ว่าการเพิ่มประสิทธิภาพจะสามารถลบออกได้ แต่การพึ่งพาการปรับให้เหมาะสมเพื่อทำงานในลักษณะใดวิธีหนึ่งนั้นไม่ได้กำหนดไว้อีก ฉันไม่แนะนำอย่างแน่นอน! :)
Graham

@KeithThompson แน่นอน แต่ OP ถามเป็นพิเศษเกี่ยวกับการเพิ่มประสิทธิภาพและผลกระทบต่อพฤติกรรมที่ไม่ได้กำหนดที่เขาจะเห็นบนแพลตฟอร์มของเขา พฤติกรรมเฉพาะนั้นอาจหายไปขึ้นอยู่กับการเพิ่มประสิทธิภาพ ดังที่ฉันได้กล่าวไว้ในคำตอบของฉันความไม่กำหนดจะไม่เป็นเช่นนั้น
Graham

0

เนื่องจากคำถามนี้ติดแท็กคู่ C และ C ++ ฉันจะพยายามแก้ไขทั้งสองอย่าง C และ C ++ ใช้แนวทางที่แตกต่างกันที่นี่

ใน C การนำไปใช้ต้องสามารถพิสูจน์ได้ว่าจะมีการเรียกใช้พฤติกรรมที่ไม่ได้กำหนดเพื่อปฏิบัติกับโปรแกรมทั้งหมดราวกับว่ามีพฤติกรรมที่ไม่ได้กำหนดไว้ ในตัวอย่าง OPs ดูเหมือนว่าจะเป็นเรื่องเล็กน้อยสำหรับคอมไพลเลอร์ที่จะพิสูจน์สิ่งนั้นดังนั้นจึงเหมือนกับว่าโปรแกรมทั้งหมดไม่ได้กำหนดไว้

เราสามารถเห็นสิ่งนี้ได้จากรายงานข้อบกพร่อง 109ซึ่งประเด็นสำคัญถามว่า:

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

int array1[5];
int array2[5];
int *p1 = &array1[0];
int *p2 = &array2[0];

int foo()
{
int i;
i = (p1 > p2); /* Must this be "successfully translated"? */
1/0; /* Must this be "successfully translated"? */
return 0;
}

คำถามสำคัญที่สุดคือโค้ดด้านบนต้อง "แปลสำเร็จ" (ไม่ว่าจะหมายถึงอะไร) หรือไม่? (ดูเชิงอรรถที่แนบมากับข้อย่อย 5.1.1.3)

และคำตอบคือ:

มาตรฐาน C ใช้คำว่า "มูลค่าที่ไม่แน่นอน" ไม่ใช่ "มูลค่าที่ไม่ได้กำหนด" การใช้วัตถุที่มีมูลค่าไม่แน่นอนจะส่งผลให้เกิดพฤติกรรมที่ไม่ได้กำหนด เชิงอรรถของข้อ 5.1.1.3 ชี้ให้เห็นว่าการนำไปใช้นั้นมีอิสระในการสร้างการวินิจฉัยจำนวนเท่าใดก็ได้ตราบเท่าที่โปรแกรมที่ถูกต้องยังคงได้รับการแปลอย่างถูกต้อง หากนิพจน์ที่มีการอพยพจะส่งผลให้เกิดพฤติกรรมที่ไม่ได้กำหนดปรากฏขึ้นในบริบทที่จำเป็นต้องใช้นิพจน์คงที่โปรแกรมที่มีจะไม่สอดคล้องกันอย่างเคร่งครัด นอกจากนี้หากการดำเนินการที่เป็นไปได้ของโปรแกรมที่กำหนดทุกครั้งจะส่งผลให้เกิดพฤติกรรมที่ไม่ได้กำหนดโปรแกรมที่กำหนดจะไม่สอดคล้องอย่างเคร่งครัด การใช้งานที่สอดคล้องต้องไม่ล้มเหลวในการแปลโปรแกรมที่สอดคล้องอย่างเคร่งครัดเพียงเพราะการดำเนินการบางอย่างที่เป็นไปได้ของโปรแกรมนั้นจะส่งผลให้เกิดพฤติกรรมที่ไม่ได้กำหนด เนื่องจากไม่สามารถเรียก foo ได้ตัวอย่างที่ระบุจะต้องได้รับการแปลสำเร็จโดยการใช้งานที่สอดคล้องกัน

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

เรามี[intro.abstrac] p5ซึ่งระบุว่า:

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


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

@supercat ฉันเชื่อว่านั่นคือสิ่งที่ฉันตอบสำหรับ C อย่างน้อยที่สุด
Shafik Yaghmour

ฉันคิดว่าเช่นเดียวกันกับข้อความที่ยกมา re C ++ เนื่องจากวลี "การดำเนินการใด ๆ เช่นนี้" หมายถึงวิธีที่โปรแกรมสามารถดำเนินการกับข้อมูลที่ระบุ หากอินพุตเฉพาะไม่สามารถส่งผลให้ฟังก์ชันทำงานได้ฉันไม่เห็นสิ่งใดในข้อความที่ยกมาเพื่อบอกว่าสิ่งใดก็ตามในฟังก์ชันดังกล่าวจะส่งผลให้เกิด UB
supercat

-2

คำตอบด้านบนคือความเข้าใจผิดที่ผิด (แต่เป็นเรื่องธรรมดา):

พฤติกรรมที่ไม่ได้กำหนดเป็นคุณสมบัติรันไทม์ * มันCAN NOT "เวลาเดินทาง"!

การดำเนินการบางอย่างถูกกำหนด (ตามมาตรฐาน) เพื่อให้มีผลข้างเคียงและไม่สามารถปรับให้เหมาะสมได้ การดำเนินการที่ทำ I / O หรือที่เข้าถึงvolatileตัวแปรจัดอยู่ในประเภทนี้

อย่างไรก็ตามมีข้อแม้: UB อาจเป็นพฤติกรรมใด ๆรวมถึงพฤติกรรมที่ยกเลิกการดำเนินการก่อนหน้านี้ ซึ่งอาจส่งผลที่คล้ายกันในบางกรณีกับการเพิ่มประสิทธิภาพโค้ดก่อนหน้านี้

ในความเป็นจริงสิ่งนี้สอดคล้องกับคำพูดในคำตอบด้านบน (เน้นของฉัน):

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

ใช่คำพูดนี้กล่าวว่า"ไม่ได้เกี่ยวกับการดำเนินการก่อนการดำเนินการที่ไม่ได้กำหนดครั้งแรก"แต่โปรดสังเกตว่านี่เป็นเรื่องเกี่ยวกับโค้ดที่กำลังดำเนินการโดยเฉพาะไม่ใช่เพียงการรวบรวม
ท้ายที่สุดพฤติกรรมที่ไม่ได้กำหนดที่ไม่สามารถเข้าถึงได้จริงจะไม่ทำอะไรเลยและสำหรับบรรทัดที่มี UB ที่จะถึงจริงรหัสที่นำหน้าจะต้องดำเนินการก่อน!

ใช่เมื่อดำเนินการ UBผลกระทบใด ๆ ของการดำเนินการก่อนหน้านี้จะไม่ถูกกำหนด แต่จนกว่าจะเป็นเช่นนั้นการทำงานของโปรแกรมจะถูกกำหนดไว้อย่างดี

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

* หมายเหตุ:นี่คือไม่สอดคล้องกับUB เกิดขึ้นที่รวบรวมเวลา หากคอมไพเลอร์สามารถพิสูจน์ได้ว่ารหัส UB จะถูกเรียกใช้สำหรับอินพุตทั้งหมดเสมอดังนั้น UB สามารถขยายเวลาคอมไพล์ได้ อย่างไรก็ตามสิ่งนี้ต้องการให้ทราบว่าโค้ดก่อนหน้าทั้งหมดจะส่งคืนในที่สุดซึ่งเป็นข้อกำหนดที่เข้มงวด อีกครั้งดูตัวอย่าง / คำอธิบายด้านล่าง


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

printf("foo");
getchar();
*(char*)1 = 1;

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

หากไม่มีgetchar()บรรทัดนั้นจะเป็นเรื่องถูกกฎหมายที่จะปรับให้เหมาะสมที่สุดก็ต่อเมื่อสิ่งนั้นจะแยกไม่ออกจากการแสดงผลfooแล้ว "ไม่ทำ"

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

  • หากสามารถบล็อกที่นี่ได้แสดงว่าโปรแกรมอื่นสามารถปฏิเสธที่จะอ่านผลลัพธ์ทั้งหมดและอาจไม่กลับมาอีกและด้วยเหตุนี้ UB อาจไม่เกิดขึ้นจริง

  • ถ้ามันสามารถกลับมาที่นี่ได้ทันทีเราก็รู้ว่ามันจะต้องกลับมาดังนั้นการปรับให้เหมาะสมที่สุดจึงแยกไม่ออกจากการเรียกใช้งานและยกเลิกการทำเอฟเฟกต์ของมัน

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


1
คุณกำลังอ่านมาตรฐานผิดโดยสิ้นเชิง มันบอกว่าพฤติกรรมเมื่อรันโปรแกรมไม่ได้กำหนด ระยะเวลา คำตอบนี้ผิด 100% มาตรฐานมีความชัดเจนมาก - การรันโปรแกรมที่มีอินพุตที่สร้าง UB ณ จุดใดก็ได้ในขั้นตอนการดำเนินการที่ไร้เดียงสานั้นไม่ได้กำหนดไว้
David Schwartz

@DavidSchwartz: ถ้าคุณทำตามการตีความของคุณไปสู่ข้อสรุปเชิงตรรกะคุณควรตระหนักว่ามันไม่สมเหตุสมผล ข้อมูลเข้าไม่ใช่สิ่งที่กำหนดอย่างสมบูรณ์เมื่อโปรแกรมเริ่มทำงาน การป้อนข้อมูลในการเขียนโปรแกรม (แม้เพียงของการแสดงตน ) ที่บรรทัดใดก็ตามได้รับอนุญาตให้ขึ้นอยู่กับทุกผลข้างเคียงของโปรแกรมจนถึงบรรทัดนั้น ดังนั้นโปรแกรมจึงไม่สามารถหลีกเลี่ยงการสร้างผลข้างเคียงที่มาก่อนบรรทัด UB ได้เนื่องจากต้องมีปฏิสัมพันธ์กับสภาพแวดล้อมดังนั้นจึงมีผลต่อการที่จะถึงบรรทัด UB หรือไม่ในตอนแรก
user541686

3
นั่นไม่สำคัญ จริงๆ. อีกครั้งคุณขาดจินตนาการ ตัวอย่างเช่นหากคอมไพเลอร์สามารถบอกได้ว่าไม่มีรหัสที่สอดคล้องกันสามารถบอกความแตกต่างได้ก็สามารถย้ายโค้ดที่เป็น UB ให้ส่วนที่เป็น UB ดำเนินการก่อนผลลัพธ์ที่คุณคาดว่าจะเป็น "นำหน้า" อย่างไร้เดียงสา
David Schwartz

2
@ Mehrdad: บางทีวิธีที่ดีกว่าในการพูดสิ่งต่าง ๆ ก็คือการบอกว่า UB ไม่สามารถย้อนเวลากลับไปยังจุดสุดท้ายที่อาจมีบางสิ่งเกิดขึ้นในโลกแห่งความเป็นจริงที่จะกำหนดพฤติกรรมได้ หากการใช้งานสามารถตรวจสอบได้โดยการตรวจสอบบัฟเฟอร์อินพุตว่าไม่มีทางใด ๆ จากการเรียก 1,000 ครั้งถัดไปที่ getchar () สามารถบล็อกได้และยังสามารถระบุได้ว่า UB จะเกิดขึ้นหลังจากการโทรครั้งที่ 1000 ก็ไม่จำเป็นต้องดำเนินการใด ๆ การโทร อย่างไรก็ตามหากการใช้งานต้องระบุว่าการดำเนินการจะไม่ผ่าน getchar () จนกว่าเอาต์พุตก่อนหน้าทั้งหมดจะมี ...
supercat

2
... ถูกส่งไปยังเทอร์มินัล 300-baud และ control-C ใด ๆ ที่เกิดขึ้นก่อนหน้านั้นจะทำให้ getchar () เพิ่มสัญญาณแม้ว่าจะมีอักขระอื่นในบัฟเฟอร์นำหน้าก็ตามการใช้งานดังกล่าวไม่สามารถทำได้ ย้าย UB ใด ๆ ผ่านเอาต์พุตสุดท้ายก่อน getchar () สิ่งที่ยุ่งยากคือการรู้ว่าในกรณีใดที่ควรคาดหวังว่าคอมไพเลอร์จะส่งผ่านโปรแกรมเมอร์พฤติกรรมใด ๆ ที่รับประกันว่าการใช้งานไลบรารีอาจนำเสนอนอกเหนือจากที่มาตรฐานกำหนด
supercat
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.