Triển khai `printf` không cần OS - Tận dụng thư viện chuẩn C trong môi trường Bare Metal
(popovicu.com)- Giới thiệu cách triển khai để có thể sử dụng các hàm chuẩn C bao gồm
printfngay cả khi không có hệ điều hành bằng cách tận dụng thư viện Newlib - Trong môi trường Bare Metal dựa trên kiến trúc RISC-V, tự triển khai driver UART và các hàm cấp phát bộ nhớ rồi kết nối chúng với Newlib
- Chỉ với việc triển khai tối thiểu các hàm system call như
_write,_sbrk,_closecũng có thể sử dụng các tính năng nâng cao nhưprintf - Hướng dẫn cách tạo toolchain dựa trên Newlib cùng với RISC-V GCC toolchain, tự động build và viết linker script
- Kết quả là đã xây dựng thành công môi trường
printfvới xuất UART, nhậpscanf, và cả cấp phát bộ nhớ động hoạt động đầy đủ
Software abstractions and C standard library
- Trong OS thông thường, khi gọi
printfsẽ có nhiều tầng trừu tượng như system call của kernel, tầng terminal, render font... cùng hoạt động - Trong môi trường Bare Metal, cần điều khiển vào/ra trực tiếp mà không có hệ điều hành, vì vậy phải tự triển khai driver
- Newlib cung cấp cấu hình có thể mở rộng bằng cách chỉ triển khai chức năng tối thiểu thay vì toàn bộ thư viện chuẩn C
Newlib concept
printfvề mặt nội bộ được xây dựng dựa trên các hàm primitive đơn giản như_write- Ban đầu Newlib định nghĩa mọi hàm ở dạng dummy, và nếu chỉ triển khai phần cần thiết thì phần còn lại có thể dùng giá trị mặc định
- Chỉ cần lập trình viên triển khai các hàm cần thiết là có thể linh hoạt sử dụng tính năng của thư viện C
Cross-compilation toolchain
- Để cross-compile từ x86_64/Linux → RISC-V, cần build trực tiếp từ mã nguồn GCC
- Thiết lập toolchain với Newlib làm thư viện C mặc định để có thể build binary cho RISC-V
Toolchain details
- Khi build toolchain sử dụng các tùy chọn
--prefix,--enable-multilib,--disable-gdb,--with-cmodel=medany medanylà thiết lập cho phép truy cập vùng nhớ địa chỉ cao trên RISC-V- Sau khi build xong, có thể sử dụng cross-compiler và thư viện Newlib tại đường dẫn
/opt/riscv-newlib
Implementing the memory and UART building blocks
- Tự truy cập trực tiếp địa chỉ phần cứng 16550A UART trong môi trường QEMU để triển khai truyền và nhận ký tự
- Kết nối với Newlib bằng cách triển khai các hàm thay thế system call như
_write,_sbrk,_close _sbrkhoạt động theo cách mở rộng bộ nhớ heap từ vị trí_endđến_stack_bottom
Application example: input and output
- Trong hàm
maincó thể sử dụngprintf,scanf, và giá trị nhập vào cũng được xử lý bình thường - Không hỗ trợ echo, nhưng vẫn có thể nhận chuỗi bằng
scanfrồi in ra - Tự triển khai runtime riêng để khởi tạo stack, zero-fill vùng BSS rồi sau đó gọi
main
Linker script
- Địa chỉ bắt đầu thực thi là
0x80000000, và mã runtime được đặt tại vị trí này - Bố trí bộ nhớ theo thứ tự
.text,.rodata,.data,.bss, và heap được đặt từ_endđến trước stack - Stack có kích thước cố định 64KB, địa chỉ đỉnh là
0x80000000 + 64MB - Dùng câu lệnh
ASSERTđể ngăn heap và stack va chạm nhau
The ‘gotcha’ moment
- Khi cấu hình toolchain, cần dùng
--with-cmodel=medanyđể tạo ra mã máy có thể xử lý các địa chỉ từ0x80000000trở lên - Nếu mô hình địa chỉ của thư viện C và mã ứng dụng khác nhau thì sẽ phát sinh lỗi link
Running the app
- Có thể tự động hóa cross-compile và chạy QEMU thông qua Makefile
- Dùng các tùy chọn
-specs=nosys.specs,-nostartfiles,-T link.ldđể áp dụng cấu hình Newlib tối thiểu và runtime tự định nghĩa - Khi chạy
make debug, nhập và xuất qua UART hoạt động bình thường trên console QEMU - Có thể kiểm tra trace lệnh thực tế thông qua
qemu_debug.log
Conclusion
- Đã triển khai bằng Newlib một cấu trúc có thể dùng
printf,scanf,malloc... mà không cần hệ điều hành - Điểm mấu chốt là tận dụng cấu trúc kiểu building block của Newlib để chỉ triển khai tối thiểu những chức năng cần thiết
- Về sau cũng có thể bổ sung thêm các chức năng như hệ thống file, quản lý bộ nhớ..., đồng thời vẫn giữ được tính tương thích thư viện để tái sử dụng trong môi trường Bare Metal
- Toàn bộ kết quả dự án có dung lượng khoảng 220KB, tương đối nhỏ gọn và hiệu quả
Mã nguồn GitHub: popovicu/bare-metal-cstdlib
Chưa có bình luận nào.