เหตุใด goto จึงเป็นอันตราย
goto
ไม่ก่อให้เกิดความไม่แน่นอนด้วยตัวเอง แม้จะมีประมาณ 100,000 goto
s, เคอร์เนล Linuxยังคงเป็นรูปแบบของความมั่นคง
goto
ด้วยตัวเองไม่ควรทำให้เกิดช่องโหว่ความปลอดภัย อย่างไรก็ตามในบางภาษาการผสมกับtry
/ catch
block การจัดการข้อยกเว้นอาจทำให้เกิดช่องโหว่ตามที่อธิบายไว้ในคำแนะนำ CERTนี้ คอมไพเลอร์หลัก C ++ ตั้งค่าสถานะและป้องกันข้อผิดพลาดดังกล่าว แต่น่าเสียดายที่คอมไพเลอร์รุ่นเก่าหรือแปลกใหม่ไม่ได้ทำ
goto
ทำให้โค้ดที่อ่านไม่ได้และไม่สามารถแก้ไขได้ นี่เรียกอีกอย่างว่ารหัสสปาเก็ตตี้เพราะเช่นเดียวกับในจานสปาเก็ตตี้มันเป็นเรื่องยากมากที่จะติดตามการไหลของการควบคุมเมื่อมี gotos มากเกินไป
แม้ว่าคุณจะจัดการเพื่อหลีกเลี่ยงรหัสสปาเก็ตตี้และถ้าคุณใช้เพียงไม่กี่ gotos พวกเขายังคงอำนวยความสะดวกเช่นข้อบกพร่องและการรั่วไหลของทรัพยากร:
- โค้ดที่ใช้การเขียนโปรแกรมโครงสร้างพร้อมบล็อกซ้อนและลูปหรือสวิทช์ที่ชัดเจนง่ายต่อการติดตาม การไหลของการควบคุมสามารถคาดเดาได้มาก ดังนั้นจึงง่ายกว่าที่จะตรวจสอบให้แน่ใจว่ามีการเคารพค่าคงที่
- ด้วย
goto
คำสั่งคุณแบ่งการไหลที่ตรงไปตรงมาและทำลายความคาดหวัง ตัวอย่างเช่นคุณอาจไม่สังเกตเห็นว่าคุณยังมีทรัพยากรฟรีอยู่
- หลายแห่ง
goto
ในสถานที่ที่แตกต่างกันสามารถส่งคุณไปยังเป้าหมาย goto เดียว ดังนั้นจึงไม่ชัดเจนที่จะรู้ว่าสถานะที่คุณอยู่เมื่อมาถึงสถานที่นี้ ความเสี่ยงในการตั้งสมมติฐานผิด / ไม่มีมูลความจริงจึงค่อนข้างใหญ่
ข้อมูลเพิ่มเติมและคำพูด:
C จัดทำgoto
คำสั่งและป้ายกำกับที่ไม่สามารถใช้งานได้อย่างไร้ขอบเขตเพื่อแยกสาขา อย่างเป็นทางการgoto
ไม่จำเป็นและในทางปฏิบัติมันเกือบจะง่ายต่อการเขียนโค้ดโดยไม่ต้องมัน (... )
อย่างไรก็ตามเราจะแนะนำบางสถานการณ์ที่อาจไปหาสถานที่ การใช้งานทั่วไปส่วนใหญ่คือการละทิ้งการประมวลผลในโครงสร้างที่ซ้อนกันอย่างลึกล้ำเช่นการแตกออกเป็นสองลูปในคราวเดียว ( ... )
แม้ว่าเราจะไม่ได้ดันทุรังเกี่ยวกับเรื่องที่มันจะดูเหมือนว่างบข้ามไปควรจะใช้เท่าที่จำเป็นถ้าที่ทั้งหมด
เมื่อไหร่ที่จะสามารถใช้ goto ได้?
เช่นเดียวกับ K&R ฉันไม่เชื่อเรื่อง gotos ฉันยอมรับว่ามีสถานการณ์ที่ข้ามไปอาจทำให้ชีวิตของคน ๆ หนึ่งสงบลงได้
โดยทั่วไปใน C, goto อนุญาตให้ออกจากลูปหลายระดับหรือการจัดการข้อผิดพลาดที่ต้องไปถึงจุดออกที่เหมาะสมที่ปลดปล่อย / ปลดล็อกทรัพยากรทั้งหมดที่ได้รับการจัดสรรจนถึงขณะนี้ (การจัดสรร iemultiple ตามลำดับหมายถึงฉลากหลายรายการ) บทความนี้แสดงปริมาณการใช้ goto ที่แตกต่างกันในเคอร์เนล Linux
ส่วนตัวฉันชอบที่จะหลีกเลี่ยงมันและใน 10 ปีของ C ฉันใช้ 10 gotos สูงสุด ฉันชอบใช้งานซ้อนกันif
ซึ่งฉันคิดว่าอ่านง่ายขึ้น เมื่อสิ่งนี้นำไปสู่การทำรังที่ลึกเกินไปฉันจะเลือกที่จะย่อยสลายฟังก์ชั่นของฉันในส่วนเล็ก ๆ หรือใช้ตัวบ่งชี้บูลีนในน้ำตก goto
วันนี้คอมไพเลอร์เพิ่มประสิทธิภาพจะฉลาดพอที่จะสร้างเกือบรหัสเดียวกันกว่ารหัสเดียวกันกับ
การใช้ goto อย่างหนักขึ้นอยู่กับภาษา:
ใน C ++ การใช้ที่เหมาะสมของRAIIจะทำให้คอมไพเลอร์ทำลายวัตถุที่อยู่นอกขอบเขตโดยอัตโนมัติดังนั้นทรัพยากร / ล็อคจะถูกล้างอีกต่อไปและไม่จำเป็นต้องกลับไปใช้อีกต่อไป
ใน Java มีความจำเป็นสำหรับการกลับไปข้าง (ดูของ Java ผู้เขียนอ้างข้างต้นและที่ยอดเยี่ยมคำตอบกองมากเกิน ): เก็บขยะที่ทำความสะอาดระเบียบbreak
, continue
และtry
/ catch
จัดการข้อยกเว้นครอบคลุมทุกกรณีที่goto
อาจจะเป็นประโยชน์ แต่ในความปลอดภัยมากขึ้นและดีขึ้น ลักษณะ. ความนิยมของ Java พิสูจน์ได้ว่าคำสั่ง goto สามารถหลีกเลี่ยงได้ในภาษาสมัยใหม่
ขยายช่องโหว่SSL ที่มีชื่อเสียงออกไปไม่ได้
ข้อจำกัดความรับผิดชอบที่สำคัญ:ในมุมมองของการสนทนาที่รุนแรงในความคิดเห็นที่ฉันต้องการชี้แจงว่าฉันไม่ได้แกล้งทำเป็นว่าคำสั่ง goto เป็นเพียงสาเหตุของข้อผิดพลาดนี้ ฉันไม่แสร้งว่าไม่มีถ้าข้ามไปแล้วจะไม่มีข้อผิดพลาด ฉันแค่ต้องการแสดงให้เห็นว่ามีข้อผิดพลาดร้ายแรงสามารถมีส่วนร่วมในข้อผิดพลาดร้ายแรง
ฉันไม่ทราบว่ามีข้อบกพร่องร้ายแรงจำนวนเท่าใดที่เกี่ยวข้องgoto
ในประวัติศาสตร์ของการเขียนโปรแกรม: รายละเอียดมักไม่ถูกสื่อสาร อย่างไรก็ตามมีข้อผิดพลาด Apple SSL ที่มีชื่อเสียงซึ่งทำให้ความปลอดภัยของ iOS อ่อนแอลง คำสั่งที่นำไปสู่ข้อผิดพลาดนี้เป็นgoto
คำสั่งที่ไม่ถูกต้อง
บางคนอ้างว่าสาเหตุของข้อผิดพลาดไม่ใช่คำสั่ง goto ในตัวมันเอง แต่มีการคัดลอก / วางผิดการเยื้องที่ทำให้เข้าใจผิดหายไปวงเล็บปีกกาที่ขาดหายไปรอบ ๆ บล็อกเงื่อนไขหรือบางทีนิสัยการทำงานของนักพัฒนา ฉันไม่สามารถยืนยันใด ๆ ของพวกเขา: ข้อโต้แย้งเหล่านี้เป็นสมมติฐานที่เป็นไปได้และการตีความ ไม่มีใครรู้จริงๆ (ในขณะเดียวกันสมมติฐานของการรวมที่ผิดพลาดตามที่มีคนแนะนำในความคิดเห็นนั้นดูเหมือนจะเป็นตัวเลือกที่ดีมากในมุมมองของการเยื้องที่ไม่สอดคล้องกันอื่น ๆ ในฟังก์ชันเดียวกัน )
ความจริงวัตถุประสงค์เพียงอย่างเดียวคือการที่ซ้ำกัน goto
นำไปสู่การออกจากฟังก์ชั่นก่อนเวลาอันควร การดูรหัสคำสั่งเดียวอื่น ๆ ที่อาจทำให้เกิดเอฟเฟกต์เดียวกันนั้นจะเป็นการตอบแทน
ข้อผิดพลาดอยู่ในฟังก์ชั่นSSLEncodeSignedServerKeyExchange()
ในไฟล์นี้ :
if ((err = ReadyHash(&SSLHashSHA1, &hashCtx)) != 0)
goto fail;
if ((err =...) !=0)
goto fail;
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
goto fail;
goto fail; // <====OUCH: INDENTATION MISLEADS: THIS IS UNCONDITIONDAL!!
if (...)
goto fail;
... // Do some cryptographic operations here
fail:
... // Free resources to process error
วงเล็บปีกกาที่แน่นอนรอบ ๆ บล็อกเงื่อนไขสามารถป้องกันข้อผิดพลาด:
มันจะนำไปสู่ข้อผิดพลาดทางไวยากรณ์ที่รวบรวม (และด้วยเหตุนี้การแก้ไข) หรือไปที่ไม่เป็นอันตรายซ้ำซ้อน อย่างไรก็ตาม GCC 6 จะสามารถตรวจพบข้อผิดพลาดเหล่านี้ได้เนื่องจากคำเตือนเพิ่มเติมเพื่อตรวจหาการเยื้องที่ไม่สอดคล้องกัน
แต่ในตอนแรก gotos ทั้งหมดสามารถหลีกเลี่ยงได้ด้วยโค้ดที่มีโครงสร้างมากขึ้น ดังนั้นอย่างน้อยก็เป็นสาเหตุของข้อผิดพลาดนี้ทางอ้อม มีอย่างน้อยสองวิธีที่สามารถหลีกเลี่ยงได้:
วิธีที่ 1: ถ้าประโยคหรือซ้อนกันif
s
แทนที่จะทำการทดสอบเงื่อนไขจำนวนมากเพื่อหาข้อผิดพลาดตามลำดับและแต่ละครั้งที่ส่งไปยังfail
ป้ายกำกับในกรณีที่เกิดปัญหาเราสามารถเลือกที่จะดำเนินการเข้ารหัสลับในสถานะif
ที่ระบุว่าจะทำได้เฉพาะเมื่อไม่มีเงื่อนไขผิดปกติ:
if ((err = ReadyHash(&SSLHashSHA1, &hashCtx)) == 0 &&
(err = ...) == 0 ) &&
(err = ReadyHash(&SSLHashSHA1, &hashCtx)) == 0) &&
...
(err = ...) == 0 ) )
{
... // Do some cryptographic operations here
}
... // Free resources
วิธีที่ 2: ใช้ตัวสะสมข้อผิดพลาด
วิธีนี้ขึ้นอยู่กับข้อเท็จจริงที่ว่าคำสั่งเกือบทั้งหมดในที่นี้เรียกใช้ฟังก์ชั่นบางอย่างเพื่อตั้งerr
รหัสข้อผิดพลาดและดำเนินการส่วนที่เหลือของรหัสเฉพาะถ้าerr
เป็น 0 (เช่นฟังก์ชั่นการดำเนินการโดยไม่มีข้อผิดพลาด) ทางเลือกที่ปลอดภัยและสามารถอ่านได้ดีคือ:
bool ok = true;
ok = ok && (err = ReadyHash(&SSLHashSHA1, &hashCtx))) == 0;
ok = ok && (err = NextFunction(...)) == 0;
...
ok = ok && (err = ...) == 0;
... // Free resources
ที่นี่ไม่มีการข้ามไปครั้งเดียว: ไม่มีความเสี่ยงที่จะข้ามไปยังจุดออกความล้มเหลวอย่างรวดเร็ว และจะทำให้มองเห็นเส้นที่ไม่ถูกต้องหรือเส้นที่ลืมok &&
ได้ง่าย
โครงสร้างนี้มีขนาดกะทัดรัดมากขึ้น มันขึ้นอยู่กับความจริงที่ว่าใน C ส่วนที่สองของตรรกะและ ( &&
) จะถูกประเมินเฉพาะในกรณีที่ส่วนแรกเป็นจริง ในความเป็นจริงประกอบที่สร้างขึ้นโดยการเพิ่มประสิทธิภาพคอมไพเลอร์เกือบจะเทียบเท่ากับรหัสเดิมที่มี gotos: เพิ่มประสิทธิภาพการตรวจจับอย่างดีห่วงโซ่ของเงื่อนไขและสร้างรหัสซึ่งมูลค่าที่แรกที่ไม่ใช่ผลตอบแทน null กระโดดไปยังจุดสิ้นสุด ( หลักฐานออนไลน์ )
คุณสามารถมองเห็นการตรวจสอบความสอดคล้องที่ส่วนท้ายของฟังก์ชันที่อาจเกิดขึ้นในระหว่างขั้นตอนการทดสอบเพื่อระบุความไม่ตรงกันระหว่างการตั้งค่าสถานะ ok และรหัสข้อผิดพลาด
assert( (ok==false && err!=0) || (ok==true && err==0) );
ข้อผิดพลาดของการ==0
แทนที่โดยไม่ตั้งใจด้วย!=0
ข้อผิดพลาดหรือตัวเชื่อมต่อแบบลอจิคัลจะถูกตรวจพบได้ง่ายในระหว่างขั้นตอนการดีบัก
ตามที่กล่าวไว้: ฉันไม่ได้แสร้งว่าโครงสร้างทางเลือกจะหลีกเลี่ยงข้อผิดพลาดใด ๆ ฉันแค่อยากจะบอกว่าพวกเขาอาจทำให้บั๊กยากขึ้น