裸机上的 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.mzph.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、攻击者…