尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

gcc/g++ 库编译与链接完全指南:静态库、动态库与链接参数详解

gcc/g++ 库编译与链接完全指南:静态库、动态库与链接参数详解 gcc/g 链接库的编译与链接——一次把静态库、动态库、链接参数和那些玄学报错讲明白入行这些年几乎每隔一段时间就会看到有人在群里发一条编译报错undefined reference to xxx或者是cannot find -lxxx再不然就是编译过了、一运行直接报cannot open shared object file。这些问题的根子全都在 gcc/g 编译和链接库这件事上。说实话C/C 的链接机制并不复杂但它藏在编译器背后初学者看不见摸不着出了问题就只能靠猜。这篇文章我就把 gcc/g 编译链接库这条线完整地捋一遍从库是怎么生成的、链接参数是怎么解析的到运行时为什么找不到库、怎么排查全部用实际命令和示例过一遍。不管你是刚接触 Linux 下 C/C 开发的新手还是被链接报错折磨过几次的进阶开发者这篇都能给你省下不少时间。1. 编译和链接到底各干了什么活很多人写 C/C 的时候一条gcc main.c -o app就完事了中间发生了什么完全是个黑盒。但只要你开始接触多个文件、开始引入第三方库黑盒就开始漏风。所以我们先把编译和链接这两个阶段彻底拆开搞清楚问题出在哪一环。1.1 从源码到可执行文件编译器其实做了四件事一条最简单的gcc test.c -o test背后经历了预处理、编译、汇编、链接四个阶段。预处理处理#include、#define、#ifdef这些指令把头文件内容原样展开到源文件里。这一步可以用gcc -E test.c -o test.i单独查看结果展开后的文件会非常长因为标准库头文件全被塞进来了。编译把预处理后的.i文件翻译成汇编代码也就是.s文件。这一步做的是语法分析、语义分析、优化等等。用gcc -S test.c就能生成对应的test.s。汇编把汇编代码转换成机器指令生成目标文件.o。用gcc -c test.c生成test.o这一步之后代码已经变成了二进制但还不能运行。链接把多个目标文件、库文件合并成一个可执行文件解析各个文件之间互相引用的符号函数、全局变量分配最终的虚拟地址。前三步通常被统称为“编译”最后一步单独叫“链接”。我们这篇文章的主角就是最后这一步。链接的本质可以打个比方你写代码时引用了一个函数add()但编译阶段编译器只知道“有这么一个函数”它的具体实现可能在另一个.c文件里也可能在某个库里。链接器的工作就是把这些散落在各处的“零件”按清单组装起来把每个add()调用点指向add()函数真正的实现地址。如果某个零件找不到或者型号对不上链接器就罢工了。1.2 链接器报错属于“最后一道防线”为什么链接报错往往比编译报错更难排查因为编译错误是局部的哪一行错了编译器都会明确告诉你。但链接错误是全局的它要把所有文件放到一起看符号找不到、符号重复定义、符号类型不匹配这些问题的锅可能在一个你完全没想过的文件里。举个例子你写了main.c里面调用了foo()但foo()在foo.c里而你编译时只写了gcc main.c -o app没把foo.c加进来。编译器不会报错因为它在main.c里只看到foo()的声明通常在头文件里知道这个函数存在就够了。但链接器不干了你让我生成可执行文件结果foo()的实现在哪我上哪儿找去于是抛出undefined reference to foo。理解这个逻辑很重要因为所有链接库的问题本质上都是符号解析的问题要么是符号没提供库没链接进来、库不完整、库文件不对要么是符号提供了但链接器没去找库搜索路径不对、库的顺序不对。2. 静态库和动态库从生成到使用一次说透库本质上是若干目标文件.o的集合目的是把可复用的代码打包让多个程序共享避免每次都把源码重新编译一遍。在 Linux 下库分为静态库和动态库两大类它们的生成方式、链接时机、运行机制都不一样。2.1 静态库就是一堆 .o 文件的“压缩包”静态库在 Linux 下的后缀是.aarchive归档文件它的本质就是把一堆.o文件打包在一起。打包工具不是 gcc而是ararchiver。假设我们要做一个简单的数学工具库包含两个源文件// add.c int add(int a, int b) { return a b; }// mul.c int mul(int a, int b) { return a * b; }首先生成目标文件gcc -c add.c -o add.o gcc -c mul.c -o mul.o然后用ar打包ar rcs libmath.a add.o mul.orcs三个字母的含义分别是r把文件插入归档如已存在则替换c创建归档文件不显示提示信息s写入索引相当于给这个“压缩包”建了个目录方便链接器快速查找符号。使用静态库连接时gcc main.c -L. -lmath -o app这里-L.告诉链接器在当前目录找库-lmath表示链接名为libmath.a的库。注意-l后面的名字是去掉lib前缀和.a后缀后的部分这是 gcc 的命名约定。链接器从libmath.a中提取出main.o需要的目标文件把它们的内容合并进最终的可执行文件app。从此app不再依赖libmath.a这个库删掉也不影响运行。2.2 动态库运行时才加载的“外挂模块”动态库在 Linux 下的后缀是.soshared object共享对象。它的链接时机比静态库晚得多编译链接程序时链接器只记录“这个程序需要libmath.so里面包含add和mul”并不把实现代码拷进可执行文件。真正加载是在程序启动时由动态链接器ld-linux.so负责把.so映射进进程地址空间。生成动态库的命令gcc -fPIC -c add.c -o add.o gcc -fPIC -c mul.c -o mul.o gcc -shared add.o mul.o -o libmath.so这里有两个关键点-fPIC和-shared。-fPICPosition Independent Code位置无关代码是生成动态库时必须加的。它生成的代码里不包含绝对地址所有地址都是相对当前指令位置的偏移量。为什么必须这样因为动态库被加载到哪个内存地址是不确定的——程序运行时系统看哪个地址空闲就往哪儿放。如果代码里写死了某个变量的绝对地址库换个位置就崩了。-fPIC让所有访问都变成“相对于当前位置偏移多少”这样不管库加载到哪都能正确工作。-shared告诉 gcc生成的目标是共享库而不是可执行文件。不加这个参数gcc 会尝试生成一个可执行文件并且通常会因为没有main函数而报错。使用动态库编译gcc main.c -L. -lmath -o app编译命令看起来和静态库一样但如果当前目录下同时存在libmath.a和libmath.sogcc 默认优先链接动态库。想强制用静态库可以加-static全部静态链接或者手动指定完整路径gcc main.c ./libmath.a -o app。动态库的优势是节省磁盘和内存空间多个进程可以共享同一份.so的物理内存而且更新库的时候只要接口不变不需要重新编译调用方程序。代价就是“运行时依赖”程序运行的环境里必须有这个.so否则直接启动失败。后面第 3 章会专门讲这部分坑。2.3 静态库和动态库的对比与取舍用一个表把两者的关键差异列出来方便大家按需选择。对比维度静态库.a动态库.so生成工具ar打包.ogcc 的-shared选项链接时机编译链接时合并进可执行文件链接时仅记录依赖运行时加载可执行文件体积大小运行时是否依赖库不依赖依赖且库必须在系统中可找到更新库需重新编译链接所有使用方接口不变则无需重新编译多个进程共享内存不共享各有一份拷贝共享同一物理内存页面典型场景嵌入式、工具链、需要独立分发的场景常规 Linux 应用、插件化架构做项目的时候我的习惯是第三方大库如 OpenSSL、libcurl优先用动态库打包发布时把.so一起带上并设置好 rpath自己项目内部的小工具模块如果会被频繁改动也用动态库但如果是给客户交付一个独立的命令行工具不希望客户环境里缺依赖那就全静态链接或者把.so放到固定相对路径下。3. 链接参数与搜索路径搞懂这些才能少报错关于 gcc/g 的编译链接参数很多人是背了忘、忘了背。其实不需要背只要理解了每个参数对应的是链接过程的哪个环节就自然记住了。3.1 -I、-L、-l三个最常用的头文件与库参数先看一张参数对照表参数作用对应阶段典型写法-I指定头文件搜索路径预处理/编译-I./include-L指定库文件搜索路径链接-L./lib-l指定要链接的库名去前缀去后缀链接-lmath-static强制使用静态链接链接-static-shared生成共享库链接-shared -fPIC-fPIC生成位置无关代码编译-fPIC -c xxx.c-Wl,rpath指定运行时动态库搜索路径写入可执行文件链接-Wl,-rpath,$ORIGIN/lib-Wl,--no-as-needed链接时保留所有显式指定的动态库链接-Wl,--no-as-needed -lfoo其中有几个点需要展开讲讲。-I和-L的行为逻辑几乎一样系统默认会在一组标准路径里搜索头文件和库-I和-L是在标准路径之外追加你自己的搜索路径。搜索顺序是先搜-I/-L指定的路径再搜系统默认路径标准路径的顺序定义可以通过gcc -v和gcc -print-search-dirs查看。-l的库名匹配规则-lmath会让链接器去找libmath.so或libmath.a。注意这个查找顺序不是随机的modern 链接器默认优先.so。也就是说即使你有一个旧的.a和一个新的.so同时存在不加额外参数时链接器会用.so这可能出乎你的意料。库的位置和顺序比你想的重要-L只告诉链接器“去哪儿找”并不管“要不要找”。链接器什么时候去找库只有当它处理完前面的目标文件遇到了未解析的符号时才会到库里去找。这个顺序问题直接导致了一个经典的“坑”——库的顺序写反了就会报 undefined reference。3.2 经典坑库的顺序为什么这么重要这是新手最容易踩的坑之一。看下面这条命令gcc -lmath main.c -o app很多新手会觉得把-lmath写在前面和写在后面没区别。但事实上如果libmath.a是静态库这条命令极可能报undefined reference to add。原因在于链接器的工作机制链接器从左到右扫描输入的源文件和库文件维护一个“待解析符号表”。遇到目标文件.c编译出来的.o时把它需要的符号加入符号表遇到库文件时只提取库中能解决当前符号表中未解析符号的那部分.o文件。如果用-lmath main.c的顺序链接器先遇到libmath.a。此时符号表中还没有任何未解析的符号main.c还没被处理所以链接器从libmath.a中提取了 0 个文件。接着处理main.o发现add()未定义但这时库已经被“扫描”过了不会回头再找于是undefined reference。正确写法是gcc main.c -lmath -o app也就是把库放在源文件/目标文件之后让源文件先贡献“需求”再用库来满足需求。如果同时有多个库库之间可能也存在依赖关系。比如libA.a依赖libB.a那么顺序应该是-lA -lB因为链接器需要在扫描完libA后知道缺libB里的符号紧接着扫描libB来补齐。如果写反了同样会报 undefined reference。这也是为什么有些大型项目的链接命令里同一个库会出现好几次——就是为了应对复杂的交叉依赖。解决循环依赖的另一个办法是用-Wl,--start-group和-Wl,--end-group让链接器在指定的库集合里反复扫描直到解不出来为止gcc main.c -Wl,--start-group -lA -lB -Wl,--end-group -o app这个办法好用但不建议无脑用因为重复扫描会拖慢链接速度。3.3 环境变量与默认搜索路径离线安装 GCC 后常见的困惑除了-I和-Lgcc 本身还有一组环境变量控制搜索路径。CPATH和C_INCLUDE_PATH控制头文件搜索路径LIBRARY_PATH控制链接时库的搜索路径。LIBRARY_PATH对链接期有效和-L的作用一样只是用环境变量设置而已。很多人在离线环境比如 CentOS 8 上手动编译安装了新版 gcc结果发现命令行敲gcc -v显示的版本还是旧版或者头文件、库用的还是旧的。这里最常见的原因有三个新装 gcc 的路径不在PATH的最前面shell 找到的是旧版本。用which gcc看看实际指向再调整PATH顺序。shell 缓存了旧命令路径。修改PATH后执行hash -r清除缓存。头文件和库还是从旧路径找的。因为 gcc 内部的默认搜索路径是编译 gcc 时设定的一般是/usr/local/include和/usr/local/lib之类如果旧版本的系统库优先就会出现“版本显示新的但用的还是老库”的情况。可以用gcc -print-search-dirs查看 gcc 实际搜索的路径用echo | gcc -E -v -查看头文件搜索路径确认后决定是否需要配置LIBRARY_PATH或把新库路径加入链接参数。离线安装 gcc 时常见的做法是先在一台能联网的同版本机器上下载好所有依赖的 RPM 包通过yum install --downloadonly或apt-get download方式拉下来再拷贝到离线机器上安装。这类方式我后面在讲问题排查的时候还会再提一嘴因为它引出的“开发者环境和运行环境不一致”问题非常常见。4. 完整实操从零编译一个带静态库和动态库的程序前面讲了原理这一节我们完整地走一遍从源码到可执行文件把静态库、动态库两种方式都实操一遍。你在自己电脑上跟着敲每一步都会有明确的输出出错了也能对照排查。4.1 准备示例代码与目录结构先准备一个非常小的项目模拟真实的开发场景有一个数学库模块一个主程序调用它。mathlib/ include/mathlib.h src/add.c src/sub.c app/ main.c文件内容// mathlib/include/mathlib.h #ifndef MATHLIB_H #define MATHLIB_H int add(int a, int b); int sub(int a, int b); #endif// mathlib/src/add.c #include mathlib.h int add(int a, int b) { return a b; }// mathlib/src/sub.c #include mathlib.h int sub(int a, int b) { return a - b; }// app/main.c #include stdio.h #include mathlib.h int main(void) { printf(3 5 %d\n, add(3, 5)); printf(3 - 5 %d\n, sub(3, 5)); return 0; }注意main.c里的#include mathlib.h编译器需要在头文件搜索路径里能找到这个头文件否则预处理阶段就会报No such file or directory。4.2 编译并链接静态库的完整流程第一步编译库的目标文件。进入mathlib目录执行gcc -I./include -c src/add.c -o build/add.o gcc -I./include -c src/sub.c -o build/sub.o-I./include是让编译器在./include目录下找mathlib.h。-c表示只编译不链接生成目标文件。第二步用ar打包成静态库ar rcs build/libmathlib.a build/add.o build/sub.o此时可以用ar t build/libmathlib.a查看库中包含的目标文件。也可以用nm build/libmathlib.a查看库里导出的符号表你会看到add、sub的符号都列出来了这是链接器将来定位符号的依据。第三步编译主程序并链接静态库gcc -I../mathlib/include main.c -L../mathlib/build -lmathlib -o app_static关键点有两个-L../mathlib/build指向库文件所在目录-lmathlib让链接器找libmathlib.a或libmathlib.so。因为我们没有生成.so所以只会匹配到libmathlib.a。运行./app_static输出3 5 8 3 - 5 -2如果想确认app_static是否真的把库代码合并进来了可以用ldd app_static查看动态库依赖正常情况下静态链接的可执行文件只依赖系统最基础的库libc.so等不会出现libmathlib。再用nm app_static | grep add你会看到add符号的地址已经确定并且类型是Ttext section 已定义符号。4.3 编译并链接动态库的完整流程先生成动态库gcc -fPIC -I./include -c src/add.c -o build/add_pic.o gcc -fPIC -I./include -c src/sub.c -o build/sub_pic.o gcc -shared build/add_pic.o build/sub_pic.o -o build/libmathlib.so这里单独用_pic.o是为了和前面的静态库目标文件区分实际上你可以在同一份源码上分别编译出两套.o互不影响。然后编译主程序并链接动态库gcc -I../mathlib/include main.c -L../mathlib/build -lmathlib -o app_shared这时候你会遇到一个非常经典的“坑”——如果当前目录下同时存在libmathlib.a和libmathlib.so链接器默认优先选.so但如果只有.a不管你有没有想过用动态库链接器也只能用静态库。所以要确认自己到底链接的是哪个可以用ldd app_shared查看linux-vdso.so.1 (0x00007ffd...) libmathlib.so not found libc.so.6 /lib/x86_64-linux-gnu/libc.so.6 (0x00007f...)这里出现了本文章前文预告的最大坑编译链接都通过了但一运行就报错error while loading shared libraries: libmathlib.so: cannot open shared object file: No such file or directory。原因很简单编译链接时-L参数帮助链接器找到了库但程序运行时负责加载动态库的是动态链接器/lib64/ld-linux-x86-64.so.2它完全不认识-L参数。它有自己的一套搜索路径默认包括/lib、/usr/lib、/usr/local/lib以及/etc/ld.so.conf和/etc/ld.so.conf.d/*.conf里配置的目录。我们的libmathlib.so在../mathlib/build下不在它的搜索路径里所以“找不到”。解决这个问题下面三种方式任选其一后面第 5 章还会细讲。方式一用环境变量LD_LIBRARY_PATH临时指定export LD_LIBRARY_PATH/path/to/mathlib/build:$LD_LIBRARY_PATH ./app_shared方式二写入系统的动态链接器配置sudo nano /etc/ld.so.conf.d/mathlib.conf # 内容一行/path/to/mathlib/build sudo ldconfig ldd app_shared # 此时应该显示 libmathlib.so /path/to/mathlib/build/libmathlib.so方式三编译时指定 rpath把搜索路径固化进可执行文件gcc -I../mathlib/include main.c -L../mathlib/build -lmathlib -Wl,-rpath,../mathlib/build -o app_shared推荐的做法分场景。如果是开发调试LD_LIBRARY_PATH最方便如果是给用户交付的程序应该用 rpath并且推荐用$ORIGIN相对路径示例见第 5 章这样整个目录拷走也不影响运行。4.4 用 Makefile 管理库的编译链接真实项目不可能每次手动敲命令把上面的流程整理成 Makefile 可以省掉大量重复劳动CC gcc AR ar CFLAGS -Wall -O2 -Iinclude LDFLAGS -Lbuild -lmathlib LIB_SRCS src/add.c src/sub.c LIB_OBJS $(LIB_SRCS:src/%.cbuild/%.o) LIB_PIC_OBJS $(LIB_SRCS:src/%.cbuild/%_pic.o) all: static shared app_static app_shared build: mkdir -p build build/%.o: src/%.c include/mathlib.h | build $(CC) $(CFLAGS) -c $ -o $ build/%_pic.o: src/%.c include/mathlib.h | build $(CC) $(CFLAGS) -fPIC -c $ -o $ static: build/libmathlib.a build/libmathlib.a: $(LIB_OBJS) $(AR) rcs $ $^ shared: build/libmathlib.so build/libmathlib.so: $(LIB_PIC_OBJS) $(CC) -shared $^ -o $ app_static: app/main.c static $(CC) $(CFLAGS) app/main.c -Lbuild -lmathlib -o $ app_shared: app/main.c shared $(CC) $(CFLAGS) app/main.c -Lbuild -lmathlib -Wl,-rpath,$$ORIGIN -o $ clean: rm -rf build app_static app_shared注意这里app_shared的 rpath 写的是$ORIGIN表示“可执行文件所在的目录”。在 Makefile 中写$$ORIGIN是为了转义成 shell 环境的$ORIGIN避免 Makefile 把$O解析成变量。运行时只要把libmathlib.so放在和app_shared同一个目录或者放在$ORIGIN/lib这样的子目录并相应调整 rpath就能稳定找到库。5. 动态库运行时搜索路径避开最常见的三个坑动态库比静态库难搞重点就在运行时的搜索路径问题上。编译链接时的路径是-L和LIBRARY_PATH管的运行时的路径是动态链接器管的两者不是一个体系很多人在这里反复踩坑。5.1 动态链接器的搜索顺序到底是什么当 Linux 启动一个可执行文件时内核首先加载动态链接器interpreter然后动态链接器负责找到程序依赖的所有.so并加载。它的搜索顺序大致如下可执行文件里DT_RPATH段指定的路径已被弃用但部分老程序还在用。LD_LIBRARY_PATH环境变量指定的路径。可执行文件里DT_RUNPATH段指定的路径即通过-Wl,--enable-new-dtags -Wl,-rpath,...设置的 rpath现代系统默认生成这个。/etc/ld.so.cache缓存中的路径由ldconfig根据/etc/ld.so.conf.d/*.conf生成。默认路径/lib、/usr/lib等。注意顺序上的一个细节LD_LIBRARY_PATH的优先级高于DT_RUNPATH。这就意味着如果用户设置了LD_LIBRARY_PATH它可能覆盖掉你编译时精心设置的 rpath。在某些对库版本敏感的部署场景这个优先级设计常被用来做“临时切换库版本”但也可能导致“为什么我 rpath 设了不管用”的困惑。还有一个容易被忽略的点-Wl,-rpath默认生成的是DT_RPATH还是DT_RUNPATH取决于链接器版本和参数。较新的 binutils 在你不指定的时候默认生成DT_RUNPATH。区别在于DT_RPATH的优先级在LD_LIBRARY_PATH之前而DT_RUNPATH在LD_LIBRARY_PATH之后。如果代码里依赖的顺序很敏感可以用-Wl,--disable-new-dtags强制生成旧的DT_RPATH以获得更高优先级。不过一般不建议过度依赖这个老老实实把库部署到明确的位置更省心。5.2 rpath 的正确用法与 $ORIGIN 技巧rpath 的全称是 run-time search path它解决的核心问题是让可执行文件在运行时知道从哪里加载自己的依赖库而不是依赖用户手动去配LD_LIBRARY_PATH。最常见的用法是在编译时把库的绝对路径写进程序gcc main.c -L/path/to/lib -lfoo -Wl,-rpath,/path/to/lib -o app但如果程序需要被拷贝到其他机器、其他目录运行绝对路径就失效了。这时候$ORIGIN就派上用场了。$ORIGIN是动态链接器提供的一个特殊变量展开为可执行文件自身所在的目录。举个例子发布目录结构如下myapp/ bin/app lib/libmathlib.so编译时这样写gcc main.c -L./lib -lmathlib -Wl,-rpath,$ORIGIN/../lib -o app这样无论myapp这个目录被移动到哪个位置app启动时都会从“自己所在目录的上一级目录下的 lib 文件夹”里找libmathlib.so。这是目前最推荐的可移植部署方案很多商业软件也是这么组织的。检查 rpath 是否写入成功用readelf -d app | grep -i path你能看到类似这样的输出0x000000000000001d (RUNPATH) Library runpath: [$ORIGIN/../lib]5.3 LD_LIBRARY_PATH 与 ldconfig什么时候用什么LD_LIBRARY_PATH的原理前面提过它是在环境变量层面给动态链接器临时加搜索路径。它的优点是好使、立即生效、不需要 root 权限缺点是全局影响——你设置的目录对该 shell 里启动的所有程序都生效可能影响系统其他命令加载库的行为所以在生产环境要谨慎使用。ldconfig则是把库信息写进系统缓存/etc/ld.so.cache需要 root 权限。写好配置后执行sudo ldconfig所有程序都能按需找到你的库。适合系统级安装的库。一个实用的技巧是在不确定某个程序到底为什么找不到库时先用LD_DEBUGlibs ./app启动程序动态链接器会打印出每个库的搜索过程包括它尝试了哪些路径、为什么失败。这是排查动态库问题最犀利的工具之一比瞎猜高效得多。6. 常见报错与排查技巧实录不管原理讲得多清楚真到报错的时候人还是容易慌。这一节我按“报错信息 → 原因 → 解决”的方式把最常遇到的几个问题整理成速查表并给出实际排查的命令和方法。6.1 链接报错速查表报错现象常见原因解决办法undefined reference to xxx库没链接、库顺序错误、库中确实没有该符号检查-l是否包含把库放在源文件后面用nm查看库符号cannot find -lxxx-L指定的路径里没有对应的libxxx.a或libxxx.so确认库文件确实存在、命名是否正确用find查看实际文件名cannot open shared object file: No such file or directory运行时找不到动态库改用LD_LIBRARY_PATH、ldconfig或 rpath 解决skipping incompatible xxx when searching for -lxxx找到的库架构不匹配比如 32 位库给 64 位程序用检查目标架构安装对应架构的库版本multiple definition of xxx符号重复定义多个库或目标文件都包含了该符号检查是否链接了重复依赖的库检查全局变量是否在头文件里定义cannot find crt1.ogcc 安装不完整或目标架构不匹配确认 gcc 安装完整检查是否指定了错误的--sysrootgcc -v显示版本没变PATH顺序不对或 shell 缓存了旧路径用which gcc查实际路径hash -r刷新缓存这些报错里面undefined reference是出现频率最高的。我的排查顺序通常是这样先看报错信息里的符号名判断它是函数还是全局变量是谁引用的。如果是第三方库的函数先确认-l参数加了没有、加对了没有。确认了库加对了但还报错就用nm -C 库文件.so | grep 符号名看库里到底有没有这个符号。库里确实有这个符号但还是 undefined reference那就要考虑库的顺序问题和依赖关系了。6.2 三板斧nm、ldd、readelf 怎么用排查链接问题nm、ldd、readelf这三个工具每次都帮了我大忙。nm看符号。列出目标文件或库里的符号表nm libmathlib.a nm -D libmathlib.so # 查看动态库的动态符号表 nm -C app_static | grep add # -C 把 C 符号 demangle方便阅读输出里大写字母是符号类型T表示代码段里已定义U表示未定义。如果nm的结果里某个符号显示U说明这个库自己还缺依赖如果T存在但你链接仍报 undefined多半是库没被链接进来或者顺序不对。ldd看依赖关系ldd app_shared输出里not found就说明某个动态库运行时加载不到。这是最快定位运行时问题的命令。readelf看 ELF 文件内部结构readelf -d app_shared | grep -i needed # 查看依赖了哪些动态库 readelf -d app_shared | grep -i runpath # 查看 rpath readelf -d app_shared | grep -i rpath # 查看旧式 rpath当ldd显示的顺序和你预期不一致时用readelf确认可执行文件里的NEEDED条目和RUNPATH是否写对了。6.3 升级 gcc 后版本为何还是旧的这个热搜词让我想起很多人在 CentOS 上手动升级 gcc 后敲gcc -v还是老版本就开始怀疑是安装失败了。其实大概率不是安装失败而是“用错了 gcc”。排查步骤依次是which gcc看当前 shell 解析到的是哪个路径下的 gcc。如果是/usr/bin/gcc而你自己装到了/usr/local/bin/gcc说明PATH里/usr/bin排在/usr/local/bin前面。echo $PATH确认一下顺序。想要自己的版本优先把/usr/local/bin放到最前面或者用绝对路径调用。hash -r清除 shell 的命令路径缓存。有时候PATH已经改对了但 bash 还在用缓存的旧路径导致看起来没生效。gcc -v真正输出时除了版本号还会打印gcc version xx、Target和Thread model。如果版本号正常但编译时头文件、库还是旧的再查gcc -print-search-dirs的路径配置。关于离线环境装 gcc 依赖包我多说一句CentOS 8 这种已经停止维护的发行版离线装 gcc 最省事的方式是提前在另一台同版本联网机器上用yumdownloader gcc --resolve或dnf download gcc --resolve把所有依赖拉下来打包再到目标机器rpm -Uvh *.rpm。遇到依赖版本冲突时用rpm -ivh逐个装从依赖树的底层往上装比一上来就rpm -Uvh全部要稳得多。装完之后记得检查/usr/bin/gcc和/usr/local/bin/gcc的情况别又掉进 PATH 的坑里。7. 关于动态库裁剪与链接优化的一点补充最后这部分是我在实际项目中踩过坑之后总结的进阶内容。正常情况下把该链接的库写进命令就能工作但遇到大型项目、大量第三方依赖时链接器还有一些默认行为会让你猝不及防。7.1 as-needed 默认裁剪了没用的动态库在很多现代 Linux 发行版上gcc 默认开启了--as-needed选项。它的含义是如果一个动态库没有提供当前链接命令中尚未解析的符号那这个库的NEEDED条目不会被写入最终可执行文件里。换句话说链接器会自动帮你“裁剪”掉“看起来没用”的库。这在大多数情况下是好事能让程序启动更快、依赖更干净。但有个坑如果一个库被链接进来不是为了直接使用而是为了顺带传递它的依赖库--as-needed会把这个“桥接库”裁掉导致它依赖的库也丢失了运行时直接崩。举个例子libfoo.so本身依赖libbar.so你的程序只调用了libfoo的接口。正常情况下链接命令写-lfoo -lbar即使程序不直接调用libbarlibbar也会因为libfoo的依赖关系被加载。但如果--as-needed认为libbar对程序自身“无用”就可能裁剪掉它让动态链接器在加载libfoo时因为找不到libbar而报错。遇到这类问题有一个简单粗暴的解法在链接命令里加-Wl,--no-as-needed -lfoo -lbar -Wl,--as-needed这样指定libfoo和libbar必须被保留同时不影响其他库的裁剪行为。这个技巧在交叉编译和系统移植时特别有用。7.2 静态链接时的整库拉取与符号裁剪静态库的链接行为和动态库不太一样。前面讲过链接器从静态库中提取目标的单位是“目标文件”.o也就是说只要某个.o里有符号被需要整个.o的所有代码都会被拉进可执行文件除非用了编译器插桩做函数级裁剪-ffunction-sections-Wl,--gc-sections。这就带来一个优化技巧写静态库的时候尽量把功能拆细每个函数或高内聚的小模块放一个.c文件。这样使用方只链接自己需要的部分可执行文件体积更小。反之如果把一堆函数堆在一个大.c里使用者只要用到其中一个函数整个大.o都会被拉进去产物体积剧增。另外一个相关选项是-ffunction-sections -fdata-sections配合-Wl,--gc-sections。前者把每个函数、全局变量放到独立的 section后者在链接时回收没有被引用的 section。很多嵌入式项目的体积优化就是这么做的。7.3 pkg-config管理第三方库编译参数的正确姿势手动为每个第三方库写-I、-L、-l参数既繁琐又容易错。Linux 生态早就有了标准解决方案pkg-config。它为每个库维护一个.pc文件里面记录了该库所需的编译参数、链接参数和依赖关系。使用方式pkg-config --cflags --libs openssl输出类似-I/usr/include -L/usr/lib/x86_64-linux-gnu -lssl -lcrypto编译时直接替换gcc main.c $(pkg-config --cflags --libs openssl) -o app这样不仅不用记忆参数还能自动处理库的依赖链比如openssl依赖libcryptopkg-config会自动带出-lcrypto。如果某个库没安装或者版本不匹配pkg-config会报错提示你Package xxx was not found in the pkg-config search path这时候先确认.pc文件是否在PKG_CONFIG_PATH里。我自己写 Makefile 时对第三方库一律用pkg-config而不是手写死路径。好处是换机器、换发行版的时候Makefile 一行都不用改只要系统里装了对应开发包就能编译。8. 一点实战体会回顾这些年写的 C/C 项目链接库的问题占比其实不高但每次遇到都要花不少时间。原因还真不是这个知识点有多难而是它藏在几个容易混淆的概念里编译期和运行期、静态库和动态库、-L和LD_LIBRARY_PATH、DT_RPATH和DT_RUNPATH。把这几对对立的边界划清楚大部分问题都能在几分钟内定位。我自己养成的习惯是项目一开始就把库的目录结构定好头文件放include/库文件放lib/编译脚本统一走 Makefile 配pkg-config动态库部署一律用$ORIGIN的相对 rpath遇到诡异的链接报错先nm后ldd再readelf按这个顺序排查基本不会走弯路。希望这篇文章能帮你把这条路上的坑提前填平少一点深夜抓头。最后再分享一个压箱底的小技巧在链接命令后面藏一个-Wl,--verbose它会输出整个链接过程的详细信息包括链接器搜索了哪些路径、读取了哪些库、最终采用了哪个版本的库。当你觉得“我明明指了路径为什么还是用了系统库”的时候这个参数能直接告诉你答案。
返回列表