ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Xilinx SDK静态库创建与链接指南:从工程结构到配置实践

Xilinx SDK静态库创建与链接指南:从工程结构到配置实践 我记得第一次在Xilinx SDK里折腾静态库是因为一个Zynq项目里要同时跑三四个外设驱动AES加解密、SD卡日志、还有一堆自定义寄存器操作全堆在main.c里代码量超过五千行之后每次编译都要等半天改一个函数就得整个工程重新链接。当时就想着能不能把那些稳定的模块抽出来像PC上写C程序那样搞成.a库SDK里点了半天没找到入口后来才搞明白这套基于Eclipse的开发环境里库工程和普通应用工程的创建方式完全不一样。这篇就围绕Xilinx SDK也就是Vitis统一软件平台的前身现在很多老项目还在用它里静态库的创建和使用把我踩过的坑和总结出来的流程完整写一遍。内容适合正在做Zynq、Zynq UltraScale或者MicroBlaze软核开发的人看尤其是那些已经开始觉得工程越来越臃肿、想开始整理公共代码的工程师。我会把创建库工程、配置编译选项、链接进应用工程这几个关键环节全部拆开讲最后附上我实际排查过的问题记录。1. 为什么要在Xilinx SDK里做静态库1.1 静态库到底解决了什么问题先别急着点鼠标建工程先想明白静态库在嵌入式开发里到底扮演什么角色。静态库本质上就是一组目标文件.o的打包集合在链接阶段会被整体复制进最终的可执行文件里。你在Xilinx SDK里写代码的时候每个C文件编译出来就是一个.o链接器把所有的.o和库文件里的目标模块组合到一起生成最终的.elf。那为什么不直接把所有.c文件全扔进应用工程里编译非要搞个库出来我自己的感受是三个字省心、清爽、复用。省心是因为库里的代码已经被验证过了不需要每次编译都重新检查清爽是因为工程结构干净了看代码的人一眼就能分清哪些是公共模块、哪些是业务逻辑复用在多项目场景下特别重要比如你手上有Zynq-7010和Zynq-7045两款板子驱动层做了条件编译那你完全可以把这套驱动做成静态库两个应用工程直接链同一个库文件只改BSP设置就行。还有个容易被忽略的点增量编译。静态库工程修改内部某个源文件后SDK只会重编那个源文件再重新打包应用工程那边如果源文件没动就不用重新编译一大堆东西。这一点在大工程里节省的时间非常可观我那个五千行的main.c就是被这个体验逼着做了重构。提示静态库不是拿来编译时省时间的而是拿来管理代码结构的。它的核心价值在复用和工程组织不要在性能优化上对它寄予不切实际的期望。1.2 SDK支持哪两种库选哪个Xilinx SDK里新建工程时会让你在“OS Platform”里选Standalone、Linux或FreeRTOS然后在“Available Templates”里能看到几种模板。跟库相关的有两种Static Library和Dynamic Library。很多新手第一次看到这两个选项会愣住书上讲动态库在嵌入式里也用不了啊为什么会出现在这里其实Dynamic Library在SDK里确实存在但它在裸机环境下的意义非常有限。因为动态库需要运行时加载机制而Standalone本身没有完整的动态链接器想在Zynq上跑动态库你得先有Linux系统。所以如果你做的是裸机Bare-metal开发比如用Standalone跑PS端的程序或者给MicroBlaze写固件老老实实选Static Library。就算以后想换Linux静态库在Linux用户空间程序里也是能照常链接的只是少了一个动态加载的优势在工程管理上没有任何损失。选库的时候还有一个更细节的点库的类型静态还是动态不只影响生成文件的扩展名.a vs .so还会影响后续链接时是否要额外处理运行时依赖。SDK生成的文件名规则是lib你起的名字.a比如你建了一个叫my_drivers的库工程最终产物就是libmy_drivers.a。记住这个命名规则后面配置链接器的时候要反复用到。2. 创建静态库工程的完整流程2.1 建工程前的准备动手之前先确认两件事第一你的SDK版本是不是和你用的Vivado版本匹配。举个例子Vivado 2019.1配的SDK是2019.1你非要拿2020.2的SDK去打开老版本的硬件导出的平台文件八成会报错或者丢定义。第二确保你已经有一个导出了硬件信息的平台工程。在Vivado里完成综合、实现、生成比特流之后用File Export Export Hardware导出勾选Include bitstream这样SDK里才能新建对应的平台工程和BSP。准备工作里面还包含一个基础认知静态库工程本身需要绑定一个平台Platform平台里面带着硬件描述文件.hdf/.xsa取决于版本和BSP。很多人不知道这一点直接在File New Other里搜Library结果发现建出来的工程没法选硬件平台最后链接的时候一堆东西找不到。正确路径是走File New Xilinx C/C Project在向导里选择平台和模板。注意SDK里库工程的本质是“一个绑定到具体硬件平台和BSP的代码容器”它跟你最开始创建的Application Project一样都有对应的BSP组件。只不过它最终不生成.elf而是生成.a。2.2 逐步创建一个静态库工程我来把整个创建过程完整过一遍这个过程我在多台电脑上做过几十次步骤基本稳定。打开SDK之后工作空间里应该已经有了你的平台工程比如叫zcu104_wrapper之类的。然后按这个顺序操作菜单栏点File New Xilinx C/C Project不要选Other里的普通C Project。弹出的对话框会列出当前工作空间里所有的平台工程选你正在用的那个比如zcu104_wrapper。注意这里有个下拉选项问你要用standalone还是freertos裸机就选standalone。给工程起名字。这个名字会有讲究比如我想做一个日志模块的库起名log_lib那么生成的文件就是liblog_lib.a。往下翻模板列表找到Static Library选中它Finish。工程创建完成后SDK会自动生成一个src目录和一个xxx_bsp的BSP工程如果你选的是独立BSP模式。把你准备封装成库的.c和.h文件拖进src目录里。有一个地方经常有人搞混模板列表最上面通常还有一个Empty Application选项如果你选择了这个然后去配置里硬改成静态库也不是不能做但工程属性的很多默认值都是按应用工程配的后面要手动改一堆地方。所以老老实实选Static Library模板最省事。建好之后看一下工程目录结构正常情况下有一个src文件夹和一个Debug或Release文件夹取决于你当前选的构建配置。.a文件会生成在Debug或Release文件夹里不会被直接显示在SDK的Project Explorer的应用视图下如果你找不到它去文件系统里看工程目录下的Debug/libxxxx.a。2.3 库工程的编译选项怎么配创建完库工程以后必须把编译选项过一次。右键库工程选Properties C/C Build Settings这里面的ARM v7 gcc compiler那一类选项不是摆设它们会直接影响库能不能被应用工程正常调用。最常用的几个配置优化级别-O0/-O1/-O2/-O3我建议Debug版用-O0Release版用-O2。-O3在某些边界情况下面会有未定义行为的风险比如C语言里的指针别名问题在嵌入式里出这种问题排查起来极其痛苦。-g调试信息Debug配置默认开了Release默认关。如果你想在Release下也能看部分调试符号手动加-g代价是.a文件变大。消息宏定义-DXXX比如你库的内部代码用了#ifdef LOG_LEVEL_DEBUG这种条件编译那在库工程的编译器选项里就要加-DLOG_LEVEL_DEBUG。这一步经常被忽略导致库编译出来的行为跟预期不一致。头文件路径-I如果库的源文件引用了库外部的头文件比如BSP里的xparameters.hSDK一般会自动加好。但如果你的公共头文件放在某个共享目录比如C:/common_inc就得手动添加。可以在Includes标签页里加也可以直接改arm-none-eabi-gcc的flag。有一点值得单独拧出来说千万不要在库工程里把优化和调试符号的配置和应用工程弄成两套逻辑否则最后调试时指令跳转和源码行号对不上你会怀疑人生。我见过有人库编译用-O3应用编译用-O0最后单步调试时发现库里的printf日志顺序都变了还以为是代码逻辑出了问题查了三天。所以库和应用工程的优化级别建议保持一致。实操建议库里尽量输出纯逻辑代码不要在里面做有副作用的事比如直接操作寄存器或者调用BSP的初始化函数。把和外设强相关的操作再包一层接口这样库本身的可移植性会好很多换平台的时候只需要改薄薄一层适配代码。3. 关键环节在应用工程里链接静态库3.1 链接器配置的原理库工程建好了.a文件也生成了但它不会自动被你的应用工程链接进去。这一步我发现很多人卡住因为SDK的图形界面把链接器的配置藏得比较深而且术语上有些迷惑。在嵌入式链接的语境下链接器arm-none-eabi-ldSDK里走的是arm-none-eabi-gcc封装的一层需要知道两件事第一要去哪个目录里找libxxx.a这叫库搜索路径Library search path-L第二要链接哪个库这叫库名Library name-l。链接器找库的时候有一个隐式规则你告诉它库名是mylib它实际上是去找libmylib.a。这也就是为什么前面强调SDK生成的文件名是lib前缀拼工程名比如liblog_lib.a对应库名log_lib。这个规则跟PC上Linux的gcc链接完全一致如果你写过Makefile理解起来会非常快。在SDK的图形界面里应用工程右键Properties C/C Build Settings ARM v7 gcc linker Libraries能看到两个关键配置框Libraries (-l)在这个列表里填库名也就是不带lib前缀和.a后缀的名字填log_lib。Library search path (-L)在这个列表里填.a文件所在的目录。如果库工程和应用工程在同一个工作空间且库工程处于Debug配置那么路径通常是${workspace_loc:/log_lib/Debug}。如果库工程处于Release配置则换成/log_lib/Release。有的版本会显示${ProjName}/Debug这种相对路径的写法反正你心里要清楚最终展开出来的就是一个工作空间中的绝对路径。注意如果链接器提示找不到库文件多半是搜索路径写错了或者库工程当前构建配置跟你预期的不一致。SDK里库工程切换Debug/Release配置的方法是右键库工程选Build Configurations Set Active。3.2 三步把库用起来我把从零开始把库链接进应用工程的正常操作整理成三步每一步都有明确的检查标准照着走基本不会出问题。**第一步在应用工程里配库搜索路径和库名。**打开应用工程的Properties C/C Build Settings ARM v7 gcc linker Libraries在Library search path里添加库工程生成的目录路径比如${workspace_loc:/log_lib/Debug}在Libraries (-l)里填log_lib。点OK保存后强制重新编译应用工程右键应用工程选Clean Project再点Build Project。**第二步把头文件路径加进应用工程的编译器搜索路径。**这一步很多人会漏掉漏了的话应用代码里#include log.h会报找不到头文件。打开应用工程Properties C/C General Paths and Symbols Includes在GNU C下面点Add添加库工程的src目录路径比如${workspace_loc:/log_lib/src}。如果你用的是C那GNU C下面也要加同样的路径。**第三步在应用代码里正常调用。**这一步看起来简单但有个值得注意的点。如果你库的头文件里声明了一些接口然后头文件是用extern关键字修饰全局变量的那你要确保变量在库的某个.c文件里被定义了。我习惯把静态库的对外接口都收敛成函数不暴露全局变量这样链接的耦合度最小。三步做完之后重新构建整个工程注意是整个工程包括库工程和应用工程如果链接成功生成的.elf文件大小通常会比不链接库时大一些这是正常的因为静态库中的目标代码被拷贝进来了。下面用表格把关键的配置项列出来方便你对照检查配置项位置填法作用库名Application Properties C/C Build Settings ARM v7 gcc linker Libraries Libraries(-l)log_lib不带lib前缀和.a后缀告诉链接器去匹配liblog_lib.a库搜索路径同上Libraries Library search path(-L)${workspace_loc:/log_lib/Debug}告诉链接器去哪个目录找库文件头文件路径Application Properties C/C General Paths and Symbols Includes GNU C${workspace_loc:/log_lib/src}让编译器能编译你的应用代码全局宏定义Application Properties C/C Build Settings ARM v7 gcc compiler Symbols按需添加控制条件编译逻辑3.3 C与C混编时的extern C这个坑我印象特别深。有一次我把一个C写的驱动库链接进了一个以C为主的应用工程在头文件里明明包含了函数声明但链接的时候一直报undefined reference to xxx。当时第一反应是库名或者路径错了检查了半天都没问题后来才意识到是C的name mangling机制在作怪。C编译器在编译函数名的时候会做名字改编name mangling比如log_init这个符号在C编译后可能变成了_Z8log_initv而C语言的静态库里符号还是log_init。链接器拿C那套名字去找C的库当然找不到。解决方法就是头文件写C语言接口时用extern C包一层#ifdef __cplusplus extern C { #endif int log_init(void); int log_write(int level, const char *msg); #ifdef __cplusplus } #endif这样在C的编译单元里看到这个头文件时会告诉编译器按C语言的符号命名规则来处理这些函数链接时就能正确匹配了。如果你写的是纯C的头文件也要习惯性地加上这段宏因为不知道哪天这个库就会被一个C工程引用。这是一个成本极低的习惯收益却能省掉几小时的排查时间。4. 常见问题与排查心得4.1 链接报undefined reference这是静态库使用里最高频的错误。.text.log_write referenced in section .text.main of main.o、undefined reference to log_write看到这种字眼基本就是链接器找不到符号。但找不到符号的原因可能有四种我按排查优先级排列一下库名拼写错误。检查-l后面填的名字和实际.a文件名是否匹配注意大小写。Linux下的.a文件名是大小写敏感的但SDK在Windows上开发的时候Windows文件系统不区分大小写这会造成一种错觉在Windows上新建的工程名是LogLib生成的库文件可能是libLogLib.a但你在Windows下填libloglib.a它也能找到因为它不区分大小写。等你把工程挪到Linux服务器上编译就突然找不到库了。所以从第一天开始就统一用小写命名能省掉这种跨平台问题。搜索路径不对。打开链接器配置确认-L后的路径实际存在而且里面真的有对应的.a文件。去文件系统上看一眼别只看SDK的Project Explorer因为库工程的应用视图默认不显示生成物。库工程没编译或者编译到了另一个配置。库工程处于Debug配置但你配的搜索路径指向Release或者库工程还没构建是空的。可以先回到库工程右键Build Project确认Debug文件夹下生成了libxxx.a。函数名因为C的name mangling对不上。刚才讲的extern C场景检查头文件里有没有加保护。链接器报错的时候会给出具体的符号名你可以用arm-none-eabi-nm工具去查看.a文件里的符号表。在SDK的终端里运行arm-none-eabi-nm D:/workspace/log_lib/Debug/liblog_lib.a这个命令会列出库里所有导出的符号看到T log_write这种输出T表示这是一个位于文本段的函数符号说明函数确实被编译进去了。如果看不到那就是库工程本身的问题。4.2 库更新了但程序不生效这个问题非常隐蔽而且SDK不会主动提示你。场景是这样的你把静态库里的一个函数实现改了比如log_write里加了一个时间戳字段然后回到应用工程点Build发现生成的.elf行为还是旧的。你以为代码没生效其实是因为应用工程根本没重新链接。原因在于SDK的依赖关系跟踪有时候会漏掉库文件的时间戳变化。应用工程只记录了它依赖的.a文件路径但SDK构建系统没有在每次编译时都检查库目录的内容。要解决这个问题我的办法是手动强制重链右键应用工程选Clean Project然后Build Project。这样会把原来的.elf删掉强制链接器重新走一遍链接流程就会重新读取最新的.a文件。如果你用的是命令行方式构建可以在应用工程目录下运行make clean make -j8同样可以达到效果。这里有个额外建议养成做库前先make clean的习惯。静态库的增量编译本身是可靠的但构建系统对跨工程依赖的处理并不是100%可靠特别是在Windows环境下文件系统时间戳的粒度不够细时容易出现“库改了但应用工程认为没改”的错觉。4.3 依赖顺序、Debug/Release混用等其他坑剩下的坑我整理成一个速查表都是我实际碰到过的不一定每个都会遇到但遇到了能省不少时间现象原因解决方案链接报错但符号明明在库之间存在依赖顺序问题比如libnet.a依赖liblog.a但链接命令里libnet.a写在liblog.a前面把被依赖的库放在依赖者的后面即先填net再填log应用工程能编译但运行异常Debug版的库链到了Release版的应用里或者反过来检查库工程和应用工程的Active配置是否一致BSP相关函数找不到库工程BSP和应用BSP版本不一致或者库工程引用了别的硬件平台右键库工程查看Platform确认统一点同一个平台.a文件体积很大编译时开了-O0且带了完整调试信息可以在Release配置下用-O2重新编译库SDK里找不到生成好的.a默认视图没有显示输出文件用系统文件管理器去工作空间/工程目录/Debug/下找关于库依赖顺序这个点我想再展开说两句因为它背后的原理能帮你理解链接器的工作方式。链接器处理库的顺序是单向的它从左到右依次扫描命令行里的库如果一个库里的符号在之前没有被任何目标文件引用这个库的模块就不会被加载。所以我前面说的“被依赖的库放在后面”本质上是要让链接器在扫到依赖库的时候已经知道前面有未解析的符号需求。现实项目里一个应用可能要链四五个库为了避免依赖顺序问题我常用的做法是写一个链接脚本片段或者在SDK的链接器配置里把库按依赖关系从上层往下层排列。最上层的库是业务逻辑库最下层的是基础驱动库。每次新增库的时候心里默认这套顺序就不会出现链接错误。最后再分享一个个人习惯给静态库工程命名的时候我会用模块名 _lib的方式比如log_lib、flash_lib、net_lib。因为SDK生成.a文件的规则是lib 工程名 .a如果你工程名里再加个lib最终文件名就会变成liblog_lib_lib.a看着非常别扭。这个小细节不算技术问题但工程多了以后命名统一对经验积累和管理效率的提升是很真实的。
返回列表