ARTICLE DETAIL

资讯详情

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

undefined reference to sqrt:libm/-lm 解析

undefined reference to sqrt:libm/-lm 解析 链接阶段最后一行蹦出一句undefined reference to sqrt十有八九的人第一反应是回头翻math.h到底包含没包含、函数名是不是写成了sqr。我最早也这么干过对着头文件来回看了半天才发现——这句话压根不是编译器说的是链接器ld说的。它跟头文件、跟语法、跟函数名拼写一点关系都没有。真正的原因往往只有一句话你调用了数学库里的平方根函数但没有把数学库交给链接器。这篇东西就是把这件小事彻底说透。我会从为什么这条报错属于链接期而不是编译期讲起然后解释为什么同一份代码在有些机器上不报错、改一个常量就报错再给出一套能直接抄的复现步骤、三条查看链接链路的命令最后分构建体系命令行、Makefile、CMake、qmake/QML、交叉编译给出具体写法。适合刚接触 C/C 构建的同学也适合已经写了几年代码、但每次遇到这个错都靠随手补一句 -lm糊过去的人——后一种人其实更多。1. 先把这个错归对类它是链接错误不是编译错误1.1 gcc 一条命令背后的四段路很多人把gcc main.c -o main当成一个原子操作其实它内部拆成四步预处理cpp把宏展开、头文件塞进来编译cc1把 C 源码翻译成汇编汇编as把汇编翻成目标文件.o链接ld把一堆.o和库拼成一个可执行文件。关键点在于前三步都是针对单个源文件独立进行的第四步才把所有人拉到一起。undefined reference这个词组只可能出现在第四步。所以当你看到报错信息长成这样/usr/bin/ld: /tmp/cc8xK2p.o: in function main: main.c:(.text0x1f): undefined reference to sqrt collect2: error: ld returned 1 exit status开头那个/usr/bin/ld已经告诉你答案了。如果是编译期的问题报错会以main.c:5:10: error:这种格式出现带上文件名、行号和列号甚至还会贴一段源码加个波浪线。链接期没有行号因为它面对的是一堆二进制目标文件早就不知道你的sqrt写在第几行了。理解这一点非常实际。你去看头文件包含、去检查分号、去确认sqrt拼写全是在错误的战场上浪费时间。正确的第一反应是谁提供了这个符号我把它交给链接器了吗1.2 头文件管形状库管实体这里要拆开一个新手最容易混淆的概念。#include math.h做的事情是把一行声明塞进你的翻译单元extern double sqrt(double __x);这行东西的作用只有一个——让编译器知道有一个叫sqrt的函数接收一个double返回一个double。有了这个形状信息编译器就能正确地生成调用代码把参数放进寄存器然后留一条跳转指令跳向符号sqrt。但函数体在哪在libm里。头文件从来不含实现它只是一张名片。打个比方math.h是电话簿上的一行字张三138xxxxxxxxlibm是真正接电话的那个人。你翻了电话簿知道了号码格式是合法的但电话打出去没人接——这就是undefined reference。包含头文件成功只能证明格式对完全不能证明人在。所以这两件事是正交的math.h缺失 → 编译期报implicit declaration of function sqrtlibm没链接 → 链接期报undefined reference to sqrt。同一句代码可以只中一个也可以两个都中。分清楚报错来自哪一段排查效率能差出十几倍。1.3 链接器为什么不说你要加 -lm有人会问链接器既然知道符号名叫sqrt难道不知道它属于libm吗为什么不直接提示答案很朴素链接器真的不知道。符号和库之间的对应关系不存在于链接器内部的知识库里。同一台机器上libm.so里有sqrt你自己写的libmine.so里也可以有一个sqrt链接器凭什么替你决定用哪个它只会老老实实地报告在所有你给我的输入里我找不到sqrt的定义。这个设计其实是对的做法。如果链接器自作聪明去猜那符号冲突、版本覆盖这类问题会变得完全不可控。代价就是——加库这件事必须由你显式做。2. 为什么有时候不加 -lm 也能编过2.1 常量折叠改一行代码报错就消失了这是最让人抓狂的场景。先看第一段代码#include math.h #include stdio.h int main(void) { printf(%f\n, sqrt(2.0)); return 0; }gcc t.c -o t直接过连-lm都不用加。你再改成这样#include math.h #include stdio.h int main(void) { double x 2.0; printf(%f\n, sqrt(x)); return 0; }gcc t.c -o t立刻报undefined reference to sqrt。为什么会这样因为 GCC 有一个叫常量折叠的优化当sqrt的参数是编译期就能确定的字面量时编译器干脆在编译阶段把结果算出来直接写进目标文件。第一段代码里sqrt(2.0)直接被替换成1.414214这个浮点常量目标文件里对这个符号没有任何引用链接器自然无事可做。第二段里x是个运行期变量编译器不知道它的值哪怕它上一行刚赋过值只要没开优化到极致也不行只能老老实实生成一条call sqrt指令。这条指令在目标文件中留下一个未解析的符号链接器必须找到定义找不到就报错。我见过不少人因为刚才还能编过我就加了个变量怎么就崩了开始怀疑自己的逻辑写错了。其实逻辑一点没错只是常量折叠消失了。2.2 GCC 内建函数在这个过程里的角色GCC 会把一批数学函数识别为内建函数builtinsqrt、sin、cos、pow、fabs都是。识别成内建之后编译器在优化阶段可以做的事情就多了常数折叠、指令替换、参数检查。想确认编译器到底有没有真的生成对sqrt的调用有几个办法gcc -O2 -S t.c生成汇编grep call.*sqrt看有没有这条调用gcc -c t.c nm -u t.o输出里如果有U sqrt说明这个符号没有被解析链接时就必须靠库来填加-fno-builtin或更精确的-fno-builtin-sqrt强制禁用内建让编译器老老实实按普通函数调用处理。这里有个实操上的小坑-fno-builtin写在源码文件前面和后面效果可能一样但在某些老的构建脚本里如果它出现在-O2之前是会被后来的优化选项部分覆盖的。我一般把它直接跟在目标文件后面写图个心安。2.3 顺带说一句能不能彻底不依赖 libm能。加-fno-math-errno-ffast-math会把它包含进去之后GCC 被允许把sqrt(x)编译成一条硬件平方根指令。在 x86-64 上就是sqrtsd一条指令搞定根本不需要调用函数也就不需要链接libm。但代价要看清楚-fno-math-errno告诉编译器你不需要按 C 标准的要求在出问题时设置errno-ffast-math更激进还会放弃 NaN/Inf 的部分特殊值语义、改变浮点结合律。如果你在做严格的数值计算、或者依赖边界值行为的逻辑开这个就是给自己埋雷。我的原则是性能不敏感的代码一律不开性能敏感的代码开了之后必须补一套边界用例测试。为了省一句-lm去改浮点语义这笔账怎么算都不划算。3. 三条命令把链接链路钉死3.1 最小复现先建立确定的事实新建三个文件分别是a.c、b.c、c.c内容就是上面那个sqrt(x)版本只是变量名不同。然后逐个编译gcc a.c -o a # 报 undefined reference to sqrt gcc a.c -lm -o a # 还是可能报见第 4 节 gcc a.c -o a -lm # 过注意第三行和第二行的差别——-lm的位置。这是第 4 节要展开的内容。先在命令行上把这两种写法都试一遍亲手确认一下顺序确实会影响结果比看十篇文章都管用。3.2-Wl,-y让链接器自己说清楚ld有一个非常好用但很少有人知道的选项-y symbol作用是打印所有引用了这个符号的地方以及所有定义了它的地方。通过gcc转发给链接器要写成-Wl,-y,sqrtgcc a.c -o a -lm -Wl,-y,sqrt输出里会出现类似这样的行a.o: reference to sqrt /lib/x86_64-linux-gnu/libm.so.6: definition of sqrt第一行告诉你谁在用第二行告诉你谁提供的。把它和报错信息放在一起看缺失这件事立刻就从抽象变具体了。如果第二行压根不出现那就是库没链接上方向明确。这个技巧我第一次用的时候有种原来链接器早就知道只是没主动说的感觉。它比在构建脚本里瞎试快得多。3.3nm、readelf、ldd三件套想再往下挖一层配合下面这几条nm -u a.o # 列出目标文件里所有未定义符号 nm -D /lib/x86_64-linux-gnu/libm.so.6 | grep sqrt readelf -d a | grep NEEDED # 看最终可执行文件依赖了哪些 .so ldd a # 运行时依赖列表第一条命令是排查的起点。如果nm -u a.o里出现了U sqrt说明链接必须解决它如果压根没出现说明编译器内联掉了那报错一定是别的原因。第二条确认库本身确实提供这个符号。某些精简过的嵌入式根文件系统里libm被裁掉了部分函数这时候nm一下就能看出来。readelf -d和ldd用在已经编过了但行为不对的场景比如你加了-lm但程序跑起来还是异常可以确认NEEDED里到底有没有libm.so.6。这里有个细节如果你用-static链接readelf -d什么都不会输出因为根本没有动态段别被这个吓到。3.4 看一眼真实的链接命令还有一个终极手段让gcc把它实际执行的链接命令打出来。gcc -v a.c -o a 21 | tail -3输出的最后几行会是一条完整的collect2调用里面包含了所有-l选项和它们的顺序。构建系统里那些我明明配置了的问题看这条真实命令往往一眼就破。CMake、qmake 这类工具生成的链接命令和你在配置文件里写的意图之间隔着一层翻译出偏差是常事。4. 加了 -lm 还报错问题基本都在顺序上4.1 链接器是从左往右扫的传统 Unix 链接器的扫描模型是这样的从左到右依次处理命令行上的每个输入遇到未定义符号就记下来遇到能提供符号的目标就填坑扫完之后还有没填上的就报错。这个规则对静态库尤其致命。静态库.a本质上是一堆.o打包链接器只会从里面挑出当前确实需要的那些成员。如果你把库写在需要它的目标文件前面链接器扫到库的时候符号还没被引用它认为这个库没用直接跳过等到后面扫到.o、发现缺符号时库已经过去了不会回头。正确顺序只有一条引用者在前提供者在后。gcc main.o -lm -o main # 建议的写法 gcc -lm main.o -o main # 静态链接场景下会出问题这里要泼一盆冷水也是很多网上教程说错的地方在动态链接场景下gcc -lm main.c往往是能编过的。因为共享库的符号解析是全局的libm.so一旦被记入链接链接器最终会把它放进NEEDED里符号查找不受扫描顺序限制。所以顺序问题这个坑不会在你随手写的 hello world 里暴露出来。真正会被它坑到的场景有三个都很常见用-static做静态链接链接的是libm.a扫描顺序变成硬约束嵌入式工具链里只提供静态库的libm.a链接你自己写的静态库且库之间还有依赖关系。第三种情况最阴。比如libA.a里的函数调用了libB.a里的函数命令行必须写成-lA -lB。写成-lB -lA就会报一个看起来毫无道理的undefined reference。这个规则和sqrt没直接关系但同一个道理遇到库与库之间的符号找不到时第一件事就是检查顺序。4.2--as-needed会把顺序问题放大很多主流发行版的 GCC 默认带了-Wl,--as-needed。它的含义是只有当某个库真的提供了被用到的符号时才把它写进最终可执行文件的NEEDED列表。这个开关的初衷是减小依赖、加快启动副作用是让顺序错误更难被发现也更难排查。顺序错了链接器认为这个库没用上于是不写进NEEDED程序编过了运行时报找不到符号或者干脆行为异常。想临时关掉看看效果可以加-Wl,--no-as-needed再链一次。如果加上就正常了基本能确认是顺序或者--as-needed判断的问题。4.3 静态库之间的循环依赖如果两个静态库互相调用单纯调顺序已经救不了了得上分组gcc main.o -Wl,--start-group -lA -lB -Wl,--end-group -o main--start-group和--end-group之间的库会被链接器反复扫描直到没有新的符号可以解析为止。代价是链接变慢所以只在真的需要时用。我一般会在注释里写清楚为什么加了这组标记不然接手的人过半年看到会一头雾水。5. 不同构建体系下该怎么写5.1 命令行与 Makefile命令行最简单把-lm追加到所有目标文件后面。Makefile 里要注意区分变量。GNU make 的内置链接规则大致是这样的形式$(CC) $(LDFLAGS) $(TARGET_ARCH) $^ $(LOADLIBES) $(LDLIBS) -o $顺序上$^所有依赖也就是.o文件在LDLIBS前面所以库应该写进LDLIBS而不是LDFLAGS。LDFLAGS出现在最前面写在那里就正好踩了第 4 节的顺序坑。CC gcc CFLAGS -O2 -Wall LDLIBS -lm calc: main.o solver.o $(CC) $(CFLAGS) $^ $(LDLIBS) -o $如果你的项目里出现了sqrt、pow、log、atan2一起报错的情况别一个一个加它们全在libm里一句-lm解决。这是判断是不是缺 libm的一个快速信号报错的符号成组出现且都是数学函数。5.2 CMake别在 Windows 上写死 mCMake 里最直接的写法是target_link_libraries(myapp PRIVATE m)但这句在 Windows 上会出问题——MSVC 工具链里没有独立的libm数学函数在 C 运行时里写m会找不到库。跨平台项目要做条件判断include(CheckLibraryExists) check_library_exists(m sqrt HAVE_LIB_M) if(HAVE_LIB_M) target_link_libraries(myapp PRIVATE m) endif()或者用find_libraryfind_library(MATH_LIBRARY m) if(MATH_LIBRARY) target_link_libraries(myapp PRIVATE ${MATH_LIBRARY}) endif()这两种写法的差别值得说一句。check_library_exists是探测这个库里有没有这个符号语义更准确find_library只是找库文件存不存在理论上可能出现文件存在但符号缺失的情况裁剪过的嵌入式根文件系统。我个人偏好check_library_exists多花几秒配置时间换来的是更确定的结论。还有一个 CMake 层面的经验链接关系尽量用PRIVATE/PUBLIC表达清楚。如果sqrt的调用发生在某个内部静态库的源文件里那m应该PRIVATE链在那个静态库上而不是链在最终可执行文件上。前者是谁用谁负责后者容易随着重构漏掉。我在几个项目里都遇到过重构完之后某个子模块编不过根因就是链接依赖挂错了层级。5.3 qmake / QML 工程里的坑Qt 项目里出现undefined reference to sqrt通常有两种来路一是你在 C 侧自己加了一个.c文件或引用了第三方 C 库二是某些 Qt 模块的底层实现间接用到了平方根而工具链配置不完整。qmake 里加库要用LIBSLIBS -lm不要用QMAKE_LFLAGS -lm。这是我在真实项目里踩过的坑值得展开说。QMAKE_LFLAGS里的内容会被拼接到链接命令的靠前位置而LIBS里的内容会被拼到目标文件和 Qt 库之后。qmake 生成的典型链接命令长这样简化过g QMAKE_LFLAGS -o app main.o widget.o -L... -lQt5Core -lQt5Gui ... LIBS把-lm塞进QMAKE_LFLAGS它就跑到main.o前面去了。在静态链接或者某些交叉工具链下这个位置会导致符号找不到。而放进LIBS它稳稳地待在最后符合引用者在前、提供者在后的规则。这个问题之所以阴是因为它在你的开发机上可能完全不报错动态链接、--no-as-needed一到 CI 或者交叉编译环境就炸。我当时排查了两天才定位到是变量选错了现在写 qmake 工程一律只用LIBS。另外提一句 QML 相关的报错。如果你看到的是 QML 运行时的类型错误或者qmlscene加载失败那和sqrt无关别混为一谈。undefined reference to sqrt一定发生在编译链接阶段和 QML 引擎没有关系QML 只是恰好在同一个工程的构建流程里。5.4 交叉编译与嵌入式工具链这一块的坑比本地编译多得多因为工具链的构成千差万别。第一种情况是工具链只提供静态库。很多厂商提供的 ARM 工具链里libm.a是主力甚至没有libm.so。这时候顺序要求就变成硬性的-lm必须写在所有.o之后。第二种情况是 sysroot 配错链接器跑到宿主机的/usr/lib里去找libm。表现是链接能过但程序在目标板上跑不起来架构不匹配或者报一堆奇怪的符号版本问题。排查时可以看链接命令里-L的路径确认指向的是工具链的 sysroot 而不是宿主机目录。第三种是-nostdlib/-nodefaultlibs场景。这两个选项会阻止编译器自动加入标准库和启动文件常见于写裸机程序或者自己实现运行时的时候。这种情况下libm也不会被自动加必须手动链接而且顺序和启动文件的关系也要理清楚。第四种是 newlib 之类的精简 C 库。它的libm同样是独立的一份需要显式-lm。有些厂商在工具链的 spec 文件里做了预置换个版本就失效了所以别指望它。Android NDK 项目里libm和liblog都是需要显式声明的。CMake 写法如下target_link_libraries(native-lib PRIVATE m log)m代表数学库log是日志库。这两个是 NDK 开发里最常被漏掉的一对。6. 一张对照表几种典型场景怎么处理6.1 现象、根因、处理对照报错现象根因处理方式只报sqrt命令里没有-lm缺数学库在命令末尾加-lm一次性报sqrt、pow、log、atan2同一个原因缺libm一句-lm全覆盖加了-lm仍报错且用了-static静态库扫描顺序把-lm调到所有.o之后qmake 工程里报错QMAKE_LFLAGS里写过-lm变量选错改用LIBS -lm常量改成了变量之后开始报错常量折叠消失正常现象补上-lmCMake 工程Linux 正常、Windows 报错平台差异用check_library_exists条件链接交叉编译能过但板子上跑不起来sysroot 或架构不对检查-L指向和工具链前缀报cannot find -lm库路径不对或库名不对确认工具链里libm的实际名称6.2 几个容易走偏的方向去改头文件包含路径。math.h只要能被找到一次就够改-I对这个错误毫无帮助。以为要装什么开发包。libm从来不单独发布它和 C 运行库是一起装的。系统上找不到libm通常意味着 C 库本身缺失那是另一个量级的问题。在 C 里给它套extern C。sqrt本来就是 C 符号cmath已经处理好了名字修饰的事再套一层没有任何作用反而可能因为嵌套写法出错。认为加-lm会影响性能。加库只是让链接器知道去哪里找符号对生成的代码没有直接影响。真正影响性能的是-O级别和浮点选项。把-lm当成万能补丁到处加。如果某个.c文件根本没用到数学函数给它链接libm只是徒增依赖。我在一个项目里见过十几个模块全都挂-lm最后谁也不知道到底是哪个模块真的需要它。6.3 我自己的处理习惯在项目最底层的构建脚本里做一次平台判断把数学库抽象成一个目标或者变量业务代码只管引用不在各处散落-lm。报错符号成组出现时先按缺库而不是缺代码来假设nm -u一条命令就能验证。遇到改了一行就报错的情况第一反应是常量折叠而不是逻辑问题。静态链接的项目单独跑一轮完整构建因为顺序问题只在静态链接下才会暴露。7. 写在经验之后undefined reference to sqrt这个问题本身只有一行解法但它牵扯出来的东西比看上去多得多编译期和链接期的分界、头文件与库的职责划分、常量折叠对内联的影响、链接器从左到右的扫描模型、--as-needed的副作用、各构建工具对命令行顺序的控制能力。我在实际项目里判断一个人对构建系统的熟悉程度有个土办法问他为什么gcc main.c -lm和gcc -lm main.c有时候都能过。能说清楚动态链接和静态链接差异的人大概率也踩过交叉编译的坑、也知道 CMake 的链接依赖该怎么分层。最后再分享一个我用了好几年的小习惯凡是在命令行上手试出来的链接参数必须在构建脚本里原样复现一次并且验证生成的链接命令。用gcc -v或cmake --build . --verbose把真实命令行打出来看一眼比在配置文件里反复猜快得多。链接问题几乎从来不是想不明白而是没看到真正的命令行长什么样。把那条命令看清楚剩下的事情基本就只剩填空了。
返回列表