Objdump: Disassembling Executable Machine Code, Inspecting Section Relocations, and Auditing Binary Security in Production
The usual diagnostics offer no comfort. The production servers run inside ultra-minimalist containers stripped of every convenienceβthere is no interactive debugger installed, no memory profiler available, and restrictive disk quotas ensured that when the process collapsed, it left behind no crash dump file to inspect. The only clue is a single cryptic line buried in the operating system's kernel log: a sequence of hexadecimal numbers marking the exact memory address where execution ceased.
When standard monitoring dashboards and log aggregators go blind, you must stop guessing and interrogate the compiled application directly. Rather than treating the software as an impenetrable black box, you can turn to objdump, an indispensable diagnostic workhorse in the GNU Binutils Suite that translates raw binary files back into readable instructions.
To immediately inspect what an application is telling your computer's processor to do, you can run a single command from your terminal:
objdump -d -M intel -C --no-show-raw-insn /usr/bin/sys_collector | head -n 25
This command translates the cryptic machine code of any Linux executable into clear, structured assembly language using familiar Intel syntax, demangling complex function names so you can read them at a glance. In seconds, the opaque binary reveals its inner anatomy, displaying every function, jump, and memory operation exactly as the silicon will execute them. It is the closest thing systems engineering has to an X-ray machine for running software.
What It Does in Plain English
Every program you runβwhether written in C, C++, Rust, or Goβultimately gets translated by a compiler into machine code: a dense sequence of raw bytes that tell your computer's central processing unit (CPU) exactly how to shuffle data between memory and registers. In Linux environments, these instructions and their supporting metadata are packaged into files using the Executable and Linkable Format (ELF).
At its heart, objdump is a binary inspection engine and disassembler. It opens compiled executables, shared libraries (.so files), and object files (.o files), reads the raw hexadecimal bytes stored within their code sections, and translates them back into human-readable assembly instructions. Crucially, objdump does this through static analysisβmeaning it never actually executes the target program. It allows you to safely dissect an unknown, crashed, or potentially malicious program without having to run it or risk destabilising your system.
Core Flags and Bytecode Anatomy
Navigating the interior of a compiled binary requires precision. The objdump command provides several foundational switches tailored for low-level systems triage:
-d(--disassemble): Disassembles executable machine instructions contained exclusively within active code sections (such as.text).-D(--disassemble-all): Disassembles every section in the object file, including non-executable data sections.-M intel(--disassembler-options=intel): Formats x86/x86-64 assembly using Intel syntax (<instruction> <destination>, <source>) rather than the default AT&T syntax.-C(--demangle): Demangles low-level symbol names generated by C++ or Rust compilers into human-readable function signatures.-t(--syms): Dumps the static symbol table, exposing internal function names, global variables, and compilation units.-T(--dynamic-syms): Displays dynamic symbol table entries required during runtime linking by the dynamic loader (ld.so).-r/-R(--reloc/--dynamic-reloc): Dumps static and dynamic relocation entries, exposing address patch points managed by the linker.-S(--source): Interleaves original source code lines with their corresponding assembly instructions when debug information is present.-h(--section-headers): Prints concise summary headers for all sections present in the object file.-j <section>(--section=<section>): Restricts analysis or disassembly to a specific named section (e.g.,-j .text,-j .rodata).-s(--full-contents): Displays the full raw hexadecimal and ASCII contents of requested sections.
Deconstructing the Bytecode Stream
When conducting forensic analysis, preserving raw instruction bytes alongside the assembly text is essential for checking processor alignment and instruction widths:
objdump -d -M intel -C /usr/local/bin/crypto_worker
/usr/local/bin/crypto_worker: file format elf64-x86-64
Disassembly of section .text:
00000000000011a0 <crypto_init_context>:
11a0: f3 0f 1e fa endbr64
11a4: 55 push rbp
11a5: 48 89 e5 mov rbp,rsp
11a8: 48 83 ec 20 sub rsp,0x20
11ac: 48 89 7d e8 mov QWORD PTR [rbp-0x18],rdi
11b0: 48 8b 45 e8 mov rax,QWORD PTR [rbp-0x18]
11b4: 48 8b 00 mov rax,QWORD PTR [rax]
11b7: 48 85 c0 test rax,rax
11ba: 74 0b je 11c7 <crypto_init_context+0x27>
11bc: b8 00 00 00 00 mov eax,0x0
11c1: 48 83 c4 20 add rsp,0x20
11c5: 5d pop rbp
11c6: c3 ret
11c7: e8 84 fe ff ff call 1050 <abort@plt>
Anatomical Breakdown of Disassembly Columns
Every line in the disassembly output represents a direct command given to the underlying processor:
- Virtual Memory Address (
11a0:,11ac:): The memory offset where the instruction resides within the program. In Position-Independent Executables (PIE), this displays the offset from the base address where the operating system loads the file. - Raw Machine Bytecode (
f3 0f 1e fa,48 89 7d e8): The actual hexadecimal numbers read by the CPU. Prefix bytes like48(the REX prefix) inform a 64-bit processor to work with full 64-bit wide registers. - Instruction Mnemonic (
endbr64,mov,test,je,call): The human-readable name of the action the processor must perform (e.g. move data, compare values, or jump to another address). - Operands (
QWORD PTR [rbp-0x18], rdi): The targets and sources of the operation, such as registers, fixed numbers, or memory locations qualified by pointer size (QWORDfor 64-bit,DWORDfor 32-bit,WORDfor 16-bit, andBYTEfor 8-bit).
5 Real-World Production Use Cases
Use Case 1: Post-Mortem Crash Triage from Kernel dmesg Offsets
The Scenario
An ultra-low latency C++ trading daemon crashes in production with a segmentation fault during a period of heavy market volatility. Core dump generation was disabled to save disk space, but /var/log/messages and the dmesg system log captured a single diagnostic trace from the Linux kernel:
traps: market_feeder[28419] general protection fault ip:55b8e90c25a1 sp:7ffd9a1c8900 error:0 in market_feeder[55b8e90c0000+8000]
The log reveals that the crash occurred at virtual memory address 0x55b8e90c25a1, while the binary's code segment was loaded starting at base address 0x55b8e90c0000. By subtracting the base address from the instruction pointer:
$$\text{Crash Offset} = \text{0x55b8e90c25a1} - \text{0x55b8e90c0000} = \text{0x25a1}$$
The engineer needs to inspect the exact instruction located at offset 0x25a1 inside the executable file.
The Command
Using the --start-address and --stop-address flags, you can target the immediate neighbourhood of the crash without scrolling through thousands of lines of irrelevant code:
objdump -d -M intel -C --start-address=0x2580 --stop-address=0x25c0 /usr/local/bin/market_feeder
Terminal Output
/usr/local/bin/market_feeder: file format elf64-x86-64
Disassembly of section .text:
0000000000002580 <OrderBook::process_depth(OrderUpdate const*)>:
2580: f3 0f 1e fa endbr64
2584: 48 85 f6 test rsi,rsi
2587: 74 25 je 25ae <OrderBook::process_depth(OrderUpdate const*)+0x2e>
2589: 48 83 ec 10 sub rsp,0x10
258d: 48 89 7c 24 08 mov QWORD PTR [rsp+0x8],rdi
2592: 48 89 74 24 00 mov QWORD PTR [rsp],rsi
2597: 48 8b 44 24 00 mov rax,QWORD PTR [rsp]
259c: 48 8b 40 18 mov rax,QWORD PTR [rax+0x18]
25a1: 48 8b 10 mov rdx,QWORD PTR [rax]
25a4: 48 89 54 24 08 mov QWORD PTR [rsp+0x8],rdx
25a9: 48 83 c4 10 add rsp,0x10
25ad: c3 ret
25ae: b8 ea ff ff ff mov eax,0xffffffea
25b3: c3 ret
Line-by-Line Disassembly Analysis
2580:endbr64verifies branch integrity for modern processor security (Intel CET).2584 - 2587: The function tests whether the incoming pointer (rsi, representingOrderUpdate const*) is null viatest rsi, rsi. If it is null, it jumps to25aeto exit safely with an error code.259c:mov rax, QWORD PTR [rax+0x18]loads an internal nested pointer offset (+24 bytes) from theOrderUpdatedata structure into theraxregister.25a1: The Crash Site (0x25a1).mov rdx, QWORD PTR [rax]attempts to read memory from the address stored insiderax. Because that nested struct member was uninitialized or pointed to invalid memory (0x0), attempting to read it triggered an immediate protection fault.
What the Admin Does Next
- Alert the development team that while the outer
OrderUpdateobject was checked for null, an inner structure pointer located at offset+24 byteswas dereferenced without validation. - Apply a patch in the C++ codebase to validate
OrderUpdate->sub_entryprior to access, recompile, and deploy the fix to staging.
Use Case 2: Diagnosing Dynamic Symbol Resolution & PIE Relocation Failures
The Scenario
During a rolling update across a server cluster, an analytics service crashes immediately on startup with a fatal dynamic linker error:
/opt/analytics/bin/engine: symbol lookup error: /opt/analytics/lib/libshim.so: undefined symbol: compute_fast_hash
The developers state that the library was built against the newest headers. As the administrator, you must inspect the dynamic symbol tables (-T) and relocations (-R) across the shared libraries to determine whether the function is truly missing, has suffered an ABI name-mangling mismatch, or failed to bind properly in the Global Offset Table.
The Command
First, check the dynamic relocation entries of the failing library to see what symbol format it expects:
objdump -R /opt/analytics/lib/libshim.so | grep compute_fast_hash
Next, inspect the exported dynamic symbol table of the upstream dependency /usr/local/lib/libcore_utils.so:
objdump -T /usr/local/lib/libcore_utils.so | grep compute_fast_hash
Terminal Output
From libshim.so:
0000000000003fd8 R_X86_64_JUMP_SLOT compute_fast_hash
From libcore_utils.so:
0000000000014b20 g DF .text 000000000000004a Base _Z17compute_fast_hashPKcm
Line-by-Line Disassembly Analysis
libshim.sooutput: The relocation entryR_X86_64_JUMP_SLOT compute_fast_hashindicates thatlibshim.soexpects a standard, unmangled C function namedcompute_fast_hashat runtime.libcore_utils.sooutput: The upstream library does not exportcompute_fast_hash. Instead, it exports the mangled string_Z17compute_fast_hashPKcm.- Passing that string to the symbol demangler (
c++filt _Z17compute_fast_hashPKcm) producescompute_fast_hash(char const*, unsigned long). The upstream library was compiled with a C++ compiler without wrapping the header declaration in anextern "C"block, changing its exported symbol name and breaking binary compatibility.
What the Admin Does Next
- Roll back
libcore_utils.soto the previous stable release on the cluster to restore production traffic immediately. - Notify the software team that an ABI breakage occurred so they can wrap the function declaration in
extern "C"and release an updated build.
Use Case 3: Auditing Compiler Optimization, Loop Unrolling, and SIMD Vectorization
The Scenario
Following an automated toolchain upgrade, a high-performance numerical computation library exhibits an unexpected 400% slowdown on AVX-512 capable hardware. You need to inspect the compiled output to verify whether the compiler successfully vectorized the core numerical calculation or reverted to slow, scalar single-instruction execution.
The Command
Disassemble the target function, interleaving original source code lines (-S) with Intel syntax instructions (-M intel) on the library built with debug symbols:
objdump -S -d -M intel -C --no-show-raw-insn /usr/local/lib/libmatrix_simd.so | sed -n '/<Matrix::vector_multiply/,/^[0-9a-f]/p'
Terminal Output
0000000000001420 <Matrix::vector_multiply(float const*, float const*, float*, unsigned long)>:
// Source line: void Matrix::vector_multiply(const float* a, const float* b, float* c, size_t n) {
1420: f3 0f 1e fa endbr64
1424: 48 85 c9 test rcx,rcx
1427: 74 37 je 1460 <Matrix::vector_multiply+0x40>
1429: 48 89 c8 mov rax,rcx
142c: 31 d2 xor edx,edx
// Source line: for (size_t i = 0; i < n; ++i) { c[i] = a[i] * b[i]; }
142e: 66 90 xchg ax,ax
1430: f3 0f 10 04 97 movss xmm0,DWORD PTR [rdi+rdx*4]
1435: f3 0f 59 04 96 mulss xmm0,DWORD PTR [rsi+rdx*4]
143a: f3 0f 11 04 92 movss DWORD PTR [rdx+rdx*4],xmm0
143f: 48 83 c2 01 add rdx,0x1
1443: 48 39 c2 cmp rdx,rax
1446: 75 e8 jne 1430 <Matrix::vector_multiply+0x10>
1448: c3 ret
1460: c3 ret
Line-by-Line Disassembly Analysis
1430:movss xmm0, DWORD PTR [rdi+rdx*4]uses a scalar instruction (movss= Move Scalar Single-Precision Floating-Point) rather than a wide vector instruction (vmovups).1435:mulss xmm0, DWORD PTR [rsi+rdx*4]performs a single multiplication per clock cycle. The CPU processes a single 32-bit float at a time instead of processing sixteen 32-bit floats simultaneously across a 512-bit register.143f - 1446: The generated loop incrementsrdxby1(add rdx, 0x1) on each cycle with no unrolling.- Root Cause: The compiler failed to vectorize because the function arguments
float* a,float* b, andfloat* cwere not marked with the__restrict__keyword. The compiler conservatively assumed that writing to arraycmight overwrite data inaorb, disabling SIMD optimization.
What the Admin Does Next
- Inform the engineering team to annotate the pointer signatures with
__restrict__to guarantee memory regions do not overlap. - Update the build flags to enforce
-O3 -mavx512f -ftree-vectorize, then re-inspect withobjdumpto verify thatvmovupsandvmulpsinstructions are generated.
Use Case 4: Auditing Embedded Strings and Raw Metadata in Untrusted Binaries
The Scenario
During a security audit, an unknown binary named kworker_update is found running in /tmp on a staging server. Company policy strictly prohibits executing suspicious software, eliminating dynamic analysis tools like strace. You must inspect embedded URLs, configuration parameters, and authentication tokens directly from the binary file without running it.
The Command
Use objdump to inspect the section headers and dump the complete contents of the .rodata (read-only data) section in combined hexadecimal and ASCII format:
objdump -s -j .rodata /tmp/kworker_update
Terminal Output
/tmp/kworker_update: file format elf64-x86-64
Contents of section .rodata:
2000 01000200 68747470 3a2f2f31 39322e31 ....http://192.1
2010 36382e31 2e323530 3a383038 302f6332 68.1.250:8080/c2
2020 5f626561 636f6e00 504f5354 202f6170 _beacon.POST /ap
2030 692f7631 2f657866 696c7472 61746500 i/v1/exfiltrate.
2040 41555448 3a204265 61726572 20396638 AUTH: Bearer 9f8
2050 33616362 65373132 30346166 61386200 3acbe71204afa8b.
Line-by-Line Disassembly Analysis
2000 - 2027: The raw hexadecimal bytes decode directly tohttp://192.168.1.250:8080/c2_beacon, exposing the remote command-and-control endpoint.2028 - 203f: Decodes toPOST /api/v1/exfiltrate, revealing the internal path used to siphon data.2040 - 205f: Extracts an embedded API authentication token:AUTH: Bearer 9f83acbe71204afa8b.
What the Admin Does Next
- Add an immediate firewall block rule for IP
192.168.1.250:8080across all perimeter firewalls. - Search central authentication logs for the extracted bearer token
9f83acbe71204afa8bto determine if other infrastructure components were compromised.
Use Case 5: Auditing Binary Hardening (Intel CET, Stack Canaries, and Full RELRO)
The Scenario
Before deploying an internet-facing reverse proxy (edge_proxy) into production, your security baseline requires verification that the binary has been compiled with essential modern exploit protections:
1. Control-Flow Enforcement Technology (CET / IBT): Ensuring indirect branch instructions (endbr64) protect function entry points against jump-oriented programming attacks.
2. Stack Smashing Protection (SSP): Ensuring stack canaries (__stack_chk_fail) protect memory buffers against buffer overflow exploits.
3. Full RELRO (Relocation Read-Only): Ensuring the Global Offset Table (.got) is marked read-only to prevent pointer overwrite attacks.
The Command
Perform a compound audit using objdump to verify instruction-level guards and section access permissions:
objdump -d -M intel -j .text /usr/sbin/edge_proxy | grep -E "(endbr64|__stack_chk_fail)"
Then check the access permissions of the Global Offset Table:
objdump -h -j .got -j .got.plt /usr/sbin/edge_proxy
Terminal Output
From Disassembly:
10b0: f3 0f 1e fa endbr64
10c4: e8 a7 ff ff ff call 1070 <__stack_chk_fail@plt>
1120: f3 0f 1e fa endbr64
1152: e8 19 ff ff ff call 1070 <__stack_chk_fail@plt>
From Section Headers:
/usr/sbin/edge_proxy: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
21 .got 000001f8 0000000000003e08 0000000000003e08 00002e08 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
Line-by-Line Disassembly Analysis
endbr64(Opcodef3 0f 1e fa): Confirms Intel CET Indirect Branch Tracking is active. Any unauthorised jump into the middle of a function will trigger an immediate hardware protection fault.call 1070 <__stack_chk_fail@plt>: Confirms stack canaries are active. If an attacker attempts to overflow a local buffer on the stack, the canary check fails and safely terminates the process..gotflags (READONLY): Combined with the absence of a separate writable.got.pltsection, theREADONLYattribute confirms that Full RELRO was enforced via-Wl,-z,relro,-z,now. The dynamic linker resolves all addresses at startup and locks the lookup table as read-only.
What the Admin Does Next
- Sign off on the binary hardening audit, approving the service for production deployment.
What Can Go Wrong: Traps, Illusions, and Failure Modes
1. The AT&T vs. Intel Syntax Misinterpretation Trap
By default on x86 architectures, objdump displays assembly in AT&T syntax. In AT&T syntax, the source operand comes before the destination operand (mov %rax, %rbx copies data from register RAX to register RBX). In Intel syntax (-M intel), this order is reversed: destination precedes source (mov rbx, rax). Confusing these two formats during an outage can lead an engineer to mistake where data was being written versus where it was being read, causing hours of wasted debugging.
Mitigation: Standardise your workflows and internal documentation around
-M intel, or configure a shell alias such asalias disasm='objdump -d -M intel -C'.
2. The Data-as-Code Disassembly Illusion (-D vs -d)
Running objdump -D tells the tool to disassemble every section of the file, including non-executable data regions like .data and .rodata. Because arbitrary numbers, image headers, and text strings often match valid CPU instruction encodings by pure chance, objdump will generate nonsensical assembly instructions that do not actually exist:
# Dangerous: Interprets plain data and text strings as processor instructions
objdump -D -j .rodata /usr/bin/target_app
Mitigation: Always restrict disassembly strictly to designated code sections using
-dinstead of-D, or explicitly specify-j .text.
3. Stripped Binaries and Base Address Offsets in PIE Binaries
When an executable has its symbol names stripped during release (strip -s), objdump will no longer display function names like <main>: or <OrderBook::process>. It will only show raw hexadecimal offsets under <disassembly of section .text>. Furthermore, modern Linux distributions compile programs as Position-Independent Executables (PIE). The offsets shown by objdump (typically starting at 0x1000 or 0x0) do not match the absolute memory addresses assigned at runtime by Address Space Layout Randomization (ASLR).
Mitigation: When diagnosing a crash address from
dmesg, always calculate the relative offset by subtracting the base address from the reported instruction pointer. For detailed specifications on ELF internals, consult the Linux manual for elf(5) and the ArchWiki Debugging Guide. For comprehensive instruction reference tables, refer to the Intel 64 and IA-32 Architectures Software Developer's Manual.
Comparative Analysis: Binary Triage Utilities
The table below outlines when to reach for objdump compared to other standard Linux diagnostic tools:
| Metric / Capability | objdump |
readelf |
nm |
gdb |
|---|---|---|---|---|
| Primary Specialty | Static Instruction Disassembly & Section Dumping | Pure ELF Structural Header & Metadata Parsing | Symbol Name & Address Enumeration | Interactive Dynamic Execution & Memory Debugging |
| Parsing Engine | GNU BFD (Binary File Descriptor Library) | Native Direct ELF Parser (Zero BFD Dependency) | GNU BFD Library | GDB Process Control Engine / ptrace |
| Runtime Execution Required | No (Static Analysis) | No (Static Analysis) | No (Static Analysis) | Yes (Dynamic Attachment via ptrace) |
| Disassembly Quality | High (Supports Intel/AT&T, Architecture Overrides) | None (Displays Metadata Only) | None (Symbol List Only) | Full Interactive Frame-by-Frame Disassembly |
| Production Risk | Zero (Read-Only Static Inspection) | Zero (Read-Only Static Inspection) | Zero (Read-Only Static Inspection) | High (Can pause processes, inject latency, or alter state) |
Today's Takeaway
Learning to read raw machine code directly from a compiled file gives you a fundamental advantage when standard observability tools fail. Right now, open a terminal on your computer and run this five-minute command:
objdump -d -M intel /bin/ls | head -n 35
Scroll to the entry point labeled <_start> and observe how your system initializes its CPU registers before passing control to the standard C library runtime. Seeing how raw hexadecimal numbers translate into direct hardware instructions turns compiled software from an intimidating black box into an open book. For a complete reference of supported flags and options, visit the official Linux man-pages: objdump(1).