Back to Explore
Xây dựng hệ điều hành từ con số 0 với Rust: Kỹ thuật khởi động và quản lý bộ nhớ (Phần 2)

Xây dựng hệ điều hành từ con số 0 với Rust: Kỹ thuật khởi động và quản lý bộ nhớ (Phần 2)

Tiếp nối hành trình kiến tạo hệ điều hành bằng Rust, bài viết này đi sâu vào kỹ thuật khởi động CPU, quản lý phân đoạn bộ nhớ (BSS) và cấu hình Linker Script cho kiến trúc ARM64 trên Raspberry Pi.

Website
Upvote this postSign in to upvote this article.

Bài viết được dịch và tổng hợp từ tin tức gốc. Bạn có thể đọc bài viết gốc bằng tiếng Anh tại đây.

Điểm tin nhanh:

  • Tối ưu hóa việc định vị địa chỉ symbol bằng kỹ thuật PC-relative addressing để tránh phụ thuộc vào bộ nhớ khi stack chưa sẵn sàng.
  • Triển khai logic chọn CPU core chính (primary core) và park các core còn lại để đảm bảo tính nhất quán khi khởi động kernel.
  • Thiết lập Linker Script để định nghĩa layout bộ nhớ, bao gồm các phân đoạn quan trọng như .text, .data, và .bss.

Việc xây dựng một hệ điều hành từ con số 0 không chỉ là bài tập về kỹ thuật, mà là thử thách tối thượng đối với bất kỳ lập trình viên hệ thống nào muốn thấu hiểu bản chất của phần cứng. Khi bạn không còn sự hỗ trợ từ các thư viện chuẩn hay hệ điều hành host, mọi dòng lệnh Assembly đều trở thành ranh giới giữa sự sống và cái chết của kernel. Trong phần tiếp theo của series Xây dựng hệ điều hành từ con số 0 với Rust: Khởi đầu hành trình kiến tạo nhân hệ thống, chúng ta sẽ giải mã cách thức khởi động nhân hệ thống trên kiến trúc ARM64.

Kỹ thuật định vị địa chỉ không phụ thuộc bộ nhớ

Trong giai đoạn khởi động sớm, khi BSS chưa được zero-out và stack chưa được thiết lập, chúng ta không thể dựa vào các bảng địa chỉ trong bộ nhớ. Giải pháp là sử dụng kỹ thuật PC-relative addressing. Bằng cách kết hợp các lệnh như ADR_REL và #:lo12:symbol, chúng ta có thể xác định địa chỉ của bất kỳ symbol nào chỉ với hai lệnh mà không cần truy cập RAM.

Cover image for Building an Operating System In Rust Part 2

Boot vào CPU core chính

Khi hệ thống khởi động, tất cả các CPU core đều sẽ cố gắng thực thi kernel. Để tránh xung đột, chúng ta cần chọn một core làm core chính và đưa các core còn lại vào trạng thái chờ (parking). Dưới đây là bảng logic lựa chọn core dựa trên mask:

Core Giá trị ID (Binary) Mask (0b11) Kết quả Trạng thái
1 01110000 0b11 0b00 Primary
2 01110001 0b11 0b01 Parked
3 01110010 0b11 0b10 Parked
4 01110011 0b11 0b11 Parked

Mẹo hay: Bạn có thể tùy chỉnh CONST_CORE_ID_MASK thành 0b111 hoặc 0b1111 để hỗ trợ lần lượt 7 hoặc 15 core tùy theo kiến trúc phần cứng cụ thể.

Khởi tạo phân đoạn BSS và chuyển sang Rust

Phân đoạn BSS lưu trữ các biến toàn cục chưa được khởi tạo. Trước khi thực thi mã Rust, chúng ta phải dọn sạch vùng nhớ này bằng cách lặp qua các địa chỉ từ __bss_start đến __bss_end_exclusive và ghi giá trị 0 vào đó. Sau khi hoàn tất, chúng ta thiết lập Stack Pointer (sp) và thực hiện cú nhảy b _start_rust để chuyển giao quyền điều khiển cho Rust runtime.

Ảnh bìa bài viết

Cấu hình Linker Script

Linker script đóng vai trò là bản đồ chỉ dẫn cho linker, định nghĩa cách bố trí các phân đoạn trong file binary cuối cùng. Việc quản lý bộ nhớ hiệu quả là yếu tố sống còn, tương tự như cách chúng ta Tối ưu hóa hiệu năng và hiệu suất: Chiến lược sống còn cho hệ thống phần mềm hiện đại. Chúng ta cần định nghĩa rõ các phân đoạn .text, .rodata, .data.bss với các quyền truy cập (R, RX, RW) phù hợp để đảm bảo tính bảo mật và ổn định cho kernel.

Đánh giá & Lời khuyên Thực tiễn

Việc viết bootloader bằng Assembly kết hợp với Rust mang lại quyền kiểm soát tuyệt đối, nhưng cũng đi kèm với rủi ro cao.

  • Ưu điểm: Hiệu năng tối đa, không phụ thuộc vào bất kỳ runtime nào, hiểu sâu sắc về kiến trúc CPU.
  • Nhược điểm: Độ phức tạp cực lớn, khó debug, dễ gây ra lỗi segmentation fault nếu cấu hình linker script sai lệch.
  • Lưu ý: Khi triển khai trên môi trường thực tế, hãy luôn kiểm tra kỹ các ràng buộc về alignment (căn chỉnh bộ nhớ) và đảm bảo rằng các vùng nhớ được đánh dấu NOLOAD không gây ra xung đột với các thiết bị ngoại vi.

Việc nắm vững các kỹ thuật này cũng giúp bạn có tư duy tốt hơn khi làm việc với các hệ thống nhúng hoặc khi cần Tối ưu hóa hiệu năng trước khi ra mắt: Chiến lược sống còn cho mọi dự án phần mềm.

Câu hỏi thường gặp (FAQ)

Tại sao phải dùng Assembly thay vì dùng Rust ngay từ đầu?

Vì ở giai đoạn khởi động sớm, môi trường thực thi chưa có stack, chưa có heap và chưa có runtime của Rust. Assembly là ngôn ngữ duy nhất có thể thiết lập các điều kiện tiên quyết này.

Làm sao để debug khi kernel bị treo ở giai đoạn boot?

Bạn nên sử dụng QEMU với tính năng GDB stub để theo dõi từng lệnh thực thi của CPU, hoặc sử dụng UART để xuất log ra console nếu phần cứng hỗ trợ.

Việc zero-out BSS có thực sự cần thiết?

Có, vì theo tiêu chuẩn C và Rust, các biến static chưa khởi tạo phải có giá trị mặc định là 0. Nếu không zero-out, kernel sẽ chứa các giá trị rác từ bộ nhớ, dẫn đến hành vi không xác định.

Kết luận

Chúng ta đã hoàn thành việc xây dựng khung khởi động cho kernel bằng Assembly và cấu hình linker script. Đây là nền tảng vững chắc để bắt đầu viết các logic phức tạp hơn bằng Rust. Hãy tiếp tục theo dõi hi_dev để cập nhật những bài viết chuyên sâu về hệ thống và kỹ thuật lập trình hiện đại. Nếu bạn có bất kỳ thắc mắc nào về quá trình triển khai, đừng ngần ngại để lại bình luận phía dưới để chúng ta cùng thảo luận.

Discussion (0)

You need to log in to post comments. Log In

No comments yet. Start the discussion!