FForge
← Labsbigcode-02Large codebases●●○○

ELF forensics — what kind of binary is this, and where are its symbols?

bigcode/02_elf_forensics.mdneeds: `file`, `readelf`, `nm`, `ldd`, `ar`, `size`, `objdump`
evidence to leave with
Distro clang = 157 KB launcher + 145 MB libLLVM.so; local clang = 209 MB static with 207k symbols; identification checklist filled for both

BIGCODE LAB #02: ELF forensics — what kind of binary is this, and where are its symbols?

TRACK: bigcode | DIFFICULTY: 2 | NEEDS: file, readelf, nm, ldd, ar, size, objdump

What this teaches

Before profiling or debugging someone else's binary you must know its shape: executable or shared object, PIE or fixed address, static or dynamic, stripped or not, and where its code really lives. Two clangs on this machine make a perfect contrast: Fedora's /usr/bin/clang-21 and the locally built $LLVM_BUILD/bin/clang.

STEP 1 — file says most of it

$ file -L /usr/bin/clang-21 $LLVM_BUILD/bin/clang /lib64/libLLVM.so.21.1 /usr/lib64/libc.so.6 /usr/bin/ls
/usr/bin/clang-21:              ELF 64-bit LSB pie executable, x86-64, dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2 ...
.../build/bin/clang:            ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2 ...
/lib64/libLLVM.so.21.1:         ELF 64-bit LSB shared object, x86-64, dynamically linked, BuildID[sha1]=621e6ac6...
/usr/lib64/libc.so.6:           ELF 64-bit LSB shared object, x86-64, dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2 ...
/usr/bin/ls:                    ELF 64-bit LSB pie executable, x86-64, dynamically linked ...
  • pie executable = position-independent, loaded at a random base each run (ASLR); addresses in a profile or a crash are offsets from that base. /usr/bin/clang-21 and ls are PIE (distro default).
  • executable (no "pie") = fixed load address (0x400000 here); the local clang and the lab binaries are built this way by this clang's defaults (-fno-pie is the default for the local build's linker invocation). readelf -h shows Type: EXEC vs DYN.
  • shared object = a library, or a PIE (both are DYN); libc.so.6 even has an interpreter because it can be executed to print its version.
  • "dynamically linked" tells you there is a NEEDED list; "statically linked" would mean everything is inside.

STEP 2 — What does it depend on, and how big is it really?

$ ls -la /usr/bin/clang-21 | awk '{print $5}'
157400
$ ldd /usr/bin/clang-21 | grep -E "clang-cpp|libLLVM"
        libclang-cpp.so.21.1 => /lib64/libclang-cpp.so.21.1
        libLLVM.so.21.1 => /lib64/libLLVM.so.21.1
$ ls -la /lib64/libLLVM.so.21.1 | awk '{print $5}'
145147968
$ readelf -d $LLVM_BUILD/bin/clang | grep NEEDED
 0x0000000000000001 (NEEDED)             Shared library: [libzstd.so.1]
 0x0000000000000001 (NEEDED)             Shared library: [libstdc++.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [libm.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [libgcc_s.so.1]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [ld-linux-x86-64.so.2]
$ size $LLVM_BUILD/bin/clang | tail -1
   text	   data	    bss	    dec	    hex	filename
208996649	3870420	 888480	213755549	cbda69d	.../build/bin/clang

Fedora's clang-21 is a 157 KB launcher: the compiler is in libclang-cpp.so and the 145 MB libLLVM.so. The local clang is a 209 MB monolith that links every LLVM component statically and needs only the system C/C++ runtime. Same source, opposite packaging, and it matters for everything downstream: symbols, startup cost (lab 04: the loader relocating 145 MB of shared library costs real milliseconds), profiling attribution (samples land in libLLVM.so vs in clang), and deployment.

STEP 3 — Where are the symbols?

$ nm --defined-only /usr/bin/clang-21 | grep -c " [Tt] "
0
$ nm -C --defined-only $LLVM_BUILD/bin/clang | grep -c " [Tt] "
207389
$ nm -D --defined-only /lib64/libLLVM.so.21.1 | grep -c " T "
28916
$ nm -D --defined-only /lib64/libclang-cpp.so.21.1 | grep -c " T "
26985
  • The distro launcher has no symbol table (nm finds 0 text symbols): stripped, as packages are. Its debug info lives in a separate debuginfo package, fetchable on demand via debuginfod (DEBUGINFOD_URLS is set on this box: gdb and perf can download symbols for distro binaries without root).
  • The local clang has 207,389 text symbols: an unstripped Release build (symbols but no DWARF; lab 03).
  • The distro libraries have their exported symbols in .dynsym (nm -D): ~29k in libLLVM. perf can name any sample that lands in an exported function; internal (hidden-visibility) functions show as the nearest exported symbol or as an address unless debuginfo is present.

Which tool for which table: nm reads .symtab (full, stripped away by strip); nm -D reads .dynsym (what the dynamic linker needs, survives stripping); readelf -s shows both with section indexes; nm -C/c++filt demangle.

STEP 4 — Inside a static archive, and the stripped/debug pair

$ ar t $LLVM_BUILD/lib/libLLVMSupport.a | head -3; ar t .../libLLVMSupport.a | wc -l
blake3.c.o
blake3_dispatch.c.o
blake3_portable.c.o
180
$ readelf -n perf/bin/03_hot_function.stripped | grep -A1 "Build ID"        # from the perf track
$ readelf -p .gnu_debuglink perf/bin/03_hot_function.stripped | grep -o "03_hot_function.debug"
03_hot_function.debug

A .a is just a bag of .o files (ar t lists, ar x extracts one; nm works on the whole archive). The link pulls in only the members that resolve an undefined symbol, which is why link order matters and why --whole-archive exists. A stripped binary keeps a build-id note and, if the packager did it right, a .gnu_debuglink naming the file with the DWARF: perf, gdb and llvm-symbolizer follow both (perf lab 03).

The identification checklist for an unknown binary

QuestionCommand
kind, arch, PIE, stripped?file -L bin; readelf -h bin | grep Type
what does it load?ldd bin (or safer, readelf -d bin | grep NEEDED; ldd executes the loader)
where is the code?size bin; readelf -S bin | grep -E "\.text|\.data|\.bss"
which symbols exist?nm --defined-only, nm -D, readelf -s
is there debug info?readelf -S bin | grep debug_info; llvm-dwarfdump --debug-info bin | head
can I get debug info?build-id (readelf -n) + debuginfod / .gnu_debuglink / distro -debuginfo package
what will run at startup?readelf -d (INIT_ARRAY count, RUNPATH), LD_DEBUG=libs ./bin
what compiled it?lab 03

Try this yourself

  1. readelf -S /lib64/libLLVM.so.21.1 | grep -E "\.text|\.rela\.dyn|\.gnu\.hash": the relocation section is what the loader processes on every start (lab 04 measures it).
  2. LD_DEBUG=statistics /usr/bin/clang-21 --version 2>&1 | grep relocations: how many relocations at startup.
  3. Build one lab binary with -static and compare file, ldd and size.
◇
Finish the lab: write the evidence note

A lab is done when you can say what it showed on your machine — not when you have read it.