Out-of-bounds Array access: หายนะจากการทะลุกรอบหน่วยความจำ

สวัสดีครับน้องๆ วิศวกรและเพื่อนนักพัฒนาชาว www.123microcontroller.com ทุกคน! หลังจากที่เราได้ทำความรู้จักกับความน่ากลัวของ “Undefined Behavior (UB)” กันไปแล้ว วันนี้วิศวกรขอบตาดำๆ จะขอหยิบยกหนึ่งในสาเหตุยอดฮิตที่ทำให้เกิด UB มากที่สุดในภาษา C แบบหลีกหนีไม่พ้น นั่นก็คือปัญหาคลาสสิกอย่าง “Out-of-bounds array access” หรือการเข้าถึงข้อมูล Array นอกขอบเขตนั่นเองครับ

ในโลกการทำงานจริงของฮาร์ดแวร์และโปรเจกต์ Embedded Systems ที่เรามีทรัพยากรหน่วยความจำ (RAM) ค่อนข้างจำกัดมาก การใช้งานและบริหารจัดการกับตัวแปร Array รวมถึง Pointer นั้นคือชีวิตประจำวันที่วิศวกรหลีกเลี่ยงไม่ได้ แต่รู้หรือไม่ครับว่า… การเผลอเรอเขียนลอจิกให้อ่านหรือเขียนข้อมูลทะลุเลยขอบเขตของ Array เพียงแค่ 1 ไบต์! อาจส่งผลกระทบร้ายแรงทำให้บอร์ดไมโครคอนโทรลเลอร์ของเราค้างกระตุก ทำงานผิดเพี้ยนเดาไม่ได้ หรือแม้กระทั่งเป็นการอ้าแขนเปิดช่องโหว่ความปลอดภัยระดับมหึมาให้แฮกเกอร์สามารถเจาะทลวงเข้ามายึดระบบควบคุมได้เลย! ชักจะน่ากลัวแล้วใช่ไหมครับ วันนี้เราจะมาแหวกโค้ดเจาะลึกกันว่า ปัญหานี้มันดันไปผูกโยงสวิตช์ชนวนกับ Undefined Behavior ได้อย่างไร และทำไมผู้สร้างมาตรฐานภาษา C ถึงใจร้ายปล่อยให้เรื่องพรรค์นี้เกิดขึ้นทิ้งรอยแผลให้พวกเราครับ

Out-of-bounds กับบริบทการตื่นของ Undefined Behavior

การอธิบายเปรียบเทียบ (Analogy) ให้เข้าใจได้รวดเร็วง่ายๆ คือ การประกาศสร้าง Array ในภาษา C มันเปรียบเสมือน “การตั้งเสาตู้จดหมายที่เรียงติดกัน” หากเราได้ประกาศยื่นเอกสารจองตู้จดหมายไว้เบ็ดเสร็จเรียบร้อยแล้วที่จำนวน 5 ตู้ (ทำให้เรามีป้าย Index กำกับคือ 0 ถึง 4) แต่พอลับหลัง เราดันทะลึ่งเอาของชิ้นสำคัญไปยัดใส่ในตำแหน่งตู้ที่ 6 (ซึ่งก็คือช่อง Index 5) หรือแอบเอาไปหย่อนใส่ตู้หมายเลขประหลาดที่ Index -1 สิ่งที่เกิดขึ้นคือเรากำลังกระทำความผิดละเมิดเอาของไปแหย่ฝากใส่ในพื้นที่ตู้ของคนอื่น! ซึ่งในหลักการเขียนโค้ดภาษา C สิ่งที่อธิบายไปทั้งหมดนี้เรียกว่า Out-of-bounds access ซึ่งมาตราฐานภาษา C ระบุโทษทัณฑ์ไว้อย่างชัดเจนเลยครับว่า การกระทำเช่นนี้ถือว่าทำผิดกฎรุนแรงและก่อให้เกิดสภาวะ Undefined Behavior (UB) ทันที!

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

  • ปรัชญา “Trust the Programmer” และความบ้าบิ่นในเรื่องความเร็วสูงสุด: กลิ่นอายแรกเริ่มของภาษา C นั้นถูกบรรจงออกแบบมาเพื่อรีดเค้นดันประสิทธิภาพการทำงานของฮาร์ดแวร์ให้วิ่งเร็วทะลุมิติที่สุด (Efficiency) และโครงสร้างใช้ทรัพยากรน้อยนิด (Small footprint) การที่ผู้สร้างจะบีบให้ Compiler หรือสอดไส้ระบบใส่โค้ดมาเพื่อคอยตรวจสอบเช็คขอบเขตความถูกต้อง (Bounds checking) ทุกสเต็ปการขยับตัวเข้าถึง Array ในระหว่างรันไทม์ มันย่อมจะทำให้เกิดสิ่งที่เรียกว่า Overhead แบบหนักหน่วง ทั้งในแง่ของไซส์ขนาดโค้ดเฟิร์มแวร์ที่จะอ้วนใหญ่ขึ้น และยังรวมถึงการผลาญแย่งกินไซเคิลประมวลผลของ CPU ไปอย่างไร้ค่า… ภาษา C จึงเชิดใส่ปัญหาและเลือกใช้คติ “เชื่อใจระดับสุดวิญญาณ” ว่าโปรแกรมเมอร์ที่ใช้งานนั้นจะสามารถคิดคำนวณและตั้งสติเขียนขอบเขตได้ถูกต้อง 100% เสมอครับ
  • โครงสร้างปริศนาของ Pointer Arithmetic และ Memory Layout: ในพื้นฐานภาษา C นั้น การอ้างอิงตำแหน่ง Array แสนเบสิกคลาสสิกที่เราคุ้นเคยเช่นตัวแปรโค้ด arr[i] แท้จริงแล้วแกมนิยามของมันคือเทคนิคการจับบวกรวม Pointer ชนิดหนึ่งที่เรียกว่า *(arr + i) กฎหมายมาตรฐานกำหนดและห้ามปรามชัดเจนว่า การบวกหรือลบตำแหน่ง Pointer ให้กระโดดชี้เป้าหลุดออกไปไกลกว่าขอบเขตพื้นที่ที่ตัวแปร Array นั้นครองอำนาจอยู่ (โดยมีเงื่อนข้อยกเว้นแค่ให้ชี้ล้ำไปหยุดอยู่ที่ตำแหน่งว่างเปล่าด้านท้ายของกล่องถัดจากตัวสุดท้ายได้พอดิบพอดี หรือชี้อิงแบบ One past the last element เท่านั้น) จะถือเป็นการจุติเรียกหาหายนะ UB มาเยือนทันที
  • สร้างผลลัพธ์พิศวงที่คาดเดาไม่ได้ (The Consequences of UB): เมื่อระบบแหกกฎเกิดเหตุการณ์ Out-of-bounds ขึ้นหน้างาน โปรแกรมอาจจะทำท่าลวงโลกเหมือนยังวิ่งทำงานได้ปกติราบรื่น, ระบบประมวลผลอาจจะเก็บโกยเจอค่าขยะตัวเลขอะไรก็ไม่รู้ (Garbage/Junk values) สุ่มติดกลับมาใช้งานคำนวณต่ออย่างหน้าตาเฉย, หรือจู่ๆ ก็อาจจะดึงเบรกทำให้โปรแกรมค้างรวนข้ามกระดานล่มแครช (Crash) ดับอนาถลงได้เลยง่ายๆ
  • ภัยร้ายแรงขีดสุดปูทางสู่ Buffer Overflow: นี่คือหายนะผลกระทบที่น่ากลัวที่สุดในจักรวาลของการลุยโค้ด System Programming เลยครับ! การพยายามเขียนลงบรรจุข้อมูลลานล้นเกินเลยขอบเขตตู้จดหมาย (Out-of-bounds write) คือการชักนำยกระดับปมไปสู่ปัญหา Buffer Overflow ซึ่งตัวเลขปริมาณข้อมูลสารพัดที่ไหลกระฉอกล้นหลุดลอยออกไปนั้น อาจจะเข้าไปเหยียบทำลายเขียนทับตัวแปรเป้าหมายสำคัญอื่นๆ ที่ตั้งหลักแคมป์อยู่ติดใกล้ๆ กันพอดิบพอดี หรือที่สยองกว่านั้นคือ หากตัว Array มีตังค์เช่าจัดสรรพื้นที่วางจองโฉนดอยู่บนแผงควบคุมหลัก Stack ข้อผิดพลาดทับถมนั้นมันอาจจะไปเขียนเซ่นทับ Address รหัสขาลง (Return Address) ของการเชื่อมฟังก์ชัน… ทำให้แฮกเกอร์ตัวดีคอยรับจับตาดู สบช่องเสียบแทรกเปลี่ยนแปลงกำหนดเส้นทางการบังคับทำงานของตัวโปรแกรม แฮกสวิตช์เด้งไปสั่งรันโค้ดคำสั่งไวรัสอันตรายระดับลึก (Arbitrary code execution) เข้าฮุบยึดครองบอร์ดของเราได้ลื่นไหลสบายๆ เลยล่ะครับ!

สถาปัตยกรรมและหลักการเกิดปัญหาของ Out-of-bounds Array

ตัวอย่างโค้ดอันตรายรันเรียกน้ำตา (Code Example)

มาสูดลมหายใจส่องดูตัวอย่างบั๊กคลาสสิกหลอนๆ ที่มักจะย้อนมาเกิดฝังในโค้ดไมโครคอนโทรลเลอร์บ่อยครั้ง เมื่อบรรดาวิศวกรหลับในหรือหน้ามืดลืมตัวว่าการเซ็ตตำแหน่ง Array มันเริ่มต้นนับที่เลข Index 0 เสมอ (หลักการ Zero-indexed) ครับ!

#include <stdio.h>

int main(void) {
    /* ลองจินตนาการสมมติว่านี่คือตัวแปรหลักที่ใช้เก็บดึงสวิตช์สถานะความปลอดภัยเปรี้ยงป้างของระบบ 
       (เช่น สมมติเป็นตัวเช็คประเมินว่าระบบรหัสผ่านตรวจเจาะผ่านหรือไม่) */
    int system_unlocked = 0; 
    
    /* สั่งการประกาศ Array จองล็อคกล่องขนาด 4 ช่องพอดิบพอดี (ซึ่งจะได้ครอบครองแผ่นป้าย Index คือ 0, 1, 2, และแค่วางจบที่ 3 เท่านั้นนะ) */
    int sensor_data[4] = {10, 20, 30, 40}; 
    
    /* ❌ ผิดพลาดบรรลัย: ก่อให้เกิด Undefined Behavior ทันตาจาก Out-of-bounds access
       โปรแกรมเมอร์เผลอนิ้วเบลอไปกดใช้สัญลักษณ์เครื่องหมาย <= เข้าไปแทนที่จะเป็น < 
       ส่งผลบีบบังคับทำให้กระแสลูปวิ่งทำงานหลุดยาวไปชนถึงตำแหน่ง Index ที่ 4 
       ซึ่งที่จริงมิติ Index ขยาดนั้นมันไม่เคยมีอยู่จริงในความยาวของกลุ่ม sensor_data เลยด้วยซ้ำ! */
    for (int i = 0; i <= 4; i++) { 
        sensor_data[i] = 99; // ตอนจังหวะลูปทำงานวนบรรจบครบเมื่อ i=4 ข้อมูลตัวเลข 99 สุดพิลึกนี้จะถูกเทสาดเขียนอัดทะลักล้นขอบกระจายออกไปทันที!
    }
    
    /* ในรูปแบบกลไกยัดของ Compiler หลายยี่ห้อดังๆ ตัวแปรทรงเกียรติอย่าง 
       system_unlocked มักจะถูกระบบสุ่มจัดสรรโควต้าเนื้อที่หน่วยความจำ 
       เอาไปวางซุกไว้จี้ติดขอบอิงกับพื้นที่ปักร่มของ sensor_data เลย...
       ดังนั้นการรันกระบวนการจงใจเขียนข้อมูลทะทักทะลักล้นในลูปหลอนข้างต้น 
       จึงอาจบังเอิญไปเหยียบสาดเขียนทับลงบนค่าข้อมูลของเจ้าตัว system_unlocked กลางอากาศแบบเต็มๆ
       ทำให้ระบบหน้ามืดเผลอพลิกเซ็ตสถานะ ปลดล็อกระห่ำความปลอดภัยให้ตัวเองโดยไม่ได้ตั้งใจซะงั้น! */
    if (system_unlocked) {
        printf("DANGER: ชิปหายแล้ว! System is UNLOCKED due to Buffer Overflow!\n");
    } else {
        printf("System is SECURE.\n");
    }
    
    return 0;
}

ข้อควรระวังและการจัดระเบียบฝึกโค้ด (Best Practices)

เพื่อกางเกราะปีกเหล็กอุดรอยรั่วสกัดกั้นและป้องกันไม่ให้ Undefined Behavior จากรอยแผล Out-of-bounds มาแทรกซึมทำร้ายระบบไมโครคอนโทรลเลอร์ฮาร์ดคอร์ของเรา, นัยยะกฎความปลอดภัย Secure Coding และตำราคัมภีร์ C สากลได้แนะนำสั่งสอนวิชากันไว้ดังนี้ครับ:

  1. ลูกผู้ชายโปรแกรมเมอร์ต้องกางร่มตรวจสอบขอบเขตคุมเองเสมออย่ามักง่าย! (Manual Bounds Checking): ในเมื่อวิถี C มันไม่ชอบเช็คจู้จี้จุกจิกให้ ภาระก็ต้องตกเป็นของเราที่ต้องเช็คป้องกันเองแหละครับ! ก่อนที่จะมักนำส่งตัวแปรไดนามิกใดๆ เข้าพิกัด (โดยเฉพาะอย่างยิ่งพวกรหัสค่าปริศนาสุ่มมั่วๆ ที่รับดูดโอนกรอกมาจากนิ้วผู้ใช้งานหรือยิงอ่านเข้ามาจากพอร์ตอ่านเซ็นเซอร์ภายนอก) เพื่อตีตั๋วเข้ามาเป็นตำแหน่ง Index ระบุจี้ตัว Array, คุณต้องบังคับสวมหมวกรักษาความปลอดภัยใช้คำสั่งตระกูล if เพื่อตีวงตรวจสอบสกรีนก่อนหน้าเสมอว่าค่านั้นได้พักพิงจำกัดพื้นที่อยู่ระหว่างโซน 0 ไปจนถึงตัวขอบ ARRAY_SIZE - 1 หรือไม่? ขอแนะนำให้จัดสรรพฤติกรรมใช้เครื่องหมายแฮช #define คงที่ หรือสร้างตัวแปรตระกูลล็อก const เพื่อระบุตรึงกำหนดไซส์ขนาดตายตัวของตัว Array ไว้แต่แรกเสมอ จะได้จัดการอุดช่องโหว่แก้ไขง่ายกระชับและลดข้อผิดพลาดหลงลืมในการพิมพ์โค้ดได้มหาศาลครับ
  2. สั่งแนบส่งป้ายบอกขนาดไซส์พ่วงติดกระเป๋าไปพร้อมกับโค้ด Array ทุกครั้ง (Pass Size with Array): พฤติกรรมดั้งเดิมแสนอันตรายคือ เมื่อเราสั่งโยนจัดส่ง Array อัดวิ่งเข้าไปสวมบทไปเป็น Parameter หน้าตาใสๆ ให้ฟังก์ชันอื่นนึง มันจะแอบโดกดดันลดรูปแปลงร่าง (พฤติกรรม Decay) สลัดตัวกลายเป็นแค่จดหมาย Pointer ชี้ทิศทางไปยังหน่วยความจำเริ่มจุดแรกแค่นั้นเอง ส่งผลให้ฟังก์ชันฝ่ายผู้ตรวจรับกักเก็บปลายทาง ไม่รู้สี่รู้แปดหูหนวกตาบอดไม่รู้ประสีประสาเลยว่าหางว่าวเนื้อผ้า Array ตัวเดิมนี้จริงๆ แล้วมันยาวใหญ่อ้วนเท่าไหร่กันแน่! เราจึงต้องลงสลักเสลาตั้งกฎเกณฑ์บีบให้สั่งจับผูกส่งตัวแปรแพ็กคู่ที่ระบุเก็บขนาดมาด้วยกัน (ตัวอย่างเช่นแนบตัววัด size_t size ติดสอยห้อยตามทบไป) ตามสอดแทรกควบคู่เข้าไปคู่ชีพด้วยเสมอ เพื่อให้อุปกรณ์ฟังก์ชันรับเหมาปลายทางสามารถเบิกอาวุธขึ้นมาเซทตรวจตราวินิจฉัย Bounds checking คัดกรองตัวเลขก่อนบรรจุได้สำเร็จนั่นเองครับ
  3. จับขังคุกถาวรประกาศเลิกใช้ฟังก์ชันสุดหลอนที่ไม่มีการตรวจสอบขอบเขต (Avoid Unsafe Functions): ห้ามเด็ดขาดเลยนะที่จะไปรื้อฟังก์ชันรุ่นดึกดำบรรพ์ทรงอานุภาพทำลายล้างสูงอย่าง gets() ออกมาใช้งาน เพราะมันถูกมาร์กชื่อว่าเป็นตัวการระดับปรมาจารย์ตำนานที่ก่อให้เกิดมหันตภัยบั๊ก Buffer Overflow แตกกระจุยข้ามเมือง! ควรฝึกพลิกตัวด่วนปรับจูนมาเปลี่ยนหันไปใช้กงจักรใหม่สุดฮิตเช่นฟังก์ชันตระกูล fgets() แทน หรือตีอัปเกรดหยิบฟังก์ชันสายเซฟระดับไฮโซในชุดคลังกลุ่ม C11 Annex K Bounds-Checking Interfaces (ซึ่งหน้าตาของกระบวนท่าฟังก์ชันปลอดภัยเหล่านั้นมักจะคล้องจองมีคำลงท้ายปิดหลังป้ายพ่วงด้วย _s ทั้งส้นแหละครับ ยกตัวอย่างรหัสก็เช่น gets_s, memcpy_s, strcpy_s เป็นต้น) ซึ่งบรรดาโค้ดฟังก์ชันผู้สูงศักดิ์ชุดนี้ ถูกสร้างออกแบบตั้งด่านประทับตราบรรจงมาเพื่อให้ต้องบังคับยื่นส่งค่านับจำนวนของกระเป๋าพารามิเตอร์ขนาดของหลุม Buffer แทรกมัดรวมเชือกเข้าไปประกอบการด้วยเสมอ เพื่อเพิ่มวุฒิความปลอดภัยกันทะลุกรอบให้สถาปัตยกรรมระดับซอฟต์แวร์ได้อย่างเหนียวแน่นหนึบขึ้นครับ!

สรุปทิ้งท้าย

การเข้าถึงข้อมูลล้ำฝืนทะลุกรอบ Array นอกขอบเขต (กระบวนจำเพาะรหัส Out-of-bounds access) มันไม่ได้เป็นเพียงแค่เรื่องกระจุ๊กกระจิ๊กอย่างการอ่านค่าตัวเลขมาแสดงผลผิดพลาดรวนมึนๆ บนหน้าจอบอร์ดทั่วไปหรอกครับ แต่มันคือการอุตริปลดล็อกดึงสลักระเบิดทะลวงเปิดประตูบานใหญ่ที่เปิดทางแหวกกฎอ้าแขนอัญเชิญไปสู่หายนะ Undefined Behavior สุดหลอน ซึ่งเราอาจจะเปรียบกงเกลี้ยงหายนะตัวนี้ได้เหมือนดั่งการซุกระเบิดเวลาเงียบๆ สอดแทรกเอาไว้ใต้แผงเมนบอร์ดเพื่อใจเย็นรอคอยซุ่มวันเวลาที่จะถล่มเด้งทำลายความเสถียรมั่นคงตายตัวและความมั่นคงความปลอดภัยของระบบโครงสร้างแบบ Embedded รนหาจุดจบของเราโดยไม่รู้ตัว… การเริ่มหมั่นพากเพียรแกะรอยตีความเข้าใจปรัชญาเบื้องหลังของการสร้าง C ที่เจาะจงเน้นหิวแต่ความเร็วระเบิดและสลัดทิ้งถีบส่งภาระหน้าที่การตรวจสอบหลักให้มากองทับถมไว้ที่สติปัญญาเบื้องลึกของเรา จะช่วยบ่มเพาะติดปีกเสริมทักษะนิสัยความรอบคอบวิเคราะห์ให้เรามีวุฒิภาวะวิชาและเขียนโค้ดผงาดได้น่าเกรงขามอย่างรัดกุมระแวดระวังหนาแน่นและสวมลักษะทักษะความเป็นมืออาชีพสุดแข็งแกร่งและก้าวร้าวในการสกัดบั๊กได้เก่งกาจมากขึ้นนั่นเองครับ!

สำหรับเพื่อนๆ คนไหนถ้าโดนป้ายยาและสนใจอยากจะด่ำดิ่งฟินกับการจับลุยเจาะลึกสุดยอดเทคนิคการฝึกสมาธิเขียนโค้ดลอจิกภาษา C ฮาร์ดคอร์ให้สกัดเซียนปลอดภัยล้ำสมัยในระดับนักพัฒนาโปร (Secure Coding System) หรือกระหายขอดื่มด่ำอยากลองดูรหัสตัวอย่างเดโมโค้ดเจ๋งๆ ของกระบวนท่าเซียนเทคนิคการสยายปีกจัดการเจาะลึกแผง Memory ขูดลึกลงระดับโครงสร้างไมโครคอนโทรลเลอร์แบบดุดันและลึกซึ้งทะลุกรอบ… สามารถแวะเวียนสไลด์หน้าจอเข้ามาอัปเดตเกาะติดกระดานคอนเทนต์ความรู้ระดับคุณภาพ และขยับคีย์บอร์ดร่วมกระแทกคุยแบ่งปันคอมเมนต์โต้ตอบสนทนากันต่อเนื่องแบบไฟลุกได้ที่แหล่งชุมชนเว็บโซเชียล www.123microcontroller.com ของพวกเราได้เลยนะครับ! ห้ามพลาดด้วยประการทั้งปวง แล้วพบกันมันส์ๆ โฉมใหม่ได้รอดูในบทความถัดหน้าหน้า สวัสดีและ Happy Coding ให้สนุกสุดเหวี่ยงทุกคนครับ!