ARTICLE DETAIL

资讯详情

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

【金丹·66】为什么你的代码编译过了但链接报错

【金丹·66】为什么你的代码编译过了但链接报错 【金丹·66】为什么你的代码编译过了但链接报错码农修仙传 · 金丹期 · 第66篇我是玄芯散人带你从炼气修到大乘。境界标识╔══════════════════════════════════╗ ║ 金丹期 · 第66篇 ║ ║ 编译过了但链接报错 ║ ║ undefined reference 实战 ║ ║ 预计阅读14分钟 ║ ╚══════════════════════════════════╝修仙引入上一篇讲了 ELF 文件的内部结构。这一篇拆修真界最常见的渡劫现场undefined reference这行报错。前两篇你学了符号解析、静态与动态链接这一组底层知识ELF 格式也跟着过了一遍。这一篇换个视角从实战出发把最常见的几种链接报错挨个摆开讲排查顺序和 nm 等工具的用法。链接器这一关的金丹期修士报错了 5 秒内判断属于哪一类比会背readelf输出更管用。金丹期修道者的内功在于从一行报错倒推出具体原因。百度搜索之前先把nm跑一遍。硬核主体五个最常见的链接错误情境链接器那一关的报错五花八门按出现频率排下面这五种占了大半。第一种情境函数只声明没实现或者实现没进编译命令入门弟子最常踩的坑。头文件里写了void foo(void);但foo.c没写出来或者写了却没加到gcc命令里。// foo.hvoidfoo(void);// 仅有声明// main.c#includefoo.hintmain(void){foo();return0;}$ gcc main.c-omain /tmp/ccXXXX.o:infunctionmain:main.c:(.text0x12): undefined reference tofoocollect2: error: ld returned1exitstatus报错信息三个要点undefined reference to foo是要什么in function main是谁在要ld returned 1是链接阶段失败。解决办法也直白补上foo.c并加进编译命令或者确认这个函数本来就在某个.c里、只是漏传了。$ gcc main.c foo.c-omain# 把 foo.c 加进来$ ./main# 链接通过修真比喻每个弟子.o都登记了我会某种功法UND 符号但翻遍整座祠堂链接器扫遍所有输入都没找到这门功法的真正典籍执事只能贴告示说此法无人习得。第二种情境库的顺序反了GCC 链接静态库有一条铁则左到右扫描遇过不候。链接器扫到-lfoo时从libfoo.a里挑出未定义符号对应的.o再把这些.o里的未定义符号去后面的库找。已经处理过的库绝不会回头再扫一次。依赖关系决定顺序被依赖的库必须放后面。libfoo.a引用了libbar.a里的bar_func时必须是foo在前bar在后反过来bar在前foo在后链接器扫过libbar.a时还没人需要bar_func等扫到libfoo.a找出bar_func引用时libbar.a已经处理过了不会回头找于是报错。# libfoo.a 引用 libbar.a 的函数libbar 写在了前面被依赖方在前错误$ gcc main.c-lbar-lfoo-omain undefined reference tobar_func# 把 libfoo 放前面依赖方在前被依赖方在后$ gcc main.c-lfoo-lbar-omain# 链接通过-Wl,--start-group ... -Wl,--end-group是一组反复扫描指令能处理循环依赖。代价是链接器会对组内库做多遍扫描拖慢链接速度。gcc 官方都建议非必要不用。$ gcc main.c -Wl,--start-group-lfoo-lbar-Wl,--end-group-omain实际工程里更推荐的做法是把库顺序理清楚或者把被依赖的库放在后面。第三库依赖复杂时比如 OpenCV FFmpeg 系统库用pkg-config --libs自动生成正确顺序。修真比喻执事捧着一摞功法目录从前往后翻。看到弟子需要的功法核对姓名收下再翻下一本。前一本没找到的回头再翻执事说不行规矩就是这样。第三种情境C 和 C 混编的 name manglingC 支持函数重载编译器会把函数名编码成包含参数类型的字符串比如add(int, int)变成_Z3addii。这就是 Itanium C ABI 规定的 name mangling 规则。C 语言没有函数重载函数名就是函数本身。如果 C 代码调用一个 C 函数链接器找的是add但.o里只有_Z3addii自然对不上号。// math.cppintadd(inta,intb){returnab;}// C 实现符号名 _Z3addii// main.cexternintadd(int,int);intmain(void){returnadd(1,2);}$ gcc main.c math.cpp-omain undefined reference toadd# 链接器找 add但 .o 里只有 _Z3addii解决办法是用extern C告诉 C 编译器这段代码用 C 的命名约定// math.cppexternCintadd(inta,intb){returnab;}// 符号名就是 add头文件里通常这么写让 C 和 C 都能 include#ifdef__cplusplusexternC{#endifintadd(inta,intb);#ifdef__cplusplus}#endifC 调用 C 库特别是厂商 SDK、嵌入式 BSP几乎一定会遇到这个问题。nm跑一下能立刻看出来库里的符号全是_Zxxx而你的代码要的是裸名。修真比喻C 弟子写功法要把第几式什么兵器都编进功法名里mangled nameC 弟子只记招式名plain name。两人隔行如隔山只有extern C这本通行证能让 C 弟子把功法名换成 C 风格。第四种情境缺库路径漏写-lxxx是基础问题更隐蔽的是库文件不在默认搜索路径里。GCC 默认去/usr/lib、/usr/local/lib、/lib找.a和.so。自家项目编译出来的库放在lib/目录下必须用-L显式告诉链接器去哪里找。# 编译报错找不到 -lmylib$ gcc main.c-lmylib-omain /usr/bin/ld: cannotfind-lmylib# 加入 -L 指定路径$ gcc main.c -L./lib-lmylib-omain# 编译通过运行时又出现新问题可执行文件找不到动态库。$ ./main ./main: errorwhileloading shared libraries: libmylib.so: cannotopenshared object file: No suchfileor directory编译期是链接器找库运行时是动态链接器ld.so找库。两边用的不是同一套搜索路径。运行时三种方式指定# 1. 环境变量临时生效$exportLD_LIBRARY_PATH./lib:$LD_LIBRARY_PATH$ ./main# 2. -Wl,-rpath 写进可执行文件生产推荐$ gcc main.c -L./lib -Wl,-rpath,./lib-lmylib-omain $ ./main# 不需要设环境变量也能找到# 3. 装到系统库目录部署用$sudocplibmylib.so /usr/local/lib/ $sudoldconfigLD_LIBRARY_PATH是运行时环境变量LIBRARY_PATH是编译时环境变量两者别混。修真比喻编译期是执事在自家藏经阁里翻书-L-l运行时是弟子下山后去各地分舵取秘籍动态链接器 -rpath。两个阶段找书的地点不一样得各给一张地图。第五种情境重复定义multiple definition头文件里写了非 inline 函数的实现多个.c文件 include 之后每个.o都会带一份同名函数的实现链接器合并时报multiple definition。// utils.hintadd(inta,intb){returnab;}// 实现不该放头文件// a.c#includeutils.hinta(void){returnadd(1,2);}// b.c#includeutils.hintb(void){returnadd(3,4);}$ gcc a.c b.c-omain /tmp/ccYYYY.o:infunctionadd:b.c:(.text0x0): multiple definition ofadd/tmp/ccZZZZ.o:a.c:(.text0x0): first defined here collect2: error: ld returned1exitstatus三个常见根因函数实现写在了头文件里应该写在.c里全局变量在头文件里声明又初始化应该extern声明、.c里定义C 里没用inline让头文件函数支持多翻译单元修真比喻每个弟子都复印了一份祖传心法夹在自家功法册里链接器合并时发现好几本册子都有同一份心法多人署名谁是正版修真界规矩是只能有一份多了就要打架。nm 等排查工具链接报错排查也讲套路。修真长老诊断弟子功法问题各有侧重链接排查也有几件工具压箱底。报错了不知道从哪里入手按下面这套流程走5 秒判断属于哪一类。nm查符号表nm列出目标文件里的所有符号。报undefined reference时先用它判断谁在引用、谁应该提供。$ nm main.o|grepfoo U foo# U undefinedmain.o 引用了 foo$ nm libfoo.a|grepfoo T foo# T text 段已定义libfoo.a 提供了 foo常用符号字母含义字母含义T / ttext 段已定义的函数大写全局小写局部Uundefined未定义符号D / d已初始化数据段B / bBSS未初始化数据段R / r只读数据段W / wweak symbol弱符号大写表示全局global小写表示局部local。U是排查 undefined reference 时的关键线索。$ nm main.o Uprintf# 引用了 printf要去 libc.so 里找0000000000000000 T main# main 函数的定义在这里objdump看反汇编和重定位objdump -d看反汇编objdump -r看重定位表。链接报错时objdump -r能直接定位哪条指令引用了哪个未定义符号。$ objdump-rmain.o main.o:fileformatelf64-x86-64 RELOCATION RECORDS FOR[.text]: OFFSET TYPE VALUE 000000000000001a R_X86_64_PC32printf# 0x1a 处引用了 printf$ objdump-dmain.o|head-300000000000000000main:0: f3 0f 1e fa endbr644:55push %rbp... 1a: e8 00 00 00 00 call 0x1fmain0x1f# 这里就是 printf 的调用点看到 0x1a 处call指令填的偏移是00 00 00 00这就是链接器待填的占位符。objdump 还有一个常见用法是看静态库的成员$ objdump-alibfoo.a# 列出所有成员文件ldd看运行时动态库依赖linker error 解决了可执行文件跑不起来用ldd看运行时依赖的动态库是不是都能找到$ ldd ./main linux-vdso.so.1(0x00007ffd123ab000)libmylib.sonot found# 这就是运行时找不到的库libc.so.6/lib/x86_64-linux-gnu/libc.so.6 /lib64/ld-linux-x86-64.so.2(0x00007f...)not found就是问题所在。再用readelf -d main | grep NEEDED看可执行文件里登记了哪些.so$ readelf-dmain|grepNEEDED 0x0000000000000001(NEEDED)Shared library:[libmylib.so]DT_NEEDED是动态链接器要找的清单链接器当时把libmylib.so写进了可执行文件但运行时找不到它。补-Wl,-rpath或者装到系统库目录都行。修真比喻nm看弟子手里的身份玉牌登记了什么招式objdump看弟子写的功法原文细节ldd看弟子下山后能进哪些分舵取秘籍。三件套配合编译期和运行时的链接问题都能定位。静态库 vs 动态库链接行为的实战差异修真界有宗门典籍和江湖流通本之分。静态库是宗门典籍抄一份带在身边动态库是江湖流通本需要时去分舵取。静态库.a# 打包静态库用 ar 把多个 .o 打成一个 .a$ ar rcs libfoo.a foo.o bar.o baz.o# 查看静态库内容$ ar t libfoo.a foo.o bar.o baz.o链接器处理静态库的规则左到右扫描命令里的库遇到-lfoo即libfoo.a从库里挑出能解决当前未定义符号的.o把这些.o加入链接这些.o引用的新未定义符号去后面的库找已经处理过的库不再回头也就是说被依赖的库必须放在后面。动态库.so# 编译动态库-fPIC 生成位置无关代码-shared 生成 .so$ gcc-fPIC-sharedfoo.c-olibfoo.so# 编译可执行文件链接动态库$ gcc main.c -L.-lfoo-omain# 指定运行时路径推荐$ gcc main.c -L.-lfoo-Wl,-rpath,./lib-omain动态库链接有几个要点-fPIC位置无关代码Position Independent Code。库要被多个进程共享必须能加载到任意地址。-fPIC让代码用相对地址而不是绝对地址。SONAME动态库的身份证号。用gcc -Wl,-soname,libfoo.so.1指定记录在.so内部。DT_NEEDED可执行文件登记运行时需要哪些.so由动态链接器ld-linux.so加载。-Wl,-rpath把运行时库搜索路径写进可执行文件。部署到目标机器时不依赖LD_LIBRARY_PATH环境变量。修真比喻静态库是宗门印发的纸质典籍弟子随身带一份副本多就占地方。动态库是江湖流通的玉简所有人都到同一个分舵读取省地方但依赖分舵一直开着。两者链接行为的差异对比对比项静态库.a动态库.so链接时机链接时整段复制进可执行链接时只留待办清单运行时加载库顺序顺序敏感反了会报错顺序相对宽松可执行大小大含库代码小只含引用升级库重新编译可执行直接替换.so即可编译依赖-fPIC不需要必须排查命令nm libfoo.a与ar tnm -D libfoo.solddreadelf -d混淆两种库的报错也是常见现象编译时用的是静态库路径-L./lib -lfoo运行时又找不到动态库。file libfoo.a和file libfoo.so一眼就能分清ar t看静态库readelf -d看动态库的 SONAME 和 NEEDED。实战排查流程5 秒决策树链接报错的排查有固定套路。把上面五种情境和三件套串起来就是一套 5 秒决策树undefined reference 排查决策树报错符号是项目里的函数→ 检查 .c 文件有没有加进编译命令没加 → 补 .c 到 gcc 命令加了 → 继续查库报错符号是库函数→ gcc 命令里有对应的 -l 吗没加 → 补 -lxxx加了 → -L 路径对吗不对 → 补 -L对 → 静态库顺序反了吗反了 → 被依赖库放后面对 → C/C 混编吗是 → 有 extern “C” 吗没加 → 加 extern “C”加了 → 查 nm 看符号是否 mangled否 → 用 nm/objdump 深入查符号把这套流程记熟看到undefined reference直接按节点走比百度搜索快得多。修真比喻长老诊断弟子功法问题先看弟子档案nm再看功法原文objdump最后查下山路线ldd。三步查下来五种常见问题一网打尽。修仙术语对照表修仙术语技术现实本篇位置祠堂告示undefined reference 报错五种情境功法典籍未补函数声明了但实现没编译情境一功法目录翻阅顺序静态库从左到右扫描情境二反复翻阅--start-group --end-group情境二招式名带兵器C name mangling情境三C 风格通行证extern C情境三藏经阁与分舵编译期库路径与运行时库路径情境四运行时地图-Wl,-rpath情境四多弟子同修一法multiple definition情境五身份玉牌查询nm 命令三件套功法原文校对objdump 反汇编三件套山下分舵路线ldd 查看动态库依赖三件套宗门纸质典籍静态库 .a静态 vs 动态江湖流通玉简动态库 .so静态 vs 动态位置无关心法-fPIC静态 vs 动态进阶条件会用编译命令和看到链接报错能 5 秒定位根因之间差这几条能区分五种常见undefined reference情境缺源文件 / 库顺序 / C 名字修饰 / 缺路径 / 重复定义能用nm main.o | grep xxx看符号的 U/T 状态能用nm的大小写区分全局符号大写与局部符号小写能用objdump -r看重定位表定位哪条指令引用了哪个符号能用ldd ./program看运行时动态库依赖识别not found能用-Wl,-rpath把运行时库搜索路径写进可执行文件能解释 GCC 静态库从左到右扫描的规则并写出正确的库顺序知道extern C在 C/C 混编时的用法最后两条是金丹期实操分水岭。链接报错天天见能 5 秒判断属于哪一类再去查具体根因就脱离了瞎百度的阶段。把这些排查工具练熟几乎所有常见链接问题都能独立解决。下期预告 互动下一篇【金丹·67】操作系统内核是天道规则编译与链接讲完了金丹期进入第二组操作系统内核。内核是电脑里的天道普通程序跑在用户态凡间内核跑在内核态天庭。两边之间通过系统调用通信。本篇先勾勒内核的整体轮廓从用户态与内核态的边界入手再讲到系统调用的入口最后过一遍 CFS 调度器的基本概念。现在问你 你被undefined reference卡过最久的一次最后查到是哪种根因⚙️ 你项目里更常用静态库.a还是动态库.so为什么选这个评论区聊聊你跟链接器斗智斗勇的经历。我是玄芯散人带你从炼气修到大乘。本文是「码农修仙传」系列第66篇。系列导航见 xren.ren
返回列表