☰
httpd源码编译undefined reference?先查expat开发包
2026/10/19 9:47:39 网站建设 项目流程

“undefined reference to `XML_GetErrorCode'”——如果你是在源码编译httpd时看到这行报错的,说明你已经走过了configure和一大半make,就差最后这几步了。我第一次遇到这个问题,是在一个老旧的服务器环境上折腾反向代理模块,当时第一反应是“expat不是早就装了吗”,然后花了一整个晚上才搞明白,链接阶段的报错和“系统里装了某个库”完全是两码事。这篇文章就把这个报错的来龙去脉拆干净,包括链接原理、expat开发包的坑、configure的检测逻辑、链接顺序问题,以及一份可以直接照着走的排查清单。适合所有自己从源码编译httpd,尤其是需要mod_proxy_html、mod_xml2enc这些代理模块的朋友。

1. “undefined reference”这句话到底在说什么

1.1 编译的完整流水线:编译和链接不是一回事

先把这个报错的本质说清楚。一个C/C++项目从源码变成可执行文件,中间要经过预处理、编译、汇编、链接四个阶段。预处理负责处理#include和宏,编译把源码翻译成汇编,汇编把汇编翻译成目标文件(.o),链接则把一堆目标文件和库文件拼成一个完整的可执行文件或共享库。

undefined reference这个错误不在编译阶段,而是在最后的链接阶段抛出来的。这意味着你的源码语法没问题,头文件也找到了,每个.c文件都编译成功了,但在“拼装”这一步,链接器发现有一处代码引用了一个函数/变量的名字,却在整个输入集合里找不到这个符号的实现。

举个生活化的例子:你写了一篇稿子,正文里引用了一张配图,标注“见图3”。等到排版印刷那一刻,排版师傅翻遍所有文件都没找到那张图,于是书印不出来。你的源码就是稿子,链接器就是排版师傅,XML_GetErrorCode就是那张缺失的配图。编译阶段只是把文字写好,链接阶段才负责把图真的放进去。

所以看到undefined reference,第一反应应该是:某个符号有声明(头文件里有),但找不到定义(对应的库没有参与链接,或者链接的库里没有这个函数)。绝大多数情况下,不是你的代码写错了,而是库的环节出了问题。

1.2 XML_GetErrorCode是从哪冒出来的,httpd为什么非要找它

XML_GetErrorCode是expat这个XML解析库的公开API。expat是一个用C写的流式XML解析器,很多项目都在用,比如Python的pyexpat、Git的部分功能、以及Apache httpd的代理模块。

httpd在编译时默认不会强制依赖expat,但只要你启用了mod_proxy_html和mod_xml2enc这两个模块,情况就不一样了。mod_proxy_html负责把反向代理返回的HTML内容里的链接改写掉,它必须对HTML/XML做真正的语法解析,而不是简单的正则替换,所以它内部直接调用了expat的解析函数。mod_proxy_html在处理一段不完整或者畸形的HTML片段时,需要靠XML_GetErrorCode拿到具体的错误码,判断当前解析失败是标签不匹配、编码错误还是其他原因,再决定是丢弃这段还是继续处理。

因此,编译httpd时如果你执行了类似这样的configure:

./configure --prefix=/opt/httpd \ --enable-so \ --enable-proxy \ --enable-proxy-http \ --enable-proxy-html

那么--enable-proxy-html就会让configure去检测expat。检测不到,后续make阶段就会在mod_proxy_html相关目标文件链接时报出本文标题里的错误。理解了这条依赖链,排查方向就非常明确:httpd本身没问题,是expat没喂到链接器嘴里。

2. expat开发包:最常被忽视的“运行库 vs 链接库”之分

2.1 为什么系统里明明有expat,链接器还是说找不到

这是我在实际排查中遇到最多的误判。很多人跑到服务器上执行ldconfig -p | grep expat,看到libexpat.so.1安安静静躺在输出里,然后就很困惑:库明明在啊,怎么还说找不到?

问题就在这里。Linux系统里的动态库通常分成两个角色:运行库和开发库(链接库)。运行库是程序启动后加载用的,文件名一般带版本号,比如libexpat.so.1,它只需要保证程序跑起来时能找到对应的函数实现。而编译链接时,链接器需要的不是libexpat.so.1,而是一个不带版本号的符号链接文件libexpat.so。

这个符号链接通常不是库本身提供的,而是由开发包安装的。Debian/Ubuntu系里,运行库来自libexpat1,开发包叫libexpat1-dev;RHEL/CentOS系里,运行库来自expat,开发包叫expat-devel。很多精简安装的服务器只装了运行库,文件在/usr/lib64/libexpat.so.1.6.7,但没有/usr/lib64/libexpat.so这个链接文件。链接器去找-lexpat时,真正找的就是libexpat.so,找不到自然报undefined reference。

判断方法很简单:

ls -l /usr/lib*/libexpat.so*

正常情况下你会看到两个东西:

lrwxrwxrwx libexpat.so -> libexpat.so.1.6.7 -rwxr-xr-x libexpat.so.1.6.7

如果只有第二行,没有第一行那个软链接,说明开发包装没装到位。

2.2 正确安装开发包的姿势和验证手段

按系统类型装开发包:

# Debian / Ubuntu apt-get install -y libexpat1-dev # RHEL / CentOS / RockyLinux yum install -y expat-devel

装完以后,验证三件事:

  1. 符号链接存在:ls -l /usr/lib*/libexpat.so能看到不带版本号的软链接。
  2. 符号表里有XML_GetErrorCode:nm -D /usr/lib/libexpat.so | grep XML_GetErrorCode。
  3. 头文件存在:ls /usr/include/expat.h。

第2步容易被忽略,但它其实是最硬核的验证方式。nm -D列出动态库导出的符号表,如果函数确实在,你会看到:

0000000000025f30 T XML_GetErrorCode

如果这一步显示没有这个符号,那问题就不是“没装开发包”,而是expat版本太老。当然这种情况很少见,因为XML_GetErrorCode从expat 1.x早期就存在了,但只要用nm验证一下,整个思路就闭环了——我们找的不是“expat库在不在”,而是“这个库到底导没导出我们需要的符号”。

我在操作中的一个习惯:装完开发包后别急着重新configure,先cd到源码目录执行make distclean(或者干脆删掉源码目录重新解压),再重新configure。因为之前失败的configure生成了一些缓存和Makefile,直接原地重跑configure,有些变量并不会被刷新,踩过好几次这种“装完依赖重configure还是报错”的坑。

3. configure阶段:httpd是怎么决定链接expat的

3.1 configure检测expat的内部机制

httpd的configure脚本在检测expat时,本质上是autoconf的AC_CHECK_LIB那一套流程。它会生成一个很小的C测试程序,在源码里调用XML_ParserCreate、XML_GetErrorCode等函数,然后尝试用-lexpat把这个测试程序链接成可执行文件。如果这一步成功,configure就把expat标记为“可用”,并在后续生成Makefile时把-lexpat写进相关模块的链接参数;如果失败,configure会继续跑,但不会报硬错误,只是默默地标记为“不可用”。

这就是为什么很多人configure全程没看到红色的error,一路“yes yes yes”下来,结果make的时候炸了。configure对很多可选依赖的态度是“有就用,没有就放弃这个功能”,而不是“没有就立刻失败”。只有当你明确用--enable-proxy-html强制开启某个模块时,依赖缺失才会以“make阶段报错”的形式秋后算账。

所以configure阶段有一个很关键的动作:用--with-expat明确告诉它expat的位置。

./configure \ --prefix=/opt/httpd \ --enable-so \ --enable-proxy \ --enable-proxy-http \ --enable-proxy-html \ --with-expat=/usr

--with-expat=/usr表示expat的安装根目录是/usr,configure会去/usr/include找expat.h,去/usr/lib或/usr/lib64找libexpat.so。如果你的expat是自己编译安装在/usr/local的,就写--with-expat=/usr/local。

3.2 从config.log里挖出真正失败原因

如果configure检测还是失败,或者你想确认到底卡在哪一步,直接翻config.log。这是autoconf生成的日志文件,记录了每个检测步骤的具体执行命令和输出,比屏幕上那几行“checking ... no”详细一百倍。

排查命令:

grep -i "expat" config.log

你会看到类似这样的记录:

configure: checking for XML_ParserCreate in -lexpat configure: gcc -o conftest conftest.c -lexpat ... /usr/bin/ld: cannot find -lexpat collect2: error: ld returned 1 exit status configure: result: no

cannot find -lexpat说明链接器连库文件都没找到,这就是“开发包没装”的直接证据。如果你看到的是cannot find -lz或者-liconv之类的其他库,说明这台机器的依赖环境整体比较骨感,建议先把常用的基础开发包一次性补齐。

这里有个经验之谈:很多教程会让你先跑一个prefix固定的configure,失败后改参数再跑一个。但configure生成的config.status、config.cache以及各种Makefile是会自动复用的,你第二次跑configure时,它可能根本就不会重新检测expat,直接用旧的“no”结果。所以一旦你修改了任何依赖库、头文件路径、编译参数,第一件事就是清理现场。最省心的方式:把源码目录整个删掉,重新解压,重新configure。别嫌慢,源码编译最大的成本不是编译时间,而是重复踩同一个坑的时间。

4. 链接顺序问题:装了库、configure过了,make还是报错

4.1 链接器“从左到右”的脾气

有一种更气人的情况:expat开发包装了,configure也明确检测到expat了,make时却依然在原位置报undefined reference to 'XML_GetErrorCode'。我第一次遇到时百思不解,直到手动复现了那条实际的链接命令才看清真相:库放错了位置,链接顺序不对。

在Linux下,gcc/ld处理库的顺序是从左往右的。当一个目标文件引用了某个符号,链接器是按命令行顺序一个个扫描库文件,把「当前未解析的符号」和「库中定义的符号」做匹配。如果引用XML_GetErrorCode的目标文件排在-lexpat后面,链接器在扫描目标文件时还没意识到需要这个符号,扫描-lexpat时就不会把它纳入考虑;等扫描到目标文件时,-lexpat已经翻过页了,于是符号始终没法被解析。

标准做法是“目标文件在前,库文件在后”:

gcc -o mod_proxy_html.la mod_proxy_html.o -lexpat

如果错误地写成:

gcc -o mod_proxy_html.la -lexpat mod_proxy_html.o

在部分老版本链接器上就会复现undefined reference。

4.2 在httpd的构建环境里怎么定位和调整

httpd的构建不是直接手写gcc命令,而是通过libtool包装了一层。libtool会读取模块的.la文件,自动处理依赖库的顺序。正常情况下不太会出问题,但如果你在前面的configure里给LDFLAGS或LIBS变量加了东西,或者系统里存在多个版本的expat导致libtool选错了库,顺序就容易被搅乱。

定位实际链接命令的方法:

make -n 2>/dev/null | grep -i "mod_proxy_html"

make -n只打印将要执行的命令而不真正执行,这是排查链接问题最实用的手段之一。你会看到类似这样的输出:

/bin/sh /opt/httpd/srclib/apr/libtool --mode=link gcc \ -o mod_proxy_html.la mod_proxy_html.lo \ -rpath /opt/httpd/modules/proxy \ -module -avoid-version \ -lexpat

注意-lexpat出现的位置。如果它出现在依赖它的.lo文件前面,就把EXTRA_LDFLAGS或AP_LIBS里关于expat的部分调整一下,或者更简单的办法,检查--with-expat指向的目录里是不是存在一份很老或者不完整的libexpat.so,把这份干扰库挪走,让链接器只看到正确的那一份。

另外,不要忽略mod_xml2enc。它和mod_proxy_html是配套的模块,也依赖expat。有时候你处理完了mod_proxy_html的报错,紧接着又看到mod_xml2enc.la报同样的undefined reference,别慌,这不是新问题,是同一个根因在另一个模块上的投影。

5. 边角暗坑:多版本expat、32/64位、config缓存残留

5.1 系统里装了两份expat时,configure可能选错那个

这是我最想重点写的一个场景。很多服务器上,操作系统自带一份expat,运维后来又在/usr/local手动编译了一份新版expat。于是你的/usr/lib64里有一个,/usr/local/lib里也有一个,两个版本都提供libexpat.so。

当你指定--with-expat=/usr/local时,configure用的头文件和链接库都来自/usr/local,看起来一切正常。但编译时系统环境变量里可能还残存着LDFLAGS=-L/usr/lib64之类的设置,导致libtool在链接时先扫描了/usr/lib64,链接到了老版本的libexpat.so。如果老版本符号表不全,就会出现在“configure检测成功、make链接失败”的诡异情况。

排查方法还是一招:make -n看实际命令,然后在实际命令里手工加-Wl,-t或者直接把-L参数换成-v看搜索路径,哪份库被选中一目了然。嫌麻烦的话,最直接的手段是只保留一份expat,把另一份的libexpat.so软链接暂时改名,比如mv /usr/local/lib/libexpat.so /usr/local/lib/libexpat.so.bak,再重新编译。这样做虽然粗暴,但能最快确认是不是多版本干扰。

5.2 32位和64位架构不匹配,符号表里什么都找不到

如果你的系统是64位的,但误装了一份32位的expat开发包,也会出现undefined reference。因为链接器明显是64位的,它去扫描32位的libexpat.so时,会提示“架构不匹配”,或者更隐晦地直接忽略掉这份库。这个时候nm -D看符号表其实看得到符号,但链接器就是不用,因为位数对不上。

验证方式非常直接:

file /usr/lib/libexpat.so

输出如果是ELF 64-bit,而你的编译目标是32位(或者反过来),就说明开发包架构不对。换源安装对应位数的开发包即可。在64位系统上,这通常表现为/usr/lib和/usr/lib64同时存在,且一个下的是32位库、一个是64位库,包的依赖关系稍微一乱就会出错。

5.3 一个很容易被忽略的缓存文件:config.cache

有些人在configure时用了-C参数(即--config-cache),这会生成一个config.cache文件,把前面检测的结果缓存下来。好处是后续重新configure很快,坏处是依赖库变化后它不会自动失效。我遇到过好几次这样的状况:明明装了expat-devel,重新跑configure时还是说找不到expat,最后发现是config.cache里缓存了第一次失败的NO结果。

处理办法:要么跑configure之前把它删掉,要么干脆按前面说的,清理目录重新解压。如果不想重新解压,至少执行:

make distclean

然后确认config.cache、config.status都被清理干净了,再重新configure。实际上我自己现在养成了一个习惯:源码编译的项目,只要准备换参数重跑configure,一律把源码目录当一次性的用,坏了就删了重新解压,绝不吝啬那两分钟的解压时间。

6. 一份可以直接照着走的完整排查清单

6.1 按优先级排列的检查步骤

这一节写给时间紧、已经被报错搞烦了的朋友。不用从头看原理,按下面的顺序一项项操作:

步骤检查项操作判断标准
1确认报错阶段回看终端输出configure阶段报错还是make阶段报错
2安装expat开发包apt-get install libexpat1-dev或yum install expat-devel无报错
3确认软链接存在ls -l /usr/lib*/libexpat.so能看到不带版本号的libexpat.so
4确认符号存在nm -D /usr/lib/libexpat.so | grep XML_GetErrorCode输出包含T XML_GetErrorCode
5确认架构匹配file /usr/lib/libexpat.so位数与系统一致
6清理编译现场make distclean或删除源码目录重新解压无残留
7重新configure加--with-expat=/usr,确保开启--enable-proxy-html输出里没有expat相关no
8查看实际链接命令make -n 2>/dev/null | grep mod_proxy_html-lexpat在目标文件之后
9正式编译make -j$(nproc)链接通过

注意第8步很关键,不要跳。我自己在“装好了开发包、清理了缓存、configure全绿”之后,如果make还是报错,最后基本都是在这一步发现链接参数有问题的。

6.2 修复之后怎么验证真的没问题

编译通过不代表万事大吉,这是源码编译项目的通病:链接成功只能说明符号找到了,不代表运行时加载没问题。编译阶段链接的是libexpat.so(软链接),运行阶段加载的是libexpat.so.1(真实库文件)。如果开发包装了但运行库没有,或者运行库版本与编译时不一致,会在启动时冒出error while loading shared libraries: libexpat.so.1之类的错误。

所以安装完httpd后,建议按下面几步做最终验证:

# 安装 make install # 检查配置文件语法 /opt/httpd/bin/apachectl -t # 确认模块被加载 /opt/httpd/bin/httpd -M | grep proxy_html

httpd -M的输出里能看到proxy_html_module (shared),就说明这个模块被编译并加载成功了。如果有listdeps之类的工具,也可以用来确认mod_proxy_html.so依赖的动态库列表:

ldd /opt/httpd/modules/mod_proxy_html.so

看到libexpat.so.1 => /usr/lib64/libexpat.so.1这一行,才算真正闭环。

最后分享一个排查这类问题的基本思路:先分阶段,再分模块,最后分库。先确定错误来自configure还是make,再确定是全项目报错还是某个模块报错,最后再确认是哪个符号、哪个库的问题。很多人卡在晚上一两个小时,就是因为第一反应是“重装httpd”“升级gcc”“加个什么编译参数”,这些方向都不在关键路径上。这篇文里的每一条排查路径都是我实打实踩出来的,希望对你有用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询