最近在把一个裸机嵌入式项目从Makefile迁移到GN构建系统整个迁移过程比预想中顺利唯独卡在armcc工具链的接入上折腾了不少时间。GN这套元构建系统本身设计得很精巧但它的工具链抽象几乎默认你是拿GCC或者Clang干活突然换成armcc这种“自成一派”的商用编译器默认模板基本没法直接用。这篇东西就专门聊清楚怎么在GN里把armcc工具链完整地配置起来从工具链抽象原理、命令行参数映射到完整的BUILD.gn和BUILDCONFIG.gn写法再到实际编译、链接、生成bin文件的过程中会遇到的问题。如果你也正在用GN管理嵌入式工程又因为项目历史原因必须保留armcc作为实际编译器那这篇文章能把大多数坑先帮你踩平。哪怕你暂时不用armcc看完自定义toolchain的完整写法以后想接任何非GCC系编译器到GN里思路也是一模一样的。1. 为什么说GN默认工具链玩不转armcc1.1 GN的工具链抽象和它的“默认假设”GN和Ninja的组合里GN负责生成Ninja文件Ninja只负责执行具体的构建动作。GN里构建动作的最小单位叫tool一个toolchain就是一组tool的集合。默认情况下GN会给你预设一套基于GCC/Clang习惯的toolchain里面包含cc、cxx、alink、solink、stamp、copy这些工具。问题就出在这套默认实现里。GN调用cc工具时会自动帮你在命令行里加上依赖生成参数比如-MMD -MF这类GCC风格的选项还会假设编译器能理解-O2、-g、-fno-omit-frame-pointer这类参数。你把armcc直接塞进去理论上gn gen这一步不会报错因为GN只是在生成脚本真正暴露问题的是Ninja启动构建那一刻。你会看到一串unknown option、unsupported command line option然后整条编译命令失败。更麻烦的是armcc的参数风格和GCC差异很大。GCC用-mcpucortex-m4armcc用--cpuCortex-M4GCC用-MMDarmcc用--dependGCC用-Wl,传参数给链接器armcc的链接器armlink用--scatter路径来指定分散加载文件。所以把armcc接进GN本质上不是“填几个参数”而是要重写整套工具链的command模板。这活儿绕不开只能自己动手。1.2 armcc命令行的“非主流”习惯armcc是ARM编译器套件里的C编译器命令和它配套的还有armcpp、armlink、armar、fromelf这些工具。armcc命令行选项的典型风格是双横线加长单词选项和GCC的单横线短选项完全是两套体系。这里列一组对照关系方便你理解两者差异有多大功能GCC风格armcc风格指定CPU-mcpucortex-m4--cpuCortex-M4浮点ABI-mfloat-abihard--fpuFPv4-SP生成依赖文件-MMD -MF xxx.d--dependxxx.dC语言标准-stdc99--c99编译链接命令gcc foo.c -o foo.elfarmcc --arm foo.c --scatterxxx.sct -o foo.axf最要命的是链接环节。armcc配套的链接器armlink根本不认GCC那种-Wl,--gc-sections的写法它要求你用--scatter指定分散加载文件用--entry指定入口函数用--first指定第一个加载段。这些选项在GN的默认solink工具描述里完全没有对应字段只能全部写进command字符串里。所以在动手配置之前先在心里建立这个认知GN的toolchain机制只是一个命令生成框架它本身并不绑定GCC你需要做的是把armcc世界的命令格式完整翻译成GN能理解的tool描述。2. 环境准备先确认四件事2.1 工具链组件清单接armcc之前先确认你手里的工具链是完整的。一个能正常工作的armcc工具链环境至少要有这四个可执行文件armccC编译器负责把.c编译成.oarmlink链接器负责把.o和.a链接成axf或elfarmar归档工具负责生成.a静态库fromelf镜像转换工具负责把axf/elf转成bin或hex其中fromelf虽然不参与编译链接但嵌入式项目最终要烧录到芯片里基本都要转一道bin所以我也把它纳入工具链范畴。拿到工具链后先分别在终端里验证一下版本和授权状态armcc --vsn armlink --vsn armar --vsn fromelf --vsn这一步不能省尤其是armcc这种商用工具链经常出现“命令能找到但授权无效”的情况。armcc遇到授权问题时会直接输出一段提示然后拒绝编译如果你在GN配置阶段没验证等到Ninja真正跑起来才暴露排查起来会多绕一个弯。2.2 确定CPU型号与apcs参数armcc命令行里有两个参数是必须设对的否则编译出的代码大概率跑不起来。第一个是--cpu用来告诉编译器目标内核的具体型号比如Cortex-M0、Cortex-M3、Cortex-M4、Cortex-M7或者Cortex-A53。注意大小写armcc对这里的大小写是敏感的cortex-m4会直接报错必须写成Cortex-M4。第二个是--apcs全称是ARM Procedure Call Standard也就是ARM过程调用标准。嵌入式裸机项目里最常用的是--apcsinterwork这个选项让编译器生成的代码同时支持ARM和Thumb指令集之间的安全切换。如果你的内核只支持Thumb模式比如Cortex-M0/M3/M4都是这样那interwork基本是标配。这个参数直接写进toolchain的command里不留给每个子目标单独传防止有人粗心漏掉导致链接时出现奇怪的跳转错误。3. 核心配置在GN里把armcc工具链“翻译”过来3.1 目录结构与顶层BUILDCONFIG.gn我建议的项目结构是这样project_root/ ├── BUILD.gn # 顶层目标定义 ├── BUILDCONFIG.gn # 全局配置定义默认toolchain和公共变量 ├── toolchain/ │ ├── BUILD.gn # 自定义armcc toolchain实现 │ └── armcc_config.gn # 公共config集中管理编译选项 ├── src/ │ ├── BUILD.gn │ ├── main.c │ └── ... ... ├── scatter/ │ └── project.sct # 分散加载文件 └── out/ # 输出目录由gn gen自动生成顶层BUILDCONFIG.gn的作用很关键GN在每次执行构建配置时都会先读这个文件。我习惯把target_cpu相关的armcc参数全部放在这里统一管理。比如你有一个基于Cortex-M4的项目可以这样写# BUILDCONFIG.gn set_default_toolchain(//toolchain:armcc) armcc_cpu Cortex-M4 armcc_fpu FPv4-SP armcc_apcs interworkset_default_toolchain这一行最重要它告诉GN除非某个目标显式指定其它toolchain否则全工程都用//toolchain:armcc这套工具链。变量命名不要用GN内置的target_cpu那种因为GN基于x86 host的默认值会干扰你的判断直接用自定义变量更干净。3.2 toolchain/BUILD.gn完整实现接下来是重头戏在toolchain/BUILD.gn里定义armcc工具链。GN的工具链定义语法是通过toolchain()函数配合tool()子块来写的。每个tool()块里需要描述命令模板、输出文件名规则、依赖文件规则、响应文件规则等。这里给出一个能直接跑起来的完整写法以ARM Compiler 5风格的armcc为例# toolchain/BUILD.gn import(//BUILDCONFIG.gn) toolchain(armcc) { tool(cc) { depfile {{output}}.d command armcc --c99 --cpu$armcc_cpu --fpu$armcc_fpu --apcs$armcc_apcs --depend{{output}}.d --depend_formatunix --depend_systemall {{defines}} {{include_dirs}} {{cflags}} -c {{source}} -o {{output}} deps gcc outputs [ {{source_out_dir}}/{{source_file_part}}.{{target_output_name}}.o ] output_prefix } tool(cxx) { depfile {{output}}.d command armcc --cpp --c99 --cpu$armcc_cpu --fpu$armcc_fpu --apcs$armcc_apcs --depend{{output}}.d --depend_formatunix --depend_systemall {{defines}} {{include_dirs}} {{cflags}} -c {{source}} -o {{output}} deps gcc outputs [ {{source_out_dir}}/{{source_file_part}}.{{target_output_name}}.o ] output_prefix } tool(alink) { command armar --create {{output}} {{inputs}} outputs [ {{target_out_dir}}/{{target_output_name}}{{output_extension}} ] output_prefix default_output_extension .a } tool(solink) { command armlink --cpu$armcc_cpu --apcs$armcc_apcs --scatter//scatter/project.sct --entryReset_Handler --firstReset_Handler {{inputs}} {{solibs}} --userlibpath {{target_out_dir}} -o {{output}} outputs [ {{target_out_dir}}/{{target_output_name}}{{output_extension}} ] output_prefix default_output_extension .axf } tool(stamp) { command touch {{output}} outputs [ {{output}} ] } tool(copy) { command cp {{source}} {{output}} outputs [ {{output}} ] } }有几个细节需要你特别注意。第一depfile和--depend是配套的。armcc生成依赖文件用的是--depend选项和GCC的-MMD参数不同但生成的.d文件内容格式是相似的。GN的depfile字段告诉Ninja去读这个文件里的头文件依赖关系这样才能实现“改个头文件就重编相关.c文件”的增量构建。--depend_formatunix是故意加的armcc在所有平台下默认可能生成带盘符或反斜杠的路径GN对Windows路径的解析有问题强制转成unix格式能省掉很多麻烦。第二deps gcc这个字段不是指真用了GCC而是告诉GN这个tool生成的是GCC风格的依赖文件格式。GN目前支持gcc和msvc两种依赖格式解析armcc生成的.d文件和GCC基本一致所以选gcc就行。第三链接命令里我把--entryReset_Handler --firstReset_Handler写死了。这是嵌入式项目里很常见的做法让Reset_Handler成为整个镜像的入口并且放到第一个段。如果你的工程入口函数名字不同记得改掉。第四output_extension设成了.axf这是armlink默认的可执行文件后缀。你也可以用--output等选项改成.elf但既然工具链默认是axf就直接沿用。3.3 依赖文件与输出后缀的适配实际配置过程中最容易被忽略的就是输出文件后缀的适配。GN内部对目标类型有“预期”——编译C源文件时它期望工具生成一个.o文件收集静态库时它期望工具生成一个.a文件链接可执行文件时它期望生成一个无后缀名或者自定义后缀的文件。你在tool(alink)里设置了default_output_extension .aGN才能把static_library目标正确绑定到这个工具上。armcc编译生成的对象文件默认后缀是.o这点和GCC一致所以cc工具的输出模板不需要额外改后缀。但armlink生成的默认文件是.axf这是最大的差异点。如果你在solink里不写default_output_extension .axfGN会用默认的空后缀到时候生成的文件名就会变成app而不是app.axf本身不算大问题但后续用fromelf处理时会容易混淆。依赖文件适配方面armcc的--depend生成的.d文件里目标文件名那行是用tab缩进的比如这样main.o: main.c header.hNinja对依赖文件的解析还算宽容只要源文件路径和目标文件路径能对上就行。但armcc在Windows环境下如果不加--depend_formatunix生成的路径里会出现反斜杠Ninja会把反斜杠当成转义符轻则依赖解析失败重则直接报错中断构建。所以那个--depend_formatunix务必保留。4. 实操过程把配置跑起来4.1 最小工程编译一个C文件生成可执行文件理论说完直接来一个最小可复现的工程。假设源码只有一个main.c内容大致是// main.c #include platform.h int main(void) { init_clock(); init_uart(); while (1) { send_char(A); } }对应根目录的BUILD.gn# BUILD.gn executable(app) { sources [ src/main.c ] include_dirs [ src, include ] }执行构建gn gen out ninja -C out第一次构建时GN会读取BUILDCONFIG.gn加载//toolchain:armcc然后生成对应的Ninja文件。Ninja执行时会依次调用armcc --c99 --cpuCortex-M4 --fpuFPv4-SP --apcsinterwork --depend... --depend_formatunix --depend_systemall -Isrc -Iinclude -c src/main.c -o out/src/main.o armlink --cpuCortex-M4 --apcsinterwork --scatter//scatter/project.sct --entryReset_Handler --firstReset_Handler out/src/main.o --userlibpath out -o out/app.axf构建产物里会多出一个out/app.axf。用fromelf把它转成binfromelf --bin --outputout/app.bin out/app.axf拿到app.bin就可以直接烧录了。4.2 多文件与静态库工程真实项目里不太可能只有一个C文件必然要拆模块、建静态库。GN里静态库目标用static_library表示注意这个关键字对应到toolchain里的alink工具而不是cc。比如工程里有驱动模块和主应用模块# src/driver/BUILD.gn static_library(driver) { sources [ uart.c, clock.c, gpio.c, ] include_dirs [ . ] }# src/app/BUILD.gn executable(app) { sources [ main.c ] deps [ //src/driver:driver ] include_dirs [ //src/driver ] }这种情况下GN会先生成out/src/driver/libdriver.a然后再链接app时把libdriver.a传给armlink。armlink收到.a文件后只会抽取其中被引用到的模块保证最终固件体积不会因为链接了整个库而膨胀。这里有一个GN的隐晦规则static_library目标生成的文件名默认会带上lib前缀也就是说driver会变成libdriver.a。如果你后续在链接命令里手动指定了库路径要记得用libdriver.a这个名字不要习惯性地写driver.a。armlink对这个倒是宽容用--userlibpath指定路径后它会自己去找libdriver.a。不过--userlibpath只负责告诉链接器“去哪个目录找库”具体的库文件名还是要出现在输入列表里。GN在链接时会自动把依赖的静态库加到{{inputs}}里所以上面的示例里虽然命令没直接写libdriver.a实际执行时armlink会收到完整的输入列表。5. 常见问题与排查技巧实录5.1 高频报错速查表我总结了这一套配置过程中最常见的几个坑按出现频率从高到低排了个表。看到报错先对照一下大概率能省下半小时报错关键词常见原因解决思路unknown option/unsupported optionGN默认的cflags或defines传进了armcc在目标的configs里清掉默认config改用你自定义的armcc configcould not open scatter file分散加载文件路径不对或者被GN当作文件而非路径处理检查--scatter后面的路径确保用的是GN能找到的真实路径Missing entry point链接命令没有指定--entry或--first确认入口函数符号名字并显式加--entryxxx --firstxxxdepend file ... not found--depend参数拼写有误或者路径里的反斜杠造成解析失败加--depend_formatunix并检查depfile路径是否与{{output}}.d一致unknown option: --cpparmcc的C编译命令不是armcc --cpp或者工具链版本太老确认工具链是否支持C不支持就改用只含C代码的工程验证failed to add library ...静态库后缀不是.a或armlink找不到libxxx.a检查alink的default_output_extension确认输出目录里有.a文件5.2 我在这个配置里反复踩的四个坑第一个坑和GN的默认config有关。我在最小工程里犯的第一个错误是忘记清掉默认config。GN对每个目标默认注入一套config里面带着一堆适用于GCC/Clang的编译选项比如-fno-omit-frame-pointer、-fno-ident、-Wextra之类的。armcc看到这些直接报警编译出来的东西也不对劲。解决方法是给每个目标或每个目录统一设置config(armcc_default_config) { cflags [ --c99, -Ospace, ] defines [ USE_ARMCC ] } set_defaults(static_library) { configs [ //toolchain:armcc_default_config ] } set_defaults(executable) { configs [ //toolchain:armcc_default_config ] }第二个坑是路径分隔符。我在Windows主机上配置时armcc生成的.d文件里目标名带了盘符和反斜杠Ninja解析依赖关系时直接报“missing and no known rule to make it”。这个问题的根子不在GN而在armcc的--depend_format设置。加入--depend_formatunix之后问题立刻消失。如果是在Linux主机上开发这步可能遇不到但建议无论什么平台都写上统一行为。第三个坑是静态库后缀。我曾经有一版配置里把alink的default_output_extension写成了.o结果链接阶段armlink收到的输入列表里出现了一个怪异的libdriver.o它完全懵了报了一堆failed to add library错误。排查的时候我一度以为是链接命令少了什么参数最后发现是后缀名不匹配GN按.o去命名静态库文件armlink按.a去寻找库。把后缀改回.a后一切正常。第四个坑是入口函数匹配。armcc和armlink默认生成的镜像入口并不是Reset_Handler它其实是工具链运行时库里的一个符号。如果你的工程没有显式指定--entry链接器会采用运行库默认入口这可能导致最后烧进芯片的程序不从复位向量开始执行看起来就像“固件没跑起来”。我后来在solink命令里固定加--entryReset_Handler --firstReset_Handler并且要求工程里的启动文件必须导出这个符号问题才算彻底解决。补充一个针对新一代工具链的提醒。如果你拿到的是新版工具链实际编译器命令可能不叫armcc而叫armclang参数风格也从双横线长选项变成了类似GCC的单横线选项。armclang的--cpu要换成-mcpu--apcs要换成-mthumb -mfloat-abihard这类。这属于Arm Compiler 6的世界和本文的armcc不完全是一套写法但自定义toolchain的思路完全一致你只需要把命令模板里的选项替换成armclang对应的写法就行。最后再分享一个我自己的体会。折腾这套配置最有效率的路径不是一上来就搞全工程而是先用一个最简单的单文件工程把cc和solink两个工具打通确认能生成axf和bin再逐步加入静态库、多目录依赖、分散加载这些环节。每一步都验证通过了再往下走出问题时的排查范围会小很多。GN这个系统的好处是配置逻辑非常清晰一旦你把toolchain翻译正确后续所有目标的构建都自动走在同一条路径上稳定性比手写Makefile高太多。现在这个项目里我要换芯片型号只需要改一下BUILDCONFIG.gn里的armcc_cpu变量然后重新gn gen所有目标都会自动用新的CPU参数重新编译这才是把构建配置做成“基础设施”该有的样子。
阅读完成 · 觉得有帮助?