裸機上的 printf:在無操作系統環境下構建 C 標準庫

在嵌入式開發和底層系統編程領域,裸機開發是一項極具挑戰性但又至關重要的任務。想象一下,在沒有操作系統支持的情況下,讓 C 語言的標準庫函數,如printf正常工作,這聽起來是不是很有趣又充滿挑戰?今天,我們就來深入探索如何利用 Newlib 在裸機系統上實現這一目標,以 RISC-V 平臺為例,揭開裸機編程的神秘面紗。

軟件抽象與 C 標準庫:從常規系統到裸機的轉變

在日常使用的電腦系統,比如 Mac 或 Linux 筆記本上運行printf函數,背后有著一套復雜的機制。應用程序調用printf,這個函數通常是動態鏈接的,經過多層 C 函數調用后,最終會觸發操作系統內核的系統調用。內核會通過不同的子系統來處理輸出,涉及終端和偽終端的相關操作,最后將printf的輸出呈現在屏幕上。而且,printf還要依據提供的模板對輸出字符串進行格式化處理,這一整套流程涉及眾多軟件抽象層。

然而,裸機系統與常規系統大不相同。在裸機環境下,大多數這樣的抽象層并不存在,軟件棧要簡單得多。在裸機上進行 C 編程時,C 函數下方沒有任何支持。在常規系統中,進程可以通過系統調用(由軟件中斷實現)將輸出交給內核處理,但在裸機上沒有內核可交付,可我們仍希望printf之類的函數能夠工作,最好是能輸出到像通用異步收發傳輸器(UART)這樣的簡單 I/O 設備上。這時候,Newlib 就發揮作用了。

Newlib:裸機 C 標準庫的構建利器

你或許熟悉 GNU 的glibcmusl等 C 標準庫,但如果想在裸機上啟用 C 標準庫,Newlib 絕對值得關注。從本質上講,Newlib 并不是一個完整的 C 標準庫,而是一個構建定制、精簡 C 標準庫的工具包。

Newlib 將 C 標準庫的實現簡化為幾個具有清晰接口的基本原語,這些原語可以作為獨立函數來實現。像printfmalloc這樣更復雜的函數會調用這些原語。例如,我們需要實現_write原語,它的作用是向輸出流寫入單個字符,Newlib 會在這個基礎上構建printf函數,從而實現更復雜的輸出功能。

此外,Newlib 還提供了一些預制的實現。在某些配置下,你甚至可以將底層平臺指定為 Linux,這時 Newlib 提供的實現會像glibc一樣進行系統調用。而在極簡配置中,Newlib 會以最小化的形式提供所有原語,這些原語要么返回零,要么拋出錯誤。開發者可以根據應用程序的實際需求,選擇實現自己關心的構建模塊,其余部分則依賴默認實現。

交叉編譯工具鏈:連接不同平臺的橋梁

在深入了解如何使用 Newlib 之前,我們需要先掌握交叉編譯工具鏈的概念。交叉編譯是指在一個平臺上編譯代碼,生成另一個平臺可執行的指令。例如,從 x86_64/Linux 平臺編譯代碼,使其能在 ARM64/Mac 上運行。

在 Linux 平臺下,情況更為復雜,因為不同的 Linux 發行版可能使用不同的 C 標準庫。從使用一種標準庫的平臺編譯到使用另一種標準庫的同一架構平臺,也屬于交叉編譯。比如,從 x86_64/Linux/glibc 平臺編譯到 x86_64/Linux/musl 平臺。甚至從一個版本的glibc編譯到另一個版本,同樣屬于交叉編譯,像從 x86_64/Linux/glibc_v1.0 編譯到 x86_64/Linux/glibc_v1.1。

傳統的構建和使用編譯器的方式,比如使用 GCC,在處理交叉編譯時可能會變得很復雜。不過,我們可以采用一種更便捷的方法來滿足需求。我們需要一個滿足特定要求的工具鏈:能從主機平臺生成 RISC-V 指令,并且在調用 C 標準庫功能時使用 Newlib 庫。

在典型的 Linux 發行版中,安裝的 GCC 或 clang 默認會為運行它的同一平臺進行編譯,即宿主平臺和目標平臺相同,這被稱為本地編譯。當包含<stdio.h>頭文件并調用printf函數時,編譯器會從標準位置查找相關文件和實現。例如,在 Debian 系統中,stdio.h位于/usr/include目錄,標準 C 庫glibc的動態鏈接版本位于/lib/x86_64-linux-gnu/libc.so(實際指向/lib/x86_64-linux-gnu/libc.so.6?)。

為了進行交叉編譯,我們需要獲取能為目標平臺生成指令的編譯器,為目標平臺設置 C 標準庫的路徑,并確保目標平臺的編譯器知道如何使用該庫。這一系列操作通常較為繁瑣,不過我們可以借助一些自動化工具來簡化流程。

自動化 RISC-V 工具鏈構建

為了簡化在 RISC-V 平臺上使用 Newlib 進行開發的過程,我們可以使用 RISC-V 工具鏈項目。該項目雖然仍需在主機上從源代碼構建所有內容,但會通過腳本自動化處理繁瑣的編排工作,包括編譯器的搭建。

首先,從 GitHub 克隆相關倉庫。需要注意的是,克隆時最好使用--recursive標志,以避免后續問題,盡管官方說明該標志不是必需的,但在某些系統上不使用可能會出現問題。克隆過程可能會花費較長時間,因為要下載大量源代碼。

克隆完成后,進行配置。例如:./configure --prefix=/opt/riscv-newlib --enable-multilib --disable-gdb --with-cmodel=medany。這里的prefix指定了新構建的工具鏈、C 標準庫(這里是 Newlib)等的安裝路徑;enable-multilib用于啟用針對不同 RISC-V 配置的構建,但會使構建過程變慢;disable-gdb是因為構建 GDB 時可能會出現問題,所以將其排除在工具鏈之外;with-cmodel=medany這個參數稍后會詳細解釋,它對 64 位 RISC-V 構建的正常運行很關鍵。

配置完成后,通過make命令啟動構建過程。這里有個小提示,不要嘗試使用-j16等參數進行并行構建,可能會導致構建失敗。構建過程會持續較長時間,期間可以做些其他事情。構建完成后,會在指定的prefix路徑下生成可執行文件、庫文件等。

實現內存和 UART 構建模塊

在擁有了可用的 RISC-V + Newlib 交叉工具鏈后,就可以開始構建 Newlib 的基礎模塊了。先從 UART 相關的代碼入手,創建uart.h文件:

#ifndef UART_H
#define UART_Hvoid uart_putc(char c);
char uart_getc(void);#endif

接著實現這兩個函數,在這個示例中,針對 QEMU 的 16550A UART 進行操作:

#include "uart.h"
// QEMU UART寄存器地址
#define UART_BASE 0x10000000
#define UART_THR  (*(volatile char *)(UART_BASE + 0x00))
#define UART_RBR  (*(volatile char *)(UART_BASE + 0x00))
#define UART_LSR  (*(volatile char *)(UART_BASE + 0x05))
#define UART_LSR_TX_IDLE  (1 << 5)
#define UART_LSR_RX_READY (1 << 0)void uart_putc(char c) {// 等待發送器空閑while ((UART_LSR & UART_LSR_TX_IDLE) == 0);UART_THR = c;// 特殊處理換行符,發送CR+LFif (c == '\n') {while ((UART_LSR & UART_LSR_TX_IDLE) == 0);UART_THR = '\r';}
}char uart_getc(void) {// 等待數據while ((UART_LSR & UART_LSR_RX_READY) == 0);return UART_RBR;
}

接下來是syscalls.c文件,實現printf等函數依賴的原語,同時處理輸入操作:

// 省略部分代碼...void* _sbrk(int incr) {extern char _end;         // 由鏈接器定義,靜態段結束標志extern char _stack_bottom; // 鏈接器腳本中定義,棧底地址static char *heap_end = &_end;char *prev_heap_end = heap_end;// 計算安全的棧限制,棧從_stack_top向下增長到_stack_bottomchar *stack_limit = &_stack_bottom;// 檢查堆是否會增長到太靠近棧的位置if (heap_end + incr > stack_limit) {errno = ENOMEM;return (void*) -1; // 返回錯誤}heap_end += incr;return (void*) prev_heap_end;
}

應用示例:輸入與輸出

現在來構建一個裸機應用示例。編寫main.c文件:

#include <stdio.h>int main(void) {printf("Hello from RISC-V UART!\n");char buffer[100];printf("Type something: ");scanf("%s", buffer);printf("You typed: %s\n", buffer);while (1) {}return 0;
}

這個應用會通過 UART 輸出問候語,提示用戶輸入內容,讀取用戶輸入并再次輸出。由于沒有運行在常規的 Shell 環境中,輸入時不會回顯按鍵內容。

還需要一個簡單的 C 運行時文件startup.S

.section .text.init
.global _start
_start:la sp, _stack_top# 清空BSS段,使用鏈接器腳本中定義的符號la t0, _bss_startla t1, _bss_end
clear_bss:bgeu t0, t1, bss_donesb zero, 0(t0)addi t0, t0, 1j clear_bss
bss_done:# 跳轉到C代碼call main# 如果main函數返回,進入無限循環j .

最后是鏈接器腳本link.ld

OUTPUT_FORMAT("elf64-littleriscv")
OUTPUT_ARCH("riscv")
ENTRY(_start)MEMORY {RAM (rwx) : ORIGIN = 0x80000000, LENGTH = 64M
}SECTIONS {/* 代碼段 */.text : {*(.text.init)*(.text)} > RAM/* 只讀數據段 */.rodata : {*(.rodata)} > RAM/* 已初始化數據段 */.data : {*(.data)} > RAM/* 小的已初始化數據段 */.sdata : {*(.sdata)} > RAM/* BSS段,有明確符號 */.bss : {_bss_start =.;  /* 定義BSS段開始符號 */*(.bss)*(COMMON).= ALIGN(8);_bss_end =.;    /* 定義BSS段結束符號 */} > RAM/* 小BSS段 */.sbss : {_sbss_start =.;*(.sbss)*(.sbss.*).= ALIGN(8);_sbss_end =.;} > RAM/* 堆起始標記 */.= ALIGN(8);_end =.; /* 堆從這里開始向上增長 *//* 棧從RAM末尾向下增長 */_stack_size = 64K;_stack_top = ORIGIN(RAM) + LENGTH(RAM);_stack_bottom = _stack_top - _stack_size;/* 確保堆和棧不重疊 */ASSERT(_end <= _stack_bottom, "Error: Heap collides with stack")
}

鏈接器腳本負責安排代碼和數據在內存中的位置,確保各個段正確放置,并且堆和棧不會沖突。

關鍵要點與應用運行

在構建工具鏈時,--with-cmodel=medany這個參數至關重要。由于我們構建的是 64 位 RISC-V 機器代碼,應用程序代碼需要使用能夠處理高地址的內存地址模型。如果沒有這個參數,Newlib 庫可能會使用無法有效處理高地址的 RISC-V 指令,導致鏈接錯誤。

在 GitHub 倉庫中提供了Makefile來簡化構建和運行過程。運行make debug命令,它會調用交叉編譯器編譯代碼,并使用 QEMU 進行仿真。Makefile中的CFLAGS包含-specs=nosys.specs,這會讓工具鏈使用 Newlib 的nosys版本,該版本所有構建模塊默認是存根,返回零或錯誤。鏈接器標志-nostartfiles表示我們將提供自己的最小 C 運行時。

運行make debug后,QEMU 啟動,輸入內容并回車,就能看到應用的輸出。同時,debug目標會生成一個qemu_debug.log文件,它記錄了 VM 的完整運行軌跡,有助于深入了解printf等函數的工作原理以及 RISC-V 核心的執行過程。

總結

通過這個示例,我們成功地在裸機平臺上實現了printf等 C 標準庫函數的功能,讓裸機編程有了更接近在完整內核上編程的體驗。利用 Newlib 定義的構建模塊,我們可以進一步擴展功能,實現文件訪問、更完善的內存管理等。而且,這為在裸機代碼中使用強大的庫提供了可能。盡管在極簡環境下,最終軟件鏡像的大小和指令數量需要考慮,但我們構建的ELF文件大小為220K,還算比較合理。希望這篇文章能為你的開發工作提供新的思路和方法,祝大家在裸機編程的世界中探索愉快!

科技脈搏,每日跳動。

與敖行客 Allthinker一起,創造屬于開發者的多彩世界。

圖片

- 智慧鏈接 思想協作 -

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/82306.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/82306.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/82306.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

基于STM32F103的智能機械臂識別與控制項目(課件PPT+源代碼)

以下是基于 STM32F103 的智能機械臂識別與控制項目的詳細介紹&#xff1a; 項目概述 該項目以 STM32F103 為核心控制器&#xff0c;結合多種傳感器和技術&#xff0c;實現了機械臂的智能識別與控制功能&#xff0c;可完成倉庫貨物的識別、搬運等任務&#xff0c;并支持多種控…

Codeforces Round 1023 (Div. 2)

Dashboard - Codeforces Round 1023 (Div. 2) - Codeforces 一個構造問題&#xff0c;我把最大的數放在一個數組&#xff0c;其余數放在另一個數組&#xff0c;就能保證gcd不同 來看代碼&#xff1a; #include <bits/stdc.h> using namespace std;int main() {int t;ci…

6.01 Python中打開usb相機并進行顯示

本案例介紹如何打開USB相機并每隔100ms進行刷新的代碼,效果如下: 一、主要思路: 1. 打開視頻流、讀取幀 self.cam_cap = cv2.VideoCapture(0) #打開 視頻流 cam_ret, cam_frame = self.cam_cap.read() //讀取幀。 2.使用定時器,每隔100ms讀取幀 3.顯示到Qt的QLabel…

JVM——即時編譯

分層編譯模式&#xff1a;動態平衡啟動速度與執行效率 分層編譯是現代JVM&#xff08;如HotSpot、GraalVM&#xff09;實現高性能的核心策略之一&#xff0c;其核心思想是根據代碼的執行熱度動態選擇不同的編譯層次&#xff0c;實現啟動速度與運行效率的最佳平衡。以HotSpot虛…

Auto DOP:讓并行執行實現智能調優 | OceanBase 實踐

隨著數據量的迅速增長&#xff0c;企業數據庫往往面臨著一個困局&#xff1a;復雜的分析查詢需要充分的資源來保證性能&#xff0c;但過多增加并行執行又會造成資源競爭&#xff0c;影響系統穩定性。傳統基于DBA人工干預的并行度調節機制&#xff0c;既低效又難以適應動態變化的…

【區塊鏈】Uniswap之滑點(Slippage)

一、滑點是什么&#xff1f; 滑點&#xff08;Slippage&#xff09;是指你下單預期價格和最終成交價格之間的差距。 在 DEX 中&#xff0c;你的交易會影響池子的價格&#xff08;AMM機制&#xff09;&#xff0c;所以&#xff1a; 下單越大&#xff0c;滑點越大&#xff1b;…

[前端]Javascript獲取元素寬度

元素寬度屬性對比示意圖 ---------------------------------- | 外邊距&#xff08;margin&#xff09; | -------------------------------- | | 邊框&#xff08;border&#xff09; | | | -------------------------- | | | …

數字人驅動/動畫方向最新頂會期刊論文收集整理 | AAAI 2025

會議官方論文列表&#xff1a;https://ojs.aaai.org/index.php/AAAI/issue/view/624 以下論文部分會開源代碼&#xff0c;若開源&#xff0c;會在論文原文的摘要下方給出鏈接。 語音驅動頭部動畫/其他 EchoMimic: Lifelike Audio-Driven Portrait Animations through Editabl…

Windows系統下【Celery任務隊列】python使用celery 詳解(一)

Celery 是一個基于 Python 的分布式任務隊列框架&#xff0c;它允許你在不同的進程甚至不同的服務器上異步執行任務。 特點 簡單&#xff1a;易于使用和配置&#xff0c;提供了簡潔的 API。高可用&#xff1a;支持任務的可靠交付&#xff0c;即使在出現故障時也能保證任務不丟…

移動設備常用電子屏幕類型對比

概述 LCD 家族 &#xff08;TN、STN、TFT、IPS、VA&#xff09;依賴背光&#xff0c;性能差異主要來自液晶排列和驅動方式。OLED 以自發光為核心優勢&#xff0c;但成本與壽命限制其普及。E-Paper 專為低功耗靜態顯示設計&#xff0c;與傳統屏幕技術差異顯著。 參數LCD&#…

Vue3.5 企業級管理系統實戰(十八):用戶管理

本篇主要探討用戶管理功能&#xff0c;接口部分依然是使用 Apifox mock 模擬。 1 用戶 api 在 src/api/user.ts 中添加用戶相關 CRUD 接口&#xff0c;代碼如下&#xff1a; //src/api/user.ts import request from "/api/config/request"; // 從 "./type&q…

【C】初階數據結構14 -- 歸并排序

本篇文章主要是講解經典的排序算法 -- 歸并排序 目錄 1 遞歸版本的歸并排序 1&#xff09; 算法思想 2&#xff09; 代碼 3&#xff09; 時間復雜度與空間復雜度分析 &#xff08;1&#xff09; 時間復雜度 &#xff08;2&#xff09; 空間復雜度 2 迭代版本的歸并…

【相機標定】OpenCV 相機標定中的重投影誤差與角點三維坐標計算詳解

摘要&#xff1a; 本文將從以下幾個方面展開&#xff0c;結合典型代碼深入解析 OpenCV 中的相機標定過程&#xff0c;重點闡述重投影誤差的計算方法與實際意義&#xff0c;并通過一個 calcBoardCornerPositions() 函數詳細講解棋盤格角點三維坐標的構建邏輯。 在計算機視覺領域…

RabbitMQ-運維

文章目錄 前言運維-集群介紹多機多節點單機多節點 多機多節點下載配置hosts?件配置Erlang Cookie啟動節點構建集群查看集群狀態 單機多節點安裝啟動兩個節點再啟動兩個節點驗證RabbitMQ啟動成功搭建集群把rabbit2, rabbit3添加到集群 宕機演示仲裁隊列介紹raft算法協議 raft基…

JVM之內存管理(一)

部分內容來源&#xff1a;JavaGuide二哥Java 圖解JVM內存結構 內存管理快速復習 棧幀&#xff1a;局部變量表&#xff0c;動態鏈接&#xff08;符號引用轉為真實引用&#xff09;&#xff0c;操作數棧&#xff08;存儲中間結算結果&#xff09;&#xff0c;方法返回地址 運行時…

無線射頻模塊如何通過CE RED認證?關鍵規范與準備策略詳解

隨著無線通信設備在歐洲市場的廣泛應用&#xff0c;CE RED認證已成為模塊類產品進入歐盟的強制通行證。作為專注于LoRa模塊、對講模塊與FSK射頻模塊研發的技術企業&#xff0c;我們深知從設計、測試到量產&#xff0c;每一個環節都需緊扣合規底線。本文將圍繞CE RED認證核心要求…

Golang中集合相關的庫

一切編程語言的底層結構都是數組&#xff0c;其它復雜數據結構如Map, Stack&#xff0c;Heap和Queue都是基于數組建立起來的。 Go語言主流工具庫推薦&#xff08;含常用數據結構實現&#xff09; 以下是目前Go生態中最主流且活躍的工具庫&#xff0c;包含隊列、棧、優先級隊列…

ABAP 導入Excel形成內表

文章目錄 創建導入模板程序實現代碼代碼解析運行結果 創建導入模板 程序實現 代碼 *&---------------------------------------------------------------------* *& Report Z_EXCEL_UPLOAD_LHY *&--------------------------------------------------------------…

特殊配合力(SCA)作為全基因組關聯分析(GWAS)的表型,其生物學意義和應用價值

生物學意義 解析非加性遺傳效應 特殊配合力(SCA)主要反映特定親本組合的雜交優勢,由非加性遺傳效應(如顯性、超顯性、上位性)驅動。顯性效應涉及等位基因間的顯性互作,上位性效應則涉及不同位點間的基因互作。通過SCA-GWAS,可以定位調控這些非加性效應的關鍵基因組區域…

應急響應基礎模擬靶機-security1

PS:杰克創建在流量包(result.pcap)在根目錄下&#xff0c;請根據已有信息進行分析 1、攻擊者使用的端口掃描工具是? 2、通過流量及日志審計&#xff0c;攻擊者上傳shell的時訪問web使用IP地址是多少? 3、審計流量日志&#xff0c;攻擊者反彈shell的地址及端口? 4、攻擊者…