Viết bootloader BIOS cho chế độ 64-bit
(thasso.xyz)- Bắt đầu từ boot sector 512 byte, bài viết xây dựng từng bước luồng bootloader tối thiểu để đưa CPU x86_64 từ real mode 16-bit lên long mode 64-bit
- Sau khi xác nhận được phần xuất cơ bản bằng BIOS interrupt, bài viết nối
nasm,ld,objcopy, QEMU để kiểm tra xem ảnh boot có thực sự chạy được hay không - Do giới hạn dung lượng của boot sector, mã được chia thành stage 1/stage 2; trước khi vào protected mode, BIOS
int 0x13sẽ đọc phần mã tiếp theo từ đĩa - Từ protected mode 32-bit trở đi không thể dùng BIOS routine nữa, nên cần khởi tạo phần cứng như GDT, flat segmentation và xuất trực tiếp ra VGA buffer
- Để vào long mode 64-bit, cần cấu hình đầy đủ page table, PAE,
EFER.LME,cr0.PGvà GDT cho 64-bit; sau đó có thể gọi mã C freestanding như gọi một kernel
Môi trường 16-bit khởi đầu từ BIOS
- CPU x86 sau khi reset sẽ ở real mode, và kích thước toán hạng mặc định là 16-bit
- real mode dùng segmentation để tạo không gian địa chỉ 20-bit, có thể xử lý tối đa 1MB bộ nhớ
- Mã đầu tiên BIOS chạy nằm trong boot sector của đĩa
- BIOS tìm đĩa có sector đầu tiên kết thúc bằng magic number
0xaa55 - BIOS nạp sector đó vào địa chỉ bộ nhớ
0x7c00
- BIOS tìm đĩa có sector đầu tiên kết thúc bằng magic number
- Không gian BIOS chuyển giao chỉ có 512 byte, nên đoạn mã này tập trung vào vai trò bootstrap để nạp phần còn lại của bootloader
- BIOS routine chỉ dùng được khi còn ở real mode
Chuẩn bị và môi trường build
- Để làm theo, cần Intel 64 and IA-32 Architectures Software Developer’s Manual, assembler và QEMU
- Assembler dùng trong ví dụ là nasm; bài viết giả định bạn đã biết x86 assembly và cú pháp nasm
- Ngay cả khi không có CPU x86_64, bạn vẫn có thể chạy bằng cách giả lập CPU x86 trong QEMU
- Tài liệu nền tảng cho đến chế độ 32-bit có thể tham khảo Writing a Simple Operating System — from Scratch
Tạo boot sector và kiểm tra chạy thực tế
- Boot sector đầu tiên sẽ in
"Hello, world!"bằng BIOS routine, rồi dừng bằnghltvà một vòng lặp - Để in chuỗi, dùng BIOS video services
int 0x10vớiah = 0x0e Makefiletạo object bằng nasm, link bằng linker script, rồi dùngobjcopy -O binaryđể sinh ảnh boot rawmake bootchạy ảnh bằng QEMUqemu-system-x86_64 -no-reboot -drive file=$<,format=raw,index=0,media=disk
- Linker script được cấu hình để boot sector được đặt theo mốc
0x7c00boot_sectortrongMEMORYcó origin là0x7c00, length là512- Section
.bootsignthêm0x55,0xaatại vị trí0x7c00 + 510
- Có thể tự xử lý offset và magic number ngay trong assembly của boot sector, nhưng ở đây linker script đảm nhiệm việc đó
stage 1 đọc stage 2 từ đĩa
- stage 1 là mã boot sector được BIOS nạp, với mục tiêu là đưa stage 2 vào bộ nhớ
- stage 2 chứa phần mã chuyển từ real mode 16-bit sang protected mode 32-bit
- Sau khi đã vào protected mode thì không thể dùng BIOS routine nữa, nên việc đọc sector từ đĩa phải hoàn tất trước khi chuyển chế độ
- Truy cập đĩa dùng BIOS disk services
int 0x13ah = 0x42là chức năng BIOS extended readdl = 0x80là số hiệu ổ đĩa- disk address packet chứa số sector cần đọc, địa chỉ đích và sector bắt đầu
- Ví dụ dùng
READ_SECTORS_NUM equ 64để đọc 64 sector- boot sector là sector 0 nên stage 2 được đọc từ sector 1
- địa chỉ đích là
BOOT_LOAD_ADDR + SECTOR_SIZE, tức0x7c00 + 512
- Trong mã vẫn còn xử lý tạm thời chấp nhận trường hợp số sector đọc được ít hơn yêu cầu
- Ban đầu stage 2 sao chép
print_stringcho real mode để in"Hello from stage 2", nhằm xác nhận cú nhảy từ stage 1 đã nối tiếp đúng cách
Chuyển sang protected mode 32-bit
- Để vào protected mode, trước tiên phải định nghĩa Global Descriptor Table (GDT)
- Trong protected mode, segmentation mặc định được dùng cho bảo vệ bộ nhớ
- Ở long mode 64-bit sẽ cần paging, nhưng để vào protected mode thì bước đầu tiên vẫn là thiết lập segmentation
- GDT trong ví dụ tuân theo flat model trong tài liệu Intel
- có code segment và data segment
- cả hai segment đều ánh xạ toàn bộ linear address space
- vì đây là bước trung gian để tiến tới long mode nên dùng mô hình đơn giản nhất
- GDT là một cấu trúc liên tiếp nằm trong bộ nhớ
- ở đầu có null descriptor để bắt lỗi invalid translation
- tiếp theo là code segment descriptor và data segment descriptor
- Quá trình chuyển diễn ra theo thứ tự sau
- dùng
cliđể vô hiệu hóa interrupt - dùng
lgdt [gdt32_pseudo_descriptor]để nạp địa chỉ và độ dài GDT vào GDTR - đặt
cr0.PE, tức bit 0 củacr0, để bật protected mode - thực hiện far jump để xóa instruction pipeline và cập nhật
cssang code segment mới
- dùng
- Sau khi vào protected mode, các giá trị segment cũ không còn ý nghĩa, nên
ds,ss,es,fs,gsphải được đặt thành selector của data segment mới - Muốn bật lại interrupt sau khi hoàn tất mọi thiết lập thì cần thêm các bước khác
Xuất ra màn hình mà không dùng BIOS
- Trong protected mode không thể tiếp tục gọi BIOS routine
- Việc in chuỗi chuyển sang cách ghi trực tiếp vào VGA buffer
print_string32ghi byte ký tự và byte màu vào địa chỉ0xb8000- giá trị màu là
0xf - mỗi ô ký tự dùng 2 byte
- giá trị màu là
- Hàm xuất này rất đơn giản, nên thông điệp luôn xuất hiện ở góc trên bên trái màn hình
Page table để vào long mode
- IA-32e mode trong tài liệu Intel tương ứng với long mode trong manual của AMD64
- Muốn chuyển sang long mode thì CPU phải đang ở protected mode và paging cũng phải được bật
- Có thể tham khảo khái niệm paging tại Introduction to Paging và OSTEP
- Long mode với PAE bật sử dụng page table 4 cấp
build_page_tabletạo page table 4 cấp tại địa chỉ được chỉ định- kích thước page là
0x1000 - kích thước mỗi page table là
0x1000 - số entry là
512 - trước hết khởi tạo bốn table về 0 để mọi entry đều ở trạng thái not present
- nối các entry đầu tiên theo chuỗi PML4 → PDP → PD → page table
- ở lớp page table thấp nhất, thiết lập 512 entry
- kích thước page là
GDT cho 64-bit và thứ tự chuyển sang long mode
- Dù paging đảm nhiệm không gian địa chỉ ảo và quản lý quyền truy cập, GDT vẫn cần trong long mode
- GDT cho 64-bit cũng theo flat model và gần như giống với GDT cho protected mode
- Khác biệt nằm ở các bit liên quan đến long mode
- đặt cờ 64-bit code segment cho code segment
- khi cờ này được đặt, bit kích thước thao tác mặc định phải là 0
- Việc chuyển sang long mode đi theo luồng sau
- tạo page table 4 cấp tại địa chỉ
0x1000 - ghi địa chỉ PML4 table vào
cr3 - đặt bit 5 của
cr4để bật PAE - đọc MSR
0xc0000080rồi đặtEFER.LME, tức bit 8 - đặt cờ
PGlà bit 31 củacr0để bật paging - nạp GDT cho 64-bit bằng
lgdt - far jump sang code segment 64-bit để vào chế độ 64-bit
- tạo page table 4 cấp tại địa chỉ
- Ngay sau khi bật paging, CPU đang ở trạng thái IA-32e compatibility mode; việc nhảy sang GDT có cờ segment 64-bit sẽ hoàn tất chuyển đổi sang chế độ 64-bit
- Thông báo xác nhận thành công được hiển thị ở góc trên bên trái màn hình thông qua VGA buffer
Gọi mã C freestanding
- Khi đã vào long mode 64-bit, có thể gọi mã C freestanding
kernel.cxóa VGA buffer0xb8000rồi in"Hello from C"- Ở phía assembly,
start_long_modesau khi in chuỗi 64-bit sẽ khai báoextern _start_kernelrồi gọi_start_kernel - Linker script chia vùng nhớ thành ba phần
boot_sector:0x7c00, length512stage2:0x7e00, length512kernel:0x8000, length0x10000
- Các section
.text,.data,.rodata,.bssđược đặt trong vùng kernel Makefileđược thay đổi để build cả assembly lẫn C- C compiler là
gcc - các CFLAGS chính là
-std=c99 -ffreestanding -m64 -mno-red-zone -fno-builtin -nostdinc -Wall -Wextra
- C compiler là
- Toàn bộ mã ví dụ được cung cấp tại liên kết tải về
1 bình luận
Ý kiến trên Hacker News
Có thể vào thẳng long mode với ít mã hơn nhiều mà không cần đi qua protected mode: https://wiki.osdev.org/Entering_Long_Mode_Directly
Từng có một bootloader cho một kernel 64-bit nhỏ được làm theo cách này, và kể cả bao gồm cả mã đọc kernel từ đĩa và thiết lập chế độ VESA, nó vẫn dư chỗ trong boot sector. Cũng không cần loader giai đoạn 2
Nếu dùng ổ cứng hiện đại thực sự thì nên xem GPT hơn là MBR. Có thể xử lý cả các đĩa lớn trên 2TB mà không bị giới hạn bởi bảng phân vùng. UEFI loại bỏ những vấn đề này và cho phép dùng một bố cục đĩa tử tế mà không quá khó khăn
Để vào chế độ 64-bit thì không cần protected mode. Tuy nhiên tốt nhất là không dùng BIOS. Nó lộn xộn và chỉ làm mọi việc rắc rối hơn
Dùng UEFI qua EDK2 hoặc GnuEFI thì tốt hơn; cả hai đều khá dễ và tiện để triển khai. Sẽ mất chút thời gian để quen với các khái niệm ban đầu của UEFI, nhưng nhìn các dự án ví dụ trên GitHub là có thể nắm cấu trúc khá dễ. EDK có các tệp .dec và .inf hơi khó chịu, còn GnuEFI thì phải đọc header file để tìm chức năng, nhưng vẫn tốt hơn nhiều so với giao diện BIOS có đặc tả mơ hồ. Trên phần cứng thật, thậm chí không thể giả định rằng int 0x10, int 0x15, v.v. tồn tại đúng nghĩa
Trên hệ thống UEFI, có thể giả định một nền tảng tối thiểu ổn định, và cũng có thể liệt kê các chức năng phần cứng hoặc nền tảng một cách hợp lý. Ngoài ra UEFI đã thiết lập nền tảng khá nhiều, nên trong thành phần OS loader không cần khởi tạo quá nhiều; chỉ cần tải thẳng OS, driver và các thành phần phù hợp với thiết kế kernel. Lấy memory map, truy cập hệ thống tệp EFI và đọc những thứ cần thiết là được
80286 có Machine Status Word (MSW), một thanh ghi 16-bit, còn 80386 mở rộng nó thành thanh ghi 32-bit CR0. Về sau, long mode 64-bit bổ sung EFER MSR và mở rộng CR0 lên 64-bit, nhưng ngay cả ngày nay CR0 chỉ dùng 11 bit, còn EFER cũng chỉ có 8 bit đang hoạt động
Tôi thắc mắc vì sao Intel/AMD không dùng luôn các bit còn trống trong thanh ghi hiện có, mà lại hai lần chọn cách thêm mới: https://wiki.osdev.org/CPU_Registers_x86-64#CR0
Lý do CR8 xuất hiện trong khi CR1 và CR5~CR7 vẫn ở trạng thái dự trữ có vẻ cũng tương tự
Thứ trông phức tạp không cần thiết nhất trong bài này là Makefile và linker script. NASM hỗ trợ xuất flat binary, có lẽ tác giả thấy dùng nó quá “hacky”
Trước đây OS của tôi có một tệp tên make.sh để châm chọc chuyện này. Giờ thì tôi dùng những thứ xịn như ‘định dạng tệp’, nên -fbin và --oformat=binary đã thành ký ức thoáng qua. Tôi từng cố rất lâu việc tách C file dữ liệu và C file mã, dump chúng thành binary rồi lắp ghép con quái vật từ đó, nhưng liên kết và nạp trở nên quá khó. Dùng ELF hoặc PE thì tốt hơn, và thực tế có vẻ các định dạng đó sinh ra để làm những việc như vậy
Trông hay và giống một bài luyện tập tốt, nhưng tôi không biết nó có hữu ích không. Tôi tò mò liệu có một UX kiểu đồ chơi Fisher-Price nào để kiểm tra hoặc thay đổi cấu hình lúc đang chạy không
Khởi động là quá trình đi từ chế độ tối giản, single-user mode, recovery mode đến trạng thái bay lên
Tôi đã dùng Unix cùng với các sản phẩm Microsoft từ thời Xenix/DOS, chắc khoảng 40 năm rồi. Tôi tự hỏi trong thời gian đó mọi thứ đã tiến bộ được bao nhiêu
Tôi cũng dùng Linux từ bản Thụy Điển, tức bản phát hành đầu tiên, và cũng từng dùng GNU 0.1
Xin lỗi vì đã gọi Xenix là Unix. Xenix là một sản phẩm me-too lộn xộn, ngay từ sau khi ra mắt cho đến khi suy tàn đã muốn trở thành thứ thuộc về quá khứ
Microsoft giống một công ty không phát hành sản phẩm, mà đổ khay cát mèo lên đầu khách hàng. Ví dụ gần đây là Copilot và 22H2
Nhìn cách xe F1, bút chì, máy tính bỏ túi đã phát triển, tôi tự hỏi chúng ta đang gần đến mức lý tưởng có thể sử dụng được đến đâu
Tôi cũng thắc mắc vì sao bootloader không phải là static kernel mode. Trước đây từng như vậy, và gần đây có người đề xuất nên quay lại như thế, tôi cũng đồng ý
https://wiki.osdev.org/A20_Line
Ngạc nhiên vì tất cả các bước cần thiết để chuyển CPU sang đúng chế độ trông đều có vẻ không cần thiết. Phần lớn có vẻ là thủ tục cần có vì tương thích ngược
Không biết Intel đã không thể cung cấp một cờ hoặc lệnh để khởi động ngay từ đầu ở đúng chế độ, hay không thể loại bỏ tương thích ngược
Tôi nhớ ARM64 cũng từng có một số vấn đề tương tự. Không biết có CPU nào ngay từ đầu được thiết kế 64-bit nên không cần tương thích ngược và mặc định đi vào trạng thái mong muốn hay không. Tự hỏi liệu mục tiêu hay thiết kế của Itanium có phải như vậy không
Itanium, tức Itanic, cũng vậy
Chính tương thích ngược mới là lý do cốt lõi để chọn x86 thay vì ARM, MIPS, RISC-V, v.v. Đáng tiếc là có vẻ một số người ở Intel và AMD không hiểu rõ điều này
Đây là một dự án tuyệt vời. Nếu những người ủng hộ UEFI ở đây thắc mắc tại sao lại phải tạo ra một cách bootloader mới, có lẽ họ đang bỏ lỡ lý do vì sao người ta làm những việc như thế này
Đúng như câu tác giả viết ở cuối, “nếu bạn đã theo đến đây thì thật tuyệt”, nó thật sự rất tuyệt
Không biết UEFI đã ra đời được bao lâu rồi. Giá mà họ cũng bỏ BIOS cùng với long mode thì tốt biết mấy, thật đáng tiếc
Nói được lên kế hoạch khai tử không có nghĩa là đã bị xóa, mà là không còn được cập nhật hay phát triển nữa với mục tiêu cuối cùng là loại bỏ
Không biết quy trình khởi động này có hoạt động trên EFI/UEFI không. Nếu có, cũng tò mò liệu trình giám sát UEFI có mô phỏng việc chuyển đổi giữa real mode, protected mode và long mode, hay thực hiện trên phần cứng thật
Tôi đã viết ở đây quá trình tạo bootloader UEFI cho OS sở thích: https://0xc0ffee.netlify.app/osdev/05-bootloader-p1.html
Không biết việc này trên ARM có đơn giản hơn không