解決 Linux 上 C/C++ 的 undefined symbol 或 undefined reference

fcamel
fcamel的程式開發心得
8 min readAug 17, 2018

使用 C/C++ 程式分成三個步驟: 編譯 (compile) → 連結 (link) → 執行 (載入 symbol)。了解每個階段的行為,才知道如何處理該階段的 undefined symbol。

基本概念

  1. 編譯 .c / .cpp 為 .o (object file) 時,需要提供 header 檔。header 不在預設搜尋路徑時,可用 -I DIR增加搜尋目錄。事實上,在編譯單一檔案時,gcc/g++ 不在意 symbol 是否存在,有宣告它就信了,所以關鍵是有引入正確的 header。這也是可分散編譯的原因 (如 distcc ),因為程式碼編譯成 .o 檔時,沒有相依性。
  2. 用 static linker (ld.bfd、gold、lld) 將 *.o 連結成 dynamic library 或執行檔時,通常需要確認找得到所有的 symbol,需要提供要連結的 library。用 -L DIR 指定目錄位置,用 -l 指定函式庫名稱。
  3. 執行的時候,dynamic linker (又稱 runtime linker) 會載入 shared library 讀出 symbol。前一個步驟只是檢查是否有 symbol。檢查通過也連結成 executable 或 shared library 後,若執行時路徑不對,或檔案不見了,仍會在執行期找不到 symbol。

常用工具

  1. 查看 BINARY 內含有或缺少的 symbol

nm -D BINARY 列出 BINARY (object file、static/dynamic library、executable) dynamic section 的 symbol:

  • T/t 分別表示 global/local symbol。
  • U 表示 undefined symbol。

nm -Du BINARY 只會顯示使用到但未定義的 symbol。

2. 用 pkg-config 列出編譯/連結用的參數

系統內安全的開發套件,可用 pkg-config --cflags LIB 列出編譯時使用 LIB 需要的參數,pkg-config --libs LIB 列出連結時使用的參數。

以 Gtk+ 為例,安裝開發套件後,用 dpkg -L 找出 pkg-config 的設定檔:

$ dpkg -L libgtk-3-dev:amd64 | grep "pc$"
/usr/lib/x86_64-linux-gnu/pkgconfig/gtk+-unix-print-3.0.pc
/usr/lib/x86_64-linux-gnu/pkgconfig/gdk-x11-3.0.pc
/usr/lib/x86_64-linux-gnu/pkgconfig/gdk-broadway-3.0.pc
/usr/lib/x86_64-linux-gnu/pkgconfig/gdk-wayland-3.0.pc
/usr/lib/x86_64-linux-gnu/pkgconfig/gtk+-broadway-3.0.pc
/usr/lib/x86_64-linux-gnu/pkgconfig/gtk+-wayland-3.0.pc
/usr/lib/x86_64-linux-gnu/pkgconfig/gtk+-mir-3.0.pc
/usr/lib/x86_64-linux-gnu/pkgconfig/gdk-mir-3.0.pc
/usr/lib/x86_64-linux-gnu/pkgconfig/gtk+-3.0.pc
/usr/lib/x86_64-linux-gnu/pkgconfig/gdk-3.0.pc
/usr/lib/x86_64-linux-gnu/pkgconfig/gtk+-x11-3.0.pc

想使用 Gtk+-3.0 的話,列出編譯時用到的參數:

$ pkg-config --cflags gtk+-3.0
-pthread -I/usr/include/gtk-3.0 -I/usr/include/at-spi2-atk/2.0 -I/usr/include/at-spi-2.0 -I/usr/include/dbus-1.0 -I/usr/lib/x86_64-linux-gnu/dbus-1.0/include -I/usr/include/gtk-3.0 -I/usr/include/gio-unix-2.0/ -I/usr/include/mirclient -I/usr/include/mircore -I/usr/include/mircookie -I/usr/include/cairo -I/usr/include/pango-1.0 -I/usr/include/harfbuzz -I/usr/include/pango-1.0 -I/usr/include/atk-1.0 -I/usr/include/cairo -I/usr/include/pixman-1 -I/usr/include/freetype2 -I/usr/include/libpng12 -I/usr/include/gdk-pixbuf-2.0 -I/usr/include/libpng12 -I/usr/include/glib-2.0 -I/usr/lib/x86_64-linux-gnu/glib-2.0/include

列出連結時用到的參數:

$ pkg-config --libs gtk+-3.0
-lgtk-3 -lgdk-3 -lpangocairo-1.0 -lpango-1.0 -latk-1.0 -lcairo-gobject -lcairo -lgdk_pixbuf-2.0 -lgio-2.0 -lgobject-2.0 -lglib-2.0

解決編譯原始碼的問題 (compiler 的工作)

若使用的是系統裝的函式庫 X,裝上 X-dev 後會有 header,然後看有沒有帶 pkg-config 的設定檔。有的話用 pkg-config 可以得知該下什麼參數找到 header。沒有的話,用 dpkg -L X-dev | grep include 找出 header 位置。若是另外下載的程式碼,在裡面找找 header 也沒什麼困難。

下面列一些相關的資料。

1. 若是網路上看到別人的範例,裡面有正確的 include 位置,可以用 header 位置找出需要安裝的套件,像是使用 GDK event 要 include “gdk/gdkevents.h”:

$ apt-file search gdk/gdkevents.h
libgtk-3-dev: /usr/include/gtk-3.0/gdk/gdkevents.h
libgtk2.0-dev: /usr/include/gtk-2.0/gdk/gdkevents.h

2. 若使用的函式有附 man page,man page 可能會提到需要的編譯/連結參數。以 man 3 sqrt 為例:

SQRT(3)             Linux Programmer's Manual            SQRT(3)NAME
sqrt, sqrtf, sqrtl - square root function
SYNOPSIS
#include <math.h>
double sqrt(double x);
float sqrtf(float x);
long double sqrtl(long double x);
Link with -lm.Feature Test Macro Requirements for glibc (see fea‐
ture_test_macros(7)):
sqrtf(), sqrtl():
_BSD_SOURCE || _SVID_SOURCE || _XOPEN_SOURCE >= 600
|| _ISOC99_SOURCE || _POSIX_C_SOURCE >= 200112L;
or cc -std=c99
  • #include 表示需要的 header。
  • 接著是函式的 signature。
  • Link with -lm 表示使用編譯時要加 -lm,linker 才會找到 libm.so。
  • Feature Test Macro 是 UNIX 跨平台用的一套規範,見 man feature_test_macros 了解作用,以及 glibc 認得的類型。feature test macro 要定義在檔案的開頭或直接用 gcc -D 定義。

3. 可用 gcc -E 列出前置處理 (cpp) 展開 #include#define#if … 等指令後的程式碼,查看是否有引入正確的檔案。細節說明見《gcc 展開前置處理的技巧》

解決連結 shared library / executable 的問題 (static linker 的工作)

類似處理編譯的作法,用系統附的套件 X 時,先看有沒有 pkg-config 的設定檔。沒有的話用 dpkg -L X-dev 找位置。

要注意的是 static library 和 shared library 行為不同。static library 只是一群 object file 的集合。它沒有記額外資訊 (像是需要那些 library)。若執行檔 X 用到 static library A,而 A 用到 library B。編 X 時,要加上 -lA-lB 的參數。編 X 的部份要知道它用到的 static library 有那些相依性, 而不是 A 自己會搞定自己的相依性。若 A 是 shared library,在編出 A 的時候,會記錄 A 需要 B,編 X 時不需另外指定 -lB

更多細節見 《Linux 編譯 shared library 的方法和注意事項》

解決執行期的問題 (dynamic / runtime linker 的工作)

可在執行時設環境變數 LD_DEBUG=libs,symbols 然後執行程式,了解載入的過程。除錯的細節見《Linux 執行時尋找 symbol 的流程以及 shared library 相關知識》

相關資料

--

--