TRACK: bigcode | DIFFICULTY: 2 | NEEDS: file, readelf, nm, ldd, ar, size, objdump
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.
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 ...
/usr/bin/clang-21 and ls are PIE (distro default).-fno-pie is the default for the local build's linker invocation). readelf -h shows Type: EXEC vs DYN.DYN); libc.so.6 even has an interpreter because it can be executed to print its version.$ 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.
$ 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
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)..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.
$ 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).
| Question | Command |
|---|---|
| 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 |
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).LD_DEBUG=statistics /usr/bin/clang-21 --version 2>&1 | grep relocations: how many relocations at startup.-static and compare file, ldd and size.A lab is done when you can say what it showed on your machine — not when you have read it.
It decides which problems come first, which drills open, which labs you see and how the Learn surface is organised. Change it any time from the switch in the top bar, or press Escape to look around first.