Inspecting the Instructions of a Compiled Program

Download the C program located here:

http://15418.courses.cs.cmu.edu/tsinghua2017content/code/instructions.cpp

As you can see below, the program computes the value 2^10 by repeatedly doubling the value 1.

#include <stdio.h>

  int main(int argc, char** argv) {

  int x = 1;
  for (int i=0; i<10; i++) {
    x = x + x;
  }

  printf("Hello, the answer is that 2^10 = %d\n", x);
  return 0;
}

Compile and run the program by typing:

g++ instructions.cpp -o instructions
./instruction 

You can use the objdump utility to view the x86 instructions output of the compiler.

 objdump -d -no-show-raw-insn instructions

Look for the section of the output labeled main:, which looks something like this (this is the output of compiling the code using g++ on my MacBook.

_main:
100000f10: pushq %rbp
100000f11: movq %rsp, %rbp
100000f14: subq $32, %rsp
100000f18: movl $0, -4(%rbp)
100000f1f: movl ?i, -8(%rbp)
100000f22: movq %rsi, -16(%rbp)
100000f26: movl $1, -20(%rbp)
100000f2d: movl $0, -24(%rbp)
100000f34: cmpl $10, -24(%rbp)
100000f38: jge 23 <_main+0x45>
100000f3e: movl -20(%rbp), ?x
100000f41: addl -20(%rbp), ?x
100000f44: movl ?x, -20(%rbp)
100000f47: movl -24(%rbp), ?x
100000f4a: addl $1, ?x
100000f4d: movl ?x, -24(%rbp)
100000f50: jmp -33 <_main+0x24>
100000f55: leaq 58(%rip), %rdi
100000f5c: movl -20(%rbp), %esi
100000f5f: movb $0, %al
100000f61: callq 14
100000f66: xorl %esi, %esi
100000f68: movl ?x, -28(%rbp)
100000f6b: movl %esi, ?x
100000f6d: addq $32, %rsp
100000f71: popq %rbp
100000f72: retq

If we break it down, it is possible to read the assembly to recover the basic behavior of the program. The following description assumes you are familiar with reading x86 assembly code... but there are many references online to help with that. I particularly like this description from some course assistants from Stanford.

We can see from line 100000f26, which moves the value 1 into an address in memory. That is the initialization of the variable x from the original program, so the address -20(%rpb) (in other words memory[rbp - 20]) is the location of the variable x.

100000f26: movl $1, -20(%rbp)

We can also see that the address -24(%rbp) (in other words memory[rbp - 20]), stores the value of the loop counter variable i.

The value is compared to the value 10 in line 100000f38.

100000f34: cmpl $10, -24(%rbp)

And in the next instruction, if i is greater than or equal to 10, the processor jumps to the instruction that's 0x45 bytes after the start of the instruction sequence labeled main (that happens to be the end of the loop)

100000f38: jge 23 <_main+0x45>

If i is not less than 10, control proceeds to instruction 100000f3e, the value of x is loaded from memory into a register, and added with itself, and stored back to memory:

100000f3e: movl -20(%rbp), ?x
100000f41: addl -20(%rbp), ?x
100000f44: movl ?x, -20(%rbp)

The loop counter variable i is incremented, and the value of i stored to memory

100000f4a: addl $1, ?x
100000f4d: movl ?x, -24(%rbp)

The processor jumps back up to the top of the loop. (note that _main+0x24 is 100000f34)

100000f50: jmp -33 <_main+0x24>

And then the comparison of the loop counter variable with 10 is repeated on line 100000f34.

Interestingly, if you tell g++ to compile the program with even a basic set of compiler compiler optimizations, g++ realizes that it has all the information it needs to compute the output of the static loop at compile time. Notice how in the instruction sequence below, the program simple just writes 1024 to the memory address containing x. ;-)

g++ -O1 instructions.cpp -o instructions
objdump -d -no-show-raw-insn instructions 

The instruction output is:

_main:
100000f50: pushq %rbp
100000f51: movq %rsp, %rbp
100000f54: leaq 51(%rip), %rdi
100000f5b: movl $1024, %esi     // just move the final value 1024 into the output
100000f60: xorl ?x, ?x
100000f62: callq 5
100000f67: xorl ?x, ?x
100000f69: popq %rbp
100000f6a: retq