- แนะนำวิธีทำให้สามารถใช้ ฟังก์ชันมาตรฐาน C รวมถึง
printfได้แม้ไม่มีระบบปฏิบัติการ โดยอาศัย ไลบรารี Newlib - ในสภาพแวดล้อม Bare Metal บนสถาปัตยกรรม RISC-V มีการเขียน ไดรเวอร์ UART และฟังก์ชันจัดสรรหน่วยความจำ ขึ้นเองแล้วเชื่อมต่อเข้ากับ Newlib
- เพียงแค่ติดตั้ง ฟังก์ชัน system call ขั้นต่ำอย่าง
_write,_sbrk,_closeเป็นต้น ก็สามารถใช้งานฟีเจอร์ระดับสูงอย่างprintfได้ - แนะนำวิธีสร้างทูลเชนที่อิง Newlib โดยอธิบาย การเขียนสคริปต์สำหรับ build อัตโนมัติและ linker script ร่วมกับ RISC-V GCC toolchain
- ผลลัพธ์คือสามารถสร้าง สภาพแวดล้อม
printfที่รองรับการส่งออกผ่าน UART, รับข้อมูลด้วยscanfและการจัดสรรหน่วยความจำแบบไดนามิก ได้สำเร็จ
Software abstractions and C standard library
- ใน OS ทั่วไป เมื่อเรียก
printfจะมี ชั้นนามธรรมหลายระดับ เช่น kernel system call, terminal layer, font rendering ทำงานอยู่เบื้องหลัง - ในสภาพแวดล้อม Bare Metal จำเป็นต้อง ควบคุมอินพุต/เอาต์พุตโดยตรงโดยไม่มีระบบปฏิบัติการ จึงต้อง เขียนไดรเวอร์ด้วยตนเอง
- Newlib ให้ โครงสร้างที่ขยายต่อได้โดยติดตั้งเฉพาะความสามารถขั้นต่ำ แทนที่จะใส่ไลบรารีมาตรฐาน C ทั้งชุด
Newlib concept
printfถูกสร้างขึ้นภายในบนพื้นฐานของ ฟังก์ชัน primitive แบบง่าย อย่าง_write- ใน Newlib ฟังก์ชันทั้งหมดถูกกำหนดเป็น dummy ไว้ในช่วงแรก และถ้าต้องใช้ส่วนใดก็สามารถเขียนเฉพาะส่วนนั้นได้ ส่วนที่เหลือใช้ค่าเริ่มต้นได้
- หากนักพัฒนาเขียนเฉพาะฟังก์ชันที่จำเป็น ก็จะสามารถ ใช้งานความสามารถของไลบรารี C ได้อย่างยืดหยุ่น
Cross-compilation toolchain
- สำหรับการคอมไพล์ข้ามแพลตฟอร์มจาก x86_64/Linux → RISC-V จำเป็นต้อง build จากซอร์สของ GCC โดยตรง
- มีการตั้งค่าให้สร้างทูลเชนที่ใช้ Newlib เป็นไลบรารี C เริ่มต้น เพื่อให้สามารถ build ไบนารีสำหรับ RISC-V ได้
Toolchain details
- ตอน build ทูลเชนใช้ตัวเลือก
--prefix,--enable-multilib,--disable-gdb,--with-cmodel=medany medanyคือการตั้งค่าบน RISC-V ที่ ทำให้สามารถเข้าถึงพื้นที่หน่วยความจำที่มีแอดเดรสสูงได้- หลัง build เสร็จจะสามารถใช้ cross-compiler และไลบรารี Newlib ได้จากพาธ
/opt/riscv-newlib
Implementing the memory and UART building blocks
- มีการเข้าถึง ที่อยู่ฮาร์ดแวร์ 16550A UART ในสภาพแวดล้อม QEMU โดยตรงเพื่อทำการรับส่งตัวอักษร
- ฟังก์ชันทดแทน system call อย่าง
_write,_sbrk,_closeถูกเขียนขึ้นเพื่อเชื่อมเข้ากับ Newlib _sbrkทำงานโดย ขยายหน่วยความจำ heap จากจุด_endไปจนถึง_stack_bottom
Application example: input and output
- ในฟังก์ชัน
mainสามารถใช้printf,scanfได้ และจัดการค่าที่ป้อนเข้ามาได้ตามปกติ - แม้จะไม่รองรับ echo แต่ก็สามารถรับสตริงผ่าน
scanfและพิมพ์ออกมาได้ - มีการเขียน runtime แยกต่างหากเพื่อเริ่มต้น stack, ทำ zero-fill ให้กับส่วน BSS แล้วจึงเรียก
main
Linker script
- ที่อยู่เริ่มต้นการทำงานคือ
0x80000000และวางโค้ด runtime ไว้ที่ตำแหน่งนั้น - จัดวางหน่วยความจำตามลำดับ
.text,.rodata,.data,.bssโดยตั้งค่า heap ให้เริ่มจาก_endจนถึงก่อน stack - stack มี ขนาดคงที่ 64KB และที่อยู่บนสุดคือ
0x80000000 + 64MB - ใช้คำสั่ง
ASSERTเพื่อป้องกันไม่ให้ heap กับ stack ชนกัน
The ‘gotcha’ moment
- ตอนตั้งค่าทูลเชนต้องใช้
--with-cmodel=medanyเพื่อให้ สามารถสร้างคำสั่ง machine code ที่จัดการแอดเดรสตั้งแต่0x80000000ขึ้นไปได้ - หากโค้ดของไลบรารี C และโค้ดแอปพลิเคชัน ใช้ address model ต่างกัน จะเกิด link error
Running the app
- สามารถทำให้การ cross-compile และการรัน QEMU เป็นอัตโนมัติได้ผ่าน Makefile
- ใช้ตัวเลือก
-specs=nosys.specs,-nostartfiles,-T link.ldเพื่อใช้การตั้งค่า Newlib แบบขั้นต่ำและ runtime ที่ผู้ใช้กำหนดเอง - เมื่อรัน
make debugอินพุตและเอาต์พุตผ่าน UART บนคอนโซล QEMU จะทำงานได้ตามปกติ - สามารถตรวจสอบ instruction trace จริง ได้ผ่าน
qemu_debug.log
Conclusion
- มีการสร้างโครงสร้างที่ ใช้งาน
printf,scanf,mallocได้แม้ไม่มีระบบปฏิบัติการ ด้วย Newlib - กลยุทธ์สำคัญคือการใช้โครงสร้างแบบ building block ของ Newlib เพื่อ เขียนเฉพาะฟังก์ชันที่จำเป็นให้น้อยที่สุด
- ในอนาคตยังสามารถเพิ่มฟีเจอร์อย่างระบบไฟล์หรือการจัดการหน่วยความจำได้ และ ยังคงความเข้ากันได้กับไลบรารีพร้อมนำกลับมาใช้ซ้ำใน Bare Metal ได้
- ผลลัพธ์ของทั้งโปรเจ็กต์มีขนาดราว 220KB ซึ่งถือว่าค่อนข้างเล็กและมีประสิทธิภาพ
ซอร์สบน GitHub: popovicu/bare-metal-cstdlib
ยังไม่มีความคิดเห็น