The Challenge of Compiled Code
When software developers write code in C or C++, they define variables, write functions, and link external libraries. When that code is compiled, the compiler transforms the human-readable text into a binary object file (.o), a shared library (.so), or a final executable binary. During this process, the names of those functions and variables are converted into numerical memory addresses, making the compiled file completely unreadable in a standard text editor.
However, unless the developer explicitly “stripped” the binary before distributing it, the compiled file still contains a hidden symbol table. This table acts as a map, linking the raw memory addresses back to the original function names.
The nm (Name List) command is a specialized Linux utility that extracts and decodes this symbol table. It allows system administrators, reverse engineers, and developers to peek inside a compiled binary and see exactly what functions it contains.
Step 1: Listing All Symbols
To inspect a compiled object file or a shared library, simply type nm followed by the file path.
For example, if you want to inspect a compiled object file named math_functions.o, run:
nm math_functions.o
The output will be a three-column list:
0000000000000000 T calculate_sum
U printf
0000000000000040 T subtract_values
Step 2: Understanding the Output Columns
The output of nm is highly dense. You must understand the three columns to interpret the data:
- Column 1 (Value): The hexadecimal memory address where the symbol is located. (If the symbol is undefined, this column will be completely blank).
- Column 2 (Type): A single letter representing the type of symbol. This is the most important column.
- Column 3 (Name): The actual name of the function or variable (e.g.,
calculate_sum).
Step 3: Decoding the Symbol Types
The single letter in the middle column dictates exactly what the function is doing. The letters are case-sensitive. A lowercase letter means the symbol is local (internal to the file), while an uppercase letter means the symbol is global (accessible to other programs).
The most common and important symbol types are:
- T (Text): The symbol represents executable code (a function) that is defined inside this specific file. In our example above,
calculate_sumis a “T”, meaning the actual math calculation code lives right here in this binary. - U (Undefined): The file calls this function, but the code for it does not exist in this file. It must be pulled from an external library at runtime. In our example,
printfis a “U” because it is a standard C library function, not something the developer wrote themselves. - D (Initialized Data): The symbol is a global variable that has been assigned a specific value (e.g.,
int max_users = 100;). - B (BSS Data): The symbol is an uninitialized global variable (e.g.,
int user_count;).
Step 4: Troubleshooting “Symbol Not Found” Errors
The most practical use of nm for system administrators is troubleshooting compilation or runtime errors. If you attempt to run a program and Linux crashes with an error stating “undefined symbol: _ZN10Networking9ConnectEPKc”, it means the program is trying to call a function that is missing from its linked libraries.
You can use nm combined with grep to search the libraries in your /usr/lib directory to find out which library actually contains the missing function:
nm -D /usr/lib/libnetworking.so | grep Connect
(The -D flag tells nm to specifically look at the dynamic symbols of a shared .so library).
If the output shows a T next to the function name, you have successfully found the correct library that your crashing program needs.