The CPU does not know what your bits mean. It knows how to manipulate bit patterns according to rules — and different instructions apply different rules to the same bit pattern. The number 0xFF stored in a byte might be the unsigned integer 255, the...
In This Chapter
- Everything Is a Bit Pattern
- Bits, Bytes, and Data Widths
- Hexadecimal as Binary Shorthand
- Unsigned Integers: Range and Overflow
- Two's Complement: Signed Integers
- Sign Extension
- The RFLAGS Register
- Fixed-Width Arithmetic: The Hardware Reality
- IEEE 754 Floating Point
- Endianness: Little-Endian x86
- Putting It Together: A Complete Arithmetic Example
- Summary
Chapter 2: Numbers in the Machine
Everything Is a Bit Pattern
The CPU does not know what your bits mean. It knows how to manipulate bit patterns according to rules — and different instructions apply different rules to the same bit pattern. The number 0xFF stored in a byte might be the unsigned integer 255, the signed integer -1, the ASCII character ÿ, or the least significant byte of some larger value. The meaning is imposed by the instruction that operates on it, not by the bit pattern itself.
This chapter is about the rules. If you understand binary representation, two's complement, IEEE 754, and the flags register, you can predict the result of every arithmetic instruction in x86-64 assembly. If you don't, arithmetic in assembly will produce results that seem random until the bugs accumulate enough to find.
This is not optional background material. This is the machine's type system.
Bits, Bytes, and Data Widths
The fundamental unit of x86-64 memory is the byte — 8 bits. The architecture processes data in four sizes:
| Size | Name | Bits | x86-64 Suffix | NASM Declaration | Range (unsigned) |
|---|---|---|---|---|---|
| Byte | BYTE | 8 | b |
db |
0 to 255 |
| Word | WORD | 16 | w |
dw |
0 to 65,535 |
| Double Word | DWORD | 32 | d |
dd |
0 to 4,294,967,295 |
| Quad Word | QWORD | 64 | q |
dq |
0 to 18,446,744,073,709,551,615 |
The NASM size suffixes appear on memory operands: BYTE [rax], WORD [rbp-2], DWORD [rbp-4], QWORD [rbp-8]. When the size is ambiguous from context, you must specify it.
The x86-64 instruction mnemonics also carry size implications: al, bl, cl, dl are byte registers; ax, bx are word registers; eax, ebx are dword registers; rax, rbx are qword registers. The instruction mov al, [rdi] loads a byte; mov rax, [rdi] loads a quadword.
Hexadecimal as Binary Shorthand
Binary is the machine's native language, but writing 64 bits in binary is unwieldy. Hexadecimal (base 16) is the solution: each hex digit corresponds to exactly 4 binary bits, making the conversion trivial.
Binary: 0000 0001 0010 0011 0100 0101 0110 0111
Hex: 0 1 2 3 4 5 6 7
Binary: 1000 1001 1010 1011 1100 1101 1110 1111
Hex: 8 9 A B C D E F
In NASM, hex literals are written as 0x prefix (0xFF) or h suffix (0FFh — the leading 0 is required when starting with A-F to distinguish from identifiers). In GDB output and objdump output, hex is standard.
Key conversions to memorize:
| Hex | Binary | Decimal | Notes |
|---|---|---|---|
0xFF |
1111 1111 |
255 | Max unsigned byte |
0x80 |
1000 0000 |
128 | Min signed negative (byte) |
0x7F |
0111 1111 |
127 | Max signed positive (byte) |
0x00 |
0000 0000 |
0 | |
0xFFFFFFFF |
(32 ones) | 4,294,967,295 | Max unsigned 32-bit |
0x80000000 |
1 followed by 31 zeros | 2,147,483,648 | Min signed 32-bit negative |
0x7FFFFFFF |
0 followed by 31 ones | 2,147,483,647 | Max signed 32-bit positive |
Unsigned Integers: Range and Overflow
An n-bit unsigned integer represents values from 0 to 2^n − 1. The bit pattern is the value in base 2.
Unsigned arithmetic wraps around at 2^n:
mov al, 255 ; al = 0xFF = 11111111b
add al, 1 ; al = 0x00 = 00000000b (wrapped to 0!)
; Carry Flag (CF) = 1 (overflow out of the top)
This wraparound is defined behavior, not an error. The processor signals it by setting the Carry Flag (CF). If you're tracking unsigned overflow, check CF after the operation.
Range check example:
; Check if rax is a valid array index (unsigned, must be < 100)
cmp rax, 100 ; compare rax with 100 (unsigned comparison)
jae out_of_bounds ; jump if rax >= 100 (unsigned: JAE = Jump if Above or Equal)
; rax is in range [0, 99]
Note: jae for unsigned "above or equal"; jge for signed "greater or equal". These are different instructions operating on different flag combinations.
Two's Complement: Signed Integers
Two's complement is the system x86-64 uses to represent signed integers. It is brilliant for one reason: addition and subtraction work identically whether the operands are signed or unsigned. The hardware doesn't need to know the sign — the programmer decides the interpretation.
The Representation
For an n-bit two's complement number: - The most significant bit (MSB) has weight −2^(n−1) instead of +2^(n−1) - All other bits have their normal positive weights
For an 8-bit value b7 b6 b5 b4 b3 b2 b1 b0:
value = -128×b7 + 64×b6 + 32×b5 + 16×b4 + 8×b3 + 4×b2 + 2×b1 + 1×b0
Example: 10110100 binary
= -128×1 + 64×0 + 32×1 + 16×1 + 8×0 + 4×1 + 2×0 + 1×0
= -128 + 32 + 16 + 4
= -76
The bit pattern 0x80 (10000000) represents −128. The bit pattern 0xFF (11111111) represents −1. The bit pattern 0x7F (01111111) represents +127.
The Flip-and-Add-1 Rule (and Why It Works)
To negate a two's complement number: flip all bits, then add 1.
Why? For any bit pattern P, its bitwise complement ~P satisfies P + ~P = 0xFF...FF (all ones, which is −1 in two's complement). Therefore:
P + ~P = -1
P + ~P + 1 = 0
~P + 1 = -P
So ~P + 1 is the negative of P. This is not a trick — it's a theorem.
Example: negate 5 in 8-bit two's complement:
5 = 00000101
~5 = 11111010 (flip all bits)
+1 = 11111011 = -5 ✓
Verify: 5 + (-5) = 00000101 + 11111011 = 100000000
^-- 9th bit: Carry, discarded in 8-bit arithmetic
Result: 00000000 = 0 ✓
In x86-64 assembly, the NEG instruction performs two's complement negation:
mov rax, 5
neg rax ; rax = -5 (0xFFFFFFFFFFFFFFFB)
Signed Arithmetic Overflow vs. Carry
This is one of the most confused concepts in assembly. Two flags govern arithmetic errors:
- Carry Flag (CF): Set when an arithmetic result cannot fit in the destination as an unsigned value. Unsigned overflow.
- Overflow Flag (OF): Set when an arithmetic result cannot fit in the destination as a signed value. Signed overflow.
They are independently set and independently checked:
; Example 1: CF set, OF clear (unsigned overflow, no signed overflow)
mov al, 200 ; 200 = 0xC8 (interpreted as signed: -56)
add al, 100 ; 200 + 100 = 300 (doesn't fit in 8-bit unsigned: wraps to 44)
; Result: al = 44, CF = 1, OF = 0
; -56 + 100 = 44 (fits in signed 8-bit: no signed overflow)
; Example 2: OF set, CF clear (signed overflow, no unsigned overflow)
mov al, 100 ; 100 = 0x64 (fits as both signed and unsigned)
add al, 100 ; 100 + 100 = 200 (fits in unsigned 8-bit)
; Result: al = 200 = 0xC8, CF = 0, OF = 1
; Signed: 100 + 100 = 200, but max signed 8-bit is 127: overflow!
; Example 3: Both CF and OF set
mov al, 200 ; 0xC8 (unsigned 200, signed -56)
add al, 200 ; Unsigned: 200+200=400, doesn't fit: CF=1, result=144
; Signed: -56 + (-56) = -112, fits (-128 to 127): OF = 0
; Wait -- let me recalculate with actual hardware behavior:
; al = 0xC8 + 0xC8 = 0x190, lower 8 bits = 0x90 = 144
; Signed: 0x90 = -112 (bit 7 set). -56 + -56 = -112: fits! OF = 0
; Example 4: True both-set case
mov al, 0x80 ; -128 (signed), 128 (unsigned)
add al, 0x80 ; 0x80 + 0x80 = 0x100, lower 8 bits = 0x00
; CF = 1 (unsigned 128+128=256, doesn't fit in 8 bits)
; OF = 1 (signed -128 + -128 = -256, doesn't fit in signed 8-bit)
The rule for signed overflow: OF is set when two positive numbers add to a negative result, or two negative numbers add to a positive result. Equivalently: OF = XOR of the carry into the MSB and the carry out of the MSB.
Register Trace: Arithmetic with Flags
| Instruction | RAX | RFLAGS (CF, OF, SF, ZF) |
|---|---|---|
| (initial) | 0x0000000000000000 |
— |
mov eax, 100 |
0x0000000000000064 |
unchanged |
add eax, 200 |
0x000000000000012C |
CF=0, OF=0, SF=0, ZF=0 |
mov eax, 0x7FFFFFFF |
0x000000000x7FFFFFFF |
unchanged |
add eax, 1 |
0x0000000080000000 |
CF=0, OF=1, SF=1, ZF=0 |
mov eax, 0xFFFFFFFF |
0x00000000FFFFFFFF |
unchanged |
add eax, 1 |
0x0000000000000000 |
CF=1, OF=0, SF=0, ZF=1 |
Note: After add eax, 1 in the last row, the full RAX is 0x0000000000000000 because writing EAX zeros the upper 32 bits.
Sign Extension
When moving a smaller signed value into a larger register, you must sign extend — fill the upper bits with copies of the sign bit.
movsx rax, al ; sign-extend 8-bit AL to 64-bit RAX
movsx rax, ax ; sign-extend 16-bit AX to 64-bit RAX
movsx rax, eax ; sign-extend 32-bit EAX to 64-bit RAX (also: movsxd)
movzx rax, al ; zero-extend 8-bit AL to 64-bit RAX (unsigned extension)
movzx rax, ax ; zero-extend 16-bit AX to 64-bit RAX
; Example:
mov al, -5 ; al = 0xFB = 11111011
movsx rax, al ; rax = 0xFFFFFFFFFFFFFFFB = -5 in 64-bit
movzx rax, al ; rax = 0x00000000000000FB = 251 in 64-bit (different!)
Sign extension matters when calling functions that expect 64-bit arguments but you're computing with narrower values. If you compute a loop index in eax and then use it in a memory address, the compiler uses movsx rax, eax (or lets the hardware do it implicitly if the index fits) to ensure the full 64-bit address is computed correctly.
The CDQ instruction sign-extends EAX into EDX:EAX (32-bit → 64-bit), and CQO sign-extends RAX into RDX:RAX (64-bit → 128-bit). Both are used to prepare the dividend for IDIV.
The RFLAGS Register
RFLAGS is a 64-bit register (the lower 32 bits are EFLAGS, the lower 16 bits are FLAGS). Most programs only touch a few of its bits:
Bit Symbol Name Set when...
0 CF Carry Flag Unsigned overflow; also used by shift/rotate, ADCX
2 PF Parity Flag Low 8 bits of result have even number of 1s
4 AF Adjust Flag BCD arithmetic carry from bit 3 to bit 4
6 ZF Zero Flag Result is zero
7 SF Sign Flag Result has bit 7 (or MSB) set (negative if signed)
8 TF Trap Flag (System) single-step mode; triggers debug exception per instruction
9 IF Interrupt Flag (System) enables hardware interrupts; set by STI, cleared by CLI
10 DF Direction Flag String ops increment (DF=0) or decrement (DF=1) SI/DI
11 OF Overflow Flag Signed overflow
12-13 IOPL I/O Privilege Level (System) minimum CPL for I/O instructions
14 NT Nested Task (System) set if current task was called with CALL
16 RF Resume Flag (System) suppresses debug fault for one instruction
17 VM Virtual-8086 Mode (System) sets V86 mode
18 AC Alignment Check Triggers alignment exception if set and CPL=3
21 ID ID Flag Can be toggled if CPUID is supported
For user-space assembly, the flags you care about are CF, ZF, SF, OF, DF, and PF (occasionally).
Reading RFLAGS
You cannot mov rax, rflags — RFLAGS is not a general-purpose register. To read it:
pushfq ; push RFLAGS onto the stack (8 bytes)
pop rax ; rax now contains RFLAGS
To modify specific bits:
pushfq
pop rax
or rax, (1 << 8) ; set bit 8 (TF — trap flag)
push rax
popfq ; restore RFLAGS with modification
How Flags Enable Conditional Jumps
The conditional jump instructions (jz, jne, jl, jge, etc.) test one or more flags:
| Instruction | Mnemonic | Flag Condition | Use Case |
|---|---|---|---|
jz / je |
Jump if Zero/Equal | ZF=1 | cmp a, b; jz equal |
jnz / jne |
Jump if Not Zero/Equal | ZF=0 | Loop until zero |
jc / jb |
Jump if Carry/Below | CF=1 | Unsigned underflow |
jnc / jae |
Jump if No Carry/Above-or-Equal | CF=0 | Unsigned no-overflow |
js |
Jump if Sign | SF=1 | Result is negative (signed) |
jns |
Jump if Not Sign | SF=0 | Result is non-negative |
jo |
Jump if Overflow | OF=1 | Signed overflow |
jno |
Jump if No Overflow | OF=0 | No signed overflow |
jl / jnge |
Jump if Less | SF≠OF | Signed less-than |
jge / jnl |
Jump if Greater-or-Equal | SF=OF | Signed ≥ |
jle / jng |
Jump if Less-or-Equal | ZF=1 or SF≠OF | Signed ≤ |
jg / jnle |
Jump if Greater | ZF=0 and SF=OF | Signed > |
ja / jnbe |
Jump if Above | CF=0 and ZF=0 | Unsigned > |
jbe / jna |
Jump if Below-or-Equal | CF=1 or ZF=1 | Unsigned ≤ |
The distinction between jl (signed less-than) and jb (unsigned below) is critical. Using the wrong one on an unsigned comparison produces silently wrong results.
Fixed-Width Arithmetic: The Hardware Reality
Every arithmetic instruction in x86-64 operates on a fixed-width operand: 8, 16, 32, or 64 bits. The hardware computes the mathematical result and then truncates it to the operand width. The flags record whether the truncation lost information.
This is different from how most programmers think about arithmetic. In mathematics, 127 + 1 = 128. In 8-bit two's complement arithmetic, 127 + 1 = -128 (with OF=1, indicating signed overflow). The hardware is correct within the rules of fixed-width arithmetic; the programmer's interpretation determines whether the result is meaningful.
; 64-bit addition: no overflow for reasonable values
mov rax, 9000000000000000000 ; fits in 64-bit signed
add rax, 9000000000000000000 ; result: -446744073709551616 (overflow! OF=1)
; But unsigned? 9e18 + 9e18 = 1.8e19, max 64-bit unsigned = 1.84e19...
; Actually this still fits unsigned.
; Maximum 64-bit signed overflow:
mov rax, 0x7FFFFFFFFFFFFFFF ; INT64_MAX
add rax, 1 ; rax = 0x8000000000000000 = INT64_MIN, OF=1
The INTO instruction triggers an overflow exception when OF=1, but it doesn't exist in 64-bit mode. Instead, check OF with jo after arithmetic.
IEEE 754 Floating Point
Integer arithmetic is exact (within its range). Floating-point arithmetic is approximate, with rules so subtle that entire books have been written about them.
The Representation
IEEE 754 single precision (32-bit float):
31 30 23 22 0
┌─┬─────────────┬──────────────────────────┐
│S│ Exponent │ Mantissa │
│1│ 8 bits │ 23 bits │
└─┴─────────────┴──────────────────────────┘
Value = (-1)^S × 2^(E-127) × (1 + M/2^23)
IEEE 754 double precision (64-bit double):
63 62 52 51 0
┌─┬────────────────────┬─────────────────────────────────────┐
│S│ Exponent │ Mantissa │
│1│ 11 bits │ 52 bits │
└─┴────────────────────┴─────────────────────────────────────┘
Value = (-1)^S × 2^(E-1023) × (1 + M/2^52)
The value is: sign × 2^(exponent - bias) × (1.mantissa in binary).
The implicit leading 1 is the key: the mantissa always starts with 1. (which is not stored, saving one bit). This is called the "implicit leading bit" or "hidden bit."
Why 0.1 + 0.2 ≠ 0.3
The decimal number 0.1 cannot be represented exactly in binary. Here's why:
0.1 in binary = 0.0001100110011001100110011... (repeating)
Just like 1/3 = 0.33333... repeats in decimal, 1/10 = 0.0001100110011... repeats in binary. The 23-bit mantissa of a float can only store a finite number of these bits.
The closest float to 0.1 is:
0.1 exactly: 0.1000000000000000055511151231257827021181583404541015625
Stored float: the nearest representable value
When you add two approximate values and compare to a third approximate value, the accumulated rounding errors mean the comparison fails.
In assembly terms:
; XMM registers hold floating-point values
; ADDSS = Add Scalar Single-precision
; SUBSS = Subtract Scalar Single-precision
; COMISS = Compare ordered Scalar Single-precision
section .data
val1 dd 0.1 ; NASM converts 0.1 to the nearest float
val2 dd 0.2
val3 dd 0.3
; Note: val1 + val2 ≠ val3 due to rounding
section .text
movss xmm0, [val1]
addss xmm0, [val2] ; xmm0 = 0.1 + 0.2 (approximately 0.30000000000000004)
movss xmm1, [val3]
comiss xmm0, xmm1 ; compare: xmm0 == xmm1?
je they_are_equal ; this jump is NOT taken
; We end up here: 0.1 + 0.2 ≠ 0.3 in float!
Special Values
IEEE 754 reserves special bit patterns for non-numeric states:
| Value | Exponent bits | Mantissa | Meaning |
|---|---|---|---|
| ±0 | all zeros | all zeros | Positive/negative zero (they compare equal) |
| ±∞ | all ones | all zeros | Positive/negative infinity |
| NaN | all ones | nonzero | Not a Number (result of 0/0, √-1, etc.) |
| Denormal | all zeros | nonzero | Subnormal numbers near zero (no implicit 1) |
NaN has a critical property: NaN ≠ NaN. A comparison of NaN with any value (including itself) returns "unordered" (false for ordered comparisons). This is how you detect NaN: x != x is true only if x is NaN.
Denormal (subnormal) numbers cause significant performance problems. When a floating-point result would underflow to zero with a normal representation, the CPU produces a denormal value instead — a number with no implicit leading 1 bit. Computing with denormals is handled in software on many CPUs and can be 10-100x slower than normal floating-point. The MXCSR register has bits to flush denormals to zero (FTZ) and treat denormals as zero (DAZ) for performance-critical code.
Exact Representation
Which numbers CAN be exactly represented?
All integers up to 2^53 can be exactly represented as double-precision floats (the mantissa has 52 explicit bits plus the implicit 1). Beyond that, consecutive integers begin to be separated by gaps larger than 1.
All fractions of the form p/2^n can be exactly represented if the result has at most 52 significant binary digits. So 0.5, 0.25, 0.125, 0.0625 are all exact. But 0.1, 0.2, 0.3, 0.4, 0.6, 0.7, 0.8, 0.9 are not.
This is fundamental: floating-point is exact only for powers-of-2 fractions and small integers.
Endianness: Little-Endian x86
When a multi-byte value is stored in memory, the bytes must be arranged in some order. x86-64 is little-endian: the least significant byte is stored at the lowest address.
; Store the value 0x01020304 at address [buffer]
section .data
buffer: dd 0x01020304 ; stored in memory as: 04 03 02 01 (little-endian!)
; In memory at buffer's address:
; [buffer+0] = 0x04 (least significant byte)
; [buffer+1] = 0x03
; [buffer+2] = 0x02
; [buffer+3] = 0x01 (most significant byte)
This is counterintuitive when you first encounter it — the bytes are "backwards" compared to how you write the number. But it means that the least significant byte is always at the base address, which simplifies some operations.
Network byte order is big-endian. Internet protocols (TCP/IP, DNS, HTTP/2 binary framing) use big-endian byte order. When reading network data on an x86-64 machine, you must byte-swap multi-byte fields.
The BSWAP instruction reverses the byte order of a 32-bit or 64-bit register:
; Convert a 32-bit value from network order (big-endian) to host order (little-endian)
mov eax, [network_data] ; load big-endian value
bswap eax ; byte-reverse: 0x01020304 → 0x04030201
; eax now contains the host-order value
For 16-bit values, BSWAP is not directly available; instead use XCHG al, ah.
Why little-endian? Little-endian has one practical advantage: you can read a multi-byte integer at its base address regardless of its size. The byte at address A is always the least significant byte, whether you're reading a byte, word, dword, or qword. This simplifies certain protocols and data structures. Big-endian has the advantage that the hexdump of a binary file "reads" correctly left-to-right, which some argue is more natural for humans.
Neither is universally better. x86 was little-endian from its earliest days; ARM and MIPS can operate in either mode; network protocols historically chose big-endian. Live with it.
Putting It Together: A Complete Arithmetic Example
Here is a complete program that exercises the concepts from this chapter: it reads two 64-bit integers, adds them, checks for overflow, and prints the result.
; arith.asm — demonstrates integer arithmetic, flags, and overflow detection
; nasm -f elf64 arith.asm -o arith.o && ld arith.o -o arith
section .data
overflow_msg db "Overflow detected!", 10
overflow_len equ $ - overflow_msg
result_msg db "Result: "
result_len equ $ - result_msg
section .bss
result_buf resb 20 ; buffer for number-to-string conversion
section .text
global _start
; Helper: convert RAX to decimal string in result_buf
; Returns: RCX = length
int_to_string:
mov rdi, result_buf
add rdi, 19 ; point to end of buffer
mov BYTE [rdi], 0 ; null terminator
mov rbx, 10 ; divisor
xor rcx, rcx ; character count
.loop:
xor rdx, rdx ; zero RDX before division (required!)
div rbx ; rax = rax/10, rdx = rax%10
add dl, '0' ; convert digit to ASCII
dec rdi
mov [rdi], dl
inc rcx
test rax, rax ; is quotient zero?
jnz .loop ; if not, continue
; rdi points to start of string, rcx = length
mov rsi, rdi ; for sys_write
ret
_start:
; Compute 0x7FFFFFFFFFFFFFFF + 1 (max signed 64-bit + 1 = signed overflow)
mov rax, 0x7FFFFFFFFFFFFFFF
add rax, 1 ; OF=1 (signed overflow), SF=1, ZF=0, CF=0
; Check for signed overflow
jo .overflow ; jump if OF=1
; No overflow: print the result
push rax ; save result
mov rax, 1 ; sys_write
mov rdi, 1 ; stdout
mov rsi, result_msg
mov rdx, result_len
syscall
pop rax ; restore result
call int_to_string
mov rax, 1
mov rdi, 1
; rsi already set by int_to_string
mov rdx, rcx
syscall
jmp .exit
.overflow:
mov rax, 1 ; sys_write
mov rdi, 1 ; stdout
mov rsi, overflow_msg
mov rdx, overflow_len
syscall
.exit:
mov rax, 60 ; sys_exit
xor rdi, rdi ; exit code 0
syscall
Register trace for the key arithmetic section:
| Instruction | RAX | RDX | RFLAGS (OF, SF, ZF, CF) |
|---|---|---|---|
| (initial) | ? | ? | — |
mov rax, 0x7FFFFFFFFFFFFFFF |
0x7FFFFFFFFFFFFFFF |
? | unchanged |
add rax, 1 |
0x8000000000000000 |
? | OF=1, SF=1, ZF=0, CF=0 |
The result 0x8000000000000000 is −9,223,372,036,854,775,808 interpreted as signed (INT64_MIN), which is correct: we overflowed from the maximum positive to the minimum negative value. The OF flag tells us this happened. The jo instruction detects it.
Summary
The machine's number system has rules, and those rules are precise:
- Unsigned arithmetic wraps with CF; signed arithmetic overflows with OF
- Two's complement is mathematically principled, not just a convention
- Sign extension is mandatory when widening signed values; zero extension for unsigned
- Floating-point is approximate; only powers-of-2 fractions and small integers are exact
- x86-64 is little-endian; network protocols are big-endian;
bswapconverts between them - RFLAGS records arithmetic outcomes; conditional jumps test flags
Every assembly bug involving arithmetic can be diagnosed using these rules. When a value is wrong, ask: which instruction set the wrong value? What did the flags say? Was it signed or unsigned arithmetic? Was there an overflow that wasn't checked? The rules answer the question.
🔄 Check Your Understanding: What is the value of
raxafter these two instructions?nasm mov rax, 0x0000000100000000 add eax, eax
Answer
The instructionadd eax, eaxadds the lower 32 bits of RAX to themselves. EAX before the add is0x00000000(the lower 32 bits of0x0000000100000000).0 + 0 = 0. The result written to EAX is 0, which also zeros the upper 32 bits. Sorax = 0x0000000000000000.This is a classic gotcha: the upper 32 bits of RAX (which contained
0x00000001) are destroyed by the 32-bit write. If the intent was to double the 64-bit value, the instruction should have beenadd rax, rax.