From Assembly to Machine Code: How Compilers Build Executables

Modern processors do not execute human readable assembly code. They operate exclusively on machine code, which consists of binary encoded instructions defined by the processor instruction set architecture. Assembly language exists only as a textual representation of those instructions. During compilation, this text must be translated into raw machine code that the processor can execute directly. This translation step is handled by the assembler.

This article walks through how assembly code, data, and external references are transformed into machine code and laid out in memory on 32 bit Windows systems using the Portable Executable format.


Sections, Offsets, and Why They Exist

Processors cannot interpret text strings such as hi there or symbolic references like MessageBoxA. Any non instruction data must be stored separately in memory and referenced by address. To support this, compilers organize program output into sections.

Each section is assigned a file offset and a virtual offset. At runtime, the operating system maps these sections into memory at specific addresses relative to the module image base.

Typical sections include:

  • .text
    Contains executable machine code.
  • .rdata
    Stores read only data such as string literals, constants, and lookup tables.
  • .idata
    Stores import related structures, including pointers to external API functions.

These section names are conventions, not guarantees. Different compilers may merge or repurpose sections depending on optimization and linker behavior.


Example: Strings and Absolute Addressing

Consider two string literals used by a program:

  • "info"
  • "hi there"

The compiler encodes these strings as raw bytes using ASCII encoding:

  • "info" becomes 69 6E 66 6F 00
  • "hi there" becomes 68 69 20 74 68 65 72 65 00

These byte sequences are placed consecutively in a data section, commonly .rdata.

Assume the following:

  • Image base address: 0x400000
  • .rdata section virtual offset: 0x2000

Then the absolute addresses become:

  • "info" at 0x402000
  • "hi there" at 0x402005

The processor never sees the string itself, only the address where the data resides.


External Functions and the Import Address Table

API calls such as MessageBoxA are not understood by the processor as symbolic names. The compiler and linker resolve these calls through an Import Address Table.

The Import Address Table stores function pointers to external routines loaded from system libraries. At runtime, the loader fills this table with the actual addresses of the imported functions.

For example:

  • MessageBoxA pointer stored at 0x403018
  • The code performs an indirect call through this address

This allows the program to jump to the correct system function at runtime without hardcoding addresses.


Machine Code Generation

Once data and imports are organized into sections, the compiler translates assembly instructions into machine code using the x86 instruction set encoding rules.

Examples:

  • push 0
    Encoded as 6A 00
  • push 0x402005
    Encoded as 68 05 20 40 00
  • call ds:[0x403018]
    Encoded as FF 15 18 30 40 00

Each instruction is converted into one or more bytes and written sequentially into the .text section.

The processor executes these bytes directly, without any awareness of the original assembly text.


Object Files and COFF

At this stage, the compiler output is not yet a runnable executable. Instead, it produces an object file using the Common Object File Format.

A COFF file contains:

  • An IMAGE_FILE_HEADER describing the file and section count
  • An array of IMAGE_SECTION_HEADER structures
  • The raw data for each section

The .text section usually appears first, followed by data sections. The object file records where each section should be placed, but it does not yet define final runtime addresses.


Linking and Final Executable Generation

The linker takes one or more COFF object files and produces a Portable Executable file such as an .exe or .dll.

During linking, the linker:

  • Assigns a final image base
  • Resolves imports and relocations
  • Builds the PE headers
  • Aligns sections in memory

Only after this step does the program become a valid executable that the Windows loader can map into memory.


Endianness Considerations

On x86 systems, multi byte values are stored in little endian format. This means the least significant byte is stored at the lowest memory address.

For example:

  • 0x402005 is stored as 05 20 40 00

Instruction encoding, pointers, and integers all follow this convention. Strings and byte arrays, however, are stored in increasing address order.


Summary

The transformation from source code to executable involves several distinct stages:

  1. Source code is compiled into assembly.
  2. Assembly is assembled into machine code.
  3. Data and imports are placed into structured sections.
  4. The compiler emits COFF object files.
  5. The linker produces a final PE executable.

Throughout this process, symbolic constructs are replaced with absolute or relative addresses that the processor can execute directly. Understanding these mechanics is foundational for reverse engineering, exploit development, and low level systems programming.