startup.s不直接调用__libc_init_array,因为其核心职责是硬件初始化(栈指针、.data拷贝、.bss清零),而全局对象构造属于C运行时(CRT)阶段,需由CRT启动代码(如_start)在main前自动触发;手动调用易因栈未稳、符号未链接或模式不匹配导致崩溃。

为什么 startup.s 里不直接调用 __libc_init_array
单片机裸机环境下,startup.s 的核心任务是初始化栈指针、清零 .bss、复制 .data,然后跳转到 main。它**不负责 C++ 全局对象构造**——这个职责属于 C 运行时(CRT)的初始化阶段,由链接器脚本和 C 库约定保障。__libc_init_array 是 GNU libc / newlib / picolibc 提供的符号,用于遍历 .init_array 段中的函数指针并逐个调用,而全局对象构造函数正是被编译器(如 GCC)自动收集到这里面的。
如果你在 startup.s 里硬加一条 bl __libc_init_array,大概率会崩溃:栈可能没设好、.init_array 地址未被正确解析、依赖的 __aeabi_unwind_cpp_pr0 或其他辅助函数未就位,甚至 __libc_init_array 根本没被链接进来(因为链接器发现没人引用它,就丢弃了整个 .init_array 段)。
怎么确保 __libc_init_array 被调用且生效
关键不在汇编里“手动调”,而在让整个初始化链条自然触发。需要三处协同:
-
编译时加
-fno-exceptions -fno-rtti(嵌入式必备),避免生成无法解析的异常表依赖 -
链接脚本中必须保留
.init_array段,例如在SECTIONS里显式声明:.init_array : { *(.init_array) } > FLASH,否则 GCC 生成的构造函数指针直接被丢弃 -
CRT 启动代码(通常是
crt0.o或类似目标)必须参与链接,它内部的_start(非你写的startup.s中的入口)会在跳main前调用__libc_init_array;如果你完全绕过 CRT 自己写startup.s并直接b main,那这一步就永远缺失
如果你坚持用自定义 startup.s,该怎么补上构造调用
可以,但得自己接管 CRT 的一部分职责。前提是你的工具链提供 __libc_init_array(检查 arm-none-eabi-nm your.elf | grep init_array 看是否存在)且已保留该段。在 startup.s 的初始化完成后、跳 main 前插入:
立即学习“C++免费学习笔记(深入)”;
ldr r0, =__libc_init_array
blx r0
注意几个坑:
- 必须保证此时
sp已初始化(通常在startup.s开头就设好了),否则blx压栈就失败 - 若使用 ARM Thumb 模式,
blx要求目标地址最低位为 1(即 Thumb 指令),而__libc_init_array符号本身不带状态位;稳妥写法是:ldr r0, =__libc_init_array orr r0, r0, #1 bx r0 - 某些精简 libc(如 picolibc)可能把该函数放在
.text外部或弱定义,需确认链接时不被 GC 掉(加--undefined=__libc_init_array或-u __libc_init_array)
验证全局对象是否真的被构造了
光看链接没报错不等于成功。最直接的办法:
- 在全局对象的构造函数里放一句
asm("bkpt");或翻转某个 GPIO,用调试器单步确认是否执行 - 检查最终 ELF 文件:
arm-none-eabi-readelf -S your.elf | grep init_array—— 输出应有.init_array段且 Size > 0 - 反汇编
__libc_init_array:arm-none-eabi-objdump -d your.elf | grep -A10 "<__libc_init_array>"</__libc_init_array>,看它是否在循环调用.init_array中的函数指针 - 如果对象是
std::vector或含虚表的类,基本可以断定失败——裸机环境没malloc和 vtable 初始化支持,这类对象必须禁用或手动管理
真正容易被忽略的是:C++ 构造顺序依赖于 .init_array 中函数指针的排列顺序,而这个顺序由源文件编译顺序和链接顺序共同决定,不是按定义先后。一旦多个模块都有全局对象,且相互调用,行为就不可控——这时候就得放弃隐式构造,改用显式 init() 函数。


















