ARTICLE DETAIL

资讯详情

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

sqrt 未定义引用:libm 链接与 -lm 排查指南

sqrt 未定义引用:libm 链接与 -lm 排查指南 1. 先把这个报错的面目认清编译过了链接挂了1.1 编译期和链接期是两道完全不同的关卡很多人第一次碰到undefined reference to sqrt会懵我明明在文件顶部写了#include math.h语法检查也过了怎么就找不到关键在于构建一个 C/C 程序并不是一步到位而是分成好几个阶段预处理、编译、汇编、链接。#include math.h只解决第一和第二阶段的问题——它告诉编译器sqrt这个函数长什么样参数是double返回double让编译器能生成正确的调用代码。但编译器在这一步并不会去关心sqrt的函数体到底躺在哪个文件里。真正去找人的是链接器。编译阶段生成的.o目标文件里sqrt只是一个悬空的符号引用链接器要拿这个引用去所有的库和别的.o文件里比对找到它的定义然后把地址填进去。找不到就吐出undefined reference——注意这个措辞它说的是未定义的引用不是未声明的函数。如果你看到的是implicit declaration of function sqrt那是编译阶段的问题跟本文要讨论的完全是两码事。所以第一条经验就是报错信息里的动词决定了你要去哪个阶段找问题。undefined reference、undefined symbol、unresolved external都属于链接期implicit declaration、expected ;、no matching function属于编译期。分不清这两者你会在错误的目录里翻半天头文件。1.2 为什么偏偏是 sqrt 被特殊对待这事得从 C 标准库的拆分方式说起。在所有类 Unix 系统上标准 C 库被拆成了两块libc里放着printf、malloc、memcpy这些核心函数链接器默认就会带上它而数学函数——sqrt、pow、sin、cos、log、exp、fmod、atan2——被单独放在libm里默认不链接。这是历史遗留的产物早期机器上浮点运算是靠软件模拟的libm体积极大链接进去会白白浪费空间所以惯例上让用户自己按需引入。你在 Linux 上随手写个#include stdio.h #include math.h int main(void) { printf(%f\n, sqrt(2.0)); return 0; }用gcc main.c -o main编译十有八九会撞上/tmp/ccXXXXXX.o: In function main: main.c:(.text0x1f): undefined reference to sqrt collect2: error: ld returned 1 exit status加个-lm就过了。这里的-l是链接某个库的开关m是库名链接器会自动把它补成libm.so动态或libm.a静态再去找。这套约定俗成的规则就是无数人被卡住的根源。1.3 报错长什么样取决于你处在哪个环境同样是找不到sqrt定义不同工具链给的措辞差别很大认清楚这些变体能省不少搜索时间。工具链/环境典型报错文本归属阶段GNU ld (Linux)undefined reference to sqrt链接期GNU ld (C 混合)undefined reference to sqrt或无修饰名链接期Clang lldundefined symbol: sqrt链接期MSVCerror LNK2019: 无法解析的外部符号 _sqrt链接期macOS ldUndefined symbols for architecture x86_64: _sqrt链接期动态加载 (dlopen)undefined symbol: sqrt运行期Qt/QML 插件加载Cannot load library ...: undefined symbol: sqrt运行期最后两行值得单独划重点运行期报undefined symbol跟链接期报undefined reference的成因完全不同处理手段也完全不同后面第 4 节会专门讲。还有个特别容易被忽略的细节某些平台上sqrt会因为编译器把字面量常量折叠掉压根不产生外部引用。比如你写的是double r sqrt(4.0);-O2下 GCC 直接算出2.0塞进代码里链接器根本看不到sqrt这个符号于是你忘了加 -lm 也没事。这时候如果你又加了一句sqrt(x)其中x来自scanf报错立刻回来。这也是为什么很多人会疑惑我昨天不写 -lm 也编译过了啊。2. 定位这类问题的三条硬核排查路径2.1 把真实的链接命令抓出来看构建系统一多你看到的报错往往是被裁剪过的摘要看不到全貌。第一步永远是让构建过程裸奔一次。Makefile 工程直接加make V1或者make VERBOSE1把完整命令打出来。CMake 工程更简单在构建目录里执行cmake --build . --verbose # 或者直接翻缓存里的命令 grep -r link.txt CMakeFiles/ | head cat CMakeFiles/your_target.dir/link.txt一眼扫过去重点看两件事命令行里有没有-lm如果有它出现在什么位置。这里有个新手最容易吃亏的坑我放在 3.2 节展开——-lm放错位置等于没加。如果你是手工编译那就把最终那条gcc/g命令复制下来一行行删参数重跑这叫二分法定位。删到某一刻错误消失你就知道是哪个参数在捣鬼。这招看着笨但在大型工程里比读文档快得多。2.2 用 nm 和 objdump 确认符号到底住在哪个库有人会问我怎么知道sqrt是在libm里而不是在libc里答案是直接问工具。# 看系统里有哪些 libm ldconfig -p | grep libm\. # 查某个库导出了哪些符号过滤出 sqrt 相关的 nm -D --defined-only /lib/x86_64-linux-gnu/libm.so.6 | grep -w sqrt # 输出大概是这种 # 000000000003f2a0 T sqrt # 000000000003f2b0 T sqrtf # 000000000003f2c0 T sqrtlT表示这个符号在代码段里被定义好了正是链接器要的东西。反过来你可以查自己的目标文件里有什么符号是未定义的nm -u build/CMakeFiles/demo.dir/main.c.o # U sqrt # U printfU就是 undefined。这个小技巧在排查一长串undefined reference时极其好用——你可以一眼看出到底缺了哪几个符号然后按类别去补库而不是每报一个错就加一个-l。我自己的习惯是遇到超过三个未定义符号先跑一遍nm -u汇总再看它们分别属于哪个库。像pow、exp、log10、hypot这种扎堆出现的基本都是同一个libm的问题而pthread_create要求加-pthreaddlopen要加-ldlcos也在这个表里出现的话那libm是跑不掉的。2.3 C 和 C 混编时命名修饰会骗你的眼睛这一条是纯 C 项目里不会遇到、但 C 工程里特别常见的陷阱。C 有函数重载所以编译器会给函数名做 name manglingsqrt可能变成_Z4sqrtd之类。如果你在.cpp里手写了一个用extern C声明的自己的数学封装库两边修饰规则不一致链接器照样找不到。判断方法很简单用nm看符号名有没有被修饰nm -D --defined-only /lib/x86_64-linux-gnu/libm.so.6 | grep -i sqrt # 显示的是干净的 sqrt说明 libm 是 C 接口libm本身是 C 接口不受影响。真正的坑在于你写了个mymath.c然后在main.cpp里#include mymath.h但头文件里没有extern C包裹结果mymath.o里定义的是summain.o里引用的是_Z3sumii链接器就报undefined reference to sum(int, int)。看不出跟sqrt有关但排查思路完全一样。顺带提一句头文件包含写法。math.h和cmath在 C 里的细微差别值得知道cmath会把符号放进std命名空间理论上应该写std::sqrt但各家实现为了兼容通常在全局命名空间也保留一份。这是一个典型的能跑但不够规范状态我个人的做法是 C 代码里统一用cmath加std::前缀避免在跨编译器移植时踩到某个实现不提供全局版本的奇怪情况。3. 各种构建方式下的具体修法抄作业级别3.1 命令行编译把 -lm 放在正确的位置最直白的方式就是在命令末尾补上-lmgcc main.c -o main -lm g main.cpp -o main -lm看起来加个后缀就完了但顺序这件事必须说清楚因为它是加了-lm还是报错的头号原因。GNU 链接器处理.a静态库和.so动态库的规则是从左到右、单向扫描。它维护一个当前还没被满足的符号集合遇到一个库就去这个库里找能不能满足扫完之后不会再回头。所以gcc -lm main.c -o main # 可能失败 gcc main.c -lm -o main # 可能也失败取决于 .o 是否在 -lm 之前 gcc main.c -o main -lm # 正确第二种之所以可能失败是因为main.c在-lm之后才被编译成.o-lm扫描的时候引用还没出现。第三种才是对的源文件先编译出包含未定义符号的.o然后-lm来补齐。那为什么有时候第一种也能过因为动态库经常被按需加载处理或者链接器版本对--as-needed的默认行为不一样。别赌这个老老实实把库放后面。3.2 库顺序与循环依赖--start-group 是兜底手段如果你的工程里有一堆自己写的静态库互相之间有依赖而且形成了环A 依赖 BB 又依赖 A单靠排序救不了。这时候用分组gcc main.o -Wl,--start-group -lA -lB -lC -Wl,--end-group -lm -o main--start-group和--end-group之间的库会被反复扫描直到不再产生新的未定义符号为止。代价是链接时间变长所以只在小范围用别整个工程一把梭。我在一个遗留项目里遇到过三个内部静态库绕成环的情况加了 group 之后链接时间从 0.3 秒涨到 1.2 秒但换来的是构建稳定这笔账很划算。与之相对的是-Wl,--no-as-needed。现代发行版默认开启--as-needed意思是这个库如果没用到就别记进 DT_NEEDED。正常情况下这是好事但如果你的库是通过dlopen运行时加载的链接器看不到直接引用就会误判成没用到把它摘掉结果跑起来就报undefined symbol。这种问题的症状是编译链接全过一运行就炸加个-Wl,--no-as-needed -lm就能验证是不是这个原因。不过要提醒一句这个开关最好不要无脑全局加它会让你的程序多出一堆无谓的运行时依赖。3.3 CMake别再用全局 link_libraries 了CMake 里写法有好几种从能跑就行到规范排个序# 能跑但污染全局大工程里迟早出事 link_libraries(m) # 好一点绑到具体目标上 target_link_libraries(myapp PRIVATE m) # 推荐同时显式声明头文件路径虽然 math.h 不需要但保持习惯 target_link_libraries(myapp PRIVATE m)为什么推荐target_link_libraries而不是link_libraries因为前者是目标属性只影响这一个可执行文件或库后者是目录属性会往下传染给所有子目录。一个几百人的团队里某个同事随手加一句link_libraries(pthread)半年后没人知道这个依赖从哪来想删都不敢删。CMake 会自动帮你处理-lm的位置问题它把库放在源文件对象之后所以你不用手动操心顺序。但如果你的目标是静态库需要留意这一点add_library(mymath STATIC mymath.c) target_link_libraries(mymath PUBLIC m) # 关键用 PUBLIC静态库本身不参与链接它只是把.o打包。如果这里用了PRIVATE链接信息不会传递给最终的可执行文件mymath里对sqrt的引用就没人管了报错照样出现。这个PUBLIC与PRIVATE的区别是 CMake 静态库场景下最常见的踩坑点我在 review 代码时见过太多次。还有一种情况是直接写全名target_link_libraries(myapp PRIVATE m) # 等价于下面这种更底层的写法一般不需要 target_link_libraries(myapp PRIVATE /usr/lib/x86_64-linux-gnu/libm.so)后者写死了路径换架构就废除非你有特殊理由否则不要这么干。3.4 qmake 与 Qt 工程LIBS 的加法用 qmake 的老项目修法是在.pro文件里加LIBS -lm如果只针对 Linux 平台用条件块更干净unix:!macx { LIBS -lm }macx要排除掉因为 macOS 上数学函数属于libSystem不需要也不存在单独的libm。你写了-lm在 macOS 上通常会得到一个library not found for -lm的新错误——恭喜你成功把一个问题换成了另一个问题。QML 项目还有一层很容易搞混的东西。QML 里的Math.sqrt()是 JavaScript 引擎内置的方法跟 C 的libm没有任何关系不会触发这个链接错误。会触发的是这种情况你在 C 侧写了Q_INVOKABLE的方法内部调用sqrt把它注册给 QML 使用。这时候报错发生在你的插件或可执行文件的链接阶段修法还是加-lm跟 QML 本身无关。4. qml 编译错误里的 sqrt两种完全不同的病4.1 编译期报错和运行期报错怎么分很多搜qml编译错误 sqrt的人实际上遇到的是运行时加载失败报错大概是这个样子QQmlApplicationEngine failed to load component qrc:/main.qml:5 module MyMathModule is not installed # 或者 Cannot load library /path/libmymathplugin.so: (libmymath.so: undefined symbol: sqrt)注意最后那半句undefined symbol: sqrt。这跟你前面看到的undefined reference to sqrt是两码事——前者发生在dlopen加载动态库的瞬间后者发生在链接生成动态库的瞬间。区别在于生成动态库时链接器默认允许未定义符号存在。这是为了支持插件架构让插件可以引用宿主程序提供的符号。所以libmymath.so在构建时可能一路绿灯直到 QML 引擎真的去加载它才开始抱怨找不到sqrt。这时候你再回头改构建脚本才有用但在运行时面板上瞎找是找不到答案的。4.2 三个层面的修法第一个层面最标准的做法是给这个库显式链接libmadd_library(mymathplugin SHARED mymath.cpp) target_link_libraries(mymathplugin PRIVATE Qt5::Qml Qt5::Core m)第二个层面如果你拿不到源码或者不方便重建可以在运行时用LD_PRELOAD先兜住LD_PRELOAD/lib/x86_64-linux-gnu/libm.so.6 qmlscene main.qml这只是应急手段用来验证确实是缺 libm 导致的不要写进生产启动脚本。第三个层面是检查DT_NEEDED有没有被误删。前面提到的--as-needed在 Qt 插件场景下特别容易出问题因为插件里的符号引用关系往往比较隐蔽# 看这个 .so 到底依赖了谁 readelf -d libmymathplugin.so | grep NEEDED如果输出里只有libQt5Core.so.5而没有libm.so.6那基本可以确认是被裁掉了。在 CMake 里可以通过链接选项关掉target_link_options(mymathplugin PRIVATE LINKER:--no-as-needed) target_link_libraries(mymathplugin PRIVATE m)4.3 QML 里一个真实的糊涂账有一种场景特别迷惑人主程序里用了sqrt一直正常加了 QML 界面之后突然报错于是很自然地认为是 QML 引入的。实际上更常见的原因是主程序的main.cpp里那个Math.sqrt被改成了 C 的std::sqrt或者本来被常量折叠掉的那句代码因为参数来源改成了从 QML 传进来的变量一下子产生了真实的符号引用。我的排查习惯是先执行一次全量的符号检查nm -u build/libmymathplugin.so | grep -i sqrt看到U sqrt就锁定了剩下的事情就是找到那个调用点在旁边确认它所在的 target 有没有链m。这套流程走下来通常十分钟内能解决比在 QML 层面反复改代码有效得多。5. 不同平台上的差异别拿一套经验到处套5.1 各平台 libm 的存在形态对照表这一点是我觉得最值得单独拿出来讲的因为它直接决定了要不要加-lm这个问题的答案因平台而异。平台libm 形态是否需要 -lm备注Linux (glibc)独立的 libm.so.6需要经典场景本文主要讨论对象Linux (musl)合并进 libc不需要加了也不报错Alpine 容器里很常见macOS属于 libSystem不需要加了反而报 library not foundWindows (MSVC)属于 ucrt不需要报错文本是 LNK2019Android (NDK)属于 libc不需要加了会提示找不到MinGW-w64通常有独立 libm需要交叉编译时常被漏掉这张表解释了一个常见困惑我在自己电脑上编译没问题丢到 CI 容器里就失败。如果你的 CI 用 Alpine 打底本地用 Ubuntu那-lm的差异就是罪魁祸首之一。反过来也一样本地的 Ubuntu 需要-lmAlpine 上加了-lm有时会报cannot find -lm虽然多数 musl 发行版会提供一个空的libm.a兼容层。5.2 交叉编译时的额外陷阱交叉编译环境下-lm找的不是宿主机的libm而是目标平台的 sysroot 里的那份。如果你手写的 Makefile 里把-L路径写成了宿主机的/usr/lib/x86_64-linux-gnu链接器可能真的找到了一份libm并且成功链上但那是给宿主机用的运行在目标板上就会崩。症状通常是启动时莫名其妙的Illegal instruction或Segmentation fault而不是链接错误。我的做法是在交叉编译的构建脚本里强制用--sysroot加上目标根文件系统并且用-v确认链接器实际搜索的路径aarch64-linux-gnu-gcc main.c -o main -lm -v 21 | grep -A2 library search path这行输出会明确告诉你它去了哪些目录找libm。养成看这个的习惯能省掉大量为什么目标板跑不起来的排查时间。5.3 关于 -lm 到底是链哪个文件还有个细节-lm究竟挑动态库还是静态库默认情况下只要libm.so存在链接器就优先选它。想强制静态链接数学库可以用gcc main.c -o main -Wl,-Bstatic -lm -Wl,-Bdynamic或者更常见的写法把整个程序都静态链接gcc main.c -o main -static -lm静态链接的好处是部署时不依赖目标机器的libm版本坑则是体积会涨而且如果程序里还用到了getaddrinfo这类依赖动态加载 NSS 的函数会有运行时告警。要不要静态链取决于你的部署场景我一般只有在做单文件绿色工具时才这么干。6. 常见问题速查与踩坑清单6.1 症状对照排查表症状最可能的原因处理动作只报 sqrt 未定义没链 libm命令行加 -lm 并把库放最后sqrt 和 pow、log 一起报同一原因多个符号加一次 -lm 就能全解加了 -lm 还是报错位置在 .o 之前把 -lm 移到命令最末本地过CI 挂基础镜像的 libc 不同检查是否 musl去掉/加上 -lm静态库内部引用失败CMake 用了 PRIVATE改成 PUBLIC 或 INTERFACE链接全过运行报 undefined symbol动态库被 --as-needed 裁掉检查 DT_NEEDED必要时 --no-as-neededQML 插件加载失败报 undefined symbol插件库没链 libm给插件 target 加 m昨天不报今天报常量折叠被打破检查 sqrt 的参数是否变成变量这张表里的每一行我都实际遇到过其中最阴的是最后一行。因为代码改动可能只发生在一个const double x 2.0;变成double x read_input();上看 diff 完全看不出跟sqrt有什么关系但链接结果就是变了。遇到这种情况先nm -u看符号比读代码快。6.2 几条用血换来的实操心得第一条心得是关于加 -lm 没反应的。新手常犯的错误是改错文件。比如用 CMake 构建却跑去改 Makefile或者改的是CMakeLists.txt但 CMake 没有重新配置因为缓存里还是旧内容。改完 CMakeLists 一定要删掉构建目录重新来一遍或者至少跑一次cmake .让它重新生成。我见过同事改了半小时CMakeLists.txt毫无进展最后发现构建目录是另一个路径下的老缓存。第二条心得关于报错信息的读法。undefined reference to sqrt后面通常跟着main.c:(.text0x1f)这样的信息这是引用发生的位置不是定义缺失的位置。很多人误以为这是库文件的位置跑去这个地址附近找那是代码段的偏移量跟库没关系。正确用法是拿这个行号回源码里定位调用点。第三条心得是关于-lm到底该加在哪个 target 上。大型工程里经常有十几个 target报错信息里不一定明说哪个 target 挂了。这时候看完整链接命令最直接CMake 的话去build/CMakeFiles/target.dir/link.txt找。别靠猜。第四条心得如果工程里同时存在-lm和-Wl,--no-as-needed要注意它们的相对顺序与作用范围。--no-as-needed是状态开关影响它之后所有的-l。所以想让它只作用于libm就得写成-Wl,--no-as-needed -lm -Wl,--as-needed把状态关回去否则后面所有库都会变成强制依赖运行时依赖列表会莫名其妙多出一堆东西。第五条心得也是最反直觉的一条有些情况下你不需要-lm但加了也无害glibc 平台。真正需要警惕的是反过来——某个平台上libm不存在你却因为习惯性加 -lm导致构建失败。所以在CMakeLists.txt或.pro里写平台条件比无脑加-lm要稳。我现在的习惯是默认不加等链接器报错了再加并且加上平台判断。这样构建脚本干净也避免了在 Alpine、macOS 上莫名其妙挂掉。最后提一个容易被忽略的排查工具ldd。链接成功但程序运行异常时ldd ./your_program | grep libm能确认运行时到底加载了哪个libm是不是从意料之外的路径来的。有过一次经历是某个环境变量LD_LIBRARY_PATH指向了一个老旧的第三方目录里面躺着一个自己编译的libm.so程序加载了它运行时数学计算结果全错。这种问题靠看代码是永远找不到的只有ldd能给你答案。
返回列表