Symbols are how ELF files name their functions and variables. When you call printf(), the linker resolves that name to an address by looking it up in a symbol table. Without symbols, there is no way to reference code or data across compilation units.
Understanding symbol tables lets you debug linking errors, inspect binary contents with nm, and understand how shared libraries export their interfaces.
A symbol table is a lookup dictionary. Each entry maps a name (like "main" or "printf") to:
The names themselves live in a separate string table (.strtab). Each symbol entry holds an offset into that string table.
ELF has two symbol tables:
| Table | Section | Purpose | Used By |
|---|---|---|---|
| .symtab | SHT_SYMTAB | Full symbol table (all symbols) | Static linker, debugger |
| .dynsym | SHT_DYNSYM | Dynamic symbols (exported/imported only) | Dynamic linker (ld.so) |
Each entry is an Elf64_Sym structure (24 bytes on 64-bit):
| Field | Type | Size | Description |
|---|---|---|---|
| st_name | Elf64_Word | 4 bytes | Offset into string table |
| st_info | unsigned char | 1 byte | Binding (high 4 bits) + Type (low 4 bits) |
| st_other | unsigned char | 1 byte | Visibility (low 2 bits) |
| st_shndx | Elf64_Half | 2 bytes | Section index (0 = undefined) |
| st_value | Elf64_Addr | 8 bytes | Symbol value (address or offset) |
| st_size | Elf64_Xword | 8 bytes | Symbol size (0 if unknown) |
The st_info field packs two values: binding and type. Use ELF64_ST_BIND(info) to extract binding, ELF64_ST_TYPE(info) for type.
$ nm -n /bin/ls | head -20
w __cxa_finalize
w __gmon_start__
w _ITM_deregisterTMCloneTable
w _ITM_registerTMCloneTable
w __cxa_atexit
U __errno_location
U __fpending
U __freading
U __malloc_hook
U __memcpy_chk
U __stack_chk_fail
U abort
U access
U backtrace
00000000000030a0 T .annobin_...
0000000000003120 T _start
0000000000003150 T .annobin_...
0000000000003190 T __do_global_dtors_auxnm -n sorts by address. U means "undefined" (imported from another library). T means "in text section" (defined here).
The .dynsym is a subset — only symbols needed for dynamic linking:
$ readelf -sW /bin/ls | grep FUNC | head -10
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __ctype_b_loc
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __errno_location
5: 0000000000000000 0 FUNC GLOBAL DEFAULT UND free
8: 0000000000000000 0 FUNC GLOBAL DEFAULT UND malloc
15: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf
3: 0000000000003120 108 FUNC GLOBAL DEFAULT 13 _start
4: 0000000000003190 186 FUNC GLOBAL DEFAULT 13 __do_global_dtors_aux
6: 0000000000003260 1162 FUNC GLOBAL DEFAULT 13 main# See all symbols in your program nm -n ./my_program # Only defined symbols (not imports) nm --defined-only ./my_program # Demangle C++ names nm -C ./my_program_cpp # See the raw symbol table structure readelf -sW ./my_program
What is the difference between .symtab and .dynsym?
If a symbol has st_shndx = SHN_UNDEF (0), what does that mean?
Each symbol has a binding (local, global, weak) and a visibility. These determine how the linker resolves symbols across object files — that is what the next two concepts cover.
Which type of symbol does this readelf output represent?
8: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_atexit
What does this symbol table entry tell you?
42: 0000000000401130 23 FUNC GLOBAL DEFAULT 13 main