• แนะนำวิธีทำให้สามารถใช้ ฟังก์ชันมาตรฐาน 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

ยังไม่มีความคิดเห็น

ยังไม่มีความคิดเห็น