Demystifying x86-64 Assembly: A Hands-On Guide to Register Sizes with DDD

If you are diving into vulnerability research, reverse engineering, or low-level programming, understanding how the CPU handles data sizes is critical. The x86-64 architecture has a unique quirk: it allows you to manipulate the main 64-bit registers in smaller chunks (8-bit, 16-bit, and 32-bit).

But what happens to the rest of the register when you only change a small part of it?

In this tutorial, we will use DDD (Data Display Debugger) to step through a simple assembly program. We will observe exactly how the RAX register behaves when we write bytes, words, doublewords, and quadwords into it.

The Program

;first_with_dnum.asm

section .data

bNum db 0x41 ; 'A' (ASCII 65)

wNum dw 0x1337 ; Hex value for clarity

dNum dd 0xDEADBEEF ; 32-bit doubleword value

qNum1 dq 0x1122334455667788 ; 64-bit quadword

qNum2 dq 0x0 ; Destination buffer initialized to 0

qNum3 dq 7.13 ; Floating-point value


section .bss


section .text

global main


main:

push rbp

mov rbp, rsp


; --- 1. Working with Bytes (8-bit) ---

mov rax, -1 ; Fill rax with 1s (0xFFFFFFFFFFFFFFFF)

mov al, byte [bNum] ; Does NOT clear upper bits of rax

xor rax, rax ; Clear the rax register completely

mov al, byte [bNum] ; Now rax has the correct clean value


; --- 2. Working with Words (16-bit) ---

mov rax, -1 ; Fill rax with 1s

mov ax, word [wNum] ; Does NOT clear upper bits of rax

xor rax, rax ; Clear the rax register

mov ax, word [wNum] ; Now rax has the correct value


; --- 3. Working with Doublewords (32-bit) ---

mov rax, -1 ; Fill rax with 1s

mov eax, dword [dNum] ; AUTOMATICALLY clears upper 32 bits of rax!

; Notice: No 'xor rax, rax' is needed here because 32-bit writes zero-extend.


; --- 4. Working with Quadwords (64-bit) ---

mov rax, -1 ; Fill rax with 1s

mov rax, qword [qNum1] ; Completely overwrites rax (64-bit to 64-bit)

mov qword [qNum2], rax ; Store register back to memory variable qNum2

mov rax, 123456 ; Load an immediate value into rax


; --- 5. Floating-Point Instruction ---

movsd xmm0, [qNum3] ; Move scalar double-precision float to XMM register


; Epilogue and Return

mov rsp, rbp ; Restore stack pointer

pop rbp ; Restore base pointer

ret ; Retrun from main



The Setup

Part 1: Compilation & Debugging Setup

Before opening DDD, the program is compiled using the provided Makefile:

  1. nasm -f elf64 -g first.asm -o first.o -l first.lst: Assembles the source code into a 64-bit ELF object file. The -g flag is crucial because it embeds debugging symbols (line numbers and variable names) so GDB and DDD can read them.
  2. gcc -o first first.o -no-pie: Links the object file into an executable named first. -no-pie (Position Independent Executa[.ble disabled) keeps memory addresses fixed and easier to track.

make file: 

cat makefile 

first: first.o 

gcc -o first first.o -no-pie 


first.o: first.asm 

nasm -f elf64 -g first.asm -o first.o -l first.lst



make
nasm -f elf64 -g first.asm -o first.o -l first.lst
gcc -o first first.o -no-pie


Launch DDD and set a breakpoint at the
main function:

ddd ./first
(gdb) break main


Set break point to understand how Register use in program

Open the Register window (Status -> Register) and click Run.


Step-by-Step Analysis

1. The Prologue: Why isn't RAX changing?

When the program first stops at line 16 (main: push rbp), you will see RAX holds a memory address (e.g., 0x401110).

If you click Step to execute lines 16 and 17 (push rbp and mov rbp, rsp), you will notice the yellow pointer moves, but RAX remains exactly the same.

  • Why? These lines are the function prologue setting up the stack frame. We haven't reached the instructions that actually modify the data registers yet.


2. Working with Bytes (8-bit)

Step until you reach line 20: mov rax, -1.
Execute this line. RAX becomes 0xFFFFFFFFFFFFFFFF. The -1 fills the entire 64-bit register with 1s.
Now execute line 21: mov al, byte [bNum].
RAX becomes 0xFFFFFFFFFFFFFF41.
The Lesson: bNum contains 0x41 ('A'). The AL register is the lowest 8 bits of RAX. Notice that the upper 56 bits are still FF. Writing to an 8-bit register does NOT clear the rest of the register. To clean it, we use xor rax, rax (line 22) to zero it out before writing to al again.

3. Working with Words (16-bit)

Step to lines 26 and 27 (mov rax, -1 followed by mov ax, word [wNum]).

  • RAX becomes 0xFFFFFFFFFFFF1337.
  • The Lesson: Just like the 8-bit operation, writing 0x1337 into the 16-bit AX register leaves the upper 48 bits of RAX completely untouched.

4. Working with Doublewords (32-bit) - The Plot Twist

Step to line 32 (mov rax, -1) and execute it. RAX is 0xFFFFFFFFFFFFFFFF again.

Now, execute line 33: mov eax, dword [dNum].

  • RAX becomes 0x00000000DEADBEEF.
  • The Lesson: This is a fundamental rule of x86-64 architecture. A write to a 32-bit register (like EAX) automatically zero-extends and clears the upper 32 bits of the 64-bit register. Notice how the upper 32 bits are now 00000000 instead of FFFFFFFF. You don't need an xor instruction here!

5. Working with Quadwords (64-bit)

Step to line 38: mov rax, qword [qNum1].

  • RAX becomes 0x1122334455667788.
  • The Lesson: A 64-bit write completely overwrites the entire register.




Step 6: Epilogue and Exit

 Click Step to execute the epilogue:
  • mov rsp, rbp (Restore stack pointer)
  • pop rbp (Restore base pointer)
  • ret (Return from main)
  • The program will exit. The GDB console will display:
[Inferior 1 (process XXXX) exited with code 0100]
(Note: 0100 here is octal, which equals 64 in decimal, which is the exit code for 0 in Linux).


Answering Common Questions

As you step through the rest of the code, you might encounter a few confusing moments. Here is what is actually happening:

Q: I stepped to line 40 (mov rax, 123456), but the register shows 0x1e240. Why?
A: DDD displays register values in Hexadecimal by default. If you convert 0x1e240 from hex to decimal, it equals exactly 123456. The CPU is doing its job perfectly!

Q: I stored RAX into qNum2 (line 39), but the value in the register window still looks different. Did it work?
A: The Register window only shows you the CPU registers, not system memory. You successfully copied the value of RAX into the memory address of qNum2, but RAX itself hasn't changed. To see the memory change, you would need to open DDD's "Data" window or inspect the memory address directly.

Q: What happened to qNum3? I don't see it anywhere.
A: On line 43, the instruction movsd xmm0, [qNum3] moves a floating-point number (7.13) from memory into the XMM0 register. qNum3 is still safely in memory. Because XMM0 is a floating-point/SIMD register, it is not shown in the default "Integer registers" view. To see it, check the "All registers" box at the bottom of the DDD Register window.

Conclusion

Understanding how registers are overwritten is crucial. If you are analyzing a binary (or writing an exploit), forgetting that a 32-bit write zero-extends can completely change your understanding of the program's state.

By using DDD to visually map the CPU's state to the assembly code, you can quickly master these low-level architectural quirks.