นี่ไม่ใช่ระบบปฏิบัติการ จริง มันเป็นเพียงระบบปฏิบัติการง่าย ๆ ที่สร้างขึ้นเพื่อการศึกษา
เป้าหมายหลักที่ฉันติดตามคือการเรียนรู้ว่าระบบปฏิบัติการทำงานอย่างไรจากพื้นดิน เริ่มต้นจากการขัดจังหวะภาคการบูตฮาร์ดแวร์และซอฟต์แวร์ของตัวเองไดรเวอร์ของตัวเอง
พื้นที่เก็บข้อมูลถูกต่อท้ายด้วย GCC เพราะฉันวางแผนที่จะเขียนระบบปฏิบัติการง่าย ๆ อีกอันหนึ่งด้วยสนิม ดังนั้นฉันหวังว่าจะมี ghaiklor-os-gcc และ ghaiklor-os-rustc
| สวัสดีโลก |
|---|
ghaiklor-os-gcc ประกอบด้วยสองไฟล์ในรูปแบบไบนารีดิบ: boot.bin และ kernel.bin พวกเขาอยู่ใน boot/boot.bin และ kernel/kernel.bin ตามหลังการคอมไพล์
Boot.bin รวบรวมผ่าน NASM MakeFile ใช้ boot/boot.asm และเรียก nasm boot/boot.asm -f bin -o boot/boot.bin มือจับ NASM รวมถึงจากโฟลเดอร์ย่อยเองดังนั้นไฟล์แอสเซมบลี ทั้งหมด จะถูกรวบรวมเป็นไบนารี ไม่มีอะไรง่าย ๆ
Kernel.bin รวบรวมผ่าน Cross-Compiler GCC และ LD ที่คุณต้องติดตั้ง ดูส่วนสภาพแวดล้อมการพัฒนา หลังจากติดตั้ง cross-compiler แล้วเราสามารถนำแหล่งที่มาจาก CPU , ไดรเวอร์ รวมถึง โฟลเดอร์ เคอร์เนล และ LIBC ซ้ำ ไฟล์ . c ทั้งหมดถูกรวบรวมผ่าน gcc ไฟล์วัตถุที่คอมไพล์ใช้สำหรับรวบรวม เคอร์เนล จากนั้นผ่าน LD ld -o kernel/kernel.bin -Ttext 0x1000 <OBJ_FILES> --oformat binary
os-image.bin คือ ซึ่งรวบรวม concatenate ของ boot.bin และ kernel.bin ทำได้อย่างง่ายดายด้วย cat boot/boot.bin kernel/kernel.bin > os-image.bin
ฉันเขียนสคริปต์ bootstrap.sh ที่คุณสามารถเรียกใช้ มันจะติดตั้งการพึ่งพาที่จำเป็นทั้งหมดสำหรับเครื่องโฮสต์ของคุณ
bash bootstrap.shเมื่อคอมพิวเตอร์เปิดหรือรีเซ็ตมันจะทำงานผ่านชุดการวินิจฉัยที่เรียกว่าโพสต์-การทดสอบด้วยตนเอง ลำดับนี้จะปิดการค้นหาอุปกรณ์ที่สามารถบู๊ตได้เช่นฟลอปปี้, CDROM หรือฮาร์ดดิสก์
อุปกรณ์สามารถบู๊ตได้หากมีการบูตภาคที่มีลำดับไบต์ 0x55 , 0xAA ในไบต์ 511 และ 512 ตามลำดับ เมื่อ BIOS พบภาคการบูตดังกล่าวจะถูกโหลดลงในหน่วยความจำที่ 0x0000:0x7C00
การใช้งานอุปกรณ์ที่สามารถบู๊ตได้อย่างง่าย:
jmp $
times 510 - ($ - $$) db 0
dw 0xAA55 $ - $$ ผลลัพธ์ใน CURRENT_POINTER - START_POINTER ด้วยวิธีนี้เรากำลังคำนวณระยะเวลาที่บันทึกการบูตของเรา หลังจากนั้นเราเป็นสัดส่วน 510 จากมันและเติมด้วยศูนย์รับบันทึกบูต 512 ไบต์ด้วยลายเซ็นภาคการบูต
ตัวอย่างเช่นเรามี $ - $$ เท่ากับ 100 ดังนั้นเรามี 510 - 100 = 410 ไบต์ฟรี เรากำลังเติมศูนย์เหล่านี้ 410 ไบต์ และสองไบต์สุดท้าย 511 และ 512 เป็นลายเซ็นที่บูตได้ซึ่งเราเติมด้วย dw 0xAA55
เสร็จแล้ว! เรามีอุปกรณ์ที่สามารถบู๊ตได้และสามารถแทนที่ jmp $ ของเราด้วยรหัสใดก็ได้ที่คุณต้องการ
การใช้งานภาคการบูต
ที่จุดเริ่มต้นรหัสของเรากำลังทำงานในโหมดจริง
โหมดจริงเป็นโหมด 16 บิตแบบง่ายที่มีอยู่ในโปรเซสเซอร์ X86 ทั้งหมด โหมดจริงคือการออกแบบโหมด X86 ครั้งแรกและถูกใช้โดยระบบปฏิบัติการเริ่มต้นจำนวนมาก เพื่อจุดประสงค์ความเข้ากันได้โปรเซสเซอร์ X86 ทั้งหมดเริ่มดำเนินการในโหมดจริง
มีอะไรดีและดีในโหมดจริง?
ข้อเสีย
ผู้เชี่ยวชาญ
เนื่องจากข้อ จำกัด และปัญหามากมายที่โหมดจริงมีเราจึงต้องเปลี่ยนไปใช้โหมดป้องกัน
โหมดที่ได้รับการป้องกันเป็นโหมดการทำงานหลักของโปรเซสเซอร์ Intel ที่ทันสมัยตั้งแต่ 80286 มันอนุญาตให้ทำงานกับช่องว่างเสมือนจริงหลายแห่งซึ่งแต่ละอันมีหน่วยความจำที่อยู่ได้สูงสุด 4 GB
เนื่องจาก CPU เริ่มต้นโดย BIOS เริ่มต้นในโหมดจริงการสลับไปยังโหมดที่ได้รับการป้องกันจะทำให้คุณไม่สามารถใช้การขัดจังหวะ BIOS ส่วนใหญ่ได้ ก่อนที่จะเปลี่ยนไปใช้โหมดป้องกันคุณต้องปิดการใช้งานการขัดจังหวะรวมถึง NMI, เปิดใช้งานสาย A20 และโหลดตาราง descriptor ทั่วโลก
อัลกอริทึมสำหรับการเปลี่ยนเป็นโหมดป้องกัน:
cli
lgdt [ gdt_descriptor ]
mov eax , cr0
or eax , 0x1
mov cr0 , eax
jmp CODE_SEG:init_pmการใช้งานเพื่อเปลี่ยนเป็น PM
ตาราง descriptor ทั่วโลก
แต่เราสามารถไปไกลกว่านี้ ...
โหมดยาวคืออะไรและทำไมจึงตั้งค่า?
นับตั้งแต่มีการเปิดตัวโปรเซสเซอร์ X86-64 โหมดใหม่ได้รับการแนะนำเช่นกันซึ่งเรียกว่าโหมดยาว โหมดยาวโดยทั่วไปประกอบด้วยสองโหมดย่อยซึ่งเป็นโหมด 64 บิตจริงและโหมดความเข้ากันได้ (32 บิต)
สิ่งที่เราสนใจคือโหมด 64 บิตเนื่องจากโหมดนี้มีคุณสมบัติใหม่มากมายเช่น:
ก่อนที่จะเปลี่ยนเป็นโหมดยาวเรา ต้อง ตรวจสอบว่า CPU รองรับโหมดนี้หรือไม่ ในกรณีที่หาก CPU ไม่รองรับโหมดยาวเราจำเป็นต้องย้อนกลับไปยังโหมดที่ได้รับการป้องกัน
ตรวจสอบว่าโหมดยาวรองรับ
ถ้าเป็นเช่นนั้นให้เปลี่ยนเป็นโหมดยาว
โหมดทั้งหมดเหล่านี้ยอดเยี่ยม แต่เราไม่สามารถเขียนระบบปฏิบัติการใน 512 ไบต์ ดังนั้นภาคการบูตของเรา จะต้อง รู้วิธีโหลดเคอร์เนลที่รวบรวมจากฮาร์ดดิสก์
เมื่อเราอยู่ในโหมดจริงเราสามารถใช้การขัดจังหวะ BIOS เพื่ออ่านจากดิสก์ ในกรณีของเราคือ INT 13,2 - Read Disk Sectors
ใช้อย่างไร?
;; al = number of sectors to read (1 - 128)
;; ch = track/cylinder number
;; cl = sector number
;; dh = head number
;; dl = drive number
;; bx = pointer to buffer
mov ah , 0x02
mov al , 15
mov ch , 0x00
mov cl , 0x02
mov dh , 0x00
mov dl , 0
mov bx , KERNEL_OFFSET_IN_MEMORY
int 0x13 รหัสนี้ส่งผลให้อ่านจากฮาร์ดดิสก์ไปยังที่อยู่ KERNEL_OFFSET_IN_MEMORY มันอ่าน 15 ภาคส่วนที่สองเริ่มต้นจากที่สองและจัดเก็บตามที่อยู่ KERNEL_OFFSET_IN_MEMORY
เนื่องจากอิมเมจระบบปฏิบัติการที่รวบรวมไว้ของเราคือการเชื่อมต่อของภาคการบูตและเคอร์เนลและเรารู้ว่าภาคการบูตของเราคือ 512 ไบต์เราสามารถมั่นใจได้ว่าเคอร์เนลของเราเริ่มต้นในภาคที่สอง
เมื่อการอ่านเสร็จสมบูรณ์แล้วเราสามารถโทรหาคำสั่งที่ KERNEL_OFFSET_IN_MEMORY ของเราและให้การดำเนินการกับเคอร์เนล
call KERNEL_OFFSET_IN_MEMORY
jmp $การใช้งานสำหรับการอ่านดิสก์
เราสามารถวาดเส้นที่นี่เกี่ยวกับภาคการบูตของเรา การไหลง่าย:
call อย่างง่ายในขั้นตอนนี้ภาคการบูตของเราทำงานเสร็จและเริ่มทำงานกับเคอร์เนล
คุณสามารถนำทางผ่านแหล่งบูตและพยายามหาวิธีการทำงาน
เมื่อเราโทรหาคำแนะนำตามที่อยู่เราสามารถมีปัญหาเล็กน้อย เราไม่แน่ใจว่าคำสั่งตามที่อยู่คือ kernel_main() การแก้ปัญหาง่าย
เราสามารถเขียน routine ย่อยที่แนบกับจุดเริ่มต้นของรหัสเคอร์เนล ฟังก์ชั่นการโทรนอกกิจวัตรย่อยของเคอร์เนลของเรา - kernel_main() เมื่อไฟล์วัตถุจะถูกเชื่อมโยงเข้าด้วยกันการโทรนี้จะถูกแปลเป็นการโทรของ kernel_main() ของเรา ()
global _start
[bits 32]
[extern kernel_main]
_start:
call kernel_main
jmp $การใช้งานรายการเคอร์เนล
ในขั้นตอนนี้เรามีจุดเข้าสู่วิธี kernel_main() ของเรา และนั่นคือจุดเริ่มต้นของเราสำหรับเคอร์เนลทั้งหมด
ฉันคิดว่าน่าเบื่อที่จะอธิบายว่า #include ทำงานอย่างไรและเกิดอะไรขึ้นใน kernel_main() ของเรา คุณสามารถทำตามวิธีการที่ฉันโทรจากมันได้อย่างง่ายดาย
รายการเคอร์เนลใน C
นั่นคือส่วนที่ง่ายที่สุด
เราจำเป็นต้องสร้างภาพ boot/boot.bin ในรูปแบบไบนารีดิบ ในการทำเช่นนั้นเราเรียก nasm Assembler ด้วยธงพิเศษ
nasm boot/boot.asm -f bin -o boot/boot.binมันส่งผลให้เกิดรูปแบบไบนารีดิบที่คุณสามารถเรียกใช้ผ่าน QEMU
ในขั้นตอนนี้เรามีการรวบรวมภาคการบูต
เราจำเป็นต้องสร้างแหล่งที่มาทั้งหมดจากโฟลเดอร์ทั้งหมดซ้ำ ๆ ยกเว้นโฟลเดอร์ boot
ไฟล์ C ทั้งหมดจะรวบรวมไว้ในไฟล์วัตถุผ่านไฟล์ gcc และแอสเซมบลีผ่าน nasm :
gcc -g -ffreestanding -Wall -Wextra -fno-exceptions -m32 -std=c11 -c < SOURCE > -o < OBJ_FILE >
nasm < SOURCE > -f elf -o < OBJ_FILE > มันส่งผลให้ไฟล์วัตถุที่จำเป็นทั้งหมดสำหรับการเชื่อมโยงไปยังรูปแบบไบนารีดิบ สิ่งที่เหลืออยู่คือเชื่อมโยงเข้าด้วยกันผ่าน ld :
ld -o kernel/kernel.bin -Ttext 0x1000 kernel/kernel_entry.o < OBJ_FILES > --oformat binary โปรดทราบว่า kernel/kernel_entry.o ตั้งแต่แรกเพราะเรามีปัญหากับการโทร kernel_main() ด้วยวิธีนี้เรารับประกันได้ว่าคำสั่งแรกจะถูกเรียกจาก boot/kernel_entry.asm ของเรา
ท้ายที่สุดเราได้รวบรวมภาพเคอร์เนลในรูปแบบไบนารีดิบ
เนื่องจากภาคการบูตและเคอร์เนลของเราเป็นรูปแบบไบนารีดิบเราจึงสามารถต่อพวกเขาได้
cat boot/boot.bin kernel/kernel.bin > os-image.bin ตอนนี้เราสามารถเรียกใช้ os-image.bin ผ่าน qemu-system-i386 BIOS พยายามค้นหาเซกเตอร์ที่สามารถบูตได้ค้นหา boot/boot.bin ของเราและเห็นลายเซ็น เริ่มเรียกใช้รหัสแอสเซมบลีของเราที่ boot/boot.bin ซึ่งโหลด kernel/kernel.bin ของเรา bin ผ่าน int 13,2 ลงในหน่วยความจำและดำเนินการ
นั่นคือวิธีที่ทุกอย่างทำงานร่วมกัน อย่าลังเลที่จะนำทางผ่านโครงการขอบคุณ?
ใบอนุญาต MIT (MIT)
ลิขสิทธิ์ (c) 2016 Eugene Obrezkov
ได้รับอนุญาตโดยไม่ต้องเสียค่าใช้จ่ายสำหรับบุคคลใด ๆ ที่ได้รับสำเนาซอฟต์แวร์นี้และไฟล์เอกสารที่เกี่ยวข้อง ("ซอฟต์แวร์") เพื่อจัดการในซอฟต์แวร์โดยไม่มีการ จำกัด รวมถึง แต่ไม่ จำกัด เฉพาะสิทธิ์ในการใช้สำเนาดัดแปลงผสานเผยแพร่เผยแพร่
ประกาศลิขสิทธิ์ข้างต้นและประกาศการอนุญาตนี้จะรวมอยู่ในสำเนาทั้งหมดหรือส่วนสำคัญของซอฟต์แวร์
ซอฟต์แวร์มีให้ "ตามสภาพ" โดยไม่มีการรับประกันใด ๆ ไม่ว่าโดยชัดแจ้งหรือโดยนัยรวมถึง แต่ไม่ จำกัด เฉพาะการรับประกันความสามารถในการค้าการออกกำลังกายสำหรับวัตถุประสงค์เฉพาะและการไม่เข้าร่วม ไม่ว่าในกรณีใดผู้เขียนหรือผู้ถือลิขสิทธิ์จะต้องรับผิดชอบต่อการเรียกร้องความเสียหายหรือความรับผิดอื่น ๆ ไม่ว่าจะเป็นการกระทำของสัญญาการละเมิดหรืออื่น ๆ ที่เกิดขึ้นจากหรือเกี่ยวข้องกับซอฟต์แวร์หรือการใช้งานหรือการติดต่ออื่น ๆ ในซอฟต์แวร์