Linux動態(tài)庫.so找不到符號表的排查指南
在 Linux 下開發(fā) C/C++ 項目時,動態(tài)庫(.so)相關(guān)的符號找不到(undefined symbol)是最常見也最令人頭疼的問題之一。本文從原理到實踐,系統(tǒng)梳理排查思路與典型場景,幫助快速定位并解決問題。
1. 背景知識
1.1 什么是符號表
符號表(Symbol Table)是 ELF(Executable and Linkable Format)文件中的一個關(guān)鍵段(Section),記錄了程序中定義和引用的所有符號(函數(shù)名、全局變量名等)及其屬性。
一個符號的典型屬性包括:
| 屬性 | 說明 |
|---|---|
| 名稱 | 符號的字符串標(biāo)識 |
| 綁定類型 | LOCAL(本文件可見)/ GLOBAL(全局可見)/ WEAK(弱符號) |
| 類型 | FUNC(函數(shù))/ OBJECT(變量)/ NOTYPE 等 |
| 值 | 符號的地址或偏移 |
| 大小 | 符號占據(jù)的字節(jié)數(shù) |
| 所在段 | 符號定義在哪個 Section 中 |

圖:ELF 文件結(jié)構(gòu)示意,標(biāo)注了 .dynsym(動態(tài)符號表)、.symtab(完整符號表)、.dynstr(動態(tài)字符串表)等符號相關(guān)段的位置關(guān)系。紅色方括號標(biāo)記的區(qū)域為符號相關(guān)段,.dynsym / .dynstr 在 strip 后保留,.symtab / .strtab 在 strip 后被刪除。
關(guān)鍵概念:
.symtab:完整符號表,包含所有符號,strip后會被刪除.dynsym:動態(tài)符號表,僅包含動態(tài)鏈接需要的符號,strip后仍保留.dynstr:動態(tài)符號字符串表,存儲符號名稱字符串
1.2 動態(tài)鏈接過程
程序加載動態(tài)庫的過程分為兩個階段:
編譯/鏈接期(Link Time):
鏈接器(ld)檢查所有未定義符號是否能從指定的共享庫中找到定義,生成可執(zhí)行文件。
運行期(Run Time):
動態(tài)鏈接器(ld-linux.so)加載程序時,按照依賴關(guān)系加載所需的 .so 文件,完成符號重定位(Relocation),將符號引用綁定到實際地址。

*圖:動態(tài)鏈接完整流程——從用戶執(zhí)行程序(execve)到內(nèi)核加載 ELF、啟動動態(tài)鏈接器 ld-linux.so,遞歸加載依賴庫并映射到內(nèi)存,然后遍歷重定位表查找符號定義并填入 GOT/PLT。紅色虛線框標(biāo)注的**步驟 8(符號查找)*是 undefined symbol 錯誤的發(fā)生點,如果所有已加載庫中都找不到該符號定義,動態(tài)鏈接器即報錯終止。
1.3 符號綁定的本質(zhì)
當(dāng)程序引用一個外部符號時,ELF 文件中會記錄一個重定位條目(Relocation Entry),指明"地址 X 處需要填入符號 Y 的實際地址"。動態(tài)鏈接器的工作就是遍歷這些重定位條目,找到符號定義,把實際地址填入。
如果找不到符號定義,鏈接器就會報 undefined symbol 錯誤。
2. 常見錯誤形態(tài)
編譯/鏈接期報錯
# 鏈接時找不到符號定義 /usr/bin/ld: main.o: in function `main': main.cpp:(.text+0x2a): undefined reference to `foo()' collect2: error: ld returned 1 exit status
運行期報錯
# dlopen 時找不到符號 ./app: symbol lookup error: ./libplugin.so: undefined symbol: _Z3foov # 程序啟動時找不到符號 ./app: /usr/lib/libmylib.so: undefined symbol: bar
區(qū)分兩種錯誤
| 特征 | 編譯/鏈接期 | 運行期 |
|---|---|---|
| 報錯時機(jī) | gcc/g++ 編譯時 | 程序啟動或 dlopen 時 |
| 報錯關(guān)鍵詞 | undefined reference | undefined symbol / symbol lookup error |
| 常見原因 | 鏈接順序、缺少庫文件 | 庫版本不一致、dlopen 標(biāo)志不對 |
3. 排查工具詳解
3.1 nm — 查看目標(biāo)文件中的符號
nm 是排查符號問題的第一工具,可以列出目標(biāo)文件和 .so 中的所有符號。
# 查看動態(tài)庫中的所有符號 nm -D libmylib.so # 常用選項組合:按符號名排序,顯示動態(tài)符號 nm -CD libmylib.so | grep foo # 輸出含義: # T / t — 代碼段中的符號(大寫=全局,小寫=局部) # D / d — 數(shù)據(jù)段中的符號 # U — 未定義符號(Undefined,需要從其他庫中解析) # W — 弱符號(Weak) # A — 絕對符號
典型輸出解讀:
$ nm -CD libmylib.so
w __gmon_start__
U __printf_chk@@GLIBC_2.17 # U = 這個符號在本庫中未定義,需要外部提供
0000000000000690 T my_function # T = 這個符號在本庫中定義,全局可見
0000000000000750 T my_class::do_work() # C++ 方法(已 demangle)
0000000000002010 D my_global_var # D = 全局變量定義
技巧:nm -D 只查看動態(tài)符號表(.dynsym),這是運行時鏈接器使用的符號表。不加 -D 會查看完整符號表(.symtab),但 strip 后該表可能不存在。
3.2 readelf — 解析 ELF 文件信息
readelf 比 nm 更底層,可以查看 ELF 文件的任意段。
# 查看動態(tài)符號表 readelf -s libmylib.so # 查看所有段頭(定位符號表段是否存在) readelf -S libmylib.so # 查看動態(tài)段(NEEDED 條目 = 運行時依賴的庫) readelf -d libmylib.so # 查看符號版本信息 readelf -V libmylib.so # 查看重定位條目(哪些符號需要被重定位) readelf -r libmylib.so
典型輸出解讀:
$ readelf -s libmylib.so
Symbol table '.dynsym' contains 15 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __printf_chk@GLIBC_2.17
2: 0000000000000690 120 FUNC GLOBAL DEFAULT 11 my_function
3: 0000000000000750 56 FUNC GLOBAL DEFAULT 11 _ZN8my_class7do_workEv
↑ 這是 C++ mangled name
$ readelf -d libmylib.so | grep NEEDED 0x0000000000000001 (NEEDED) Shared library: [libgcc_s.so.1] 0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
3.3 objdump — 反匯編與符號查看
# 查看符號表 objdump -T libmylib.so # 查看所有段頭 objdump -x libmylib.so | head -50 # 反匯編特定函數(shù)(需要非 strip 的庫) objdump -d libmylib.so | grep -A 20 '<my_function>'
3.4 ldd — 查看動態(tài)庫依賴
# 查看程序運行時依賴的所有 .so
ldd ./myapp
# 查看某個 .so 的依賴
ldd libmylib.so
# 典型輸出
$ ldd ./myapp
linux-vdso.so.1 (0x00007ffc12bfe000)
libmylib.so => /usr/local/lib/libmylib.so (0x00007f8a1c200000) # ? 找到了
libfoo.so => not found # ? 找不到!
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8a1be00000)
/lib64/ld-linux-x86-64.so.2 (0x00007f8a1c600000)
注意:ldd 對于交叉編譯場景可能不可靠,可用 readelf -d + LD_LIBRARY_PATH 替代。
3.5 LD_DEBUG — 運行時調(diào)試動態(tài)鏈接器
這是排查運行期符號問題最強(qiáng)大的工具,無需重新編譯。
# 查看符號綁定過程(最常用) LD_DEBUG=symbols ./myapp # 查看庫文件查找過程 LD_DEBUG=libs ./myapp # 查看重定位過程 LD_DEBUG=reloc ./myapp # 查看所有調(diào)試信息 LD_DEBUG=all ./myapp # 輸出到文件而非 stderr LD_DEBUG=symbols LD_DEBUG_OUTPUT=/tmp/ld_debug ./myapp
典型輸出:
$ LD_DEBUG=symbols ./myapp 2>&1 | grep foo
10567: symbol=foo; lookup in file=./myapp [0]
10567: symbol=foo; lookup in file=libmylib.so [0]
10567: symbol=foo; lookup in file=libc.so.6 [0]
10567: symbol=foo; lookup in file=ld-linux-x86-64.so.2 [0]
10567: symbol=foo; error: symbol not found # ← 所有庫都找遍了,沒找到
3.6 c++filt — C++ 符號 demangle
C++ 編譯器會對函數(shù)名進(jìn)行 name mangling,c++filt 用于還原可讀名稱。
# 還原 mangled 名稱 $ echo '_ZN8my_class7do_workEv' | c++filt my_class::do_work() # 結(jié)合 nm 使用 nm libmylib.so | c++filt # 結(jié)合 grep 使用 nm -D libmylib.so | c++filt | grep 'my_class::do_work'
4. 系統(tǒng)化排查流程
遇到 undefined symbol 錯誤時,按以下流程逐步排查:

Step 1:確認(rèn)缺失的符號名
從錯誤信息中提取符號名,注意區(qū)分 mangled name 和 demangled name:
# 如果是 mangled name(以 _Z 開頭),先 demangle $ c++filt _Z3foov foo()
Step 2:確認(rèn)該符號應(yīng)該由誰提供
# 在所有相關(guān)庫中搜索該符號
nm -CD libprovider1.so | grep foo
nm -CD libprovider2.so | grep foo
# 或者用 readelf
readelf -s libprovider1.so | grep foo
readelf -s libprovider2.so | grep foo
# 系統(tǒng)范圍搜索(需要 locate 或 find)
# 方法 1:用 locate 快速定位
locate libfoo.so | xargs -I{} sh -c 'nm -CD {} 2>/dev/null | grep -l foo && echo {}'
# 方法 2:在已知目錄下搜索
for lib in /usr/lib/*.so /usr/local/lib/*.so; do
nm -CD "$lib" 2>/dev/null | grep -q 'T foo' && echo "$lib"
done
Step 3:確認(rèn)提供者庫是否被正確加載
# 檢查依賴鏈 ldd ./myapp | grep libprovider # 檢查 RPATH/RUNPATH readelf -d ./myapp | grep -E 'RPATH|RUNPATH' # 檢查 LD_LIBRARY_PATH echo $LD_LIBRARY_PATH
Step 4:確認(rèn)符號可見性
# 檢查符號是否被導(dǎo)出 nm -CD libprovider.so | grep foo # 如果沒有 T/D 類型的條目,說明符號未導(dǎo)出 # 檢查符號是否被 strip 掉 readelf -S libprovider.so | grep -E 'symtab|dynsym' # 如果 .symtab 不存在但 .dynsym 存在,說明被 strip 了(正常) # 如果 .dynsym 也沒有,說明編譯時就沒有導(dǎo)出
Step 5:確認(rèn)符號版本
# 查看符號版本要求 readelf -V libprovider.so objdump -T libprovider.so | grep foo
5. 典型案例
5.1 案例一:C++ name mangling 導(dǎo)致找不到符號
場景:C 語言寫的庫,C++ 程序調(diào)用時找不到符號。
復(fù)現(xiàn):
// libcalc.c — 純 C 庫
int add(int a, int b) {
return a + b;
}// main.cpp — C++ 程序
#include <cstdio>
// ? 缺少 extern "C" 聲明
int add(int a, int b);
int main() {
printf("%d\n", add(1, 2));
return 0;
}# 編譯 C 庫 gcc -shared -fPIC -o libcalc.so libcalc.c # 編譯 C++ 程序 g++ main.cpp -L. -lcalc -o main # 運行報錯 $ ./main ./main: symbol lookup error: ./main: undefined symbol: _Z3addii
排查:
# 看庫中導(dǎo)出的符號名
$ nm -D libcalc.so | grep add
0000000000000690 T add # C 庫導(dǎo)出的是 "add"
# 看程序需要的符號名
$ nm main | grep add
U _Z3addii # C++ 程序找的是 "_Z3addii"(mangled name)
# demangle 確認(rèn)
$ c++filt _Z3addii
add(int, int)
結(jié)論:C++ 編譯器對 add(int, int) 做了 name mangling,生成 _Z3addii,而 C 庫導(dǎo)出的符號名是 add,兩者不匹配。
修復(fù):
// main.cpp — 正確寫法
#include <cstdio>
extern "C" { // ? 告訴編譯器按 C 的方式查找符號
int add(int a, int b);
}
int main() {
printf("%d\n", add(1, 2));
return 0;
}或者更常見的頭文件寫法:
// libcalc.h — 兼容 C 和 C++ 的頭文件
#ifdef __cplusplus
extern "C" {
#endif
int add(int a, int b);
#ifdef __cplusplus
}
#endif5.2 案例二:編譯時缺少 -fPIC
場景:編譯動態(tài)庫時未加 -fPIC,鏈接時出現(xiàn)重定位錯誤。
復(fù)現(xiàn):
// libfoo.c
int foo() { return 42; }# ? 編譯 .o 時沒有 -fPIC gcc -c libfoo.c gcc -shared -o libfoo.so libfoo.o # 可能出現(xiàn)的警告或錯誤 /usr/bin/ld: libfoo.o: relocation R_X86_64_PC32 against symbol `foo' can not be used when making a shared object; recompile with -fPIC
在某些架構(gòu)(如 x86_64)上,缺少 -fPIC 會直接報錯;在另一些架構(gòu)上可能只是性能下降或運行時異常。
排查:
# 檢查 .o 文件的重定位類型 readelf -r libfoo.o | head # 如果看到 R_X86_64_PC32 而非 R_X86_64_PLT32 / R_X86_64_GOTPCREL, # 說明編譯時沒有使用 -fPIC
修復(fù):
# ? 編譯 .o 時加 -fPIC gcc -fPIC -c libfoo.c gcc -shared -o libfoo.so libfoo.o # 或者一步完成 gcc -shared -fPIC -o libfoo.so libfoo.c
5.3 案例三:鏈接順序錯誤
場景:編譯時庫的鏈接順序?qū)е路栒也坏健?/p>
復(fù)現(xiàn):
// main.cpp
#include "liba.h" // liba 中的函數(shù)依賴 libb
int main() {
func_from_a(); // 該函數(shù)內(nèi)部調(diào)用了 func_from_b()
return 0;
}# ? 錯誤的鏈接順序:liba 在 libb 之前 g++ main.cpp -la -lb -o main # 報錯:undefined reference to `func_from_b()' # ? 正確的鏈接順序:被依賴的庫放后面 g++ main.cpp -la -lb -o main # 如果 a 依賴 b,應(yīng)該把 b 放在 a 后面
關(guān)鍵規(guī)則:GCC 鏈接器是從左到右單遍掃描的。如果庫 A 依賴庫 B 中的符號,那么在命令行上 A 必須出現(xiàn)在 B 之前。即
g++ main.o -lA -lB。
排查:
# 確認(rèn)庫之間的依賴關(guān)系
nm -D liba.so | grep ' U ' # 查看 liba 的未定義符號
nm -D libb.so | grep ' T ' # 查看 libb 定義了哪些符號
# 交叉對比
nm -D liba.so | awk '$1=="U"{print $2}' | while read sym; do
nm -D libb.so | grep -q " T $sym" && echo "libb provides: $sym"
done
5.4 案例四:符號版本不匹配
場景:編譯時使用的庫版本與運行時加載的庫版本不同,符號版本對不上。
復(fù)現(xiàn):
# 編譯時鏈接了新版本的 libfoo(有 foo@@VER_2.0) g++ main.cpp -lfoo -o main # 運行時加載了舊版本的 libfoo(只有 foo@@VER_1.0) $ LD_LIBRARY_PATH=/old/lib ./main ./main: /old/lib/libfoo.so: version `FOO_2.0' not found
排查:
# 查看程序需要的符號版本
$ objdump -T main | grep FOO
0000000000000000 DF *UND* 0000000000000000 FOO_2.0 foo
# 查看庫提供的符號版本
$ objdump -T /old/lib/libfoo.so | grep FOO
0000000000000690 g DF .text 000000000000001a FOO_1.0 foo
↑ 只有 1.0
$ objdump -T /new/lib/libfoo.so | grep FOO
0000000000000690 g DF .text 000000000000001a FOO_2.0 foo
↑ 有 2.0
# 查看庫的版本定義
$ readelf -V /old/lib/libfoo.so
Version definition section '.gnu.version_d' contains 2 entries:
Addr: 0x00000000000002d8 Offset: 0x0002d8 Link: 3 (.dynstr)
00000000: Rev: 1 Flags: BASE Index: 1 Cnt: 1 Name: libfoo.so
0x001c9880: Rev: 1 Flags: none Index: 2 Cnt: 1 Name: FOO_1.0
修復(fù):
# 方法 1:確保運行時使用正確版本的庫 LD_LIBRARY_PATH=/new/lib ./main # 方法 2:使用 LD_PRELOAD 強(qiáng)制加載特定版本 LD_PRELOAD=/new/lib/libfoo.so ./main # 方法 3:設(shè)置 RPATH 使可執(zhí)行文件記住庫路徑 g++ -Wl,-rpath,/new/lib main.cpp -lfoo -o main
5.5 案例五:dlopen 加載時缺少 RTLD_GLOBAL
場景:插件系統(tǒng)使用 dlopen 加載 .so,插件中引用了主程序或其他插件的符號,但 dlopen 時沒有設(shè)置 RTLD_GLOBAL。
復(fù)現(xiàn):
// main.cpp — 主程序
#include <dlfcn.h>
#include <cstdio>
void host_function() { // 主程序中定義的函數(shù)
printf("host_function called\n");
}
int main() {
// ? 只用了 RTLD_LAZY,沒有 RTLD_GLOBAL
void* handle = dlopen("./libplugin.so", RTLD_LAZY);
if (!handle) {
fprintf(stderr, "%s\n", dlerror());
return 1;
}
typedef void (*plugin_init_t)();
auto plugin_init = (plugin_init_t)dlsym(handle, "plugin_init");
plugin_init();
dlclose(handle);
return 0;
}// plugin.cpp — 插件
#include <cstdio>
extern void host_function(); // 引用主程序的符號
extern "C" void plugin_init() {
host_function(); // ← 運行時報 undefined symbol
}# 編譯 g++ -shared -fPIC -o libplugin.so plugin.cpp g++ -rdynamic -o main main.cpp -ldl # -rdynamic 讓主程序?qū)С龇? # 運行 $ ./main ./libplugin.so: undefined symbol: host_function
排查:
# 確認(rèn)主程序確實導(dǎo)出了該符號
$ nm -D main | grep host_function
0000000000001179 T host_function # ? 主程序?qū)С隽?
# 確認(rèn)插件需要該符號
$ nm -D libplugin.so | grep host_function
U host_function # U = 未定義,需要外部提供
# 問題在于 dlopen 的默認(rèn)作用域
根因分析:
dlopen 默認(rèn)使用 RTLD_LOCAL,意味著新加載的庫的符號不會添加到全局符號表中。當(dāng)插件引用主程序的符號時,默認(rèn)搜索范圍可能不包含主程序的符號。
修復(fù):
// 方法 1:使用 RTLD_LAZY | RTLD_GLOBAL(推薦)
void* handle = dlopen("./libplugin.so", RTLD_LAZY | RTLD_GLOBAL);
// 方法 2:主程序編譯時加 -rdynamic(已加),并確保 dlopen 之前符號可用
5.6 案例六:靜態(tài)庫中未引用的符號被丟棄
場景:將靜態(tài)庫(.a)鏈接到動態(tài)庫(.so)時,靜態(tài)庫中未被直接引用的符號被鏈接器丟棄。
復(fù)現(xiàn):
// registry.h — 自動注冊模式
#include <map>
#include <string>
struct Registry {
static std::map<std::string, int>& entries() {
static std::map<std::string, int> m;
return m;
}
};
#define REGISTER(name, val) \
static bool _reg_##name = (Registry::entries()[#name] = val, true)// foo_plugin.cpp #include "registry.h" REGISTER(foo, 1) // 全局靜態(tài)變量的構(gòu)造函數(shù)會執(zhí)行注冊
// bar_plugin.cpp #include "registry.h" REGISTER(bar, 2)
# 編譯為靜態(tài)庫 g++ -c foo_plugin.cpp -o foo_plugin.o g++ -c bar_plugin.cpp -o bar_plugin.o ar rcs libplugins.a foo_plugin.o bar_plugin.o # 鏈接到動態(tài)庫 g++ -shared -fPIC -o libmyapp.so -L. -lplugins # 運行時發(fā)現(xiàn)注冊表中為空!
排查:
# 檢查動態(tài)庫中是否有注冊相關(guān)的符號 $ nm -D libmyapp.so | grep _reg_ # 空輸出!符號被丟棄了 # 檢查靜態(tài)庫中確實有這些符號 $ nm libplugins.a | grep _reg_ foo_plugin.o: 0000000000000000 d _reg_foo bar_plugin.o: 0000000000000000 d _reg_bar
根因:鏈接器在處理靜態(tài)庫時,只提取那些被其他目標(biāo)文件引用的符號。由于 _reg_foo 和 _reg_bar 是靜態(tài)變量,沒有被顯式引用,鏈接器認(rèn)為它們"不需要"而將其丟棄。
修復(fù):
# 方法 1:使用 --whole-archive 強(qiáng)制包含所有符號
g++ -shared -fPIC -o libmyapp.so \
-Wl,--whole-archive -L. -lplugins -Wl,--no-whole-archive
# 方法 2:在代碼中顯式引用(不推薦,但簡單)
# 在某個會被引用的函數(shù)中添加:
extern bool _reg_foo;
extern bool _reg_bar;
void force_reference() {
(void)_reg_foo;
(void)_reg_bar;
}
# 方法 3:直接用 .o 文件而非靜態(tài)庫
g++ -shared -fPIC -o libmyapp.so foo_plugin.o bar_plugin.o5.7 案例七:頭文件與庫版本不一致
場景:系統(tǒng)安裝了多個版本的庫,編譯時使用了新版頭文件,但鏈接時找到了舊版庫。
復(fù)現(xiàn):
# /usr/include/mylib.h — 新版本(v2.0),聲明了 new_api() # /usr/lib/libmylib.so — 舊版本(v1.0),沒有 new_api() # /usr/local/lib/libmylib.so — 新版本(v2.0),有 new_api() # 編譯時用了新頭文件 g++ main.cpp -I/usr/include -lmylib -o main # 鏈接時找到了舊版庫 $ ldd main | grep mylib libmylib.so => /usr/lib/libmylib.so # ← 舊版! # 運行報錯 $ ./main ./main: symbol lookup error: ./main: undefined symbol: new_api
排查:
# 1. 確認(rèn)鏈接了哪個庫 ldd main | grep mylib # 2. 確認(rèn)庫中是否有該符號 nm -CD /usr/lib/libmylib.so | grep new_api # 舊版:沒有 nm -CD /usr/local/lib/libmylib.so | grep new_api # 新版:有 # 3. 確認(rèn)頭文件版本 grep new_api /usr/include/mylib.h # 有聲明
修復(fù):
# 方法 1:設(shè)置 LD_LIBRARY_PATH export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH # 方法 2:編譯時設(shè)置 RPATH g++ main.cpp -I/usr/local/include -L/usr/local/lib -Wl,-rpath,/usr/local/lib -lmylib -o main # 方法 3:使用 pkg-config 確保一致性 g++ main.cpp $(pkg-config --cflags --libs mylib) -o main
5.8 案例八:x86 交叉編譯 ARM 動態(tài)庫部署后找不到符號
場景:在 x86_64 開發(fā)機(jī)上使用交叉編譯工具鏈(如 aarch64-linux-gnu-gcc)編譯 ARM64 動態(tài)庫,拷貝到 ARM64 目標(biāo)機(jī)器后運行,出現(xiàn)找不到動態(tài)庫或符號的問題。這類問題在嵌入式開發(fā)、邊緣設(shè)備部署中極為常見。
5.8.1 子場景 A:在 x86 開發(fā)機(jī)上誤用本地工具排查 ARM 庫
這是最常見的"偽問題"——庫本身沒問題,但排查方法用錯了。
復(fù)現(xiàn):
# 在 x86 開發(fā)機(jī)上編譯 ARM64 動態(tài)庫
aarch64-linux-gnu-g++ -shared -fPIC -o libfoo.so foo.cpp
# ? 在 x86 機(jī)器上直接用 ldd 檢查 ARM 庫
$ ldd libfoo.so
not a dynamic executable
# ? 在 x86 機(jī)器上直接運行 ARM 程序
$ ./myapp
bash: ./myapp: cannot execute binary file: Exec format Error
# ? 用本地 nm 查看(雖然能看符號,但容易忽略架構(gòu)差異)
$ nm -D libfoo.so
# 能輸出符號,但無法驗證運行時依賴鏈?zhǔn)欠裢暾?
正確做法:在交叉編譯場景下,必須使用與目標(biāo)架構(gòu)匹配的工具鏈來排查,或在目標(biāo)機(jī)器上直接排查。
# ? 方法 1:使用交叉編譯工具鏈自帶的工具 aarch64-linux-gnu-nm -D libfoo.so aarch64-linux-gnu-readelf -s libfoo.so aarch64-linux-gnu-readelf -d libfoo.so | grep NEEDED aarch64-linux-gnu-objdump -T libfoo.so # ? 方法 2:直接在 ARM64 目標(biāo)機(jī)器上排查(最可靠) # 拷貝到目標(biāo)機(jī)后: ssh arm-device nm -D libfoo.so readelf -d libfoo.so | grep NEEDED ldd ./myapp # 在目標(biāo)機(jī)上 ldd 才有意義
核心原則:ldd 本質(zhì)上是執(zhí)行目標(biāo)程序來獲取依賴信息,因此無法在 x86 上對 ARM 二進(jìn)制使用。nm、readelf、objdump 是純文件解析工具,可以在 x86 上解析 ARM 二進(jìn)制,但必須使用對應(yīng)架構(gòu)的版本才能保證行為一致。
5.8.2 子場景 B:交叉編譯時鏈接了 x86 架構(gòu)的系統(tǒng)庫
復(fù)現(xiàn):
# 交叉編譯時,未正確指定 sysroot aarch64-linux-gnu-g++ main.cpp -lfoo -o myapp # 編譯可能成功(因為找到了 x86 的 libfoo.so),但生成的二進(jìn)制是混合架構(gòu) # 部署到 ARM 機(jī)器后: $ ./myapp ./myapp: error while loading shared libraries: libfoo.so: wrong ELF class: ELFCLASS64 # 或者 ./myapp: /usr/lib/libfoo.so: cannot open shared object file: Exec format error
排查:
# 1. 檢查 ELF 文件的架構(gòu)
$ readelf -h libfoo.so | grep -E 'Machine|Class'
Class: ELF64
Machine: AArch64 # ? ARM64
$ readelf -h /usr/lib/libfoo.so | grep -E 'Machine|Class'
Class: ELF64
Machine: Advanced Micro Devices X86-64 # ? x86_64!鏈接了錯誤的庫
# 2. 檢查可執(zhí)行文件依賴的所有庫的架構(gòu)
$ for lib in $(ldd myapp | awk '{print $3}' | grep -v '^$'); do
echo "=== $lib ==="
readelf -h "$lib" 2>/dev/null | grep Machine
done
# 3. 檢查編譯時鏈接了哪些路徑
$ aarch64-linux-gnu-g++ main.cpp -lfoo -o myapp -v 2>&1 | grep 'LIBRARY_PATH'
# 如果輸出包含 /usr/lib 而非 /usr/aarch64-linux-gnu/lib,說明鏈接了 x86 系統(tǒng)庫
修復(fù):
# ? 方法 1:使用 --sysroot 指定目標(biāo)架構(gòu)的根文件系統(tǒng)
aarch64-linux-gnu-g++ main.cpp -lfoo -o myapp \
--sysroot=/usr/aarch64-linux-gnu
# ? 方法 2:顯式指定庫搜索路徑
aarch64-linux-gnu-g++ main.cpp -lfoo -o myapp \
-L/usr/aarch64-linux-gnu/lib
# ? 方法 3:使用 CMake 交叉編譯工具鏈文件(推薦)
CMake 交叉編譯工具鏈文件示例:
# toolchain-aarch64.cmake set(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR aarch64) set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc) set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++) # 關(guān)鍵:指定 sysroot 和搜索路徑 set(CMAKE_FIND_ROOT_PATH /usr/aarch64-linux-gnu) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
cmake -DCMAKE_TOOLCHAIN_FILE=toolchain-aarch64.cmake .. make
5.8.3 子場景 C:ARM 目標(biāo)機(jī)上缺少依賴的 .so 或符號
場景:交叉編譯的庫在 ARM 目標(biāo)機(jī)上運行時,找不到依賴的底層庫(如 libc、libstdc++ 版本不匹配)。
復(fù)現(xiàn):
# 在 x86 開發(fā)機(jī)上用較新的交叉編譯工具鏈編譯 aarch64-linux-gnu-g++ -shared -fPIC -o libmyapp.so myapp.cpp # 部署到 ARM 目標(biāo)機(jī)(可能是較老的嵌入式系統(tǒng)) $ ./myapp ./myapp: /usr/lib/libstdc++.so.6: version `GLIBCXX_3.4.29' not found ./myapp: /lib/libc.so.6: version `GLIBC_2.33' not found
排查:
# 1. 在 x86 開發(fā)機(jī)上檢查交叉編譯的庫需要哪些符號版本 $ aarch64-linux-gnu-readelf -V libmyapp.so | grep -E 'GLIBC|GLIBCXX' Version needs section '.gnu.version_r' contains 3 entries: 0x00008000: Rev: 1 Flags: none Index: 2 Cnt: 1 Name: GLIBC_2.33 0x00008010: Rev: 1 Flags: none Index: 3 Cnt: 1 Name: GLIBCXX_3.4.29 # 2. 在 ARM 目標(biāo)機(jī)上檢查可用的符號版本 $ strings /usr/lib/libstdc++.so.6 | grep GLIBCXX GLIBCXX_3.4 GLIBCXX_3.4.9 ... GLIBCXX_3.4.21 # ← 最高只到 3.4.21,遠(yuǎn)低于需要的 3.4.29 $ strings /lib/libc.so.6 | grep GLIBC GLIBC_2.17 ... GLIBC_2.31 # ← 最高只到 2.31,低于需要的 2.33 # 3. 列出庫的所有未定義符號及其版本要求 $ aarch64-linux-gnu-objdump -T libmyapp.so | grep '*UND*' 0000000000000000 DF *UND* 0000000000000000 GLIBC_2.33 memcpy 0000000000000000 DF *UND* 0000000000000000 GLIBCXX_3.4.29 _ZNSt7__cxx1112basic_string...
修復(fù):
# 方法 1:使用與目標(biāo)系統(tǒng)匹配的交叉編譯工具鏈版本
# 如果目標(biāo)機(jī)是 Ubuntu 20.04(glibc 2.31),就用對應(yīng)版本的 sysroot
aarch64-linux-gnu-g++ -shared -fPIC -o libmyapp.so myapp.cpp \
--sysroot=/path/to/ubuntu20.04-aarch64-sysroot
# 方法 2:將交叉編譯工具鏈的運行時庫一起部署到目標(biāo)機(jī)
# 拷貝交叉編譯器的 libstdc++ 和 libgcc
scp /usr/aarch64-linux-gnu/lib/libstdc++.so.6.0.29 arm-device:/opt/lib/
scp /usr/aarch64-linux-gnu/lib/libgcc_s.so.1 arm-device:/opt/lib/
# 在目標(biāo)機(jī)上設(shè)置 LD_LIBRARY_PATH
export LD_LIBRARY_PATH=/opt/lib:$LD_LIBRARY_PATH
# 方法 3:靜態(tài)鏈接 C/C++ 運行時(簡單但增加體積)
aarch64-linux-gnu-g++ -shared -fPIC -o libmyapp.so myapp.cpp \
-static-libgcc -static-libstdc++
# 方法 4:使用 Docker 構(gòu)建可控的交叉編譯環(huán)境(推薦,可復(fù)現(xiàn))
Docker 交叉編譯示例:
# Dockerfile.cross-build
FROM ubuntu:20.04
RUN apt-get update && apt-get install -y \
gcc-aarch64-linux-gnu \
g++-aarch64-linux-gnu \
libc6-dev-arm64-cross \
libstdc++-8-dev-arm64-cross
# 此環(huán)境中的 glibc/libstdc++ 版本與 Ubuntu 20.04 一致
# 確保編譯出的 .so 在目標(biāo)機(jī)上兼容
5.8.4 子場景 D:ARM 目標(biāo)機(jī)上 .so 搜索路徑問題
場景:庫已正確交叉編譯并部署到 ARM 機(jī)器,但動態(tài)鏈接器找不到庫文件。
復(fù)現(xiàn):
# 將 libfoo.so 部署到 ARM 目標(biāo)機(jī)的 /opt/myapp/lib/ $ ./myapp ./myapp: error while loading shared libraries: libfoo.so: cannot open shared object file: No such file or directory # 但庫確實存在 $ ls -la /opt/myapp/lib/libfoo.so -rwxr-xr-x 1 root root 123456 Apr 24 10:00 /opt/myapp/lib/libfoo.so
排查:
# 1. 確認(rèn)動態(tài)鏈接器搜索了哪些路徑
$ LD_DEBUG=libs ./myapp 2>&1 | head -20
1234: find library=libfoo.so [0]; searching
1234: search cache=/etc/ld.so.cache
1234: search path=/usr/lib:/lib # ← 默認(rèn)路徑,沒有 /opt/myapp/lib
# 2. 檢查 ld.so.conf 配置
$ cat /etc/ld.so.conf
include /etc/ld.so.conf.d/*.conf
$ ls /etc/ld.so.conf.d/
libc.conf # 只包含 /usr/lib
# 3. 檢查可執(zhí)行文件的 RPATH
$ readelf -d myapp | grep -E 'RPATH|RUNPATH'
# 空輸出 — 沒有設(shè)置 RPATH
修復(fù):
# 方法 1:臨時方案 — 設(shè)置 LD_LIBRARY_PATH
export LD_LIBRARY_PATH=/opt/myapp/lib:$LD_LIBRARY_PATH
./myapp
# 方法 2:永久方案 — 添加到 ld.so.conf
echo "/opt/myapp/lib" > /etc/ld.so.conf.d/myapp.conf
ldconfig # 刷新緩存
ldconfig -p | grep libfoo # 驗證緩存中已有該庫
# 方法 3:編譯時嵌入 RPATH(推薦)
aarch64-linux-gnu-g++ main.cpp -lfoo -o myapp \
-Wl,-rpath,/opt/myapp/lib \
-L/opt/myapp/lib
# 方法 4:使用 $ORIGIN 實現(xiàn)相對路徑 RPATH(部署更靈活)
aarch64-linux-gnu-g++ main.cpp -lfoo -o myapp \
-Wl,-rpath,'$ORIGIN/lib' # $ORIGIN = 可執(zhí)行文件所在目錄
# 這樣只要 libfoo.so 在 myapp 同級的 lib/ 目錄下就能找到
5.8.5 子場景 E:ARM32 與 ARM64 混淆
場景:目標(biāo)設(shè)備是 32 位 ARM(armhf),但交叉編譯時用了 64 位工具鏈,或反之。
復(fù)現(xiàn):
# 目標(biāo)機(jī)是 ARM32,但用 ARM64 工具鏈編譯 aarch64-linux-gnu-g++ -shared -fPIC -o libfoo.so foo.cpp # 部署到 ARM32 目標(biāo)機(jī) $ ./myapp ./myapp: error while loading shared libraries: ./libfoo.so: wrong ELF class: ELFCLASS64 # 反過來:目標(biāo)機(jī)是 ARM64,但用 ARM32 工具鏈編譯 arm-linux-gnueabihf-g++ -shared -fPIC -o libfoo.so foo.cpp $ ./myapp ./myapp: error while loading shared libraries: ./libfoo.so: wrong ELF class: ELFCLASS32
排查:
# 確認(rèn) .so 的架構(gòu) $ readelf -h libfoo.so | grep -E 'Class|Machine' Class: ELF64 # 64 位 Machine: AArch64 # ARM64 # 確認(rèn)目標(biāo)機(jī)的架構(gòu) $ uname -m armv7l # ARM32 # 或者 $ dpkg --print-architecture armhf # ARM32 硬浮點
修復(fù):
# ARM32 (armhf) 交叉編譯 arm-linux-gnueabihf-g++ -shared -fPIC -o libfoo.so foo.cpp # ARM64 (aarch64) 交叉編譯 aarch64-linux-gnu-g++ -shared -fPIC -o libfoo.so foo.cpp # 始終在部署前驗證架構(gòu)一致性 readelf -h libfoo.so | grep Machine # Machine: ARM → ARM32 # Machine: AArch64 → ARM64
5.8.6 交叉編譯場景排查速查
| 排查項 | 命令 | 說明 |
|---|---|---|
| 確認(rèn) .so 架構(gòu) | readelf -h libfoo.so | grep Machine | ARM = 32位,AArch64 = 64位 |
| 確認(rèn) ELF 類 | readelf -h libfoo.so | grep Class | ELF32 = 32位,ELF64 = 64位 |
| 確認(rèn)依賴庫(交叉工具) | aarch64-linux-gnu-readelf -d libfoo.so | grep NEEDED | 不依賴 ldd |
| 確認(rèn)符號版本需求 | aarch64-linux-gnu-objdump -T libfoo.so | grep '\*UND\*' | 查看需要的符號版本 |
| 確認(rèn) glibc 版本需求 | aarch64-linux-gnu-readelf -V libfoo.so | grep GLIBC | 對比目標(biāo)機(jī) libc 版本 |
| 目標(biāo)機(jī) glibc 版本 | ldd --version 或 strings /lib/libc.so.6 | grep GLIBC | 在 ARM 目標(biāo)機(jī)上執(zhí)行 |
| 目標(biāo)機(jī) libstdc++ 版本 | strings /usr/lib/libstdc++.so.6 | grep GLIBCXX | 在 ARM 目標(biāo)機(jī)上執(zhí)行 |
| 檢查 RPATH | aarch64-linux-gnu-readelf -d myapp | grep -E 'RPATH|RUNPATH' | 編譯時嵌入的搜索路徑 |
6. 預(yù)防措施與最佳實踐
編譯選項
| 選項 | 作用 |
|---|---|
-fPIC | 生成位置無關(guān)代碼,編譯動態(tài)庫必須 |
-rdynamic | 將所有符號導(dǎo)出到動態(tài)符號表,主程序使用 dlopen 時必需 |
-Wl,-rpath,<path> | 在可執(zhí)行文件中嵌入運行時庫搜索路徑 |
-Wl,--no-undefined | 鏈接時檢查所有符號是否有定義,盡早發(fā)現(xiàn)問題 |
-Wl,--as-needed | 只鏈接實際需要的庫,減少不必要的依賴 |
-z,defs | 等同于 --no-undefined,創(chuàng)建共享庫時檢查未定義符號 |
代碼規(guī)范
// 1. C/C++ 兼容的頭文件寫法
#ifdef __cplusplus
extern "C" {
#endif
void c_api_function(void);
#ifdef __cplusplus
}
#endif
// 2. 導(dǎo)出宏(控制符號可見性)
#ifdef MYLIB_EXPORTS
#define MYLIB_API __attribute__((visibility("default")))
#else
#define MYLIB_API
#endif
MYLIB_API void exported_function(void);
// 3. 隱藏不需要導(dǎo)出的符號
__attribute__((visibility("hidden"))) void internal_function(void);CMake 配置
# 設(shè)置 -fPIC
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
# 設(shè)置 RPATH
set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/lib")
set(CMAKE_BUILD_RPATH "${CMAKE_BINARY_DIR}")
# 鏈接時檢查未定義符號
set(CMAKE_SHARED_LINKER_FLAGS "-Wl,--no-undefined")
# 使用 --whole-archive
target_link_libraries(myapp
PRIVATE
"-Wl,--whole-archive"
plugins
"-Wl,--no-whole-archive"
)
7. 速查表
| 命令 | 用途 |
|---|---|
nm -CD libfoo.so | 查看動態(tài)庫的符號(demangled) |
nm -CD libfoo.so | grep ' U ' | 查看庫中未定義的符號 |
nm -CD libfoo.so | grep ' T ' | 查看庫中導(dǎo)出的函數(shù) |
readelf -s libfoo.so | 查看完整符號表 |
readelf -d libfoo.so | grep NEEDED | 查看庫的運行時依賴 |
readelf -V libfoo.so | 查看符號版本信息 |
readelf -S libfoo.so | grep -E 'symtab|dynsym' | 檢查符號表段是否存在 |
objdump -T libfoo.so | 查看動態(tài)符號表(含版本) |
ldd ./myapp | 查看程序依賴的動態(tài)庫 |
c++filt _Z3foov | C++ 符號 demangle |
LD_DEBUG=symbols ./myapp 2>&1 | 運行時符號查找調(diào)試 |
LD_DEBUG=libs ./myapp 2>&1 | 運行時庫加載調(diào)試 |
LD_PRELOAD=libfix.so ./myapp | 強(qiáng)制優(yōu)先加載指定庫 |
strip --strip-all -o libfoo_stripped.so libfoo.so | 去除調(diào)試符號(保留動態(tài)符號) |
readelf -h libfoo.so | grep Machine | 確認(rèn) .so 的目標(biāo)架構(gòu)(ARM/AArch64/x86) |
aarch64-linux-gnu-readelf -d libfoo.so | grep NEEDED | 交叉編譯場景下查看依賴庫(不依賴 ldd) |
aarch64-linux-gnu-objdump -T libfoo.so | grep '\*UND\*' | 交叉編譯場景下查看未定義符號及版本 |
aarch64-linux-gnu-readelf -V libfoo.so | 交叉編譯場景下查看符號版本需求 |
總結(jié):排查 undefined symbol 的核心思路是三步走——確認(rèn)要找什么符號、確認(rèn)誰應(yīng)該提供這個符號、確認(rèn)提供者是否被正確加載。熟練掌握 nm、readelf、LD_DEBUG 三大工具,結(jié)合本文的排查流程,絕大多數(shù)符號問題都能快速定位。
以上就是Linux動態(tài)庫.so找不到符號表的排查指南的詳細(xì)內(nèi)容,更多關(guān)于Linux動態(tài)庫.so找不到符號表的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
ubuntu18.04 安裝qt5.12.8及環(huán)境配置的詳細(xì)教程
這篇文章主要介紹了ubuntu18.04 安裝qt5.12.8及環(huán)境配置的教程,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-05-05
CentOS 7.x編譯安裝Nginx1.10.3+MySQL5.7.16+PHP5.2 5.3 5.4 5.5 5.6
這篇文章主要介紹了CentOS 7.x編譯安裝Nginx1.10.3+MySQL5.7.16+PHP5.2 5.3 5.4 5.5 5.6 7.0 7.1多版本全能環(huán)境,需要的朋友可以參考下2018-01-01
Centos7如何備份和還原Redis數(shù)據(jù)的方法
這篇文章主要介紹了Centos7如何備份和還原Redis數(shù)據(jù)的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-06-06
Linux使用iostat命令監(jiān)控系統(tǒng)磁盤I/O性能和CPU使用情況
iostat(Input/Output Statistics)是一個用于監(jiān)控系統(tǒng)磁盤I/O(輸入/輸出)性能和CPU使用情況的強(qiáng)大工具,本文給大家介紹了Linux如何使用iostat命令監(jiān)控系統(tǒng)磁盤I/O性能和CPU使用情況,需要的朋友可以參考下2025-10-10

