Keil工具链-第01讲-MDK工程结构与编译流程解密
发布时间:2026/10/26 10:14:47 锦皓数字建站

第 1 讲 | MDK 工程结构与编译流程解密本文是《Keil MDK ARMCC 工具链深度实战》专栏第 1 讲。这一讲不讲语法、不讲寄存器只回答一个问题你按下 F7 之后Keil 到底做了什么一、从一个真实的报错说起几年前我带一个新人调试固件他改完代码点编译报错.\Objects\app.axf: Error: L6218E: Undefined symbol SystemInit (referred from startup_stm32f10x.o).他盯着屏幕问我“startup_stm32f10x.o是什么我只写了一个main.c。”这个问题很典型——很多人写了几年嵌入式却从没搞清楚工程的组成更不知道编译时都发生了什么。于是遇到链接错误、map 文件、scatter 脚本就一脸茫然只能靠搜索引擎试运气。这一讲我们把 Keil 这个黑盒子拆开。二、.uvprojx到底是什么你双击打开的那个.uvprojx文件本质是一个 XML 文本文件。用记事本就能打开?xml version1.0 encodingUTF-8 standaloneno ?Projectxmlns:xsihttp://www.w3.org/2001/XMLSchema-instanceSchemaVersion2.1/SchemaVersionTargetsTargetTargetNameapp_demo/TargetNameDevicePY32F071C8T6/DeviceToolsetNumber0x4/ToolsetNumber!-- 0x4 ARMCC --ToolsetNameARM-ADS/ToolsetNameTargetOptionTargetCommonOptionOutputDirectory.\Objects\/OutputDirectoryOutputNameapp_demo/OutputNameCreateExecutable1/CreateExecutableCreateHexFile1/CreateHexFileCreateCppFile0/CreateCppFile/TargetCommonOption.../TargetOption/Target/Targets/Project为什么要知道这个三个现实意义.uvprojx是工程定义.uvoptx是你的个人设置断点、窗口布局、当前打开的文件。所以团队协作时只提交.uvprojx把.uvoptx加进.gitignore——否则你会发现每次拉代码断点和窗口都被人改了。工程文件冲突可以手工合并。两个人同时加了文件Git 报.uvprojx冲突你不需要重新配工程直接编辑 XML把两边的File节点都保留即可。它可以被脚本生成。做 CI / 批量构建多个 Target 时用脚本改 XML 比手点界面可靠得多。一句话.uvprojx就是文本别把它当黑盒。三、工程的层级结构Target → Group → FileKeil 的工程树看着简单但三层概念必须分清Project └── Target目标 ← 一套编译/链接配置可以有多个 ├── Group: User ← 只是文件夹不影响编译 │ ├── main.c │ └── app.c ├── Group: Drivers │ └── hal_gpio.c └── Group: CMSIS └── startup_xxx.s关键认知Target 是配置集不是分类。同一个工程可以有Debug和Release两个 Target分别用-O0和-O2代码是同一套。你在工具栏选的其实就是当前 Target。Group 纯粹是目录显示用的跟你磁盘上的文件夹没有必然关系。把main.c拖到Drivers组里它还是那份main.c不会报错。不要用 Group 来管理真正的物理路径。.s启动文件是工程的一部分。上面那个L6218E报错的根源就在这里——startup_xxx.s里调用了SystemInit但你的工程里没有实现它的文件通常在厂商的system_xxx.c里。四、编译流程全景按 F7 之后发生了什么这才是本讲的核心。Keil 的构建过程可以拆成5 个阶段.c / .h 文件 │ ▼ ① 预处理Preprocessarmcc -E │ 展开 #include、替换宏、处理条件编译 ▼ ② 编译Compilearmcc / armclang │ C → 汇编做优化产生警告 ▼ ③ 汇编Assemblearmasm │ 汇编 → 机器码目标文件 .o ▼ ④ 链接Linkarmlink │ 合并所有 .o 库按 scatter 分配到地址 ▼ ⑤ 后处理fromelf │ .axf → .hex / .bin / 反汇编 ▼ 可烧录固件对应到 Keil 的Build Output窗口你会看到这些行Rebuild started: Project: app_demo *** Using Compiler V5.06 update 7 (build 960), folder: C:\Keil_v5\ARM\ARMCC\Bin Build target app_demo compiling main.c... compiling app.c... assembling startup_py32f071.s... linking... Program Size: Code38924 RO-data4216 RW-data76 ZI-data8212 FromELF: creating hex file... .\Objects\app_demo.axf - 0 Error(s), 3 Warning(s). Build Time Elapsed: 00:00:06逐行解读输出行含义Using Compiler V5.06...当前用的工具链版本。V5 armccV6 armclang这是排查明明改了配置还是老行为的第一现场compiling main.c...阶段②编译armccassembling startup_xxx.s...阶段③汇编armasmlinking...阶段④链接armlink把.o拼成.axfProgram Size: Code... RO-data...最重要的一行四类尺寸下一节细讲FromELF: creating hex file...阶段⑤生成.hex3 Warning(s)有 3 个警告第 10 讲会专门讲这些不是噪声Program Size四个数到底是什么Program Size: Code38924 RO-data4216 RW-data76 ZI-data8212Code程序指令Flash——你写的所有函数编译后的机器码RO-data只读数据Flash——const数组、字符串字面量、常量表RW-data已初始化的可读写数据Flash 和 RAM 各占一份——int g 5;这种ZI-data零初始化的数据RAM——int buf[1024];这种于是有两个公式记住它们能算清 Flash 和 RAM 的占用Flash 占用 Code RO-data RW-data RAM 占用 RW-data ZI-data代入上面那组数据Flash 38924 4216 76 43216 字节 ≈ 42.2 KB RAM 76 8212 8288 字节 ≈ 8.1 KB注意RW-data同时计入 Flash 和 RAM——因为初值存在 Flash 里启动时被拷贝到 RAM。第 5 讲会详细讲这个拷贝过程__main干的活。五、各阶段产物哪些文件是我们真正关心的编译完打开Objects目录会看到一堆文件。挑重要的说文件谁生成用途.o编译/汇编目标文件含机器码和符号链接的输入.d编译器依赖文件记录头文件依赖改头文件会触发重编译.axfarmlinkELF 格式的可执行文件调试用的核心产物.hexfromelfIntel HEX带地址信息烧录器常用.binfromelf纯二进制没有地址信息OTA 升级常用.maparmlink内存映射报告第 4 讲主角*.htmarmlinkmap 的 HTML 版浏览器打开更方便.lnpKeil链接器输入文件列表排查少链接了哪个文件的神器.lstKeil汇编列表看某函数被编译成什么指令用.lnp排查链接错误回到开头那个Undefined symbol SystemInit。除了看报错更快的办法是打开Objects\app_demo.lnp--strict --scatter .\Objects\app_demo.sct -o .\Objects\app_demo.axf --info totals --keep*.o(i.*) .\Objects\main.o .\Objects\app.o .\Objects\startup_py32f071.o一眼就能看出——列表里没有system_py32f071.o所以SystemInit没有定义。把system_xxx.c加进工程问题解决。这个技巧比盯着 L6218E 猜快十倍.lnp是链接器真正的输入清单。六、链接器自动生成的两样东西很多人不知道链接时会自动生成两个关键文件如果不看Objects目录你永远不知道它们存在1. Scatter 文件.sct如果你在Options → Linker里没有勾选 “Use Memory Layout from Target Dialog”armlink 会根据 “Target” 页里配的 ROM/RAM 地址范围自动生成一个.sct。内容类似LR_IROM1 0x08000000 0x00020000 { ; 加载区Flash 128KB ER_IROM1 0x08000000 0x00020000 { ; 执行区代码只读 *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) } RW_IRAM1 0x20000000 0x00004000 { ; 16KB RAM .ANY (RW ZI) } }它决定了代码从 0x08000000 开始、RAM 从 0x20000000 开始、各占多少。第 3 讲会把.sct讲透——它是解决变量固定地址“多区 OTA”CCM RAM的唯一手段。2. 启动文件与__main链接器会强制引入启动代码最终main()并不是第一个执行的函数。真正的入口是Reset_Handler → SystemInit → __main → __rt_entry → main这条链是第 5 讲的主题。先记住结论__main不是main它由工具链提供负责把.data从 Flash 拷到 RAM、把.bss清零、初始化栈堆。很多全局变量初值是 0的诡异问题根子就在这里。七、命令行等价脱离 IDE 也能构建理解流程后你会发现Keil 只是这些命令的图形外壳。同一个工程用命令行也能构建做 CI 或批量构建时很有用# 编译单个文件ARMCC V5armcc--c99-c-O2-DNDEBUG--apcsinterwork\-I./inc -I./cmsis\-omain.o main.c# 汇编启动文件armasm--cpuCortex-M0--apcsinterwork\-ostartup.o startup_py32f071.s# 链接armlink--strict--scatterapp.sct\--infototals-oapp.axf main.o app.o startup.o# 生成 hex / binfromelf--i32combined-oapp.hex app.axf fromelf--bin-oapp.bin app.axf这也解释了为什么换台电脑就编译不过——多半是工具链版本或命令行参数比如头文件路径、宏定义不一致。第 7 讲会详细对比 V5/V6/GCC 的参数差异。八、本讲小结.uvprojx是 XML 文本不是黑盒.uvoptx是个人设置不该进版本库。Target 是配置集Group 只是显示分组别混淆。编译 5 阶段预处理 → 编译 → 汇编 → 链接 → 后处理。Program Size四个数Code / RO-data / RW-data / ZI-data记住两条公式Flash Code RO-data RW-dataRAM RW-data ZI-data.lnp是链接器输入清单排查Undefined symbol最快。链接器会生成.sct并强制引入__main启动链——它们是后面几讲的主角。下一讲预告既然Program Size是四个数那同样的代码换个优化等级这四个数会怎么变第 2 讲我会拿一个真实工程把-O0 / -O1 / -O2 / -O3 / -Os / -Otime六个等级各编译一遍附上体积、耗时实测数据以及优化等级改变volatile语义、导致程序跑飞的真实案例。本专栏是《Keil MDK ARMCC 工具链深度实战》第 1 讲共 10 讲。点赞收藏关注欢迎进入观看其他专栏。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。