Arm Compiler 5.06实战:老工程编译链接与内存布局排错

发布时间:2026/10/27 20:27:14
Arm Compiler 5.06实战:老工程编译链接与内存布局排错 简介这是一款面向ARM处理器嵌入式开发者的Arm Compiler 5.06编译器工具链安装包适合需要为ARM架构应用做编译、链接与代码优化的初中级开发人员也可供使用Keil MDK等集成环境的学习者补齐工具链。压缩包共3个文件约80.03MB包含MSI安装包、EXE安装引导程序及HTML发布说明覆盖工具安装与版本信息查阅。资源围绕armcc、armlink、armar、fromelf等组件展开可帮助读者理解C/C编译、静态库管理、链接优化与二进制文件处理并配合-O1至-O4优化级别平衡性能与代码尺寸。目前已有22294人学习下载说明其在ARM开发入门与工程实践中具有较高参考价值。通过安装说明与工具链组件读者可快速搭建编译环境对照发布说明排查兼容性问题并为后续嵌入式项目开发、交叉编译和性能调优提供基础支持。1. Arm Compiler 5.06老工程点名要它新工程早就绕开它前段时间有人丢来一个源码包说一编译就报出一堆错误。远程看了一下第一行是Error: #5: cannot open source input file core_cm4.h第二行是Fatal error: C3903U: Argument Cortex-M33 not permitted for option cpu。前者是头文件搜索路径没配后者是编译器根本不认识这颗核。到这里问题已经不在代码层面而在工具链他手上缺的正是 Arm Compiler 5.06。这套编译器是 Arm 上一代 C/C 工具链armcc、armasm、armlink、armar、fromelf 一整套齐全覆盖 ARMv5/v6/v7对十几年前的老工程、老汇编启动文件、老运行时库的兼容度后来的编译器替代不了。它并不适合新项目但在“必须让这堆老代码编译通过”的场景里它往往就是唯一解。2. 拆开四个可执行文件armcc、armasm、armlink、fromelf 各管哪一段很多人拿到这个包之后第一反应是“双击安装然后呢”。装完打开目录看到 bin 下面十几个 exe不知道该调用哪个。其实一条 C 代码变成能烧进 Flash 的二进制只经过四道工序C 源码先由 armcc 翻成目标文件启动汇编由 armasm 翻成目标文件两者加库文件交给 armlink 按内存布局拼起来生成 .axf最后 fromelf 把 .axf 抽成 .bin 或者反汇编出来核对。把这条流水线记住后面所有报错你都能一眼定位到是哪一段出的问题。2.1 安装目录里真正需要认的几个位置装好之后的根目录结构大同小异值得关注的是下面这几处。bin 目录是命令入口include 是标准头文件lib 是预编译好的运行时库sw 下面还会带一些 mlib 的源码出问题时可以直接翻。路径内容什么时候会用到bin/armccC/C 编译器所有 .c/.cpp 文件bin/armasmARM 语法汇编器启动文件、手写汇编 .sbin/armlink链接器生成 .axf读分散加载文件bin/armar静态库打包把一堆 .o 打成 .a 交付bin/fromelf镜像转换、反汇编出 .bin/.hex查汇编include/标准 C/C 头文件报 #5 找不到stdio.h时先看这里lib/运行时库 .a报Undefined symbol且是库符号时看这里sw/mlib/MicroLIB 相关想用 microlib 又怀疑版本不对时这里有个容易被忽略的点AC5 和 AC6 的 include 目录内容差别不小。如果你机器上两个版本都装了编译时到底取的是哪一套头文件取决于你调用的那个 armcc 的路径所以不要让 PATH 里同时存在两个 bin 目录否则编译一遍通过、换台机器又挂这种鬼打墙多半就是 PATH 顺序引起的。2.2 三条命令确认这套东西是真能跑的装完别急着编工程先用三条命令把“版本对、核认识、授权可用”这三件事各确认一遍。这一步省下来后面会花十倍时间去找原因。# 1) 确认版本输出里应该有 5.06 字样别拿到 5.05 或者 6.x armcc --vsn # 2) 确认这颗核在支持列表里AC5 只到 ARMv7M33/M23 不会出现 armcc --cpu list | grep -i cortex-m # 3) 实际编译一个最小文件把授权和头文件路径一起验证掉 armcc -c --cpu Cortex-M4.fp -O2 --c99 -I ./inc -o ./build/t.o ./src/main.c第一条命令看的是产品版本和 build 信息如果输出里出现的是Arm Compiler 6或者根本找不到命令说明 PATH 里挂的是另一个工具链后面所有排查都白做。第二条列出的是这个版本认可的--cpu取值注意Cortex-M4和Cortex-M4.fp是两个不同的目标带.fp才会启用单精度浮点指令选错会让浮点运算变成软件模拟性能差好几倍还不报错是最阴的一类问题。第三条命令是真正的快速验证它同时检验了授权是否到位、include 路径是否写对、目标核名是否合法能过这条说明工具链本身没问题剩下的就是工程配置的事。2.3 在集成环境里把编译器版本切回 5多数人不是直接在命令行敲 armcc而是在 IDE 的 Target 选项里选编译器版本。新版 IDE 默认挂的是 AC6打开老工程时下拉框里要么没有 AC5 选项要么选了之后路径是空的。操作顺序一般是先确认 AC5 已经独立安装到一个不含空格和中文的路径下然后在 Target 的编译器版本下拉框里选中 5.06 这一项再到工具链路径设置里把它指向安装根目录最后全量 Rebuild别用增量编译。这里我一般会额外做一步Rebuild 完成后打开生成的 map 文件头部确认里面写的编译器版本号是 5.06 那一行以防下拉框选了但实际没生效。如果工程是脚本化构建的或者你不想动工程文件还有一条路是环境变量注入。较新版本的 AC5 支持ARMCC5_CCOPT、ARMCC5_ASMOPT、ARMCC5_LINKOPT这类变量往里面塞选项就能在不改任何工程文件的前提下追加编译参数临时打开-W看告警、临时加一个-D都很方便。它的优先级低于命令行显式给出的选项所以可以用它来做临时调试但别把它当成长期的工程配置换个人接手就会变成“为什么我这里编出来不一样”的黑匣子。提示ARMCC5_CCOPT 这类变量会被同一台机器上所有使用 AC5 的工程继承调试完记得清掉否则某天你会莫名其妙多出几个宏定义。3. 命令行把一份 C 工程编成 .bin选项、分散加载与 map 核对搞清楚四个可执行文件的分工之后最稳的落地方式是用命令行把整条流水线跑通一遍。这一步不是为了取代 IDE而是为了在出问题时能剥掉 IDE 那层壳直接看到编译器到底收到了什么参数。我见过太多情况是 IDE 里勾了一堆选项出问题根本不知道最终命令长什么样这时候一份手写构建脚本能让排查速度提升一个数量级。3.1 编译选项优化等级、C 标准、宏与包含路径armcc 的选项分两类一类决定生成的代码长什么样--cpu、-O*、--c99另一类决定能不能编过-I、-D、--diag_suppress。前者一旦定错通常不报错但行为错误后者出错就红一片所以先把后者调通再回头调前者。选项作用典型取值和改动时机--cpu指定目标核Cortex-M3、Cortex-M4.fp、Cortex-M7.fp.dp带 FPU 必须写.fp--c99打开 C99 支持老代码保持 C90 就不加加了可能冒出新告警-O0/-O1/-O2/-O3优化等级调试期-O0量产-O2别用-O3调中断密集代码-Ospace/-Otime次级优化目标Flash 紧张加-Ospace热点循环加-Otime-D宏定义板级配置、功能裁剪常改-I头文件搜索路径报#5第一个要查的-W输出告警信息建议常开不加时有些问题只提示不显眼--diag_suppressN屏蔽指定编号告警确认无害后再用编号写错会屏蔽别的东西把这些选项组织成一个构建脚本结构大致如下。注意--split_sections会让每个函数单独成段配合链接器的裁剪能把没调用的函数全部删掉这是小 Flash 型号的常用做法。# 工具链路径按本机实际安装位置改路径里不要有空格和中文 CC : /opt/arm-compiler-5.06/bin/armcc LD : /opt/arm-compiler-5.06/bin/armlink ELF : /opt/arm-compiler-5.06/bin/fromelf CPU : Cortex-M4.fp OPT : -O2 -Ospace DEFS : -DUSE_HAL_DRIVER -DBOARD_REV2 INCS : -I ./inc -I ./drivers/inc WARN : -W --diag_suppress1295 CFLAGS : --cpu $(CPU) --c99 $(OPT) $(DEFS) $(INCS) $(WARN) --split_sections SRCS : $(wildcard ./src/*.c) $(wildcard ./drivers/src/*.c) OBJS : $(patsubst ./%.c,./build/%.o,$(SRCS)) all: ./build/app.bin ./build/%.o: ./%.c mkdir -p $(dir $) $(CC) -c $(CFLAGS) -o $ $ ./build/app.axf: $(OBJS) ./link/app.sct $(LD) --cpu $(CPU) --scatter./link/app.sct \ --map --list./build/app.map \ --infosizes,totals,stack,unused \ -o $ $(OBJS) ./build/app.bin: ./build/app.axf $(ELF) --bin --output$ $这段脚本里最值得说的是--list./build/app.map和--infosizes,totals,stack,unused。前者把完整的 map 文件落到磁盘后者让链接器额外把分区大小、总量、栈使用和未使用段打印出来。很多人链接失败时只知道“RAM 不够”但不知道是谁占的这两个参数就是答案。另一个细节是--split_sections只对 C 源码里的函数生效汇编文件需要--split_sections的等价写法否则启动文件那一整段永远裁不掉。3.2 armlink 与分散加载文件内存布局是写死的armlink 的行为几乎完全由分散加载文件决定这个.sct描述的是“哪些段放到 Flash 的哪个地址、哪些放到 RAM 的哪个地址、栈和堆留多大”。它和 GNU 工具链的 linker script 概念一致但语法完全不同从 GCC 迁过来的人第一次看会觉得别扭。下面是一个最小可用版本结构和注释都写清楚。LR_IROM1 0x08000000 0x00080000 { ; 加载域起始地址 最大长度 ER_IROM1 0x08000000 0x00080000 { ; 执行域通常和加载域同址 *.o (RESET, First) ; 向量表必须排在最前偏移 0 *(InRoot$$Sections) ; 库里的根段删掉会丢启动代码 .ANY (RO) ; 其余只读代码和常量 } RW_IRAM1 0x20000000 0x0001F000 { ; 可读写区 .ANY (RW ZI) } ARM_LIB_STACK 0x2001F000 EMPTY -0x00000800 { } ; 预留 2KB 栈不占数据 }这里面有四个点经常被改错。第一First必须跟着RESET段走一旦向量表没排在 0 地址芯片上电就会跑去执行一条随机指令现象是“烧进去没反应”很多人会误判成 Flash 算法的问题。第二InRoot$$Sections是库里的根段包含__main相关的初始化逻辑删掉之后表现为全局变量初值不对。第三ARM_LIB_STACK的EMPTY -0x800表示从地址往下留出 2KB 空间这个长度要按你最深的调用栈加中断嵌套来估估小了就是随机跑飞而且不同优化等级下栈深度差别很大-O0编出来的栈通常是-O2的两倍以上。第四RW 域的起始地址和长度必须和芯片手册上的 SRAM 划分对得上写大了链接器不报错烧进去访问越界现象是“某些变量莫名其妙被改”。如果你有一块外部 SRAM 或者想给某个大数组单独划区可以在 scatter 里加一个独立的执行域再用__attribute__((section(...)))把变量钉进去。常见做法是给大缓冲区单独开一个ER_RAM2这样 map 里一眼就能看到它占了多少不会被混在.bss里。注意改了 scatter 之后一定要全量重新链接只做增量链接有时会沿用旧的布局信息改了等于没改。3.3 fromelf 出 bin再反汇编核对一遍.axf是带符号和调试信息的 ELF不能直接烧必须用 fromelf 抽成纯二进制。同时建议顺手反汇编一份用来核对关键函数的地址和指令数尤其是启动文件和中断向量。# 抽出纯二进制起始地址由 scatter 里的 IROM 起始决定 fromelf --bin --output./build/app.bin ./build/app.axf # 反汇编-c 出代码-d 出数据两者一起看才完整 fromelf -c -d --output./build/app.dis.txt ./build/app.axf # 只关心段大小的时候可以直接看汇总 fromelf --text -z ./build/app.axf | head -40第一条命令生成的 bin起始地址是加载域LR_IROM1的起始地址长度就是实际用掉的字节数。如果你发现 bin 比预期大了几十 KB通常不是代码变多了而是某个const大数组忘记加const被放进了 RAM 又被初始化值带进了加载域镜像——这种问题看 map 的Image component sizes一节最直接。第二条命令的反汇编里重点看三处向量表的前 16 个字节是不是你期望的初始 MSP 和 Reset_Handler 地址、Reset_Handler里的跳转是不是指向了__main、以及那些被标成热点函数的指令条数和你预期是否一致。有时候-O2把某个函数完全内联掉了你在反汇编里找不到它别急着以为代码被裁了去看 map 的符号表。4. 避坑清单五条错误信息背后的真实原因这套工具链的报错信息普遍偏简洁一行错误背后可能是四种完全不同的原因。下面五条是我在老工程上反复遇到的按“现象 → 原因 → 解决”整理遇到类似的先对照着排除。4.1 C9932E授权拿不到所有编译都失败现象是任何一个文件编译都返回Error: C9932E: Cannot obtain license for Compiler连 hello world 都过不去但换成别的编译器同一个文件就正常。原因是 AC5 走的是独立的授权机制靠环境变量指向授权文件或者授权服务器新装的机器上这个变量往往是空的。解决分三步走先把ARMLMD_LICENSE_FILE指向可用的授权来源确认本机系统时间准确时间跳变会导致授权判定失败如果是浮动授权还要确认网络能通到授权服务器。都确认完再跑一次armcc --vsn如果它不报授权错就说明这条路通了。4.2 L6218EUndefined symbol_sys_exit/_ttywrch现象是代码里一加printf链接就挂报L6915E: Library reports error: __use_no_semihosting was requested, but _ttywrch was referenced。原因是标准库的输入输出默认走半主机模式需要调试器在背后接管裸机环境没人应答就链接失败。解决有两条路要么老老实实实现_sys_exit、_ttywrch、_sys_write这几个桩函数要么在链接时加--library_typemicrolib换成精简库精简库只需要你实现一个fputc把字符甩到串口。后者代码量小得多代价是不支持某些格式化特性也不支持浮点格式化的完整实现这一点在打日志时很容易踩。/* microlib 下唯一的桩把字符丢给串口注意这里是阻塞写 */ int fputc(int ch, FILE *f) { while ((USART1-SR 0x40) 0) { } /* 等发送寄存器空 */ USART1-DR (unsigned char)ch; return ch; }4.3 L6406ENo space in execution regions现象是加了一个全局数组之后突然链接失败报.ANY selector matching xxx.o(.bss)找不到空间。原因两种一种是 RAM 真的被占满了另一种是 scatter 里 RW 域的长度写小了明明芯片有 64K 你只写了 32K。判断方法是打开 map看Image component sizes里 ZI 的总量再对比 RW 域声明的长度。确认是超了之后优先把大缓冲区挪到独立的执行域其次检查是不是有本该const的表被放进了 RAM。还有一类隐蔽情况是栈预留过大ARM_LIB_STACK那行从 1KB 改成 8KB 直接把 RAM 吃光了。4.4 协议解析结果对不上但代码逻辑没问题现象是同一份解析代码在别的编译器上跑出正确的 CRC用 AC5 编出来结果就是错的而且只在某些数据上错。原因多半是 plain char 的符号性AC5 在 C 模式下默认把 char 当无符号处理其它编译器大多默认有符号于是if (buf[i] -1)这种写法两边行为不同。解决方式是所有涉及字节的变量一律用uint8_t显式声明不要写裸char确实改不动老代码的可以加--signed_chars让行为对齐。同类问题还包括位域的内存排布和结构体对齐跨编译器传结构体之前最好加__packed并用sizeof静态断言卡一遍。4.5 C3903U这颗核不在支持列表里现象是新建工程直接死在Fatal error: C3903U: Argument Cortex-M33 not permitted for option cpu。原因很明确AC5 只覆盖到 ARMv7ARMv8-M 架构的核它根本不认识不是配置问题也不是授权问题。这种情况没有绕过路径只能换 AC6。与此同类的是启动文件从别处拿来的 GNU 语法汇编armasm 会报一堆莫名其妙的语法错因为 armasm 只认 ARM 语法要换必须重写成 ARM 语法或者干脆用 AC6 的统一汇编器。提示报错编号可以在编译器手册的告警与错误章节直接查比在论坛里翻旧帖快得多尤其是L6xxx这类链接期错误。5. AC5 与 AC6 共存哪些代码注定过不去怎么逐文件切现实情况往往是这样的老工程必须用 AC5 编新模块想用 C11 和 C14 的特性一台机器上两个编译器都得有。这不是技术选择问题是历史包袱问题。关键是搞清楚哪些代码在 AC6 下必然报错以及怎么安排两套工具链在同一个工程里各管一段而不是一刀切全切过去然后花两周修编译。5.1 在 AC6 下必然过不去的几类写法AC6 基于另一套前端语言特性支持更好但专有扩展全变了。以下这几类在老代码里出现频率最高。第一类是编译器专有属性。AC5 的__weak、__packed、__align(8)、__attribute__((at(0x0800F000)))这套写法AC6 只认标准的__attribute__((weak))、__attribute__((packed))、__attribute__((aligned(8)))而at属性在 AC6 里没有直接对应必须改成__attribute__((section(.ARM.__at_0x0800F000)))配合 scatter 使用。第二类是内联汇编AC5 的__asm { ... }块在 AC6 里不能原样用要么改成 GNU 扩展语法要么拆成独立的.s文件。第三类是 ARM 语法的启动文件AC6 的汇编器不认AREA、EXPORT、DCD这套指令必须整体重写或者换成厂商提供的新版启动文件。第四类是 C 标准选项的写法变迁--c99在 AC6 里换成了-stdc99或-stdgnu11--diag_suppress变成了-Wno-xxx构建脚本不跟着改就是一片红。第五类是 C 的隐式转换AC5 的 C 基本停在 C03AC6 默认用 C14一些早年靠隐式转换混过去的代码会被判为错误。第六类是最难缠的行为差异。比如移位负数、有符号溢出、未定义行为的优化假设两边给出的结果可能不同这类问题编译期不报错运行期才暴露只能靠全量回归测试兜。5.2 两个编译器共存的工程做法比较稳妥的做法是保持整个工程的主编译器不变只对“确实需要新标准”的模块做单独编译然后把编出来的.o交给同一个链接器。因为 AC6 和 AC5 的产物都是标准的 ELF 目标文件armlink 可以吃这一点的可行性比很多人想象的高。# 默认全部用 AC5 CC5 : /opt/arm-compiler-5.06/bin/armcc # 只有新模块用 AC6产物格式一致可以混链 CC6 : /opt/arm-compiler-6/bin/armclang # 新模块单独打成一个静态库边界清晰回溯也方便 ./build/libnewmod.a: $(NEW_SRCS) mkdir -p ./build/newmod $(CC6) -c --targetarm-arm-none-eabi -mcpucortex-m4 \ -stdgnu11 -O2 -I ./inc -o ./build/newmod/newmod.o $^ $(AR6) -r $ ./build/newmod/newmod.o这段脚本的关键在两层隔离文件系统上把新模块的中间产物单独放一个目录避免和 AC5 的.o混在一起交付形态上打成一个.a链接时整包引入出问题只需怀疑这一个包。需要特别注意的是两边的 ABI 相关选项必须完全一致--cpu和-mcpu要指向同一个目标浮点 ABI 也要对齐否则链接阶段不一定报错运行期传参就错位。我一般会在混链之后先跑一遍极小规模的自检用例确认跨编译单元的调用、结构体传参、浮点返回值这三样都正常再往下做业务。另一个实操建议是给两个编译器分别保留一份 map 文件命名带上编译器版本。真出问题时对比两份 map 里同一个函数的 Code 大小和地址往往能直接看出是不是优化策略差异导致的。这一步多做一次后面省下的时间远超成本。6. 用 map 文件做一次内存体检三处必看位置和两组参数map 文件是这套工具链里信息密度最高的产物但大多数人只在链接报错时才打开它。我的习惯是每次版本发布前过一遍只看三个位置。第一处是Image component sizes它按目标文件列出 Code、RO Data、RW Data、ZI Data 四项按 ZI 倒序排一遍谁在吃 RAM 一眼就能看出来。第二处是Memory Map of the image这里给出每个执行域的实际起始地址、长度和 Used/Unused 比例如果某个域只剩下不到 5% 余量下一个需求就会失败属于事前预警。第三处是符号表重点找__initial_sp的取值和它下面最近的全局符号两者之差就是当前栈能用的空间和你在 scatter 里预留的长度对一下差太多说明有地方在悄悄吃栈。# 生成带完整信息的 map并单独打印未使用段 armlink --cpu Cortex-M4.fp --scatter./link/app.sct \ --map --list./build/app.map \ --infosizes,totals,stack,unused \ -o ./build/app.axf $(OBJS) # 快速看谁最占 ZI按第五列倒序 awk /Image component sizes/,/Grand Totals/ ./build/app.map | sort -k5 -n -r | head -15参数上我常用两组组合。日常开发用--split_sections加默认的段裁剪配合--infounused每次链接都会告诉你删掉了哪些函数能及时发现有函数因为被错误地取地址而无法裁掉。排查阶段再加--keep某个.o保留可疑目标文件的所有段防止它被裁掉之后你在反汇编里怎么都找不到。还有个小技巧--diag_suppress不是越多越好我曾经为了消掉一条对齐告警屏蔽了一个编号结果把同编号的另一条真问题一起吞了两个月后才发现是 DMA 缓冲区没对齐。注意map 文件里的地址是链接期算出来的改了 scatter 或者增删文件之后必须重新生成别拿旧 map 对地址。从那以后我每次接手一份老工程第一件事都不是打开main.c而是先armcc --vsn确认工具链再要一份最新的 map把 RAM 余量和栈深度先量一遍然后才开始改代码。这个顺序救过我很多次因为大部分“代码写错了”的直觉最后追下去都是工具链配置或者内存布局的问题。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询