How to Disassemble Compiled C-Programs for x86 and RISC-V Architectures

To disassemble compiled programs/executables there are several great open source tools available. Executables are created for a specific combination of processor architecture and operating system. This piece provides an overview on how to analyze executables targeted at the x86 and RISC-V architectures on Linux utilizing gcc, objdump and readelf. These tools should come preinstalled on most Linux distributions. It is assumed that the readers host system uses the x86 (AMD, Intel) architecture.

Compilation Process overview

During the compilation process can be distinguished in multiple steps, after each a temporary file is created.
These are Preprocessing, Compilation, Assemble and Linking, see image:

The creation of an executable from C-Code happens in four steps, namely Preprocessing, Compilation, Assembly and Linking.

In the first step, the preprocessor removes comments, expands macro definitions and inserts headers as defined via #include.
During compilation the compiler implements the operations defined in the source code using assembly instructions. Assembly instructions are defined in the instruction set (x86, RISC-V, ARM). It outputs the assembly file.
The assembler translates the assembly instructions into machine code, i.e. their binary representation and outputs an object file. Each source code file results in an object file.
Finally the linker connects all necessary objects files together into a single executable file.
On Linux object files and executable use the .elf-Format, which stands for executable and linkable file.

General switches for gcc

-o name of output file
-g insert debugging information, to be used for example by GNU debugger gdb
-Oapplied optimizations during compilation (default: O0 no optimizaitons, O1 to O3 increase the number of optimizations performed, further explanations see Gentoo Wiki )

x86

Your PC proably contains a processor from Intel or AMD using the x86 ISA. By default gcc will produce an executable using the same processor architecture, i.e. x86.
x86 assembly comes in two syntaxes: att and intel. Most Linux tools use by default the att syntax.
I prefer the intel syntax as it follows the same assembly operand conventions as ARM or RISC-V:
<instr> <destination> <src1> <src2>
Also it uses less special symbols.
My examples will include switches to change the output to intel syntax.

Simply produce executable using gcc

Preprocessing -> Compilation -> Assemble -> Linking = Executable

gcc -o example example.c

The executable can be inspected with readelf. Show all information with -a. View other options by entering just readelf.

readelf -a example

Compile and save temporary files .i , .s,.o

gcc -g --save-temps -masm=intel -o example example.c

--save-temps Save intermediate files (preprocessed .i , assembled .s, compiled.o to disk):

Create List File .lst

A list file interleaves source and the corresponding assembly making it easy to compare the two.

gcc -g -Wa,-adhln -masm=intel example.c > example.lst

Disassemble object files .o / machine code

Create object file from source. Preprocessing -> Compilation -> Assemble = Object Files (contain machine code)

gcc -c example.c -o example.o

Create Listing file (interleave source code and corresponding assembly). Object file needs to contain debug symbols.

objdump -S --disassembler-options=intel test.o

Store disassembly (Interpret Machine Code as Assembly) in file example.asm:

objdump -d -M intel test.o > example.asm

Works also on final executable program.

RISC-V

The RISC-V architecture defines a different instruction set and memory model that is incompatible with x86. For this reason a separate version of these tool was created. It can be downloaded and build from https://github.com/riscv-collab/riscv-gnu-toolchain.
This version of gcc creates executables targeting processors implementing the RISC-V architecture. They won't run on a x86 (AMD or Intel) computer. But we can inspect them never the less.
The functionality provided is similar:

Compile:

riscv64-unknown-linux-gnu-gcc -o example example.c

Compile, save intermediates for debugging:

riscv64-unknown-linux-gnu-gcc --save-temps -g -o example example.c

Create Listing File:

riscv64-unknown-linux-gnu-objdump -S example.o > example.lst

Disassemble:

riscv64-unknown-linux-gnu-objdump -d example.o > example.asm

Show gcc Include Paths

gcc -v -E -x c -

Src: https://stackoverflow.com/questions/65421161/visual-studio-code-cannot-open-source-file-iostream