• บทอธิบายเชิงเทคนิคที่อธิบายกระบวนการ ตั้งแต่วินาทีที่กดปุ่มเปิดเครื่องคอมพิวเตอร์จนถึงลินุกซ์เคอร์เนลเริ่มทำงาน แบบเป็นลำดับขั้น
  • กล่าวถึงกระบวนการที่ CPU เริ่มต้นใน real mode แล้วเข้าสู่ protected mode และ long mode อย่างเจาะจง
  • อธิบายบทบาทและหลักการทำงานของแต่ละขั้นตอนอย่างละเอียด เช่น เฟิร์มแวร์ BIOS/UEFI, bootloader (GRUB), การคลายการบีบอัดเคอร์เนลและการย้ายตำแหน่งแอดเดรส
  • อธิบายแนวคิดสำคัญที่จำเป็นต่อการเริ่มต้นเคอร์เนล เช่น memory mapping, interrupt, page table, kASLR พร้อมตัวอย่างสั้น ๆ ที่เข้าใจง่าย
  • การเข้าใจกลไกภายในของการบูตลินุกซ์ช่วยให้มองเห็นภาพเรื่อง สถาปัตยกรรมระบบ, ความปลอดภัย, และการปรับแต่งประสิทธิภาพ ได้ลึกขึ้น

Part 1 — จากปุ่มเปิดเครื่องสู่การรันครั้งแรกของเคอร์เนล

  • เมื่อกดปุ่มเปิดเครื่อง CPU จะถูกรีเซ็ตเข้าสู่ real mode และเริ่มรันคำสั่งเริ่มต้น

    • real mode เป็นระบบแอดเดรสแบบเรียบง่ายที่มีมาตั้งแต่ยุค 8086 โดยคำนวณแอดเดรสจริงจากการรวม segment และ offset
    • ตัวอย่าง: physical_address = (segment << 4) + offset
    • หลังรีเซ็ต CPU จะกระโดดไปยังแอดเดรส 0xFFFFFFF0 (reset vector) เพื่อส่งการควบคุมต่อให้เฟิร์มแวร์
  • register คือช่องเก็บข้อมูลความเร็วสูงภายใน CPU เช่น CS (code segment), IP (instruction pointer)

    • CS ระบุตำแหน่งของโค้ดปัจจุบัน ส่วน IP ชี้ไปยังคำสั่งถัดไปที่จะรัน

BIOS และ UEFI

  • BIOS เป็นเฟิร์มแวร์แบบดั้งเดิม ซึ่งหลังจาก POST (การทดสอบตัวเองเมื่อเปิดเครื่อง) จะตรวจลำดับการบูตและค้นหาดิสก์ที่บูตได้
    • ดิสก์ที่บูตได้จะมีท้ายเซกเตอร์ 512 ไบต์แรกเป็นค่า 0x55AA
    • BIOS จะคัดลอกเซกเตอร์นี้ไปยังแอดเดรส 0x7C00 แล้วกระโดดไปเริ่มรัน
  • UEFI เป็นเทคโนโลยีสมัยใหม่ที่มาแทน โดยสามารถเข้าใจไฟล์ซิสเต็มได้โดยตรงและโหลดโปรแกรมบูตที่มีขนาดใหญ่กว่าได้
    • ต่างจาก BIOS ตรงที่ไม่มีข้อจำกัดแบบ “เซกเตอร์แรก” และส่งข้อมูลระบบให้ OS ได้มากกว่า

Bootloader

  • bootloader คือโปรแกรมที่โหลดเคอร์เนลเข้าสู่หน่วยความจำและเตรียมให้พร้อมสำหรับการรัน
    • โดยทั่วไปใช้ GRUB ซึ่งจะอ่านไฟล์คอนฟิก แล้วโหลดเคอร์เนลและ initial ramdisk (initrd) เข้าสู่หน่วยความจำ
    • ไฟล์เคอร์เนลประกอบด้วย โปรแกรมตั้งค่าขนาดเล็กสำหรับ real mode และ ตัวเคอร์เนลหลักที่ถูกบีบอัดไว้
    • GRUB จะบันทึกข้อมูลอย่างตำแหน่งเคอร์เนล, command line, และตำแหน่ง initrd ลงในโครงสร้าง setup header แล้วกระโดดไปยังโค้ดตั้งค่าของเคอร์เนล

โปรแกรมตั้งค่า (setup code)

  • ทำหน้าที่สร้าง พื้นที่ทำงานที่คาดการณ์ได้ล่วงหน้า ก่อนรันเคอร์เนล
    • จัดแนว segment register (CS, DS, SS) และล้าง direction flag เพื่อให้การคัดลอกหน่วยความจำทำงานได้สม่ำเสมอ
    • สร้าง stack เพื่อเก็บข้อมูลชั่วคราวระหว่างการเรียกฟังก์ชัน
    • กำหนดค่าเริ่มต้นของ พื้นที่ BSS (พื้นที่ตัวแปรโกลบอลที่ต้องเริ่มจากค่า 0) ให้เป็น 0
  • หากมีออปชัน earlyprintk ก็สามารถตั้งค่า serial port เพื่อพิมพ์ข้อความดีบักช่วงต้นได้
  • ขอ RAM map (e820) จากเฟิร์มแวร์เพื่อดูว่าหน่วยความจำส่วนใดใช้งานได้และส่วนใดถูกจองไว้
  • เมื่อเตรียมทุกอย่างเสร็จแล้ว จะเรียก main ซึ่งเป็นฟังก์ชัน C ตัวแรก จากนั้นจึงเข้าสู่ขั้นตอนเปลี่ยนโหมด

Interrupt

  • interrupt คือกลไกที่ทำให้ CPU หยุดงานปัจจุบันชั่วคราวเพื่อไปจัดการเหตุการณ์เร่งด่วน
    • ตัวอย่างที่พบได้บ่อยคือการกดคีย์และสัญญาณจากตัวจับเวลา
    • maskable interrupt สามารถบล็อกชั่วคราวได้ ส่วน NMI (Non-Maskable Interrupt) จะต้องถูกจัดการเสมอ
    • ระหว่างเปลี่ยนโหมดจะมีการบล็อกชั่วคราวเพื่อป้องกัน interrupt ที่ไม่คาดคิด

Part 2 — จาก real mode สู่ 32 บิต และต่อไปยัง 64 บิต

  • ลินุกซ์สมัยใหม่ทำงานใน long mode ของ สถาปัตยกรรม x86_64
    • จึงต้องเปลี่ยนผ่านตามลำดับจาก real mode → protected mode → long mode

Protected mode

  • เป็นโหมด 32 บิตที่ถูกนำมาใช้เพื่อก้าวข้ามข้อจำกัดในทศวรรษ 1980 โดยมีโครงสร้างสำคัญ 2 อย่าง
    • GDT (Global Descriptor Table): กำหนดแอดเดรสเริ่มต้น ขนาด และสิทธิ์ของ segment
      • ลินุกซ์ใช้ flat model เพื่อลดความซับซ้อน โดยมองพื้นที่ 32 บิตทั้งหมดเป็นพื้นที่ต่อเนื่องเดียว
    • IDT (Interrupt Descriptor Table): เก็บแอดเดรสของ handler ที่จะถูกเรียกเมื่อเกิด interrupt
      • ระหว่างบูตจะโหลดเพียง IDT ขั้นต่ำก่อน แล้วค่อยติดตั้ง IDT แบบสมบูรณ์หลังเคอร์เนลเริ่มต้นเสร็จ

กระบวนการเปลี่ยนโหมด

  • โค้ดตั้งค่าจะทำ ปิด interrupt, หยุดชิป PIC, เปิดใช้งานสาย A20, และ กำหนดค่าเริ่มต้นให้ตัวประมวลผลคณิตศาสตร์เสริม ก่อน
    • สาย A20 เป็นกลไกทางประวัติศาสตร์ที่ใช้แก้ปัญหาการวนกลับของแอดเดรสที่ 1MB
  • จากนั้นโหลด GDT และ IDT ขั้นต่ำ แล้วตั้งค่า PE bit ในรีจิสเตอร์ CR0 และทำ far jump
    • ขั้นตอนนี้จะทำให้เข้าสู่ protected mode และตั้งค่า segment กับ stack pointer ใหม่ให้สอดคล้องกับระบบแอดเดรสแบบใหม่

Control registers

  • CR0: เปิดใช้งาน protected mode
  • CR3: เก็บแอดเดรสระดับบนสุดของ page table
  • CR4: เปิดใช้งานความสามารถเสริม เช่น PAE

การเตรียมเข้าสู่ long mode

  • การสลับไปยังโหมด 64 บิตต้องมี 2 เงื่อนไข
    • ต้องเปิดใช้งาน paging: เพื่อทำ mapping ระหว่าง virtual address กับ physical address
    • ต้องตั้งค่า LME (Long Mode Enable) bit ของ EFER register
  • page table จะทำ mapping หน่วยความจำเป็นหน้า ๆ ขนาด 4KB แต่ในการบูตช่วงต้นมักกำหนดแบบง่ายด้วย identity map ระดับ 2MB

ขั้นตอนการเปิดใช้งาน paging

  • เปิดฟีเจอร์ PAE ใน CR4 แล้วสร้าง page table ขั้นต่ำเพื่อครอบคลุมพื้นที่แอดเดรสต่ำด้วยหน่วย 2MB
  • เขียนแอดเดรสของตารางระดับบนสุดลงใน CR3 แล้วเปิดใช้งาน paging
  • ตั้งค่า LME bit ของ EFER แล้วกระโดดไปยังโค้ด 64 บิตเพื่อ เข้าสู่ long mode
  • เมื่อแอดเดรสและรีจิสเตอร์ถูกขยายเป็น 64 บิตแล้ว ก็พร้อมสำหรับการรันเคอร์เนล

Part 3 — การคลายการบีบอัดเคอร์เนล การปรับแอดเดรส และการย้ายตัวเอง

  • ตอนนี้ CPU อยู่ในโหมด 64 บิตแล้ว และในหน่วยความจำมี อิมเมจเคอร์เนลที่ถูกบีบอัดไว้
    • สตับ 64 บิตขนาดเล็กจะทำหน้าที่คลายเคอร์เนลและปรับแอดเดรสให้เหมาะสม

การจัดการเบื้องต้นและการตั้งค่าความปลอดภัย

  • สตับจะคำนวณตำแหน่งรันจริงของตัวเอง และหากมีโอกาสทับกับเคอร์เนลก็จะย้ายไปยังตำแหน่งที่ปลอดภัยด้วย self-relocation
  • กำหนดค่าเริ่มต้นให้ พื้นที่ BSS ของตัวเอง และโหลด IDT แบบง่าย (รวม handler ของ page fault และ NMI)
    • หากเกิด page fault ก็จะเพิ่ม mapping ที่ขาดอยู่ทันทีเพื่อกู้คืนการทำงาน
  • สร้าง identity mapping สำหรับพื้นที่ที่จำเป็น เช่น เคอร์เนล, boot parameter, และ command line buffer

การคลายการบีบอัดเคอร์เนล

  • ฟังก์ชัน extract_kernel จะทำงานเพื่อคลายการบีบอัดเคอร์เนล
    • รองรับอัลกอริทึมการบีบอัดหลายแบบ เช่น gzip, xz, zstd, lzo
    • หลังคลายแล้วจะอ่าน ELF header เพื่อคัดลอกส่วนของโค้ด/ข้อมูลไปยังแอดเดรสที่ถูกต้อง
  • หากแอดเดรสที่ใช้ตอน build เคอร์เนลไม่ตรงกับแอดเดรสที่ถูกโหลดจริง จะต้องทำ relocation
    • โดยแก้ไขคำสั่งหรือ pointer ที่มีแอดเดรสอยู่ภายในให้ตรงกับตำแหน่งจริงในหน่วยความจำ
  • เมื่อทุกอย่างพร้อมแล้ว จะกระโดดไปยัง ฟังก์ชัน start_kernel และเริ่มกระบวนการกำหนดค่าเริ่มต้นของเคอร์เนลอย่างเต็มรูปแบบ

การสุ่มตำแหน่งของเคอร์เนล (kASLR)

  • kASLR (Kernel Address Space Layout Randomization) คือการสุ่ม physical และ virtual address ของเคอร์เนลเพื่อเพิ่มความยากในการโจมตี
    • ระหว่างบูตจะสุ่มเลือก base อยู่ 2 ค่า
      • physical base: แอดเดรส RAM ที่เคอร์เนลจะถูกวางจริง
      • virtual base: จุดเริ่มต้นของ virtual address ที่เคอร์เนลจะใช้
  • ขั้นตอนการเลือก
    • จัดทำ รายการพื้นที่ที่ต้องปกป้อง เช่น bootloader, initrd, และ command line buffer
    • สแกน memory map ของเฟิร์มแวร์เพื่อหาพื้นที่ว่างที่มีขนาดใหญ่เพียงพอ
    • ใช้ entropy ที่ได้จากคำสั่งสุ่มของฮาร์ดแวร์เป็นต้น เพื่อเลือกสล็อตแบบสุ่ม
  • หากไม่พบพื้นที่ที่เหมาะสมก็จะกลับไปใช้แอดเดรสเริ่มต้น และหากมีออปชัน nokaslr ก็จะปิดการสุ่มนี้

สรุปคำศัพท์

  • Hexadecimal (เลขฐานสิบหก) : แสดงด้วยคำนำหน้า 0x เหมาะกับโครงสร้างบิตและการจัดแนวของฮาร์ดแวร์
  • Register: หน่วยเก็บข้อมูลชั่วคราวภายใน CPU (CS, DS, SS, IP, SP ฯลฯ)
  • Segment/Offset: วิธีคำนวณแอดเดรสใน real mode (segment * 16 + offset)
  • BIOS/UEFI: เฟิร์มแวร์ที่ทำหน้าที่เริ่มต้นระบบและโหลดโปรแกรมบูต
  • Bootloader (GRUB) : โหลดเคอร์เนลและส่งข้อมูลระบบต่อ
  • Stack/BSS: พื้นที่เก็บชั่วคราวของฟังก์ชัน และพื้นที่ตัวแปรโกลบอลที่ถูกกำหนดค่าเริ่มต้นเป็น 0
  • Interrupt/NMI: กลไกจัดการเหตุการณ์จากฮาร์ดแวร์/ซอฟต์แวร์
  • GDT/IDT: ตารางนิยาม segment และ interrupt
  • A20 Line: สวิตช์ป้องกันการวนกลับของแอดเดรสที่ 1MB
  • Protected Mode/Long Mode: โหมดการรันแบบ 32 บิต และ 64 บิต
  • Paging/Page Tables: การทำ mapping ระหว่าง virtual address กับ physical address

ยังไม่มีความคิดเห็น

ยังไม่มีความคิดเห็น