How an Executable File Operates

1. Assembly

  • The machine language understood by the CPU is binary code. Assembly expresses machine language in human-readable mnemonic instructions.
  • Therefore, the programming language most similar to machine code is Assembly.
  • To understand how machine code is executed, we will examine a simple Assembly program.
The following Assembly code performs the operation of printing the result of "2 + 3".

; Ubuntu 24.04 x86-64 (NASM, ELF64)

section .data
    msg     db "result: "
    msg_len equ $ - msg
    newline db 0x0a

section .bss
    buf     resb 1

section .text
    global _start

_start:
    mov     al, 2
    add     al, 3
    mov     bl, al

    mov     rax, 1
    mov     rdi, 1
    mov     rsi, msg
    mov     rdx, msg_len
    syscall

    mov     al, bl
    add     al, '0'
    mov     [buf], al

    mov     rax, 1
    mov     rdi, 1
    mov     rsi, buf
    mov     rdx, 1
    syscall

    mov     rax, 1
    mov     rdi, 1
    mov     rsi, newline
    mov     rdx, 1
    syscall

    mov     rax, 60
    xor     rdi, rdi
    syscall
add.asm
  
; ============================================================
; Ubuntu 24.04 x86-64 (NASM, ELF64)
; ============================================================
 
section .data               ; Initialized data
    msg     db "result: "
    msg_len equ $ - msg
    newline db 0x0a
 
section .bss                ; Uninitialized data
    buf      resb 1         ; buffer for converting a single-digit number to ASCII
 
section .text               ; Executable code section
global _start
 
_start:
    ;----------------------------------------------------------
    ; [1] Calculate 2 + 3 (use 8-bit AL)
    ;----------------------------------------------------------
    mov     al, 2           ; al = 2
    add     al, 3           ; al = 2 + 3 = 5
    mov     bl, al          ; bl = 5 (preserve in BL because syscalls may overwrite RAX)
 
    ;----------------------------------------------------------
    ; [2] Print "result: " string (sys_write)
    ;----------------------------------------------------------
    mov     rax, 1          ; syscall: sys_write (rax, rcx, r11 may be clobbered)
    mov     rdi, 1          ; fd: stdout
    mov     rsi, msg
    mov     rdx, msg_len
    syscall
 
    ;----------------------------------------------------------
    ; [3] Convert result to ASCII and print
    ;----------------------------------------------------------
    mov     al, bl          ; al = 5 (restore result from BL)
    add     al, '0'         ; 5 + 48 = 53 '5'
    mov     [buf], al       ; store ASCII character in buf
 
    mov     rax, 1          ; syscall: sys_write
    mov     rdi, 1
    mov     rsi, buf
    mov     rdx, 1
    syscall
 
    ;----------------------------------------------------------
    ; [4] Print newline
    ;----------------------------------------------------------
    mov     rax, 1
    mov     rdi, 1
    mov     rsi, newline
    mov     rdx, 1
    syscall
 
    ;----------------------------------------------------------
    ; [5] Exit program (sys_exit, code=0)
    ;----------------------------------------------------------
    mov     rax, 60         ; syscall: sys_exit
    xor     rdi, rdi
    syscall

1.1. Build

  • To create an executable file from this Assembly program, the following process is required.


$ nasm -f elf64 -o add.o add.asm
$ ld -o add add.o

$ ./add
result: 5

1.2. Executable File

  • Executable files running in a Linux (Ubuntu 24.04) environment use the ELF64 (Executable and Linkable Format 64-bit) format.


$ objdump -h add
Sections:
Idx Name           Size      VMA               LMA                    File off   Algn
  0 .text          0000006c 0000000000401000  0000000000401000         00001000   2**4
                   CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .data          00000009 0000000000402000  0000000000402000         00002000   2**2
                   CONTENTS, ALLOC, LOAD, DATA
  2 .bss           00000004 000000000040200c  000000000040200c         00002009   2**2
                   ALLOC

$ objdump -s -j .text add
Contents of section .text:
 401000 b0020403 88c3b801 000000bf 01000000   ................
 401010 48be0020 40000000 0000ba08 0000000f   H.. @...........
 401020 0588d804 30880425 0c204000 b8010000   ....0..%. @.....
 401030 00bf0100 000048be 0c204000 00000000   ......H.. @.....
 401040 ba010000 000f05b8 01000000 bf010000   ................
 401050 0048be08 20400000 000000ba 01000000   .H.. @..........
 401060 0f05b83c 00000048 31ff0f05            ...<...H1...

$ objdump -s -j .data add
Contents of section .data:
 402000 72657375 6c743a20 0a                  result: .

$ objdump -s -j .bss add
Contents of section .bss:
 40200c 00000000

1.3. Executable Code

  • The code section (.text) is typically placed at 0x401000, which is the 4KB page boundary (0x1000 = 4096) following the header.
    • This is the default layout in x86-64 Linux. The address may vary depending on compiler options or security features such as ASLR.


$ objdump -d -M intel add
Disassembly of section .text:
0000000000401000 <_start>:
  401000:       b0 02                   mov    al,0x2
  401002:       04 03                   add    al,0x3
  401004:       88 c3                   mov    bl,al
  401006:       b8 01 00 00 00          mov    eax,0x1
  40100b:       bf 01 00 00 00          mov    edi,0x1
  401010:       48 be 00 20 40 00 00    movabs rsi,0x402000
  401017:       00 00 00
  40101a:       ba 08 00 00 00          mov     edx,0x8
  40101f:       0f 05                   syscall
  401021:       88 d8                   mov     al,bl
  401023:       04 30                   add     al,0x30
  401025:       88 04 25 0c 20 40 00    mov     BYTE PTR ds:0x40200c,al
  40102c:       b8 01 00 00 00          mov     eax,0x1
  401031:       bf 01 00 00 00          mov     edi,0x1
  401036:       48 be 0c 20 40 00 00    movabs rsi,0x40200c
  40103d:       00 00 00
  401040:       ba 01 00 00 00          mov     edx,0x1
  401045:       0f 05                   syscall
  401047:       b8 01 00 00 00          mov     eax,0x1
  40104c:       bf 01 00 00 00          mov     edi,0x1
  401051:       48 be 08 20 40 00 00    movabs rsi,0x402008
  401058:       00 00 00
  40105b:       ba 01 00 00 00          mov     edx,0x1
  401060:       0f 05                   syscall
  401062:       b8 3c 00 00 00          mov     eax,0x3c
  401067:       48 31 ff                xor     rdi,rdi
  40106a:       0f 05                   syscall

The binary code at this address b0020403 88C3... is interpreted as follows.


b0 02                   mov    al,0x2
04 03                   add    al,0x3
88 C3 mov bl,al ...

This shows the result of interpreting the instruction code executed by the CPU into Assembly.

2. Execute

2.1. CPU Registers

  • CPU registers are ultra-fast storage spaces directly used by the CPU to perform operations, and all core program execution processes are based on registers.
x86 32bit CPU (IA-32) Register

2.2. OP Code to CPU Resisters

  • The CPU fetches instructions from memory (Fetch → Decode) and interprets them into μOps.
  • After performing the operation (Execute → Memory), the result is written back to registers (WriteBack).
  • Then RIP is incremented to the next instruction, and this process repeats.

  • RIP (Register Instruction Pointer)
    • A register in x86-64 CPUs that points to the address of the next instruction to execute
5-Stage Pipeline (Simplified x86-64 Model)

2.3. Execution Process

  • During the build stage, source code is converted into machine code, creating an ELF executable file.
  • The OS kernel loads the ELF via execve, places segments into virtual memory, and sets RIP to the entry point to begin execution.
  • The CPU then repeatedly executes instructions through the IF → ID → EX → MEM → WB pipeline.

Popular posts from this blog

C Language - The Grammar of a Programming Language

Vibe Coding - Creating Code with Natural Language

Computer Operations Explained with Gate Circuits