Generating Mixed C and Assembly Listings with GCC and objdump
When debugging and optimizing C/C++ programs, you need to see how the compiler translates your source code into assembly. A mixed source and assembly listing lets you trace exactly what machine instructions correspond to each line of your source.
Using objdump (Recommended Modern Approach)
The most straightforward method is to compile with debugging symbols and use objdump to generate mixed listings without recompilation:
gcc -g source.c -o binary
objdump -S binary > listing.txt
The -S flag intermixes source code (from -g symbols) with disassembly. For C++ projects, demangle symbols with -C:
objdump -SC binary > listing.txt
To use Intel syntax instead of AT&T (GCC’s default):
objdump -S -M intel binary > listing.txt
This approach is cleaner than older methods because it separates compilation from listing generation, making it easier to work with existing binaries and avoiding the complexity of passing assembler flags.
Using GCC’s Assembler Flags (Alternative)
If you need to generate listings during compilation, use the -Wa,-adhln flags:
gcc -Wa,-adhln -g source.c -o binary > listing.s
The assembler flags control output:
a: Enable listingsd: Omit debugging directives (reduces noise)h: Include high-level source codel: Include assembly instructionsn: Omit forms processing (no pagination)
The listing goes to stdout; redirect it to a file while the binary is produced at the -o location.
Single-File Example
For a simple program:
#include <stdio.h>
int main()
{
printf("Hello world!\n");
return 0;
}
Compile and generate listing:
gcc -g helloworld.c -o helloworld
objdump -S helloworld > helloworld.txt
The resulting file contains interleaved source and assembly:
4:helloworld.c **** printf("Hello world!\n");
0000000000001139 <main>:
1139: 55 push %rbp
113a: 48 89 e5 mov %rsp,%rbp
113d: bf f4 11 00 00 mov $0x11f4,%edi
1142: e8 e9 fe ff ff call 1030 <puts@plt>
The left columns show the instruction address in hex (1139, 113a), machine bytecode, and mnemonics. Each source line is mapped back to its corresponding instructions.
Multi-File Projects
For projects with multiple source files, compile all together:
gcc -g fun1.c main.c -o binary
objdump -S binary > combined.txt
Given these files:
fun1.c:
#include <stdio.h>
void fun1()
{
printf("Hello world!\n");
}
main.c:
#include <stdio.h>
void fun1();
int main()
{
fun1();
return 0;
}
The combined listing shows each function’s prologue and epilogue (stack frame setup/teardown) aligned with their source lines, making it easy to understand calling conventions and register allocation decisions.
Comparing Optimization Levels
Assembly output changes dramatically with optimization flags. Generate listings at different levels to understand compiler behavior:
gcc -g -O0 source.c -o binary_O0
objdump -S binary_O0 > opt0.txt
gcc -g -O2 source.c -o binary_O2
objdump -S binary_O2 > opt2.txt
gcc -g -O3 source.c -o binary_O3
objdump -S binary_O3 > opt3.txt
At -O0, you’ll see obvious one-to-one source-to-instruction mapping. At -O2 and -O3, the compiler reorders instructions, eliminates dead code, inlines functions, and restructures loops—sometimes making single source lines compile to dozens of instructions or disappear entirely.
Filtering Large Outputs
For large projects, listings can reach thousands of lines. Extract specific functions with grep:
objdump -S binary | grep -A 50 "<main>:"
Or use awk to print lines around a specific function:
objdump -S binary | awk '/^[0-9a-f]* <my_function>:/,/^[0-9a-f]* <[^>]*>:/' | head -n 50
To extract and save a function’s assembly:
objdump -S binary | sed -n '/^[0-9a-f]* <target_function>:/,/^[0-9a-f]* <[^>]*>:/p' > function.asm
Understanding the Output Format
Each non-empty line contains:
- Instruction address: Hexadecimal memory address (1139, 113a)
- Machine code: Raw bytes the processor executes
- Mnemonic: Human-readable instruction name (mov, call, push)
- Operands: Registers, memory addresses, or immediate values
- Source line: (if available) The corresponding source code line
CFI directives (.cfi_*) encode call frame information for stack unwinding. These aren’t executable code but are used by debuggers (GDB, LLDB) and exception handling mechanisms (libunwind) to traverse the stack correctly during debugging or unwinding exceptions.
Architecture Considerations
Assembly output is architecture-specific. The examples above use x86-64 with AT&T syntax (GCC’s default). On other architectures, instruction names and register conventions differ:
- ARM:
ldr,str,blinstead ofmovl,call - ARM64 (AArch64):
ldr,str,blwith 64-bit registers - RISC-V:
addi,sw,jalwith different register naming - PowerPC: Different register conventions and instruction formats
Check your target architecture’s instruction set documentation for specific details. Force Intel syntax with:
objdump -S -M intel binary > listing.txt
Using gdb for Interactive Analysis
For more control, use GDB directly:
gdb ./binary
(gdb) disassemble main
(gdb) disassemble /m main
The /m flag shows mixed source and assembly interactively. Set breakpoints at specific addresses and step through assembly while viewing source:
(gdb) break *0x1139
(gdb) run
(gdb) stepi
Practical Debugging Workflow
Combine mixed listings with a debugger for efficient analysis:
- Compile with
-gand your desired optimization level (-O0for clarity,-O2for realistic behavior) - Generate the listing with
objdump -S binary > listing.txt - Search for the function of interest in the listing
- Cross-reference addresses with GDB breakpoints
- Step through assembly while viewing the source-instruction correlation
- Compare behavior at different optimization levels to spot compiler-related issues
Mixed source-assembly listings are invaluable for understanding compiler behavior, optimizing hot code paths, debugging low-level issues, and reverse-engineering binaries where source isn’t available.

Excellent!
Hi,
I have found the above commands you have suggested quite useful but i would like to know how can i use this command when there are mutiple files of code to be executed and I am interested in the assembly code of only one of those files ?
You can use the same method to generate the source-assembly list from several source files. Note that the line numbers are followed with source file names like “1:fun1.c”. You may only check the parts that you are interested in.
Hi Sir,
Thank you very much for your explanation.
I have one query, So from the above option we can get the list of source file but is it possible that “Can we get to know what and particular source file is going to be used for particular executable file (In case when source code generate two or more executable’s)” ..?
Lets say:
We have 5 source file, and 3 source files are used for “a.out” and rest 2 source files are used for “b.out” then “How can we get to know it that which source files is used for which “executable file”..?
Thanks in advance
For your purpose, I feel checking the Makefile or other files specifying the dependence of files in your project will be better.
Thanks a lot, will check the same.
How can I make gcc within Eclipse IDE being aware of my assembly code and allow me to debug it?
great post,
Is their any tool to identify what are all different register used in assembly code generated by gcc or any other compiler? or is their any way we can find out which registers are used in assembly code and total access count of these registers.
Different compilers may generate different instruction flows for the same program. It may not be easy to do a side-by-side comparison. There are only a small number of registers in the common CPU ISAs. To get best performance, compilers usually generate instructions to use as registers as much as possible.
The total access count of the registers used may be counted during runtime and may be different with different input to the programs. If you need such info, you may use some techniques like those used in JIT compilers or virtual machines to “record” the instructions executed and count the register usage. But I am not aware of any existing tools doing this.
Thanks a lot Eric for inputs.
I really appreciate you quick help.
This is very helpful. Could you please post also information what are corresponding compiler options for g++?
getting errors at the end
d:/progs/gcc_riscv_v12/bin/../lib/gcc/riscv-none-elf/12.2.0/../../../../riscv-none-elf/bin/ld.exe: warning: cannot find entry symbol _start; defaulting to 0000000000010094
d:/progs/gcc_riscv_v12/bin/../lib/gcc/riscv-none-elf/12.2.0/../../../../riscv-none-elf/bin/ld.exe: C:\Users\mrx23dot\AppData\Local\Temp\cc8KCAnd.o: in function `adc_filter_task’:
D:\DRIVE\Projects\FW_ch32v003_template\build/..\src\adcFilter/adcFilter.c:45: undefined reference to `adc_read’
d:/progs/gcc_riscv_v12/bin/../lib/gcc/riscv-none-elf/12.2.0/../../../../riscv-none-elf/bin/ld.exe: C:\Users\mrx23dot\AppData\Local\Temp\cc8KCAnd.o: in function `adc_readFilt’:
D:\DRIVE\Projects\FW_ch32v003_template\build/..\src\adcFilter/adcFilter.c:61: undefined reference to `assert_halt’
collect2.exe: error: ld returned 1 exit status
Tries to do more than asm?