# x86 and x64 Registers and Calling Conventions

> A practical guide to CPU registers, partial register access, flags, and how function arguments are passed on x86 and x64 Linux.

- Published: 2025-08-07
- Difficulty: beginner
- Series: Linux Exploitation Fundamentals
- Tags: Linux, Assembly, x86, x64, Internals
- Source: https://stevenfoerster.com/tutorials/x86-x64-registers-and-calling-conventions/

## Prerequisites

- Basic Linux command line knowledge
- Familiarity with hexadecimal notation
- GDB installed (`sudo apt install gdb` or equivalent)

Every exploitation technique, buffer overflows, ROP chains, shellcode, ultimately comes down to controlling what's in the CPU's registers. Before you can redirect execution or set up a syscall, you need to understand what each register does, how they relate to each other, and how the calling convention determines where function arguments live.

This tutorial covers the registers and calling conventions for both x86 (32-bit) and x64 (64-bit) Linux, with a focus on what matters for exploit development.

## x86 Registers (32-bit)

### General-Purpose Registers

x86 has eight 32-bit general-purpose registers. While any of them _can_ hold arbitrary data, each has a conventional role:

| Register | Name              | Conventional Use                              |
| -------- | ----------------- | --------------------------------------------- |
| EAX      | Accumulator       | Return values, syscall numbers                |
| EBX      | Base              | Base pointer for memory access, syscall arg 1 |
| ECX      | Counter           | Loop counters, syscall arg 2                  |
| EDX      | Data              | I/O operations, syscall arg 3                 |
| ESI      | Source Index      | Source pointer for string operations          |
| EDI      | Destination Index | Destination pointer for string operations     |
| EBP      | Base Pointer      | Points to the base of the current stack frame |
| ESP      | Stack Pointer     | Points to the top of the stack                |

### Special-Purpose Registers

| Register | Purpose                                                          |
| -------- | ---------------------------------------------------------------- |
| EIP      | Instruction Pointer, address of the next instruction to execute |
| EFLAGS   | Status flags set by arithmetic and comparison operations         |

EIP is the register you're trying to control in a buffer overflow. You cannot write to it directly with `mov`; it's modified implicitly by instructions like `ret`, `call`, and `jmp`.

## x64 Registers (64-bit)

x64 extends every x86 register to 64 bits and adds eight new general-purpose registers:

### General-Purpose Registers

| Register | Conventional Use                                                                                               |
| -------- | -------------------------------------------------------------------------------------------------------------- |
| RAX      | Return values, syscall numbers                                                                                 |
| RBX      | Callee-saved (preserved across function calls)                                                                 |
| RCX      | 4th function argument, syscall clobbered (overwritten by the syscall instruction: the value is not preserved) |
| RDX      | 3rd function argument, syscall arg 3                                                                           |
| RSI      | 2nd function argument, syscall arg 2                                                                           |
| RDI      | 1st function argument, syscall arg 1                                                                           |
| RBP      | Base pointer (or general-purpose if frame pointer omitted)                                                     |
| RSP      | Stack pointer                                                                                                  |
| R8       | 5th function argument                                                                                          |
| R9       | 6th function argument                                                                                          |
| R10      | Syscall arg 4 (replaces RCX for syscalls)                                                                      |
| R11      | Syscall clobbered                                                                                              |
| R12–R15  | Callee-saved                                                                                                   |

### Special-Purpose Registers

| Register | Purpose                             |
| -------- | ----------------------------------- |
| RIP      | Instruction Pointer                 |
| RFLAGS   | Status flags (extended from EFLAGS) |

## Partial Register Access

One of the most important concepts for shellcode development: x86 and x64 registers can be accessed in smaller pieces.

### x86 Partial Access

Each 32-bit register contains smaller accessible portions:

```text
EAX (32 bits)
├── AX (lower 16 bits)
│   ├── AH (upper 8 bits of AX)
│   └── AL (lower 8 bits of AX)
```

The same pattern applies to EBX/BX/BH/BL, ECX/CX/CH/CL, and EDX/DX/DH/DL.

ESI, EDI, EBP, and ESP only expose a 16-bit lower half (SI, DI, BP, SP); no 8-bit access on x86.

### x64 Partial Access

x64 extends this scheme. Every 64-bit register provides access to its lower 32, 16, and 8 bits:

```text
RAX (64 bits)
├── EAX (lower 32 bits)
│   ├── AX (lower 16 bits)
│   │   ├── AH (upper 8 of AX)
│   │   └── AL (lower 8 of AX)
```

The new registers R8–R15 use a different naming convention:

```text
R8 (64 bits)
├── R8D (lower 32 bits)
│   ├── R8W (lower 16 bits)
│   │   └── R8B (lower 8 bits)
```

### Why This Matters for Exploitation

Partial register access is essential for avoiding null bytes in shellcode. Compare:

```asm
mov eax, 0xb         ; b8 0b 00 00 00  — contains three null bytes
```

```asm
xor eax, eax         ; 31 c0           — zeros EAX, no nulls
mov al, 0xb          ; b0 0b           — sets only the low byte
```

Both produce `EAX = 0x0000000b`, but the second approach is null-free.

> **Note**: On x64, writing to a 32-bit register (like EAX) automatically zeros the upper 32 bits of the full 64-bit register. Writing to 16-bit or 8-bit portions does _not_ zero the upper bits. This behavior matters when constructing values in shellcode.

## The Flags Register

The EFLAGS/RFLAGS register contains status bits set by arithmetic and comparison instructions. The flags you'll encounter most in exploitation:

| Flag | Name           | Set When                                        |
| ---- | -------------- | ----------------------------------------------- |
| ZF   | Zero Flag      | Result of last operation was zero               |
| CF   | Carry Flag     | Unsigned overflow occurred                      |
| SF   | Sign Flag      | Result was negative (Most Significant Bit is 1) |
| OF   | Overflow Flag  | Signed overflow occurred                        |
| DF   | Direction Flag | Controls string operation direction             |

### How Flags Drive Execution

Conditional jumps read flags to make decisions:

```asm
cmp eax, 0           ; Sets ZF=1 if EAX is zero
je  target           ; Jump if ZF=1 (Equal / Zero)
jne target           ; Jump if ZF=0 (Not Equal / Not Zero)
jl  target           ; Jump if SF≠OF (Less Than, signed)
jb  target           ; Jump if CF=1 (Below, unsigned)
```

In GDB-PEDA, the EFLAGS display shows active flags in brackets:

```text
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
```

Uppercase means the flag is set; lowercase means cleared.

## Calling Conventions

A calling convention defines how functions receive arguments and return values. Getting this right is critical for ROP chains and ret2libc attacks, if you put arguments in the wrong place, the target function won't see them.

### x86: cdecl (C Declaration)

On 32-bit Linux, the dominant calling convention is **cdecl**:

- **Arguments**: pushed onto the stack right-to-left
- **Return value**: in EAX
- **Caller-saved**: EAX, ECX, EDX (caller must save these if it needs them after the call)
- **Callee-saved**: EBX, ESI, EDI, EBP (callee must restore these before returning)
- **Stack cleanup**: caller removes arguments after the call returns

Example, calling `printf("Value: %d\n", 42)`:

```asm
push 42               ; Second argument (pushed first — right-to-left)
push format_string     ; First argument
call printf
add esp, 8            ; Caller cleans up 2 arguments (4 bytes each)
```

Stack layout at the moment `printf` begins executing:

```text
        ┌─────────────────────┐
ESP → │ return address       │  ← pushed by CALL
        ├─────────────────────┤
        │ format_string ptr   │  ← arg 1
        ├─────────────────────┤
        │ 42                  │  ← arg 2
        └─────────────────────┘
```

This is why ret2libc payloads on x86 are structured as:

```text
[ function address ][ return address ][ arg1 ][ arg2 ][ ... ]
```

The function pops the return address from ESP, then reads arguments from the stack positions above it.

### x64: System V AMD64 ABI

On 64-bit Linux, function arguments go in **registers** first:

| Argument | Register              |
| -------- | --------------------- |
| 1st      | RDI                   |
| 2nd      | RSI                   |
| 3rd      | RDX                   |
| 4th      | RCX                   |
| 5th      | R8                    |
| 6th      | R9                    |
| 7th+     | Stack (right-to-left) |

- **Return value**: in RAX (and RDX for 128-bit returns)
- **Caller-saved**: RAX, RCX, RDX, RSI, RDI, R8, R9, R10, R11
- **Callee-saved**: RBX, RBP, R12, R13, R14, R15, for exploit development, callee-saved registers are useful because their values survive function calls, meaning a value you place in RBX before calling a library function will still be there when the function returns
- **Stack alignment**: RSP must be 16-byte aligned before a `call` instruction

Example, calling `write(1, buf, len)`:

```asm
mov rdi, 1             ; fd = stdout
mov rsi, buf           ; buffer address
mov rdx, len           ; byte count
call write
```

No stack manipulation needed for six or fewer arguments. This is why x64 ROP chains require `pop rdi; ret` gadgets; you need to load argument registers from the stack.

### 16-Byte Stack Alignment

The System V AMD64 ABI requires RSP to be 16-byte aligned at the point of a `call` instruction. Since `call` pushes an 8-byte return address, RSP will be misaligned (8-byte aligned, not 16) at the start of the called function. Functions that use SSE instructions (common in libc) will crash with a segfault if alignment is wrong.

In ROP chains, if your exploit crashes inside a library function, try adding a single `ret` gadget before the function call to adjust alignment:

```text
[ pop rdi; ret ] [ "/bin/sh" addr ] [ ret ] [ system() addr ]
```

The extra `ret` pops 8 bytes off the stack, shifting RSP from 8-byte to 16-byte alignment.

## Syscall Conventions

Syscalls use a different convention from function calls. This matters when you're writing shellcode or building ROP chains that invoke syscalls directly.

### x86 Syscalls (int 0x80)

| Purpose        | Register |
| -------------- | -------- |
| Syscall number | EAX      |
| Arg 1          | EBX      |
| Arg 2          | ECX      |
| Arg 3          | EDX      |
| Arg 4          | ESI      |
| Arg 5          | EDI      |
| Arg 6          | EBP      |

### x64 Syscalls (syscall instruction)

| Purpose        | Register |
| -------------- | -------- |
| Syscall number | RAX      |
| Arg 1          | RDI      |
| Arg 2          | RSI      |
| Arg 3          | RDX      |
| Arg 4          | R10      |
| Arg 5          | R8       |
| Arg 6          | R9       |

> **Note**: The x64 syscall convention uses R10 instead of RCX for the 4th argument. This is because the `syscall` instruction clobbers RCX (it stores the return address in RCX). Similarly, R11 is clobbered (it stores RFLAGS).

## GDB-PEDA

GDB-PEDA (Python Exploit Development Assistance for GDB) is a GDB extension that adds exploit-development features on top of the standard debugger. It provides colored register and stack displays, pattern generation for offset finding (`pattern create` / `pattern offset`), memory mapping inspection (`vmmap`), binary security checks (`checksec`), and more.

All tutorials in this series use GDB-PEDA. To install it:

```bash
git clone https://github.com/longld/peda.git ~/peda && echo "source ~/peda/peda.py" >> ~/.gdbinit
```

After installation, GDB will load PEDA automatically every time it starts, and you'll see the `gdb-peda$` prompt.

> [!note]
> ROP and ret2libc are techniques covered later in this series that rely on the register and calling convention knowledge from this tutorial.

## Observing Registers in GDB-PEDA

### Viewing All Registers

```text
gdb-peda$ info registers
```

PEDA's `context` display shows registers automatically at every breakpoint, color-coded by whether they've changed since the last stop.

### Viewing Specific Registers

```text
gdb-peda$ i r rdi rsi rdx
rdi            0x7fffffffe5a0   140737488348576
rsi            0x0              0
rdx            0x0              0
```

### Modifying Registers

Useful for testing what happens if a register has a specific value:

```text
gdb-peda$ set $rdi = 0x41414141
gdb-peda$ set $rax = 59
```

### Watching Register Changes

Set a breakpoint and step through instructions, watching how registers change:

```text
gdb-peda$ b *main
gdb-peda$ r
gdb-peda$ si
```

PEDA highlights modified registers in red after each step, making it easy to follow data flow.

## Two gotchas worth remembering

Partial register access (AL, AX, EAX, RAX) is essential for writing null-free shellcode. And x64 requires 16-byte stack alignment before `call` instructions: if your ROP chain crashes in a library function, add a `ret` gadget to fix alignment.
