กลับไปหน้าบทความ

อ่าน 8 นาที

พฤติกรรมที่ไม่กำหนด

โปรแกรม คอมพิวเตอร์ แสดง พฤติกรรมที่ไม่กำหนด ( UB ) เมื่อมีโค้ดหรือกำลังดำเนินการโค้ดที่ ข้อกำหนดของภาษาโปรแกรม ไม่ได้กำหนดข้อกำหนดเฉพาะใดๆ [ 1 ] ซึ่งแตกต่างจาก พฤติกรรมที่ไม่ระบุ...

พฤติกรรมที่ไม่กำหนด

โปรแกรมคอมพิวเตอร์แสดงพฤติกรรมที่ไม่กำหนด ( UB ) เมื่อมีโค้ดหรือกำลังดำเนินการโค้ดที่ข้อกำหนดของภาษาโปรแกรมไม่ได้กำหนดข้อกำหนดเฉพาะใดๆ[ 1 ]ซึ่งแตกต่างจากพฤติกรรมที่ไม่ระบุซึ่งข้อกำหนดของภาษาไม่ได้กำหนดผลลัพธ์ และพฤติกรรมที่กำหนดโดยการใช้งาน ซึ่งอ้างอิงถึงเอกสารประกอบของส่วนประกอบอื่นของแพลตฟอร์ม (เช่นABIหรือ เอกสารประกอบ ตัวแปล )

ในชุมชนการเขียนโปรแกรม Cพฤติกรรมที่ไม่กำหนดอาจถูกเรียกอย่างขบขันว่า " ปีศาจจมูก " ตาม โพสต์ comp.std.cที่อธิบายพฤติกรรมที่ไม่กำหนดว่าอนุญาตให้คอมไพเลอร์ทำอะไรก็ได้ตามใจชอบ แม้กระทั่ง "ทำให้ปีศาจบินออกมาจากจมูกของคุณ" [ 2 ]

ภาพรวม

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

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

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

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

สำหรับ C และ C++ คอมไพเลอร์ได้รับอนุญาตให้แสดงการวินิจฉัยในระหว่างการคอมไพล์ในกรณีเหล่านี้ แต่ไม่จำเป็นต้องทำ: การใช้งานจะถือว่าถูกต้องไม่ว่าจะทำอะไรในกรณีดังกล่าว คล้ายกับเงื่อนไขที่ไม่สนใจในตรรกะดิจิทัล เป็นความรับผิดชอบของโปรแกรมเมอร์ที่จะเขียนโค้ดที่ไม่ก่อให้เกิดพฤติกรรมที่ไม่กำหนด แม้ว่าการใช้งานคอมไพเลอร์จะได้รับอนุญาตให้แสดงการวินิจฉัยเมื่อเกิดเหตุการณ์เช่นนี้ขึ้นก็ตาม ปัจจุบันคอมไพเลอร์มีแฟล็กที่เปิดใช้งานการวินิจฉัยดังกล่าว ตัวอย่างเช่น-fsanitize=undefinedเปิดใช้งาน "undefined behavior sanitizer" ( UBSan ) ในgcc 4.9 [ 3 ]และในclangอย่างไรก็ตาม แฟล็กนี้ไม่ใช่ค่าเริ่มต้น และการเปิดใช้งานเป็นทางเลือกของผู้ที่สร้างโค้ด

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

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

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

โปรแกรมผิดพลาด

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

นอกจากข้อผิดพลาดที่มีขอบเขตแล้ว กฎของภาษายังกำหนดข้อผิดพลาดบางประเภทที่นำไปสู่การดำเนินการที่ผิดพลาด เช่นเดียวกับข้อผิดพลาดที่มีขอบเขต การใช้งานไม่จำเป็นต้องตรวจจับข้อผิดพลาดดังกล่าวทั้งก่อนหรือระหว่างเวลาทำงาน ต่างจากข้อผิดพลาดที่มีขอบเขตตรงที่ไม่มีการกำหนดขอบเขตในภาษาเกี่ยวกับผลกระทบที่อาจเกิดขึ้นจากการดำเนินการที่ผิดพลาด โดยทั่วไปแล้วผลกระทบนั้นไม่สามารถคาดเดาได้[ 4 ]

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

ประโยชน์

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

ตัวอย่างสำหรับภาษาซี:

int foo ( unsigned char x ) { int value = 2147483600 ; // สมมติว่าเป็น int 32 บิตและ char 8 บิตvalue += x ; if ( value < 2147483600 ) { bar (); } return value ; }

ค่าของxไม่สามารถเป็นค่าลบได้ และเนื่องจากการโอเวอร์โฟลว์ของจำนวนเต็มที่ มี เครื่องหมายเป็นพฤติกรรมที่ไม่กำหนดในภาษา C คอมไพเลอร์จึงสามารถสันนิษฐานได้ว่าvalue < 2147483600จะเป็นเท็จเสมอ ดังนั้นifคำสั่ง รวมถึงการเรียกใช้ฟังก์ชันbarจึงสามารถถูกละเว้นโดยคอมไพเลอร์ได้ เนื่องจากนิพจน์ทดสอบในifไม่มีผลข้างเคียงและเงื่อนไขจะไม่เป็นจริงเลย ดังนั้นโค้ดจึงมีความหมายเทียบเท่ากับ:

int foo ( unsigned char x ) { int value = 2147483600 ; value += x ; return value ; }

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

การปรับแต่งลักษณะนี้จะสังเกตได้ยากเมื่อโค้ดมีความซับซ้อนมากขึ้น และมีการปรับแต่งอื่นๆ เช่นการแทรกโค้ด (inlining)เกิดขึ้น ตัวอย่างเช่น ฟังก์ชันอื่นอาจเรียกใช้ฟังก์ชันข้างต้น:

void run_tasks ( unsigned char * ptrx ) { int z ; z = foo ( * ptrx ); while ( * ptrx > 60 ) { run_one_task ( ptrx , z ); } }

คอมไพเลอร์สามารถกำจัดลูปwhile`-loop` ได้โดยการวิเคราะห์ช่วงค่า : โดยการตรวจสอบคอมไพเลอร์foo()จะรู้ว่าค่าเริ่มต้นที่ชี้โดย `-loop` นั้นptrxไม่สามารถเกิน 47 ได้ (เพราะหากมากกว่านั้นจะทำให้เกิดพฤติกรรมที่ไม่กำหนดใน `-loop` ) ดังนั้น การตรวจสอบเบื้องต้นของ `-loop` จะเป็นเท็จเสมอในโปรแกรมที่สอดคล้องกับมาตรฐาน ยิ่งไปกว่านั้น เนื่องจากผลลัพธ์ไม่เคยถูกนำไปใช้และไม่มีผลข้างเคียง คอมไพเลอร์จึงสามารถปรับปรุงให้เป็นฟังก์ชันว่างที่ส่งคืนค่าทันทีได้ การหายไปของ ลูป `-loop` อาจเป็นเรื่องที่น่าประหลาดใจเป็นพิเศษหาก `-loop` ถูกกำหนดไว้ใน ไฟล์ออบเจ็กต์ที่คอมไพ ล์ แยกต่างหากfoo()*ptrx > 60zfoo()run_tasks()whilefoo()

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

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

ความเสี่ยง

มาตรฐาน C และ C++ มีพฤติกรรมที่ไม่กำหนดไว้หลายรูปแบบ ซึ่งให้เสรีภาพมากขึ้นในการใช้งานคอมไพเลอร์และการตรวจสอบในระหว่างการคอมไพล์ โดยแลกกับพฤติกรรมที่ไม่กำหนดไว้ในระหว่างการทำงานหากมีอยู่ โดยเฉพาะอย่างยิ่ง มาตรฐาน ISOสำหรับ C มีภาคผนวกที่แสดงรายการแหล่งที่มาทั่วไปของพฤติกรรมที่ไม่กำหนดไว้[ 6 ]ยิ่งไปกว่านั้น คอมไพเลอร์ไม่จำเป็นต้องวินิจฉัยโค้ดที่อาศัยพฤติกรรมที่ไม่กำหนดไว้ ดังนั้น โปรแกรมเมอร์ แม้แต่ผู้ที่มีประสบการณ์ ก็มักจะอาศัยพฤติกรรมที่ไม่กำหนดไว้โดยไม่ได้ตั้งใจ หรือเพียงเพราะพวกเขาไม่เชี่ยวชาญในกฎของภาษาซึ่งอาจมีหลายร้อยหน้า สิ่งนี้อาจส่งผลให้เกิดข้อบกพร่องที่ถูกเปิดเผยเมื่อใช้คอมไพเลอร์อื่น หรือการตั้งค่าที่แตกต่างกัน การทดสอบหรือการฟัซซิ่งด้วยการตรวจสอบพฤติกรรมที่ไม่กำหนดไว้แบบไดนามิกที่เปิดใช้งาน เช่นClang sanitizers สามารถช่วยตรวจจับพฤติกรรมที่ไม่กำหนดไว้ที่ไม่ได้รับการวินิจฉัยโดยคอมไพเลอร์หรือตัววิเคราะห์แบบคงที่[ 7 ]

พฤติกรรมที่ไม่กำหนดอาจนำไปสู่ ช่องโหว่ ด้านความปลอดภัยในซอฟต์แวร์ ตัวอย่างเช่น บัฟเฟอร์โอเวอร์โฟลว์และช่องโหว่ด้านความปลอดภัยอื่นๆ ในเว็บเบราว์เซอร์ หลักๆ เกิดจากพฤติกรรมที่ไม่กำหนด เมื่อ นักพัฒนา GCCเปลี่ยนคอมไพเลอร์ของพวกเขาในปี 2008 โดยละเว้นการตรวจสอบโอเวอร์โฟลว์บางอย่างที่อาศัยพฤติกรรมที่ไม่กำหนดCERTจึงออกคำเตือนเกี่ยวกับคอมไพเลอร์เวอร์ชันใหม่กว่า[ 8 ] Linux Weekly Newsชี้ให้เห็นว่าพบพฤติกรรมเดียวกันในPathScale C , Microsoft Visual C++ 2005และคอมไพเลอร์อื่นๆ อีกหลายตัว[ 9 ]ต่อมาคำเตือนได้รับการแก้ไขเพื่อเตือนเกี่ยวกับคอมไพเลอร์ต่างๆ[ 10 ]

ตัวอย่าง

ซี และ ซี++

รูปแบบหลักของพฤติกรรมที่ไม่กำหนดใน C สามารถจำแนกได้กว้าง ๆ ดังนี้: [ 11 ]การละเมิดความปลอดภัยของหน่วยความจำเชิงพื้นที่ การละเมิดความปลอดภัยของหน่วยความจำเชิงเวลาการล้นของจำนวนเต็มการละเมิดการกำหนดนามแฝงที่เข้มงวด การละเมิดการจัดแนว การแก้ไขที่ไม่เรียงลำดับ การแข่งขันข้อมูล และลูปที่ไม่ทำการ I/O หรือไม่สิ้นสุด

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

เนื่องจากสตริงลิเทอรัลมักจะถูกเก็บไว้ในหน่วยความจำแบบอ่านอย่างเดียว การพยายามแก้ไขสตริงลิเทอรัลจึงทำให้เกิดพฤติกรรมที่ไม่แน่นอน: [ 12 ]

char * p = "wikipedia" ; // รูปแบบ C ที่ถูกต้อง แต่เลิกใช้แล้วใน C++98/C++03 และผิดรูปแบบใน C++11 p [ 0 ] = 'W' ; // พฤติกรรมที่ไม่กำหนด

การหาร จำนวนเต็มด้วยศูนย์ส่งผลให้เกิดพฤติกรรมที่ไม่แน่นอน: [ 13 ]

int x = 1 ; return x / 0 ; // พฤติกรรมที่ไม่แน่นอน

การดำเนินการตัวชี้บางอย่างอาจส่งผลให้เกิดพฤติกรรมที่ไม่แน่นอน: [ 14 ]

int arr [ 4 ] = { 0 , 1 , 2 , 3 }; int * p = arr + 5 ; // พฤติกรรมที่ไม่แน่นอนสำหรับการเข้าถึงดัชนีนอกขอบเขตp = nullptr ; int a = * p ; // พฤติกรรมที่ไม่แน่นอนสำหรับการเข้าถึงค่าที่ชี้โดยพอยเตอร์ว่าง

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

int main ( void ) { int a = 0 ; int b = 0 ; return & a < & b ; // พฤติกรรมที่ไม่กำหนดใน C, พฤติกรรมที่ไม่ระบุใน C++ }

การสิ้นสุดของฟังก์ชันที่ส่งค่ากลับ (นอกเหนือจากmain()) โดยไม่มีคำสั่ง return จะส่งผลให้เกิดพฤติกรรมที่ไม่แน่นอนหากค่าของการเรียกฟังก์ชันถูกใช้โดยผู้เรียก: [ 16 ]

อินท์เอฟ() {}int x = f (); // พฤติกรรมที่ไม่แน่นอน

การแก้ไขวัตถุระหว่างจุดลำดับ สองจุด มากกว่าหนึ่งครั้งจะทำให้เกิดพฤติกรรมที่ไม่แน่นอน[ 17 ]มีการเปลี่ยนแปลงที่สำคัญในสิ่งที่ทำให้เกิดพฤติกรรมที่ไม่แน่นอนที่เกี่ยวข้องกับจุดลำดับตั้งแต่ C++11 เป็นต้นไป[ 18 ]คอมไพเลอร์สมัยใหม่สามารถแสดงคำเตือนเมื่อพบการแก้ไขที่ไม่เรียงลำดับหลายครั้งกับวัตถุเดียวกัน[ 19 ] [ 20 ]ตัวอย่างต่อไปนี้จะทำให้เกิดพฤติกรรมที่ไม่แน่นอนทั้งในภาษา C และ C++

int f ( int i ) { // พฤติกรรมที่ไม่แน่นอน: การแก้ไข i สองครั้งที่ไม่เรียงลำดับreturn i ++ + i ++ ; }

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

a [ i ] = i ++ ; // พฤติกรรมที่ไม่กำหนดprintf ( "%d %d \n " , ++ n , pow ( 2 , n )); // พฤติกรรมที่ไม่กำหนดเช่นกัน

ในภาษา C/C++ การเลื่อนบิตของค่าด้วยจำนวนบิตที่เป็นลบ หรือมากกว่าหรือเท่ากับจำนวนบิตทั้งหมดในค่านั้น จะทำให้เกิดพฤติกรรมที่ไม่แน่นอน วิธีที่ปลอดภัยที่สุด (ไม่ว่าจะใช้คอมไพเลอร์ของผู้ผลิตรายใด) คือการกำหนดจำนวนบิตที่จะเลื่อน (ตัวถูกดำเนินการทางขวาของตัวดำเนินการบิต<< ) ให้อยู่ในช่วง: [ 0 , ขนาดของค่า * CHAR_BIT - 1 ] (โดยที่ CHAR_BIT คือตัวถูกดำเนินการทางซ้าย) >>value

int num = -1 ; unsigned int val = 1 << num ; // การเลื่อนบิตด้วยจำนวนลบ - พฤติกรรมที่ไม่แน่นอนnum = 32 ; // หรือจำนวนใดๆ ที่มากกว่า 31 val = 1 << num ; // ค่า '1' ถูกแปลงเป็นจำนวนเต็ม 32 บิต ในกรณีนี้ การเลื่อนบิตมากกว่า 31 บิตถือเป็นพฤติกรรมที่ไม่แน่นอนnum = 64 ; // หรือจำนวนใดๆ ที่มากกว่า 63 unsigned long long val2 = 1ULL << num ; // ค่า '1ULL' ถูกกำหนดให้เป็นจำนวนเต็ม 64 บิต - ในกรณีนี้ การเลื่อนบิตมากกว่า 63 บิตถือเป็นพฤติกรรมที่ไม่แน่นอน

ซี#

ในภาษา C#พฤติกรรมที่ไม่กำหนดไว้สามารถเกิดขึ้นได้ในunsafeบริบท

โดยใช้ระบบ;unsafe { int * p = ( int * ) 0x12345678 ; Console . WriteLine ( * p ); // อ่านที่อยู่หน่วยความจำแบบสุ่ม}

การใช้งานหน่วยความจำสแต็กหลังจากถูกปล่อยไปแล้ว (use-after-free) อาจก่อให้เกิดพฤติกรรมที่ไม่แน่นอนได้เช่นกัน

โดยใช้ระบบ;unsafe int * GetPointer () { int x = 100 ; return & x ; }int * p = GetPointer (); Console.WriteLine ( * p ); // รับพอยเตอร์ไปยังหน่วยความจำสแต็กที่ไม่ถูกต้องอีกต่อไป

ชวา

ในภาษา Javaปัญหาการทำงานที่ไม่แน่นอนที่พบได้บ่อยที่สุดคือ การทำงานร่วมกับภาษาอื่นที่ไม่ใช่ภาษาแม่ (native interop) และการแข่งขันของข้อมูล (data races)

ข้อผิดพลาดการเข้าถึงข้อมูลพร้อมกัน (Data Race)ต่อไปนี้อาจก่อให้เกิดพฤติกรรมที่ไม่แน่นอนโดยการละเมิดแบบจำลองหน่วยความจำของ Java

int x = 0 ; boolean ready = false ;เธรดt1 = เธรดใหม่(() -> { x = 33 ; ready = true ; });Thread t2 = new Thread (() -> { if ( ready ) { System . out . println ( x ); // อาจพิมพ์ 33 หรือ 0 } });t1.start ( ) ; t2.start ( ) ;

อาจเกิดปัญหาหน่วยความจำไม่ถูกกำหนด (Undefined memory) ใน การเรียกใช้ Java Native Interfaceซึ่งอาจส่งผลให้เกิดพฤติกรรมที่ไม่แน่นอน จาก C:

#include <jni.h>JNIEXPORT jint JNICALL Java_Crash_boom ( JNIEnv * env , jclass cls ) { int * p = NULL ; return * p ; // การเข้าถึงค่าที่ชี้โดยพอยเตอร์ที่เป็นค่าว่าง}

ในภาษา Java:

แพ็คเกจorg.wikipedia.examples ;public class Crash { static { System.loadLibrary ( " crash" ) ; }private static native int boom ();public static void main ( String [ ] args ) { System.out.println ( boom ( ) ) ; } }

สนิม

แม้ว่าโดยทั่วไปแล้วพฤติกรรมที่ไม่กำหนดจะไม่เกิดขึ้นในRust ที่ปลอดภัย แต่โค้ดที่ไม่เหมาะสมและไม่ปลอดภัยก็ยังสามารถเปิดเผย UB ให้กับโค้ดที่ปลอดภัยได้ในสิ่งที่เรียกว่าช่องโหว่ของความถูกต้อง[ 22 ]

ตัวอย่างเช่น ชนิดข้อมูลหลายชนิดใน Rust ใช้เงื่อนไขคงที่ (invariants)ที่ช่วยให้เกิดการเพิ่มประสิทธิภาพที่มีประโยชน์ การอ้างอิง (references) ก็เป็นตัวอย่างหนึ่ง ที่แม้จะมีรูปแบบพื้นฐานเหมือนกับตัวชี้แบบดิบ (raw pointers) แต่ก็ไม่สามารถเป็นค่าว่าง (null) ไม่ตรง แนว (unaligned)หรือชี้ไปยังปลายทางที่ไม่ถูกต้องได้ ดังนั้น การละเมิดเงื่อนไขคงที่เหล่านี้จึงไม่มีนิยาม ไม่ว่าการอ้างอิงที่ได้จะถูกนำไปใช้ในลักษณะใดก็ตาม

ใช้std :: mem ;/// สร้างการอ้างอิงที่เป็นค่าว่างpub const fn null_ref < T : ? Sized > () -> & T { unsafe { mem :: zeroed () } }

การเรียกใช้ฟังก์ชันnull_refมักจะผิดรูปแบบเสมอเนื่องจากข้อจำกัดที่กำหนดโดยประเภทอ้างอิงทั้งหมด แม้ว่าฟังก์ชันนั้นจะไม่ใช่unsafe fnรายการและสามารถเรียกใช้ได้จากโค้ดที่ปลอดภัยก็ตาม

นอกจากนี้ การเข้าถึงค่าที่ชี้โดยตัวชี้ว่าง (null pointer) นั้นไม่มีนิยามที่แน่นอน แม้ว่าระบบโฮสต์หลายระบบจะยังคงออกแบบมาเพื่อจัดการกับกรณีดังกล่าวในกรณีที่เกิดข้อผิดพลาดในการเข้าถึงหน่วยความจำ (segmentation fault ) ก็ตาม

ใช้std :: ptr ;fn main () { let p : * const i32 = ptr :: null ();// ความปลอดภัย: `p` เป็นค่าว่างและไม่สามารถเข้าถึงค่าที่ชี้โดยตัวแปรได้unsafe { * p }; }

อ่านเพิ่มเติม

  • ปีเตอร์ ฟาน เดอร์ ลินเดนผู้เชี่ยวชาญ ด้านการ เขียนโปรแกรม C ไอเอสบีเอ็น 0-13-177429-8
  • ทีม UB Canaries (เมษายน 2015), จอห์น เรเกอร์ (มหาวิทยาลัยยูทาห์ สหรัฐอเมริกา)
  • พฤติกรรมที่ไม่แน่นอนในปี 2017 (กรกฎาคม 2017) ปาสคาล คูโอค (TrustInSoft, ฝรั่งเศส) และ จอห์น เรเกอร์ (มหาวิทยาลัยยูทาห์, สหรัฐอเมริกา)
  • เวอร์ชันแก้ไขของมาตรฐาน C99ดูรายละเอียดเพิ่มเติมได้ที่หัวข้อ 6.10.6 สำหรับ #pragma
ดึงข้อมูลมาจาก " https://en.wikipedia.org/w/index.php?title=Undefined_behavior&oldid=1347258853 "

สรุปเนื้อหา

ข้อมูลสำคัญจากบทความ

ข้อมูลสำคัญเกี่ยวกับ พฤติกรรมที่ไม่กำหนด

โปรแกรม คอมพิวเตอร์ แสดง พฤติกรรมที่ไม่กำหนด ( UB ) เมื่อมีโค้ดหรือกำลังดำเนินการโค้ดที่ ข้อกำหนดของภาษาโปรแกรม ไม่ได้กำหนดข้อกำหนดเฉพาะใดๆ [ 1 ] ซึ่งแตกต่างจาก พฤติกรรมที่ไม่ระบุ...

ภาพรวม

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

โปรแกรมผิดพลาด

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

ประโยชน์

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