- บทอธิบายเชิงเทคนิคที่อธิบายกระบวนการ ตั้งแต่วินาทีที่กดปุ่มเปิดเครื่องคอมพิวเตอร์จนถึงลินุกซ์เคอร์เนลเริ่มทำงาน แบบเป็นลำดับขั้น
- กล่าวถึงกระบวนการที่ 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แล้วกระโดดไปเริ่มรัน
- ดิสก์ที่บูตได้จะมีท้ายเซกเตอร์ 512 ไบต์แรกเป็นค่า
- 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 แบบสมบูรณ์หลังเคอร์เนลเริ่มต้นเสร็จ
- GDT (Global Descriptor Table): กำหนดแอดเดรสเริ่มต้น ขนาด และสิทธิ์ของ segment
กระบวนการเปลี่ยนโหมด
- โค้ดตั้งค่าจะทำ ปิด 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 ที่เคอร์เนลจะใช้
- ระหว่างบูตจะสุ่มเลือก base อยู่ 2 ค่า
- ขั้นตอนการเลือก
- จัดทำ รายการพื้นที่ที่ต้องปกป้อง เช่น 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
ยังไม่มีความคิดเห็น