Question
How can I list the symbols exported by a Linux shared library (.so file)? If possible, how can I determine where each symbol came from, such as whether it was included from a static library during linking?
I am using GCC 4.0.2.
Short Answer
You will learn the difference between symbols stored in an ELF shared library and symbols exported for dynamic linking. You will use nm, readelf, and objdump to inspect a .so file, and learn why the original object file or static archive is usually not recoverable from the final library alone.
Concept
A Linux .so file is usually an ELF shared object. It contains symbol tables that describe names associated with functions, variables, and other linker-visible entities.
The key distinction is:
- Defined symbol: code or data is implemented inside the shared library.
- Undefined symbol: the shared library needs the symbol from another library or executable at runtime.
- Dynamic/exported symbol: a symbol placed in the dynamic symbol table so that the dynamic linker can resolve it for other objects.
A shared library can contain many symbols that are not exported. For example, compilation may leave local or non-dynamic symbols in the normal ELF symbol table, while only selected symbols appear in .dynsym, the dynamic symbol table.
This matters because exported symbols are part of a library's ABI (Application Binary Interface). Removing or changing one can break programs that were linked against an earlier version of the library.
A final .so normally records symbol names, addresses, types, bindings, and sometimes versions. It does not generally record a reliable mapping from every symbol back to the .o file or .a archive member that supplied it. That provenance must usually be captured while linking.
Mental Model
Think of a shared library as a building with a public directory.
- The normal symbol table is the building's internal inventory. It may list many rooms, staff members, and equipment.
- The dynamic symbol table is the directory in the lobby. It lists names that visitors can use when they need to find a function or variable.
- A defined symbol is a service provided inside this building.
- An undefined symbol is a service for which this building calls another building.
After construction, the directory tells you what services are available, but it usually does not say which construction supplier delivered each part. To know whether a static archive supplied a function, inspect the link process or its map file.
Syntax and Examples
Use nm first when you want a concise list of symbols exported by a shared object:
nm -D --defined-only libexample.so
-Dreads the dynamic symbol table, which contains symbols relevant to dynamic linking.--defined-onlyexcludes symbols that the library references but does not define.
Example output:
0000000000001139 T add
0000000000004010 D library_version
Here:
addis markedT: a globally visible symbol in the text (machine-code) section, usually a function.library_versionis markedD: initialized global data.
To see dynamic symbols with more ELF-specific detail, use readelf:
readelf -Ws libexample.so
To focus on globally visible defined dynamic symbols:
readelf --dyn-syms --wide libexample.so
readelf output includes columns such as:
Type:FUNC,OBJECT,NOTYPE, and others.Bind: commonlyGLOBAL,WEAK, orLOCAL.Ndx: the section index.UNDmeans undefined.Name: the symbol name, possibly with a version suffix.
Another useful command is:
objdump -T libexample.so
objdump -T displays the dynamic symbol table and can be convenient for checking symbol versions and section information.
To inspect all symbols, including non-exported symbols when the library has not been stripped:
nm -C libexample.so
-C demangles C++ names. Do not use this result alone as a list of exported API: it is broader than the dynamic symbol table.
Step by Step Execution
Suppose this is the library source:
/* example.c */
int public_add(int left, int right) {
return left + right;
}
static int private_double(int value) {
return value * 2;
}
int public_calculate(int value) {
return private_double(value);
}
Build it as a shared library:
gcc -fPIC -shared -o libexample.so example.c
Now run:
nm -D --defined-only libexample.so
Typical relevant output includes:
0000000000001109 T public_add
000000000000112c T public_calculate
Step by step:
gcc -fPICgenerates position-independent code, which is normally required for code placed in a shared library.-sharedasks the linker to createlibexample.so.public_addandpublic_calculatehave external linkage, so they can appear in the dynamic symbol table.private_doubleis declaredstatic, giving it internal linkage. It is an implementation detail and is not exported.nm -Dreads only the dynamic symbols, so it shows the functions intended to be visible to dynamic linkers rather than every internal compiler-generated symbol.
To see symbols the library depends on but does not provide, omit --defined-only:
nm -D libexample.so | grep ' U '
A U means the symbol is undefined in this library and must be resolved elsewhere.
Real World Use Cases
- Checking a library API: Verify that a plugin or SDK exports the functions documented for users.
- Debugging linker failures: If an application reports
undefined reference to ...or runtime loader errors, inspect whether the required symbol is actually exported. - ABI compatibility checks: Compare dynamic symbol lists between library versions before releasing an update.
- Plugin loading: Confirm that a plugin exports an expected entry point such as
plugin_init. - Packaging and deployment: Diagnose why software runs on one Linux system but fails on another because of a missing symbol version.
- Security and API minimization: Check whether internal functions were accidentally exposed as public entry points.
Real Codebase Usage
In real C and C++ projects, symbol inspection is often part of the build and release workflow.
Validate an expected public API
A shell check can fail a build when a required function is absent:
nm -D --defined-only libwidget.so | grep -q ' widget_create$' \
|| { echo 'widget_create is not exported'; exit 1; }
Hide implementation details intentionally
Libraries commonly compile with hidden visibility and explicitly mark public API functions:
#if defined(__GNUC__)
#define API __attribute__((visibility("default")))
#else
#define API
#endif
API int widget_create(void);
Then build with:
gcc -fPIC -fvisibility=hidden -shared -o libwidget.so widget.c
This reduces accidental exports and makes the intended API clearer.
Capture symbol origin at link time
When creating the shared library, ask the linker to produce a map file:
gcc -shared -o libwidget.so widget.o libhelpers.a \
-Wl,-Map,libwidget.map
The map file can show which archive member, such as libhelpers.a(parse.o), was selected to satisfy a symbol. Keep this file as build output; it is much more useful for provenance than inspecting the completed .so later.
Trace archive processing
For link diagnostics, GNU ld can print processed input files:
gcc -shared -o libwidget.so widget.o libhelpers.a -Wl,-t
For a specific symbol, use a linker trace option where supported:
gcc -shared -o libwidget.so widget.o libhelpers.a \
-Wl,--trace-symbol,helper_parse
These options describe the link operation, so save their output in CI logs when symbol origin matters.
Common Mistakes
Listing every symbol and calling them exports
nm libexample.so
This can show the ordinary symbol table, including internal symbols, if it exists. It is not necessarily the list of symbols available to other dynamically linked programs.
Use this instead for exports:
nm -D --defined-only libexample.so
Treating U as an exported function
U printf
U means undefined in this library. The library uses printf; it does not provide it. Use --defined-only when you only want provided symbols.
Assuming a symbol came from a particular static library
After linking, code from a static archive is copied into the final shared object. The final .so usually does not preserve an authoritative label such as “this function came from libmath.a(foo.o).”
Avoid guessing from names. Generate a link map during the build:
gcc -shared -o libexample.so input.o libsupport.a -Wl,-Map,link.map
Confusing a static library with a static C function
libsupport.ais a static archive containing object files.static int helper(...)gives a C declaration internal linkage.
They are different uses of the word static.
Forgetting C++ name mangling
C++ function names are encoded with parameter type information. Use demangling for readability:
nm -D -C --defined-only libexample.so
If C code must call a C++ library API, the C++ header may need extern "C" for a C-compatible symbol name.
Relying on a non-stripped symbol table
A release library may be stripped, so nm libexample.so can provide less information than a development build. The dynamic symbol table must remain available for normal dynamic linking, making nm -D or readelf --dyn-syms the appropriate starting point.
Comparisons
| Tool or option | Best use | What it shows |
|---|---|---|
nm -D --defined-only libx.so | Quick export list | Defined entries in the dynamic symbol table |
nm -D -C libx.so | Readable C++ inspection | Dynamic symbols with demangled names; includes undefined symbols unless filtered |
readelf --dyn-syms --wide libx.so | Detailed ELF inspection | Dynamic symbols, type, binding, section index, size, and names |
objdump -T libx.so | Dynamic symbol and version review | Dynamic symbol table in an alternative format |
nm libx.so | Investigating unstripped internals | Ordinary symbol table, which may include local and non-exported symbols |
Linker map (-Wl,-Map,file.map) | Finding symbol origin | Link-time input and placement information, often including archive members |
Symbol marker in nm | Meaning |
|---|---|
T or t | Code/text symbol; uppercase is global, lowercase is local |
D or d | Initialized data; uppercase is global, lowercase is local |
B or b | Uninitialized data (BSS) |
R or r | Read-only data |
U | Undefined here; supplied by another linked object at runtime or load time |
W or w | Weak symbol |
The exact letter is useful context, but visibility in the dynamic symbol table is what matters when determining whether a symbol is available to dynamic consumers.
Cheat Sheet
# Defined symbols exported through the dynamic symbol table
nm -D --defined-only libname.so
# Same idea, with C++ names demangled
nm -D -C --defined-only libname.so
# Show all dynamic symbols, including dependencies marked U
nm -D libname.so
# Detailed dynamic symbol information
readelf --dyn-syms --wide libname.so
# Alternative dynamic symbol display
objdump -T libname.so
# Create a link map while building
cc -shared -o libname.so objects.o libdependency.a \
-Wl,-Map,libname.map
# Trace linker input files while building
cc -shared -o libname.so objects.o libdependency.a -Wl,-t
Rules to remember:
- Use
-Dto inspect the dynamic symbol table. - Use
--defined-onlyto exclude imported dependencies. Umeans “used but not defined by this shared object.”-Cmakes C++ names readable.- A completed
.sonormally cannot reliably reveal which.amember supplied each symbol. - Generate a linker map during the build when provenance is required.
FAQ
How do I list only exported functions in a .so file?
Start with:
nm -D --defined-only libname.so
This may also include exported data objects. Use readelf --dyn-syms --wide when you need to identify entries whose type is FUNC.
What does U mean in nm output?
U means the symbol is undefined in that file. The shared library references it, but another object—often libc or another shared library—must provide it.
Why does nm libname.so show more names than nm -D libname.so?
Without -D, nm reads the ordinary symbol table when present. That table can contain internal, local, debugging-related, or non-exported names. -D reads the dynamic symbol table used for dynamic linking.
Can I find which static library exported a symbol after the .so was built?
Usually not reliably from the final .so alone. Static archive members are incorporated into the final output. Produce a linker map with -Wl,-Map,output.map during the link to capture this information.
How do I see C++ function names clearly?
Use nm -C or c++filt:
nm -D -C --defined-only libname.so
Does GCC version change the inspection commands?
These commands inspect ELF files and are supplied by binutils, such as nm, readelf, and objdump. Their exact available options can vary by binutils version, but the nm -D approach is long-established on GNU/Linux.
Is every global symbol exported from a shared library?
No. A symbol must be included in the dynamic symbol table to be available for dynamic resolution. Visibility settings, linker version scripts, and linker options can restrict exports.
Mini Project
Description
Build a small C shared library with one public function and one internal helper, then inspect its exported dynamic symbols. The project demonstrates the difference between a library's public dynamic interface and its implementation details.
Goal
Create libgreeting.so and verify that only the intended public function appears in its exported symbol list.
Requirements
- Create a C source file that defines
greeting_lengthand an internal helper function. - Build the source as a shared library named
libgreeting.so. - Use
nmto list defined dynamic symbols in the library. - Confirm that the public function is listed and the internal helper is not.
- Generate a linker map file during the build.
Keep learning
Related questions
Advantages of Brace Initialization in C++
Learn why C++ brace initialization is often clearer and safer than other object initialization styles, with examples and common pitfalls.
Basic Rules and Idioms for Operator Overloading in C++
Learn the core rules, syntax, and common idioms for operator overloading in C++, including member vs non-member operators.
C++ Aggregates, Trivial Types, Trivially Copyable Types, and PODs Explained
Learn what aggregates, trivial types, trivially copyable types, and PODs mean in C++, how they differ, and why they matter.