มันถูกกฎหมายหรือไม่สำหรับซอร์สโค้ดที่มีพฤติกรรมที่ไม่ได้กำหนดเพื่อทำให้คอมไพลเลอร์พัง?


85

สมมติว่าฉันไปรวบรวมซอร์สโค้ด C ++ ที่เขียนไม่ดีซึ่งเรียกใช้พฤติกรรมที่ไม่ได้กำหนดดังนั้น (ตามที่พวกเขาพูด) "อะไรก็เกิดขึ้นได้"

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


22
"UB คือ UB อยู่กับมัน" ... ไม่ต้องรอ "กรุณาโพสต์ MCVE" ... ไม่รอ. ฉันชอบคำถามสำหรับการตอบสนองทั้งหมดที่ก่อให้เกิดความไม่เหมาะสม :-)
Yunnosch

14
ไม่มีข้อ จำกัด จริงๆนั่นคือเหตุผลที่บอกว่า UB สามารถเรียกปีศาจจมูกได้
โปรแกรมเมอร์บางคน

15
UB สามารถทำให้ผู้เขียนโพสต์คำถามใน SO : P
Tanveer Badar

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

9
@LeifWillerts นี่ย้อนกลับไปในยุค 80 ฉันจำโครงสร้างที่แน่นอนไม่ได้ แต่คิดว่ามันขึ้นอยู่กับการใช้ประเภทตัวแปรที่ซับซ้อน หลังจากที่ฉันแทนที่ฉันมีช่วงเวลา "คิดอะไรอยู่ - สิ่งต่างๆไม่เป็นไปตามนั้น" ฉันไม่ได้ตำหนิคอมไพเลอร์ที่ปฏิเสธการสร้างเพียงเพื่อรีบูตเครื่อง สงสัยจะมีใครเจอคอมไพเลอร์วันนี้ เป็นคอมไพเลอร์ข้าม HP C สำหรับ HP 64000 ที่กำหนดเป้าหมายไปที่ไมโครโปรเซสเซอร์ 68000
Avi Berger

คำตอบ:


71

นิยามเชิงบรรทัดฐานของพฤติกรรมที่ไม่ได้กำหนดมีดังนี้:

[defns.undefined]

พฤติกรรมที่มาตรฐานสากลนี้ไม่ได้กำหนดข้อกำหนด

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

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


43
ที่กล่าวว่าถ้าคุณสามารถเป็นจริงได้รับคอมไพเลอร์ที่จะรันโค้ดที่รวบรวมเวลาโดยไม่ต้อง sandboxing ใด ๆ แล้วคนที่รักษาความปลอดภัยต่างๆจะมากสนใจที่จะรู้เกี่ยวกับมัน เช่นเดียวกับการทำ segfaulting คอมไพเลอร์
Kevin

67
Ditto สำหรับสิ่งที่เควินพูด ในฐานะวิศวกรคอมไพเลอร์ C / C ++ / etc ในอาชีพก่อนหน้านี้ตำแหน่งของเราคือพฤติกรรมที่ไม่ได้กำหนดไว้อาจทำให้โปรแกรมของคุณทำงานผิดพลาดทำให้ข้อมูลเอาต์พุตของคุณเสียหายทำให้บ้านของคุณลุกเป็นไฟไม่ว่าอะไรก็ตาม แต่คอมไพเลอร์ไม่ควรผิดพลาดไม่ว่าอินพุตจะเป็นอย่างไร (อาจไม่ได้ให้ข้อความแสดงข้อผิดพลาดที่เป็นประโยชน์ แต่ควรสร้างการวินิจฉัยและออกจากระบบบางอย่างแทนที่จะแค่กรีดร้อง CTHULHU จับวงล้อและทำผิด)
Ti Strga

8
@TiStrga ฉันพนันได้เลยว่าคธูลูจะสร้างไดรเวอร์ F1 ที่ยอดเยี่ยม
zeta-band

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

8
@Carmeister โอ้นั่นเป็นจุดที่ดีฉันจะเตือนผู้คนให้รู้ว่าเมื่อใดก็ตามที่ "UB อนุญาตให้คอมไพเลอร์อนุญาตให้เริ่มสงครามนิวเคลียร์" จะปรากฏขึ้น อีกครั้ง.
ilkkachu

8

UB ส่วนใหญ่ที่เรามักจะกังวลเช่น NULL-deref หรือหารด้วยศูนย์คือRuntime UB การคอมไพล์ฟังก์ชันที่จะทำให้รันไทม์ UB หากดำเนินการต้องไม่ทำให้คอมไพลเลอร์หยุดทำงาน เว้นแต่จะพิสูจน์ได้ว่าฟังก์ชัน (และเส้นทางนั้นผ่านฟังก์ชัน) จะถูกเรียกใช้งานโดยโปรแกรมอย่างแน่นอน

(ความคิดที่ 2: บางทีฉันอาจไม่ได้พิจารณา template / constexpr ที่จำเป็นต้องมีการประเมินผลในเวลารวบรวมอาจเป็นไปได้ว่า UB ในระหว่างนั้นได้รับอนุญาตให้สร้างความแปลกประหลาดโดยพลการระหว่างการแปลแม้ว่าจะไม่เคยเรียกฟังก์ชันผลลัพธ์ก็ตาม)

ลักษณะการทำงานระหว่างการแปลข้อความอ้างอิง ISO C ++ ในคำตอบของ @ StoryTellerนั้นคล้ายกับภาษาที่ใช้ในมาตรฐาน ISO C C ไม่รวมเทมเพลตหรือการประเมินที่constexprจำเป็นในเวลาคอมไพล์

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


คำตอบเก่าเขียนก่อนที่ฉันจะเรียนรู้เกี่ยวกับ UB เวลาแปล เป็นเรื่องจริงสำหรับ runtime-UB และอาจยังมีประโยชน์


ไม่มีสิ่งที่เรียกว่า UB ที่เกิดขึ้นในเวลาคอมไพล์ คอมไพเลอร์สามารถมองเห็นได้ตามเส้นทางการดำเนินการที่แน่นอน แต่ในแง่ C ++ จะไม่เกิดขึ้นจนกว่าการดำเนินการจะถึงเส้นทางการดำเนินการผ่านฟังก์ชัน

ข้อบกพร่องในโปรแกรมที่ทำให้ไม่สามารถคอมไพล์ได้ไม่ใช่ UB นั่นคือข้อผิดพลาดทางไวยากรณ์ โปรแกรมดังกล่าว "มีรูปแบบไม่ถูกต้อง" ในคำศัพท์ภาษา C ++ (ถ้าฉันมีมาตรฐานที่ถูกต้อง) โปรแกรมสามารถสร้างได้ดี แต่มี UB ความแตกต่างระหว่างพฤติกรรมที่ไม่ได้กำหนดและรูปแบบที่ไม่ดีไม่จำเป็นต้องมีข้อความวินิจฉัย

เว้นแต่ฉันจะเข้าใจผิดบางอย่าง ISO C ++ ต้องการให้โปรแกรมนี้รวบรวมและดำเนินการอย่างถูกต้องเนื่องจากการดำเนินการไม่ถึงการหารด้วยศูนย์ (ในทางปฏิบัติ ( Godbolt ) คอมไพเลอร์ที่ดีเพียงแค่สร้างไฟล์ปฏิบัติการที่ใช้งานได้ gcc / clang เตือนx / 0แต่ไม่ใช่เรื่องนี้แม้ว่าจะปรับให้เหมาะสม แต่อย่างไรก็ตามเรากำลังพยายามบอกว่าISO C ++ ที่ต่ำทำให้คุณภาพของการนำไปใช้งานเป็นอย่างไรดังนั้นการตรวจสอบ gcc / clang แทบจะไม่เป็นการทดสอบที่มีประโยชน์นอกเหนือจากการยืนยันว่าฉันเขียนโปรแกรมอย่างถูกต้อง)

int cause_UB() {
    int x=0;
    return 1 / x;      // UB if ever reached.
 // Note I'm avoiding  x/0  in case that counts as translation time UB.
 // UB still obvious when optimizing across statements, though.
}

int main(){
    if (0)
        cause_UB();
}

กรณีการใช้งานสำหรับสิ่งนี้อาจเกี่ยวข้องกับตัวประมวลผลล่วงหน้า C หรือconstexprตัวแปรและการแตกแขนงบนตัวแปรเหล่านั้นซึ่งนำไปสู่ความไร้สาระในบางเส้นทางที่ไม่มีทางเข้าถึงสำหรับตัวเลือกค่าคงเหล่านั้น

เส้นทางของการดำเนินการที่ UB สาเหตุรวบรวมเวลาที่มองเห็นอาจจะคิดที่จะไม่เคยใช้เช่นคอมไพเลอร์สำหรับ x86 สามารถปล่อยud2(สาเหตุการเรียนการสอนยกเว้นผิดกฎหมาย) cause_UB()เป็นคำนิยามสำหรับ หรือภายในฟังก์ชันหากด้านใดด้านหนึ่งของif()การนำไปสู่UB ที่สามารถพิสูจน์ได้สาขานั้นสามารถลบออก

แต่คอมไพเลอร์ยังคงมีการรวบรวมทุกอย่างอื่นในการมีสติและถูกต้องวิธี เส้นทางทั้งหมดที่ไม่พบ (หรือไม่สามารถพิสูจน์ได้ว่าพบ) UB ยังต้องถูกคอมไพล์เป็น asm ที่ดำเนินการราวกับว่าเครื่องนามธรรม C ++ กำลังทำงานอยู่


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

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


ฟังก์ชั่นที่มี UB ที่เป็นไปได้หรือพิสูจน์ได้ภายในสาขา

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

int minefield(int x) {
    if (x == 3) {
        *(char*)nullptr = x/0;
    }

    return x * 5;
}

คอมไพเลอร์ต้องสร้าง asm ที่ใช้งานได้กับสิ่งxอื่น ๆทั้งหมดที่ไม่ใช่ 3 จนถึงจุดที่x * 5ทำให้เกิดการเซ็น - โอเวอร์โฟลว์ UB ที่ INT_MIN และ INT_MAX หากไม่เคยเรียกใช้ฟังก์ชันนี้x==3โปรแกรมจะไม่มี UB และต้องทำงานตามที่เขียนไว้

เราอาจเขียนด้วยif(x == 3) __builtin_unreachable();GNU C เพื่อบอกคอมไพเลอร์ว่าxไม่ใช่ 3 อย่างแน่นอน

ในทางปฏิบัติมีโค้ด "ที่วางทุ่นระเบิด" อยู่ทั่วทุกที่ในโปรแกรมปกติ เช่นการหารด้วยจำนวนเต็มสัญญากับคอมไพเลอร์ว่ามันไม่ใช่ศูนย์ ตัวชี้ใด ๆ deref สัญญากับคอมไพเลอร์ว่าไม่ใช่โมฆะ


3

"กฎหมาย" หมายความว่าอย่างไรที่นี่? สิ่งใดที่ไม่ขัดแย้งกับมาตรฐาน C หรือมาตรฐาน C ++ นั้นถูกกฎหมายตามมาตรฐานเหล่านี้ หากคุณดำเนินการคำสั่งi = i++;และเป็นผลให้ไดโนเสาร์ยึดครองโลกนั่นไม่ได้ขัดแย้งกับมาตรฐาน อย่างไรก็ตามมันขัดแย้งกับกฎของฟิสิกส์ดังนั้นมันจะไม่เกิดขึ้น :-)

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

ในมาตรฐาน C เวอร์ชันก่อนหน้ามีข้อความที่มีข้อผิดพลาดหรือไม่ขึ้นอยู่กับพฤติกรรมที่ไม่ได้กำหนดไว้:

char* p = 1 / 0;

อนุญาตให้กำหนดค่าคงที่ 0 ให้กับถ่าน * การยอมให้ค่าคงที่ที่ไม่ใช่ศูนย์ไม่ได้ เนื่องจากค่าของ 1/0 เป็นพฤติกรรมที่ไม่ได้กำหนดจึงเป็นพฤติกรรมที่ไม่ได้กำหนดว่าคอมไพลเลอร์ควรหรือไม่ควรยอมรับคำสั่งนี้ (ปัจจุบัน 1/0 ไม่ตรงตามนิยามของ "นิพจน์คงที่จำนวนเต็ม" อีกต่อไป)


4
เพื่อความแม่นยำ: ไดโนเสาร์ที่ยึดครองโลกไม่ได้ขัดแย้งกับกฎทางฟิสิกส์ใด ๆ (เช่นรูปแบบของ Jurassic Park) มันไม่น่าเป็นไปได้สูง :)
ประหลาด

-1

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

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


เราสามารถพูดในทำนองเดียวกันเกี่ยวกับตัวแปลหรือคอมไพเลอร์ในภาษาโปรแกรมใด ๆ หรือสำหรับเรื่องนั้นโปรแกรมใด ๆ ก็ตาม
Robert Harvey

@RobertHarvey: ข้อกำหนดภาษาโปรแกรมจำนวนมากมีความเฉพาะเจาะจงมากขึ้นเกี่ยวกับสิ่งเหล่านี้ หากข้อมูลจำเพาะของภาษาระบุว่าคำสั่งบางอย่างจะอ่านอินพุตจากสตรีมที่มีเส้นทางของระบบปฏิบัติการตามที่ระบุไว้และระบบปฏิบัติการทำสิ่งแปลก ๆ เมื่ออ่านเส้นทางบางอย่างนั่นจะอยู่นอกการควบคุมของข้อกำหนดภาษา แต่ฉันไม่ทำ คิดว่าข้อกำหนดภาษาส่วนใหญ่จะให้การใช้งาน carte blanche เพื่อดำเนินการตามคำสั่งดังกล่าวตามอำเภอใจในยามว่างโดยไม่ต้องจัดทำเอกสารแม้แต่บนแพลตฟอร์มที่อาจกำหนดพฤติกรรม
supercat
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.