C/C++ 运行时库
AC79xx AIoT SDK 中面向 pi32v2 处理器(杰理 AC79 系列)的 C/C++ 运行时环境:包括基于 clang/LLVM 工具链的 C 标准库补充实现、libc++ 标准库的多配置变体、异常处理(Dwarf .eh_frame)、TLS 支持以及全局构造/析构机制,以及将这些运行时组件集成进固件的完整步骤与注意事项。
Purpose and Scope
本文档说明 SDK 中 C/C++ 运行时库的组成、变体选择、集成方法与底层机制,覆盖:
- C 运行时:C 标准库中缺失函数(如
_malloc_r)的 SDK 补充实现 - C++ 运行时:
libc++.a、libc++abi.a、libunwind.a、libemutls.a各库的作用与 16 种编译配置变体 - 编译器与语言标准支持范围(clang 4.0.1、C++11/14、部分 C++17)
- 异常处理(
.eh_frame/.eh_frame_hdr)与 TLS(emulated TLS)的链接脚本与编译参数 - 全局构造函数/析构函数的启动流程(
__cxa_atexit/__cxa_finalize/__dso_handle) - 链接脚本(
.ld)中与运行时相关的段定义(.ctors、.eh_frame等)
与本文档相关的其他目录页(不在本文范围):内存布局与链接脚本的完整细节属于系统启动/链接主题;simple_pthread 的 OS 适配接口属于线程库主题;具体驱动库的段划分见 include_lib/driver 下的 .ld 文件。
Overview
SDK 的 C/C++ 运行时不是单一库,而是一组按需组合的组件。设计上最重要的取舍是体积与特性之间的平衡:嵌入式固件的 Flash/RAM 预算有限,而 C++ 标准库的某些特性(异常、locale、线程、长跳转)会显著增大代码体积。为此 SDK 将 libc++ 编译为多种配置变体(见 include_lib/c++/README.md),让开发者只链接自己实际用到的特性组合。
同时,由于工具链的 C 库并不完整(例如缺少 _malloc_r、sscanf 等重入版本函数),SDK 在 apps/common/system/init_expand.c 中提供了 C 库补充实现,并配套实现 C++ ABI 所需的 __cxa_atexit 等符号。
典型使用场景:
- 在纯 C 工程中启用少量 C++ 代码(STL 容器、
std::string等),选择无异常/无线程的最小变体 - 需要多线程 C++ 应用(
std::thread、std::mutex、std::future),选择带 pthread 的变体 - 需要 C++ 异常处理或 TLS 变量时,额外链接
libunwind.a/libemutls.a并修改链接脚本
Architecture
flowchart TD
subgraph sg_User["应用层 (Application)"]
App["用户代码<br/>(.c / .cxx / .cpp)"]
end
subgraph sg_Toolchain["编译工具链 (Toolchain)"]
Clang["clang 4.0.1<br/>pi32v2 后端"]
Llvm["LLVM 12.0.1<br/>libc++ 头文件"]
end
subgraph sg_Runtime["运行时库 (Runtime Libraries)"]
LibC["C 库 + SDK 补充实现<br/>(init_expand.c)"]
LibCpp["libc++.a"]
Abi["libc++abi.a"]
Unwind["libunwind.a (可选)"]
Emutls["libemutls.a (可选)"]
Pthread["simple_pthread (可选)"]
end
subgraph sg_Link["链接与启动 (Link & Startup)"]
LdScript["链接脚本 (.ld)<br/>.ctors / .eh_frame / .eh_frame_hdr"]
Ctors["全局构造/析构<br/>__cxa_atexit / __cxa_finalize"]
end
App --> Clang
Clang --> Llvm
Clang --> LibCpp
Clang --> Abi
Clang --> LibC
LibCpp --> Abi
LibCpp -.->|"启用异常"| Unwind
LibCpp -.->|"启用 TLS"| Emutls
LibCpp -.->|"启用线程"| Pthread
LdScript --> LibC
LdScript --> LibCpp
Ctors --> LdScript
Ctors --> Abi
架构说明:
- 应用层通过 clang 编译器编译,
.c文件按 C 模式、.cxx/.cc/.cpp文件按 C++ 模式处理(见 README L51-L53)。 - 工具链:编译器为 clang 4.0.1(支持到 C++14 及部分 C++17),libc++ 标准库基于 LLVM 12.0.1。
- 运行时库是分层组合关系:
libc++.a依赖libc++abi.a(提供new/delete/__cxa_pure_virtual等 ABI 符号);异常、TLS、线程特性分别需要额外链接libunwind.a、libemutls.a、simple_pthread。 - 链接与启动:链接脚本负责收集
.ctors、.eh_frame、.eh_frame_hdr等运行时段,启动代码在调用main()前逆序执行全局构造函数,并在退出时通过__cxa_finalize触发析构。
C++ 标准库变体(Variant Matrix)
libc++ 的每种特性组合都会影响固件体积:例如启用异常会引入基于 .eh_frame 的栈回退数据与 libunwind.a,启用长跳转(large-program)会放大库函数体积。SDK 为此编译了 16 种配置变体,按四个维度组合(见 README L7-L26):
| 长跳转 | 线程 (thread) | 异常 (exception) | locale | 库名称 |
|---|---|---|---|---|
| 未支持 | 未支持 | 未支持 | 未支持 | median |
| 未支持 | 未支持 | 未支持 | 支持 | locale-median |
| 未支持 | 支持 | 未支持 | 未支持 | pthread-median |
| 未支持 | 支持 | 未支持 | 支持 | pthread-locale-median |
| 未支持 | 未支持 | 支持 | 未支持 | exception-median |
| 未支持 | 未支持 | 支持 | 支持 | exception-locale-median |
| 未支持 | 支持 | 支持 | 未支持 | exception-pthread-median |
| 未支持 | 支持 | 支持 | 支持 | exception-pthread-locale-median |
| 支持 | 未支持 | 未支持 | 未支持 | large |
| 支持 | 未支持 | 未支持 | 支持 | locale-large |
| 支持 | 支持 | 未支持 | 未支持 | pthread-large |
| 支持 | 支持 | 未支持 | 支持 | pthread-locale-large |
| 支持 | 未支持 | 支持 | 未支持 | exception-large |
| 支持 | 未支持 | 支持 | 支持 | exception-locale-large |
| 支持 | 支持 | 支持 | 未支持 | exception-pthread-large |
| 支持 | 支持 | 支持 | 支持 | exception-pthread-locale-large |
设计意图:将特性开关前置到库的编译期,使链接期只保留需要的代码路径。median 系列是默认推荐(无任何可选特性);large 系列通过 -mllvm -pi32v2-large-program=true 启用长跳转,允许 call 更远距离的函数,代价是库函数体积增大(见 README L5)。
libc++ 支持范围(基于 LLVM 12.0.1,见 README L55-L65):
- ✅ 支持:STL(
vector、list、map、functional、algorithm等);thread、mutex、future(依赖 pthread 接口);strstream(依赖 locale) - ❌ 不支持:
filesystem、std::cin、std::cout、fstream相关操作
集成步骤(Integration Steps)
README 给出了把 C++ 标准库集成到固件的 6 步流程(见 README L28-L39):
- 选择变体:根据所需 C++ 特性(线程/异常/locale/长跳转)确定对应的 libc++ 库
- 配置头文件搜索路径:将 C++ 标准库头文件路径加在 C 标准库路径之前,例如
-I ${CXXDIR}/large/include/c++/v1需先于-I /opt/jieli/pi32v2/include - 链接参数添加库路径:链接
libc++.a、libc++abi.a;启用异常时再加libunwind.a与--eh-frame-hdr;使用 TLS 变量时再加libemutls.a(且依赖 pthread) - 缩减体积:建议加
--gc-sections以丢弃未引用的段 - 处理全局构造函数:按"注意事项 9"在链接脚本中收集
.ctors段并在main()前调用 - 补充缺失的 C 库函数:
_malloc_r、sscanf等函数可能在 C 库中缺失,需在 SDK 中补充实现
头文件搜索顺序的关键性:C++ 头文件必须优先于 C 头文件,否则会出现声明冲突;若使用 pthread,不能使用 C 标准库中的 pthread.h,而应使用 simple_pthread 文件夹提供的版本——该头文件是通过 clang -E -p 宏展开后手动提取生成的,仅含必要声明与结构体,外部依赖最少(见 README L108-L111)。
编译参数与语言标准
工具链为 clang 4.0.1,支持的语言标准及推荐写法(见 README L43-L49):
| 标准 | 编译选项 |
|---|---|
| C++11 | -std=c++11 / -std=gnu++11 |
| C++14 | -std=c++14 / -std=gnu++14 |
| C++17(部分) | -std=c++1z / -std=gnu++1z |
设计意图:建议统一使用 gnu++1n 写法,因为 -std=c++1n(不带 gnu)会定义 __STRICT_ANSI__ 宏,导致部分函数缺少声明而编译失败。此外 .c 后缀文件按 C 编译、.cxx/.cc/.cpp 后缀按 C++ 编译,文件后缀选择直接影响语言模式(见 README L51-L53)。
体积与特性推荐:一般推荐关闭异常与 RTTI(-fno-exceptions、-fno-rtti);若使用 dynamic_cast 则必须保留 RTTI(见 README L104-L106)。
异常处理机制(Exception Handling)
SDK 的 C++ 异常采用 Dwarf 静态栈回退方案:编译器在编译期静态生成栈帧回退数据结构(.eh_frame),仅在抛出异常时才实际使用。其工作机制(见 README L71-L102):
- 链接脚本必须保留
.gcc_except_table、.eh_frame、.eh_frame_hdr段,且.eh_frame/.eh_frame_hdr需KEEP()防止被垃圾回收 - 链接参数加
--eh-frame-hdr才会生成.eh_frame_hdr段(用于异常处理时的快速查找) - 编译 C 代码时需加
-funwind-tables,避免异常穿越 C 函数时无法正确回滚栈帧 - 额外链接
libunwind.a - 异常数据为只读,可放 Flash(也可放 RAM)
flowchart TD
Throw["throw 异常"] --> LibUnwind["libunwind.a<br/>解析 .eh_frame_hdr"]
LibUnwind --> Lookup{"查找 handler<br/>(栈回退信息)"}
Lookup -->|"找到"| UnwindStack["逐帧回退栈<br/>调用析构函数"]
UnwindStack --> Catch["进入 catch 块"]
Lookup -->|"未找到"| Terminate["std::terminate()"]
Catch --> End([异常处理结束])
代价提示:异常会显著增加固件体积(额外的代码与数据量),非必要场景不推荐使用。
TLS 支持(Emulated TLS)
thread_local 与 __thread 变量通过 emulated TLS 实现(见 README L67-L69):
- 编译参数:
-femulated-tls - 链接参数:
--plugin-opt=-emulated-tls - 链接库:
libemutls.a,且依赖 pthread 接口
全局构造与析构(Global Constructors/Destructors)
C++ 全局对象的构造必须在 main() 之前完成,析构在退出时触发。SDK 采用标准的 .ctors 段 + __cxa_atexit 机制(见 README L116-L154):
- 链接脚本收集构造函数指针到
___ctors_begin~___ctors_end区间(必须KEEP()) - 启动代码在调用
main()前逆序遍历该区间并逐个调用 - 构造函数内通过
__cxa_atexit注册析构函数,退出时由__cxa_finalize触发 - 需要提供
__dso_handle符号(通常定义为其自身地址)
sequenceDiagram
participant Boot as 启动代码 (_start)
participant Ctors as 全局构造函数列表
participant Main as main()
participant Abi as libc++abi / init_expand.c
Boot->>Boot: 初始化段 (.data/.bss)
Boot->>Ctors: 逆序遍历 ___ctors_begin → ___ctors_end
Ctors->>Abi: __cxa_atexit(func, arg, __dso_handle)
Abi-->>Ctors: 注册成功 (返回 0)
Ctors->>Main: 调用 main()
Main-->>Ctors: 返回/退出
Ctors->>Abi: __cxa_finalize(__dso_handle)
Abi->>Abi: 逆序执行已注册的析构函数
C 运行时补充实现(init_expand.c)
工具链自带 C 库并不完整,README 明确指出 _malloc_r、sscanf 等函数缺失,需要在 SDK 中补充实现(见 README L39)。SDK 在 apps/common/system/init_expand.c 中提供了这些补充:
__cxa_atexit(init_expand.c L24):注册全局/静态对象的析构函数,供 C++ 运行时在程序退出时逆序调用__dso_handle(init_expand.c L36):声明为hidden可见性的全局符号,作为__cxa_atexit的dso参数_malloc_r(init_expand.c L112):重入版本的内存分配接口,供 libc/libc++ 内部调用
该文件将 C++ ABI 所需的 C 库支撑函数与内存分配整合到系统初始化路径中,保证链接期符号完整。
使用示例(Usage Examples)
示例 1:链接脚本中的异常段定义
异常处理依赖链接脚本保留 .eh_frame 与 .eh_frame_hdr 段,且必须使用 KEEP() 防止被 --gc-sections 回收:
.text : {
*(.text*)
*(.gcc_except_table*);
}
.eh_frame : {
__eh_frame_start = .;
KEEP(*(.eh_frame)); // 注意要有 KEEP
__eh_frame_end = .;
}
.eh_frame_hdr : {
__eh_frame_hdr_start = .;
KEEP(*(.eh_frame_hdr)); // 注意要有 KEEP
__eh_frame_hdr_end = .;
}
Source: README.md
示例 2:链接脚本中的全局构造函数段
// 全局变量的构造函数列表
___ctors_begin = .;
KEEP(*(SORT(.ctors.*))) // 注意要有 KEEP
KEEP(*(.ctors)) // 注意要有 KEEP
___ctors_end = .;
Source: README.md
示例 3:main() 前调用全局构造函数
typedef void (*pfunc) ();
extern pfunc ___ctors_begin[];
extern pfunc ___ctors_end[];
pfunc *p;
// 调用全局构造函数(注意是要逆序)
for (p = ___ctors_end; p > ___ctors_begin; )
(*--p) (); // 注意这里是倒序调用的
Source: README.md
示例 4:__cxa_atexit / __dso_handle 支撑符号
// https://refspecs.linuxbase.org/LSB_4.1.0/LSB-Core-generic/LSB-Core-generic/baselib---cxa-atexit.html
int __cxa_atexit(void (*func) (void *), void * arg, void * dso_handle);
void __cxa_finalize(void *f);
// 这个在调用 __cxa_atexit 的时候需要用到
void *__dso_handle = &__dso_handle;
Sources:
配置选项(Configuration Options)
| 配置项 | 类型 | 默认/推荐 | 说明 |
|---|---|---|---|
-std=gnu++11/14/1z | 编译参数 | gnu++ 系列 | 语言标准;避免 __STRICT_ANSI__ 导致声明缺失 |
-mllvm -pi32v2-large-program=true | 编译参数 | 关 | 长跳转模式,允许 call 更远函数,库体积增大 |
-I ${CXXDIR}/.../include/c++/v1 | 头文件路径 | 必须位于 C 库路径之前 | C++ 头文件优先搜索,避免冲突 |
-femulated-tls | 编译参数 | 关 | 启用 TLS 变量(thread_local/__thread) |
--plugin-opt=-emulated-tls | 链接参数 | 关 | 与 -femulated-tls 配套 |
--eh-frame-hdr | 链接参数 | 仅启用异常时 | 生成 .eh_frame_hdr 段 |
-funwind-tables | 编译参数 | 仅启用异常时 | C 代码也生成栈回退表,保证异常穿越 C 帧正确回滚 |
-fno-exceptions / -fno-rtti | 编译参数 | 推荐开启 | 关闭异常/RTTI 以缩减体积;dynamic_cast 需保留 RTTI |
--gc-sections | 链接参数 | 建议开启 | 丢弃未引用段,缩减固件体积 |
libc++.a + libc++abi.a | 链接库 | 必选 | C++ 标准库与 ABI 支撑 |
libunwind.a | 链接库 | 仅启用异常时 | 异常栈回退实现 |
libemutls.a | 链接库 | 仅启用 TLS 时 | emulated TLS 实现,依赖 pthread |
simple_pthread | 头文件/库 | 仅启用线程时 | 替代 C 库中的 pthread.h,外部依赖最少 |
API 参考(API Reference)
int __cxa_atexit(void (*func)(void *), void *arg, void *dso_handle)
注册程序退出时要调用的析构函数(C++ ABI 标准接口)。
参数:
func(void (*)(void *)): 析构函数指针arg(void *): 传给析构函数的参数(通常是对象指针)dso_handle(void *): 动态共享对象句柄,通常传&__dso_handle
返回: 成功返回 0。
SDK 实现位置: apps/common/system/init_expand.c L24
void __cxa_finalize(void *f)
触发执行已注册的析构函数;传入 NULL 时执行所有已注册函数。
SDK 提供方式: 见 README.md L143-L147
void *__dso_handle
全局对象句柄符号,供 __cxa_atexit 使用;SDK 中定义为 void *__dso_handle = &__dso_handle;(init_expand.c L36)。
void *_malloc_r(size_t sz)
C 库重入版本的内存分配函数,供 libc/libc++ 内部使用;SDK 补充实现位于 init_expand.c L112。
new / delete / __cxa_pure_virtual / __cxa_deleted_virtual
这些符号已在 libc++abi.a 中定义,无需应用层额外实现(见 README L156)。
失败模式、边界情况与并发
链接期失败模式
- 符号缺失:
_malloc_r、sscanf等 C 库函数缺失会导致链接失败。解决方式是使用 SDK 的init_expand.c补充实现(README 集成步骤第 6 条明确列出)。 .eh_frame被回收:若链接脚本未对.eh_frame/.eh_frame_hdr使用KEEP(),--gc-sections会将其丢弃,异常在运行时无法回退栈帧,直接导致std::terminate()。.ctors被回收:未KEEP(*(SORT(.ctors.*)))时全局对象不会被构造,C++ 全局状态静默失效——这类错误极难排查,因此 README 反复强调KEEP的必要性。
编译期失败模式
__STRICT_ANSI__宏:使用-std=c++1n(不带gnu)时部分函数缺少声明而编译报错;应改用gnu++1n。- 头文件搜索顺序错误:C++ 头文件必须位于 C 标准库头文件之前,否则声明冲突;使用 pthread 时不得引用 C 库自带的
pthread.h,否则与simple_pthread的定义冲突。 - locale 不匹配:使用
iostream/strstream时若编译报error "Localization is not supported by this configuration of libc++",说明所选变体未包含 locale 支持,需更换为带locale的库变体。
运行时边界情况
- 异常穿越 C 代码:C 函数默认不生成栈回退表,异常穿越 C 帧时无法回滚;必须在 C 编译选项中加
-funwind-tables。 dynamic_cast与 RTTI:关闭 RTTI(-fno-rtti)后dynamic_cast无法工作,需要保留 RTTI 或改用其他类型判别方案。- 并发:
std::thread/std::mutex/std::future依赖 pthread 接口实现;选择带pthread的变体并链接simple_pthread是线程安全的先决条件。TLS 变量(thread_local)依赖 emulated TLS 与 pthread 配合,二者必须同时启用。
性能与运维注意事项
- 体积优先原则:默认选用
median系变体;仅在确实需要时启用线程/异常/locale/长跳转。异常会"增加较多的额外代码以及数据量,固件体积会增加较多"(README 明确不建议默认使用)。 --gc-sections应始终开启,配合链接脚本中的KEEP()精确控制运行时段保留。- 异常段放置:
.eh_frame与.eh_frame_hdr为只读数据,可放置于 Flash,不占 RAM。 - 长跳转取舍:
large系列允许更远距离的call,但库函数体积更大——适用于代码总量超过短跳转寻址范围的大工程。 - 全局构造顺序:构造函数按
.ctors段逆序执行(栈式语义),析构顺序与之相反,符合 C++ 语言规范。
扩展点(Extension Points)
- 补充 C 库函数:
apps/common/system/init_expand.c是 C 库补充实现的挂载点,新增缺失的 C 库符号(如格式化、重入内存接口)可在此文件扩展。 __cxa_atexit注册机制:应用可通过该接口注册自定义退出清理函数,实现"延迟注册、退出时逆序执行"的资源清理模式。- 链接脚本定制:
include_lib/**/*.ld(如include_lib/driver/cpu/wl82/system.ld、system_data.ld)定义了系统内存布局;新增运行时段(异常、TLS 相关段)需在此类脚本中同步扩展。 - 变体选择:若现有 16 种 libc++ 变体不满足需求(如自定义特性组合),可参照
-mllvm -pi32v2-large-program=true等编译选项重新编译 libc++ 变体并加入链接路径。
相关链接(Related Links)
- include_lib/c++/README.md(C++ 运行时库官方说明)
- apps/common/system/init_expand.c(C 库补充实现)
- include_lib/driver/cpu/wl82/system.ld(系统链接脚本)
- include_lib/driver/cpu/wl82/system_data.ld(数据段链接脚本)
- 线程库(simple_pthread)与系统启动流程的细节,见对应目录页:操作系统运行时、系统启动与内存布局。