ESP32-S3项目中为何有时需extern关键字,有时无需?
我使用C++、VS Code+PlatformIO及Arduino框架,为Arduino Nano ESP32(MCU为ESP32-S3)开发游戏机模拟器,通过Debian主机上的xtensa-esp32s3-elf-g++交叉编译至开发板。
测试时采用从游戏卡带提取并转换为.cpp文件的ROM,这类文件包含供模拟处理器执行的字节数组。
相关代码定义
- 头文件中的变量声明:
extern uint8_t rom[]; extern const int rom_size; extern const char bios_name[]; extern const char rom_name[];
该头文件被包含进处理器模拟器的.cpp源码文件中。
- ROM文件中的变量定义(初始版本):
const int rom_size = ROM_SIZE; const char bios_name[] = BIOS_NAME; const char rom_name[] = ROM_NAME; uint8_t rom[ROM_SIZE] = {
异常现象
此前编译链接均正常,但切换至新ROM后出现链接错误:
/home/gregoire/.platformio/packages/toolchain-xtensa-esp32s3/bin/../lib/gcc/xtensa-esp32s3-elf/8.4.0/../../../../xtensa-esp32s3-elf/bin/ld: .pio/build/arduino_nano_esp32/src/travolta.cpp.o:(.literal._Z5setupv+0xc): undefined reference to `bios_name' /home/gregoire/.platformio/packages/toolchain-xtensa-esp32s3/bin/../lib/gcc/xtensa-esp32s3-elf/8.4.0/../../../../xtensa-esp32s3-elf/bin/ld: .pio/build/arduino_nano_esp32/src/travolta.cpp.o:(.literal._Z5setupv+0x14): undefined reference to `rom_name' /home/gregoire/.platformio/packages/toolchain-xtensa-esp32s3/bin/../lib/gcc/xtensa-esp32s3-elf/8.4.0/../../../../xtensa-esp32s3-elf/bin/ld: .pio/build/arduino_nano_esp32/src/travolta.cpp.o:(.literal._Z5setupv+0x1c): undefined reference to `rom_size' collect2: error: ld returned 1 exit status *** [.pio/build/arduino_nano_esp32/firmware.elf] Error 1
经确认,新ROM编译生成的cartridge_rip.cpp.o已被纳入链接流程,但仍报错。给三个变量的定义前添加extern关键字后链接恢复正常:
extern const int rom_size = ROM_SIZE; extern const char bios_name[] = BIOS_NAME; extern const char rom_name[] = ROM_NAME; uint8_t rom[ROM_SIZE] = {
但切换回原ROM时无需添加该关键字即可正常链接,且rom变量始终无需添加extern。
这一现象的核心是C++标准中全局const变量的默认作用域规则,结合编译优化的差异导致:
const变量的默认作用域限制
在C++中,全局const变量默认拥有文件作用域(internal linkage),即只能在定义它的编译单元(.cpp文件)内部访问,不会生成可供其他编译单元引用的全局符号。而非const的全局变量(比如rom数组)默认是外部作用域(external linkage),会生成全局符号,因此其他编译单元通过extern声明就能找到它,不需要额外添加extern修饰定义。原ROM无报错的特殊情况
原ROM没有添加extern却能正常链接,大概率是编译器优化导致的:- 如果原ROM中
ROM_SIZE、BIOS_NAME、ROM_NAME是编译器在编译模拟器源码阶段就能直接解析的常量(比如是预定义的宏、字面量),编译器会直接把这些const变量的值硬编码进模拟器代码中,跳过对变量符号的链接依赖,自然不会触发未定义符号错误。 - 也可能是原ROM的编译单元被PlatformIO施加了特殊优化选项,隐式提升了
const变量的作用域为外部,让符号能被链接器找到。
- 如果原ROM中
新ROM报错的原因
新ROM的相关常量(ROM_SIZE等)可能无法在编译模拟器源码时被解析(比如是动态生成的值、或者宏定义的方式不同),编译器必须依赖链接阶段去查找const变量的符号,但这些变量默认是文件作用域,链接器找不到对应的全局符号,因此报错。添加extern修饰定义后,强制将const变量的作用域改为外部,链接器就能找到对应的符号,问题解决。
内容的提问来源于stack exchange,提问作者Grégory TELLIER

