Instruction Reference: Decisions, Stack, and Bits
Explore comparisons, branches, call stacks, masks, shifts, and debugger stops one instruction at a time.
Cheatsheet for Lesson 5.5
A one-page summary of “Instruction Reference: Decisions, Stack, and Bits” you can keep beside your notes. Plain Markdown.
Lesson 5.5 — Instruction Reference: Decisions, Stack, and Bits: cheatsheet
- Remember: A stack is last-in, first-out. Keep the return address in the right place and reverse your temporary saves in the right order. Popped bytes may remain in memory without being live stack data.
Quick reference
- Equal or affordable?:
cmpsets subtraction flags; pick signed or unsignedjccdeliberately. - Is this bit present?:
test value, maskkeeps the original value. - Where does execution go?:
jmpalways redirects;jccreads a condition; neither creates a return address. - Where does the helper return?: Near
callsaves the continuation;retconsumes it. - How is a temporary value preserved?: Balance
push/popand the convention's other stack work. - How do bits move?:
shlfills low bits with zero;shrfills high bits with zero;sarrepeats the sign. - Which bit operation is needed?:
andkeeps,orsets,xortoggles,notinverts. - Why did execution stop?:
nopcontinues;int3enters breakpoint-exception handling.
Review
- Q: A shop checks a gold amount against a cost using a comparison. What changes during that comparison alone? A: The comparison flags change while the gold value stays put
- Q: A status word contains several independent bits. You want to inspect one bit while keeping every status value intact. What operation is needed? A: A temporary masked comparison that discards its bit result
A function does more than calculate values: it chooses a path, calls other functions, and returns to the right place. These worked examples use a fictional owned, resettable 32-bit x86 game, Intel syntax, and valid toy memory labels. The traces are small teaching models; they do not execute machine code or attach to a process.
Start with values and arithmetic if registers, brackets, or ZF/SF/CF/OF are unfamiliar. Each example is already populated. Changing an input is optional exploration, not a test.
Find an operation
Section titled “Find an operation”| Instruction | Worked example | Intel entry |
|---|---|---|
cmp |
Compare gold without spending | CMP |
test |
Inspect a status bit | TEST |
jmp |
Skip a path | JMP |
jcc family |
Choose by flags | Jcc |
call |
Save where to resume | CALL |
ret |
Resume the caller | RET |
push |
Save a value | PUSH |
pop |
Restore a value | POP |
shl |
Shift left | Shifts |
shr |
Shift right with zeros | Shifts |
sar |
Shift right with the sign | Shifts |
and |
Keep selected bits | AND |
or |
Set selected bits | OR |
xor |
Toggle selected bits | XOR |
not |
Invert the word | NOT |
nop |
Continue without a data operation | NOP |
int3 |
Enter breakpoint handling | INT3 |
reg, imm, and r/m retain their meanings from the previous reference: register, encoded number, and register-or-memory. The forms below are common 32-bit forms, not the entire instruction set. Intel’s manuals give encoding, exception, mode, and special-form details.
Comparisons and paths
Section titled “Comparisons and paths”Meaning: subtract for comparison, keep the flags, and discard the numeric result.
Common forms: cmp reg, r/m/imm, cmp memory, reg/imm. Effects: read operands without writing either; set OF, SF, ZF, AF, CF, and PF as a subtraction would.
mov eax, [gold]cmp eax, 25mov ecx, eax ; gold was not spentCompare gold without spending it. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
mov eax, 100
cmp eax, 25
mov ecx, eaxecx receives the same 100 gold. A following jump can read the comparison flags because mov does not change them.
Final values: price = 25; eax = 100; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1; discarded difference bits = 0x0000004B; ecx = 100.
Read every step of the lesson’s example
Step 1, line 1. Load 100 gold into eax. Nothing has been spent.
price = 25; eax = 100; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1.
Step 2, line 2. Compute 100 minus 25 for the flags, then discard the numeric result. eax remains 100; CF=0, ZF=0.
price = 25; eax = 100; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1; discarded difference bits = 0x0000004B.
Step 3, line 3. ecx receives the same 100 gold. A following jump can read the comparison flags because mov does not change them.
price = 25; eax = 100; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1; discarded difference bits = 0x0000004B; ecx = 100.
Takeaway: the flags changed; the compared gold did not.
Meaning: compute a bitwise AND for comparison, then discard its numeric result.
Common forms: test r/m, reg or test r/m, imm. Effects: leave operands unchanged; set ZF, SF, and PF from the temporary AND, clear CF/OF, and leave AF undefined. test eax, eax asks whether the complete value is zero; test eax, 4 examines bit 2.
mov eax, [status]test eax, 4 ; bit 2 means stunned heremov ecx, eax ; all status bits surviveInspect a status bit without clearing the others. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
mov eax, 5
test eax, 4
mov ecx, eaxThe original status 5 is still in both eax and ecx. The flags, not a changed status word, report the test.
Final values: status = 5; eax = 5; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 0; discarded AND result = 4; ecx = 5.
Read every step of the lesson’s example
Step 1, line 1. eax contains all status bits. Testing one bit must not destroy the others.
status = 5; eax = 5; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1.
Memory: bit 3 = 0 (low four bits only); bit 2 = 1 (low four bits only); bit 1 = 0 (low four bits only); bit 0 = 1 (low four bits only).
Step 2, line 2. Compute the temporary AND result 4, then discard it. ZF=0 answers whether bit 2 is clear; CF and OF become zero.
status = 5; eax = 5; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 0; discarded AND result = 4.
Memory: bit 3 = 0 (low four bits only); bit 2 = 1 (low four bits only); bit 1 = 0 (low four bits only); bit 0 = 1 (low four bits only).
Step 3, line 3. The original status 5 is still in both eax and ecx. The flags, not a changed status word, report the test.
status = 5; eax = 5; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 0; discarded AND result = 4; ecx = 5.
Memory: bit 3 = 0 (low four bits only); bit 2 = 1 (low four bits only); bit 1 = 0 (low four bits only); bit 0 = 1 (low four bits only).
Takeaway: a mask can inspect a field without deleting its other bits.
Meaning: continue at another instruction unconditionally.
Common near forms: jmp label uses an encoded relative displacement; jmp reg32/memory32 reads an indirect destination. Effects: change the instruction pointer; do not create a return address or alter arithmetic flags. Memory-indirect forms read the pointer.
mov eax, 5jmp readymov eax, 99 ; skipped on this pathready: mov [ammo], eaxSkip an instruction unconditionally. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
mov eax, 5
jmp ready
mov eax, 99
ready: mov [ammo], eaxThe skipped assignment never ran. Ammo receives five even though the skipped line would have written 99.
Final values: skipped = 99; eax = 5; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1; next line = 4; ammo = 5.
Read every step of the lesson’s example
Step 1, line 1. eax starts at five. The next instruction chooses where execution continues.
skipped = 99; eax = 5; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1.
Step 2, line 2. jmp sets the instruction pointer to ready. It does not test or change the comparison flags.
skipped = 99; eax = 5; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1; next line = 4.
Step 3, line 4. The skipped assignment never ran. Ammo receives five even though the skipped line would have written 99.
skipped = 99; eax = 5; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1; next line = 4; ammo = 5.
Memory: +0 = 05 (little-endian byte); +1 = 00 (little-endian byte); +2 = 00 (little-endian byte); +3 = 00 (little-endian byte).
Takeaway: a nearby instruction is not necessarily an executed instruction.
Meaning: jump only when the selected flag condition is true; otherwise continue with the next instruction.
jcc is a name for the family, not a literal mnemonic to type. Common form: a specific conditional mnemonic such as jb label. Effects: read the specified flags and potentially change the instruction pointer; preserve registers, arithmetic flags, and game memory. Short and near relative forms have different reach and encoded lengths.
mov eax, [gold]cmp eax, 25jb cannot_buy ; unsigned gold is below costsub eax, 25 ; taken only on the other pathHere cannot_buy names an exit path outside the excerpt. The interactive shows both paths completely and lets you change the conditional mnemonic.
Choose a branch by interpreting the same flags. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
mov eax, -1
cmp eax, 2
jl selected
mov [chosen], 0
jmp done
selected: mov [chosen], 1
done:The result is chosen=1. A signed comparison and an unsigned comparison can legitimately choose different paths for identical bits.
Final values: case = signed; jump = jl; eax = -1; rhs = 2; ZF = 0; SF = 1; CF = 0; OF = 0; PF = 0; taken = true; chosen = 1.
Read every step of the lesson’s example
Step 1, line 1. eax holds 0xFFFFFFFF, signed -1 or unsigned 4294967295. The branch mnemonic decides which interpretation to use.
case = signed; jump = jl; eax = -1; rhs = 2; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1.
Step 2, line 2. cmp keeps eax, discards 0xFFFFFFFD, and sets ZF=0, SF=1, CF=0, OF=0, PF=0.
case = signed; jump = jl; eax = -1; rhs = 2; ZF = 0; SF = 1; CF = 0; OF = 0; PF = 0.
Step 3, line 3. jl asks for signed less. Its condition is true; jump to selected. No flags change.
case = signed; jump = jl; eax = -1; rhs = 2; ZF = 0; SF = 1; CF = 0; OF = 0; PF = 0; taken = true.
Step 4, line 6. Only the selected path writes chosen=1.
case = signed; jump = jl; eax = -1; rhs = 2; ZF = 0; SF = 1; CF = 0; OF = 0; PF = 0; taken = true; chosen = 1.
Step 5, line 7. The result is chosen=1. A signed comparison and an unsigned comparison can legitimately choose different paths for identical bits.
case = signed; jump = jl; eax = -1; rhs = 2; ZF = 0; SF = 1; CF = 0; OF = 0; PF = 0; taken = true; chosen = 1.
| Mnemonic and common aliases | Condition | Interpretation after cmp left, right |
|---|---|---|
je / jz |
ZF = 1 | equal |
jne / jnz |
ZF = 0 | unequal |
jb / jnae / jc |
CF = 1 | unsigned below / carry |
jae / jnb / jnc |
CF = 0 | unsigned at least / no carry |
jbe / jna |
CF = 1 or ZF = 1 | unsigned below or equal |
ja / jnbe |
CF = 0 and ZF = 0 | unsigned above |
jl / jnge |
SF ≠ OF | signed less |
jge / jnl |
SF = OF | signed at least |
jle / jng |
ZF = 1 or SF ≠ OF | signed less or equal |
jg / jnle |
ZF = 0 and SF = OF | signed greater |
jo / jno |
OF = 1 / OF = 0 | signed overflow present / absent |
js / jns |
SF = 1 / SF = 0 | top result bit set / clear |
jp / jpe, jnp / jpo |
PF = 1 / PF = 0 | even / odd low-byte parity |
jcxz/jecxz test a counter register rather than these flags. Do not apply this table blindly to them, or to flags produced by a floating-point comparison in the next reference.
Takeaway: find the producer of the flags and the type intended by the branch before naming the condition.
Calls, returns, and saved values
Section titled “Calls, returns, and saved values”The stack is a thread’s working storage for active calls and temporary data. In these ordinary 32-bit examples it grows toward lower addresses. Saving four bytes lowers esp by four; removing them advances it by four. Real functions also follow a calling convention for arguments, saved registers, alignment, and cleanup.
Meaning: save where execution should resume, then enter another function.
Common near forms: call label or call reg32/memory32. Effects: push the address immediately after the call onto the stack, update esp, and change the instruction pointer. A near call does not itself change arithmetic flags. The callee’s instructions can change registers, flags, and memory afterward.
mov eax, [gold]call grant_coin ; our helper adds one in eaxmov [gold], eax ; resume here after its retSave a continuation before entering the helper. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
mov eax, 10
call grant_coin
mov [gold], eax
jmp done
grant_coin: add eax, 1
ret
done:The round trip ends with 11 gold and the original esp. The saved stack bytes can remain, but they are no longer an active return address.
Final values: gold = 11; eax = 11; esp = 0x00008000; continuation = consumed; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 0; eip = done.
Read every step of the lesson’s example
Step 1, line 1. The caller loads gold. esp starts at an illustrative address; no argument passing convention is being modeled.
gold = 10; eax = 10; esp = 0x00008000; continuation = not saved yet; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1.
Step 2, line 2. A near 32-bit call lowers esp by four, stores the address of the instruction after call, then enters grant_coin.
gold = 10; eax = 10; esp = 0x00007FFC; continuation = line 3: store gold; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1; eip = grant_coin.
Memory: 0x00007FFC = line 3: store gold (current top: esp points here).
Step 3, line 5. The helper adds one coin. The return address is still on top of the stack.
gold = 10; eax = 11; esp = 0x00007FFC; continuation = line 3: store gold; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 0; eip = grant_coin.
Memory: 0x00007FFC = line 3: store gold (current top: esp points here).
Step 4, line 6. ret reads the saved address, advances esp by four, and resumes line 3. It does not itself put the result in eax.
gold = 10; eax = 11; esp = 0x00008000; continuation = consumed; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 0; eip = line 3: store gold.
Memory: 0x00007FFC = line 3: store gold (inactive slot: esp is above it).
Step 5, line 3. The caller stores 11 gold. eax carries the helper's result because this example chose that convention.
gold = 11; eax = 11; esp = 0x00008000; continuation = consumed; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 0; eip = line 3: store gold.
Memory: +0 = 0B (little-endian byte); +1 = 00 (little-endian byte); +2 = 00 (little-endian byte); +3 = 00 (little-endian byte).
Step 6, line 4. Jump past the helper body; otherwise ordinary fall-through could enter it a second time.
gold = 11; eax = 11; esp = 0x00008000; continuation = consumed; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 0; eip = done.
Memory: +0 = 0B (little-endian byte); +1 = 00 (little-endian byte); +2 = 00 (little-endian byte); +3 = 00 (little-endian byte).
Step 7, line 7. The round trip ends with 11 gold and the original esp. The saved stack bytes can remain, but they are no longer an active return address.
gold = 11; eax = 11; esp = 0x00008000; continuation = consumed; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 0; eip = done.
Memory: +0 = 0B (little-endian byte); +1 = 00 (little-endian byte); +2 = 00 (little-endian byte); +3 = 00 (little-endian byte).
Takeaway: a call saves a continuation; it does not guarantee what the helper will return or preserve.
Meaning: read the saved return address from the stack and resume the caller there.
Common near forms: ret; ret imm16 additionally removes a fixed argument-byte count after popping the return address. Effects: update the instruction pointer and esp; leave arithmetic flags alone. Returning a value in eax is a calling-convention choice, not work performed by ret itself.
grant_coin:add eax, 1ret ; eax was changed by add, not ret1 Before the call: gold is in eax
Registers, then the stackregistersstack: S − 4, then Sesp = Seax = goldfreeS − 4callerdata at SThe caller has read gold. esp points at S, the top of the caller’s stack.
2 Inside grant_coin, after add
Registers, then the stackregistersstack: S − 4, then Sesp = S − 4eax = gold + 1return addressthe mov after the callcallerdata at Scall lowered esp by 4 and stored the address of the next mov at S − 4. The helper then added one to eax.
3 ret: pop the address and resume
Registers, then the stackregistersstack: S − 4, then Sesp = Seax = gold + 1return addresspopped; resume therecallerdata at Sret reads that address, raises esp by 4 and continues at mov [gold], eax.
The caller reads gold and calls grant_coin. The stack holds the address of the instruction after the call until ret removes it.
Resume the caller from the stack. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
mov eax, 10
call grant_coin
mov [gold], eax
jmp done
grant_coin: add eax, 1
ret
done:The round trip ends with 11 gold and the original esp. The saved stack bytes can remain, but they are no longer an active return address.
Final values: gold = 11; eax = 11; esp = 0x00008000; continuation = consumed; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 0; eip = done.
Read every step of the lesson’s example
Step 1, line 1. The caller loads gold. esp starts at an illustrative address; no argument passing convention is being modeled.
gold = 10; eax = 10; esp = 0x00008000; continuation = not saved yet; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1.
Step 2, line 2. A near 32-bit call lowers esp by four, stores the address of the instruction after call, then enters grant_coin.
gold = 10; eax = 10; esp = 0x00007FFC; continuation = line 3: store gold; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1; eip = grant_coin.
Memory: 0x00007FFC = line 3: store gold (current top: esp points here).
Step 3, line 5. The helper adds one coin. The return address is still on top of the stack.
gold = 10; eax = 11; esp = 0x00007FFC; continuation = line 3: store gold; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 0; eip = grant_coin.
Memory: 0x00007FFC = line 3: store gold (current top: esp points here).
Step 4, line 6. ret reads the saved address, advances esp by four, and resumes line 3. It does not itself put the result in eax.
gold = 10; eax = 11; esp = 0x00008000; continuation = consumed; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 0; eip = line 3: store gold.
Memory: 0x00007FFC = line 3: store gold (inactive slot: esp is above it).
Step 5, line 3. The caller stores 11 gold. eax carries the helper's result because this example chose that convention.
gold = 11; eax = 11; esp = 0x00008000; continuation = consumed; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 0; eip = line 3: store gold.
Memory: +0 = 0B (little-endian byte); +1 = 00 (little-endian byte); +2 = 00 (little-endian byte); +3 = 00 (little-endian byte).
Step 6, line 4. Jump past the helper body; otherwise ordinary fall-through could enter it a second time.
gold = 11; eax = 11; esp = 0x00008000; continuation = consumed; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 0; eip = done.
Memory: +0 = 0B (little-endian byte); +1 = 00 (little-endian byte); +2 = 00 (little-endian byte); +3 = 00 (little-endian byte).
Step 7, line 7. The round trip ends with 11 gold and the original esp. The saved stack bytes can remain, but they are no longer an active return address.
gold = 11; eax = 11; esp = 0x00008000; continuation = consumed; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 0; eip = done.
Memory: +0 = 0B (little-endian byte); +1 = 00 (little-endian byte); +2 = 00 (little-endian byte); +3 = 00 (little-endian byte).
Takeaway: an unbalanced stack can make ret read data as an address; restoring one register is not enough.
Meaning: place a value on top of the stack.
Common 32-bit forms: push reg32/memory32/imm. Effects: lower esp by four and write the saved value there; preserve ordinary arithmetic flags. These widths are specific to this example: a normal 64-bit push usually uses an eight-byte slot.
mov ebx, [health]push ebx ; preserve this valuemov ebx, 0 ; temporary workpop ebxmov [health], ebxSave a temporary register value on the stack. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
mov ebx, 80
push ebx
mov ebx, 0
pop ebx
mov [health], ebxStore the restored health value 80. The push/pop pair balanced esp and preserved the flags.
Final values: health = 80; ebx = 80; esp = 0x00008000; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1.
Read every step of the lesson’s example
Step 1, line 1. Load health into ebx. The illustrative 32-bit stack grows toward lower addresses.
health = 80; ebx = 80; esp = 0x00008000; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1.
Step 2, line 2. push lowers esp by four and writes the current ebx value into that stack slot.
health = 80; ebx = 80; esp = 0x00007FFC; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1.
Memory: 0x00007FFC = 80 (current top: esp points here).
Step 3, line 3. Reuse ebx for temporary work. The saved copy remains on the stack.
health = 80; ebx = 0; esp = 0x00007FFC; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1.
Memory: 0x00007FFC = 80 (current top: esp points here).
Step 4, line 4. pop reads the top slot into ebx, then advances esp by four. It does not erase the old memory.
health = 80; ebx = 80; esp = 0x00008000; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1.
Memory: 0x00007FFC = 80 (inactive slot: esp is above it).
Step 5, line 5. Store the restored health value 80. The push/pop pair balanced esp and preserved the flags.
health = 80; ebx = 80; esp = 0x00008000; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1.
Memory: +0 = 50 (little-endian byte); +1 = 00 (little-endian byte); +2 = 00 (little-endian byte); +3 = 00 (little-endian byte).
Takeaway: stack storage can preserve a value while its register is reused.
Meaning: copy the top stack value to a destination and remove that slot from the active stack.
Common forms: pop reg32/memory32. Effects: read stack memory, write the destination, and normally advance esp by four. Arithmetic flags survive. Special destinations involving esp need their exact manual rules; the example uses ebx. The old memory bytes are not erased.
mov ebx, [health]push ebxmov ebx, 0pop ebx ; recover the last saved valuemov [health], ebxRecover the last saved value. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
mov ebx, 80
push ebx
mov ebx, 0
pop ebx
mov [health], ebxStore the restored health value 80. The push/pop pair balanced esp and preserved the flags.
Final values: health = 80; ebx = 80; esp = 0x00008000; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1.
Read every step of the lesson’s example
Step 1, line 1. Load health into ebx. The illustrative 32-bit stack grows toward lower addresses.
health = 80; ebx = 80; esp = 0x00008000; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1.
Step 2, line 2. push lowers esp by four and writes the current ebx value into that stack slot.
health = 80; ebx = 80; esp = 0x00007FFC; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1.
Memory: 0x00007FFC = 80 (current top: esp points here).
Step 3, line 3. Reuse ebx for temporary work. The saved copy remains on the stack.
health = 80; ebx = 0; esp = 0x00007FFC; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1.
Memory: 0x00007FFC = 80 (current top: esp points here).
Step 4, line 4. pop reads the top slot into ebx, then advances esp by four. It does not erase the old memory.
health = 80; ebx = 80; esp = 0x00008000; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1.
Memory: 0x00007FFC = 80 (inactive slot: esp is above it).
Step 5, line 5. Store the restored health value 80. The push/pop pair balanced esp and preserved the flags.
health = 80; ebx = 80; esp = 0x00008000; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1.
Memory: +0 = 50 (little-endian byte); +1 = 00 (little-endian byte); +2 = 00 (little-endian byte); +3 = 00 (little-endian byte).
Takeaway: removing a slot changes which bytes are active, not whether those bytes still contain an old value.
Shifts and masks
Section titled “Shifts and masks”Meaning: shift bits left, filling the newly opened low positions with zeros.
Common forms: shl r/m32, imm8 or shl r/m32, cl; sal is an alias. Effects: write the operand; CF records the last bit shifted out, ZF/SF/PF describe the result, and AF is undefined for a nonzero effective count. OF has a defined shift result only for a count of one. An effective count of zero preserves value and flags. For a 32-bit operand, the count is masked to its low five bits.
mov eax, [packed_value]shl eax, 1mov [packed_value], eaxShift a packed value left. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
mov eax, 5
shl eax, 1
mov [packed], eaxStore 0x0000000A. Its signed reading is 10. Left shift keeps only the bits that fit in the selected width.
Final values: value = 5; count = 1; eax = 10; signed reading = 10; eax bits = 0x0000000A; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1; packed = 10.
Read every step of the lesson’s example
Step 1, line 1. Start with 0x00000005 (5 as signed). The illustrative previous CF and OF are both one.
value = 5; count = 1; eax = 5; signed reading = 5; eax bits = 0x00000005; ZF = 0; SF = 0; CF = 1; OF = 1; PF = 1.
Memory: bit 3 = 0 (low four bits only); bit 2 = 1 (low four bits only); bit 1 = 0 (low four bits only); bit 0 = 1 (low four bits only).
Step 2, line 2. shl shifts 1 place left, filling new positions with zero. The last bit lost becomes CF=0; OF=0.
value = 5; count = 1; eax = 10; signed reading = 10; eax bits = 0x0000000A; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1.
Memory: bit 3 = 1 (low four bits only); bit 2 = 0 (low four bits only); bit 1 = 1 (low four bits only); bit 0 = 0 (low four bits only).
Step 3, line 3. Store 0x0000000A. Its signed reading is 10. Left shift keeps only the bits that fit in the selected width.
value = 5; count = 1; eax = 10; signed reading = 10; eax bits = 0x0000000A; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1; packed = 10.
Memory: +0 = 0A (little-endian byte); +1 = 00 (little-endian byte); +2 = 00 (little-endian byte); +3 = 00 (little-endian byte).
Takeaway: left shifting resembles multiplication by a power of two only while the needed bits are retained.
Meaning: shift bits right, filling the newly opened high positions with zeros.
Common forms: shr r/m32, imm8 or shr r/m32, cl. Effects: write the operand and use the shift flag/count rules above; for a one-bit shift, OF receives the old highest bit. An unsigned value is divided by a power of two with the discarded fractional part lost.
mov eax, [packed_value]shr eax, 1mov [packed_value], eaxShift a bit pattern right with zero fill. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
mov eax, -12
shr eax, 1
mov [packed], eaxStore 0x7FFFFFFA. Its signed reading is 2147483642. Zero-fill right shift treats the original bit pattern as unsigned.
Final values: value = -12; count = 1; eax = 2147483642; signed reading = 2147483642; eax bits = 0x7FFFFFFA; ZF = 0; SF = 0; CF = 0; OF = 1; PF = 1; packed = 2147483642.
Read every step of the lesson’s example
Step 1, line 1. Start with 0xFFFFFFF4 (-12 as signed). The illustrative previous CF and OF are both one.
value = -12; count = 1; eax = 4294967284; signed reading = -12; eax bits = 0xFFFFFFF4; ZF = 0; SF = 0; CF = 1; OF = 1; PF = 1.
Memory: bit 3 = 0 (low four bits only); bit 2 = 1 (low four bits only); bit 1 = 0 (low four bits only); bit 0 = 0 (low four bits only).
Step 2, line 2. shr shifts 1 place right, filling new positions with zero. The last bit lost becomes CF=0; OF=1.
value = -12; count = 1; eax = 2147483642; signed reading = 2147483642; eax bits = 0x7FFFFFFA; ZF = 0; SF = 0; CF = 0; OF = 1; PF = 1.
Memory: bit 3 = 1 (low four bits only); bit 2 = 0 (low four bits only); bit 1 = 1 (low four bits only); bit 0 = 0 (low four bits only).
Step 3, line 3. Store 0x7FFFFFFA. Its signed reading is 2147483642. Zero-fill right shift treats the original bit pattern as unsigned.
value = -12; count = 1; eax = 2147483642; signed reading = 2147483642; eax bits = 0x7FFFFFFA; ZF = 0; SF = 0; CF = 0; OF = 1; PF = 1; packed = 2147483642.
Memory: +0 = FA (little-endian byte); +1 = FF (little-endian byte); +2 = FF (little-endian byte); +3 = 7F (little-endian byte).
Takeaway: zero-fill right shift does not preserve a negative signed value’s sign.
Meaning: shift bits right while repeating the original sign bit in the new high positions.
Common forms: sar r/m32, imm8 or sar r/m32, cl. Effects: write the operand and follow the shift flag/count rules; for a one-bit shift, OF is cleared. Signed negative values round toward minus infinity when low bits are discarded. That differs from idiv truncation toward zero: −3 shifted right once becomes −2, but −3 divided by 2 becomes −1.
mov eax, [signed_delta]sar eax, 1mov [signed_delta], eaxShift a signed movement delta right. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
mov eax, -12
sar eax, 1
mov [packed], eaxStore 0xFFFFFFFA. Its signed reading is -6. For a nonzero count, signed right shift rounds negative values downward when discarded bits are nonzero.
Final values: value = -12; count = 1; eax = 4294967290; signed reading = -6; eax bits = 0xFFFFFFFA; ZF = 0; SF = 1; CF = 0; OF = 0; PF = 1; packed = 4294967290.
Read every step of the lesson’s example
Step 1, line 1. Start with 0xFFFFFFF4 (-12 as signed). The illustrative previous CF and OF are both one.
value = -12; count = 1; eax = 4294967284; signed reading = -12; eax bits = 0xFFFFFFF4; ZF = 0; SF = 0; CF = 1; OF = 1; PF = 1.
Memory: bit 3 = 0 (low four bits only); bit 2 = 1 (low four bits only); bit 1 = 0 (low four bits only); bit 0 = 0 (low four bits only).
Step 2, line 2. sar shifts 1 place right, repeating the sign bit. The last bit lost becomes CF=0; OF=0.
value = -12; count = 1; eax = 4294967290; signed reading = -6; eax bits = 0xFFFFFFFA; ZF = 0; SF = 1; CF = 0; OF = 0; PF = 1.
Memory: bit 3 = 1 (low four bits only); bit 2 = 0 (low four bits only); bit 1 = 1 (low four bits only); bit 0 = 0 (low four bits only).
Step 3, line 3. Store 0xFFFFFFFA. Its signed reading is -6. For a nonzero count, signed right shift rounds negative values downward when discarded bits are nonzero.
value = -12; count = 1; eax = 4294967290; signed reading = -6; eax bits = 0xFFFFFFFA; ZF = 0; SF = 1; CF = 0; OF = 0; PF = 1; packed = 4294967290.
Memory: +0 = FA (little-endian byte); +1 = FF (little-endian byte); +2 = FF (little-endian byte); +3 = FF (little-endian byte).
Takeaway: sign preservation and rounding are both part of a signed shift’s meaning.
Meaning: keep a one bit only where both input bits are one.
Common forms: and reg, r/m/imm, and memory, reg/imm. Effects: write the destination; clear CF/OF, set ZF/SF/PF from the result, and leave AF undefined.
mov eax, [status]and eax, 3 ; keep only bits 0 and 1mov [status], eaxKeep only the selected status bits. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
mov eax, 5
and eax, 3
mov [status], eaxStore 0x00000001 in status. ZF describes whether the complete result is zero, not whether one chosen status bit is clear.
Final values: value = 5; eax = 1; eax bits = 0x00000001; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 0; status = 1.
Read every step of the lesson’s example
Step 1, line 1. Start with 0x00000005. Previous CF and ZF are deliberately one to make flag preservation visible.
value = 5; eax = 5; eax bits = 0x00000005; ZF = 1; SF = 0; CF = 1; OF = 0; PF = 1.
Memory: bit 3 = 0 (low four bits only); bit 2 = 1 (low four bits only); bit 1 = 0 (low four bits only); bit 0 = 1 (low four bits only).
Step 2, line 2. A bit survives only where both inputs have one.
value = 5; eax = 1; eax bits = 0x00000001; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 0.
Memory: bit 3 = 0 (low four bits only); bit 2 = 0 (low four bits only); bit 1 = 0 (low four bits only); bit 0 = 1 (low four bits only).
Step 3, line 3. Store 0x00000001 in status. ZF describes whether the complete result is zero, not whether one chosen status bit is clear.
value = 5; eax = 1; eax bits = 0x00000001; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 0; status = 1.
Memory: +0 = 01 (little-endian byte); +1 = 00 (little-endian byte); +2 = 00 (little-endian byte); +3 = 00 (little-endian byte).
Takeaway: and writes the masked word; test only inspects a temporary masked result.
Meaning: set a result bit when either input bit is one.
Common forms: or reg, r/m/imm, or memory, reg/imm. Effects: write the destination; clear CF/OF, set ZF/SF/PF from the result, and leave AF undefined.
mov eax, [status]or eax, 4 ; ensure bit 2 is setmov [status], eaxSet a status bit without clearing others. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
mov eax, 5
or eax, 4
mov [status], eaxStore 0x00000005 in status. ZF describes whether the complete result is zero, not whether one chosen status bit is clear.
Final values: value = 5; eax = 5; eax bits = 0x00000005; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1; status = 5.
Read every step of the lesson’s example
Step 1, line 1. Start with 0x00000005. Previous CF and ZF are deliberately one to make flag preservation visible.
value = 5; eax = 5; eax bits = 0x00000005; ZF = 1; SF = 0; CF = 1; OF = 0; PF = 1.
Memory: bit 3 = 0 (low four bits only); bit 2 = 1 (low four bits only); bit 1 = 0 (low four bits only); bit 0 = 1 (low four bits only).
Step 2, line 2. A bit becomes one if either input has one.
value = 5; eax = 5; eax bits = 0x00000005; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1.
Memory: bit 3 = 0 (low four bits only); bit 2 = 1 (low four bits only); bit 1 = 0 (low four bits only); bit 0 = 1 (low four bits only).
Step 3, line 3. Store 0x00000005 in status. ZF describes whether the complete result is zero, not whether one chosen status bit is clear.
value = 5; eax = 5; eax bits = 0x00000005; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1; status = 5.
Memory: +0 = 05 (little-endian byte); +1 = 00 (little-endian byte); +2 = 00 (little-endian byte); +3 = 00 (little-endian byte).
Takeaway: setting a bit twice with or leaves it set both times.
Meaning: set a result bit when its two input bits differ.
Common forms: xor reg, r/m/imm, xor memory, reg/imm. Effects: write the destination; clear CF/OF, set ZF/SF/PF from the result, and leave AF undefined. xor eax, eax produces zero and changes flags; it is not interchangeable with every use of mov eax, 0.
mov eax, [status]xor eax, 4 ; toggle bit 2mov [status], eaxToggle one status bit. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
mov eax, 5
xor eax, 4
mov [status], eaxStore 0x00000001 in status. ZF describes whether the complete result is zero, not whether one chosen status bit is clear.
Final values: value = 5; eax = 1; eax bits = 0x00000001; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 0; status = 1.
Read every step of the lesson’s example
Step 1, line 1. Start with 0x00000005. Previous CF and ZF are deliberately one to make flag preservation visible.
value = 5; eax = 5; eax bits = 0x00000005; ZF = 1; SF = 0; CF = 1; OF = 0; PF = 1.
Memory: bit 3 = 0 (low four bits only); bit 2 = 1 (low four bits only); bit 1 = 0 (low four bits only); bit 0 = 1 (low four bits only).
Step 2, line 2. A bit becomes one when the two input bits differ.
value = 5; eax = 1; eax bits = 0x00000001; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 0.
Memory: bit 3 = 0 (low four bits only); bit 2 = 0 (low four bits only); bit 1 = 0 (low four bits only); bit 0 = 1 (low four bits only).
Step 3, line 3. Store 0x00000001 in status. ZF describes whether the complete result is zero, not whether one chosen status bit is clear.
value = 5; eax = 1; eax bits = 0x00000001; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 0; status = 1.
Memory: +0 = 01 (little-endian byte); +1 = 00 (little-endian byte); +2 = 00 (little-endian byte); +3 = 00 (little-endian byte).
Takeaway: toggling a bit twice returns it to its original state.
Meaning: invert every bit of the operand.
Common form: not r/m. Effects: write the register or memory operand and preserve all arithmetic flags. This is bitwise complement, not a Boolean comparison.
mov eax, [status]not eax ; invert all 32 bits heremov [status], eaxInvert every bit in the word. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
mov eax, 5
not eax
mov [status], eaxStore 0xFFFFFFFA in status. The prior comparison flags still survive.
Final values: value = 5; eax = 4294967290; eax bits = 0xFFFFFFFA; ZF = 1; SF = 0; CF = 1; OF = 0; PF = 1; status = 4294967290.
Read every step of the lesson’s example
Step 1, line 1. Start with 0x00000005. Previous CF and ZF are deliberately one to make flag preservation visible.
value = 5; eax = 5; eax bits = 0x00000005; ZF = 1; SF = 0; CF = 1; OF = 0; PF = 1.
Memory: bit 3 = 0 (low four bits only); bit 2 = 1 (low four bits only); bit 1 = 0 (low four bits only); bit 0 = 1 (low four bits only).
Step 2, line 2. Every bit flips, including the 28 upper bits absent from these boxes. Unlike and/or/xor, not leaves the flags alone.
value = 5; eax = 4294967290; eax bits = 0xFFFFFFFA; ZF = 1; SF = 0; CF = 1; OF = 0; PF = 1.
Memory: bit 3 = 1 (low four bits only); bit 2 = 0 (low four bits only); bit 1 = 1 (low four bits only); bit 0 = 0 (low four bits only).
Step 3, line 3. Store 0xFFFFFFFA in status. The prior comparison flags still survive.
value = 5; eax = 4294967290; eax bits = 0xFFFFFFFA; ZF = 1; SF = 0; CF = 1; OF = 0; PF = 1; status = 4294967290.
Memory: +0 = FA (little-endian byte); +1 = FF (little-endian byte); +2 = FF (little-endian byte); +3 = FF (little-endian byte).
Takeaway: not flips the entire selected width, including bits a small diagram may not show.
Continuing and pausing
Section titled “Continuing and pausing”Meaning: advance to the next instruction without performing a game-data operation.
Common form: nop; multi-byte no-op encodings also exist. Effects: leave general registers, arithmetic flags, and game memory unchanged; execution still advances and takes time. Replacing code with no-ops must preserve whole instruction boundaries and the surrounding program’s assumptions.
mov eax, [ammo]nopsub eax, 1 ; this later instruction still runsmov [ammo], eaxDo nothing for one instruction, then continue. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
mov eax, 6
nop
sub eax, 1
mov [ammo], eaxThe final ammo is 5. A nop is not a pause, a return, or a guarantee that later code will be safe.
Final values: ammo = 5; eax = 5; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1.
Read every step of the lesson’s example
Step 1, line 1. Load ammo. The illustrative prior CF is one.
ammo = 6; eax = 6; ZF = 0; SF = 0; CF = 1; OF = 0; PF = 1.
Step 2, line 2. nop advances to the next instruction without changing general registers, comparison flags, or game memory. Time still passes.
ammo = 6; eax = 6; ZF = 0; SF = 0; CF = 1; OF = 0; PF = 1.
Step 3, line 3. The following sub, not nop, spends one ammo and produces fresh arithmetic flags.
ammo = 6; eax = 5; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1.
Step 4, line 4. The final ammo is 5. A nop is not a pause, a return, or a guarantee that later code will be safe.
ammo = 5; eax = 5; ZF = 0; SF = 0; CF = 0; OF = 0; PF = 1.
Memory: +0 = 05 (little-endian byte); +1 = 00 (little-endian byte); +2 = 00 (little-endian byte); +3 = 00 (little-endian byte).
Takeaway: doing nothing for one instruction does not stop the following instructions.
Meaning: raise the breakpoint exception used by a debugger.
Form here: int3, the one-byte CC instruction. Effects: transfer control through exception handling; the saved instruction pointer is after that byte. Hardware and the operating system manage exception-entry state, rather than producing an ordinary arithmetic comparison result. A debugger may pause and inspect the saved context; an unhandled exception can end the program.
mov eax, [ammo]int3 ; owned debug build, intentional stopsub eax, 1mov [ammo], eaxRaise a breakpoint exception before the next action. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
mov eax, 6
int3
sub eax, 1
mov [ammo], eaxInspection shows 6 ammo, not 5. Exception-entry state and the operating-system handler are outside this small model.
Final values: ammo = 6; action = pause; eax = 6; state = paused for inspection; saved next instruction = sub eax, 1.
Read every step of the lesson’s example
Step 1, line 1. The toy program loads ammo before reaching its one-byte CC breakpoint instruction.
ammo = 6; action = pause; eax = 6; state = running.
Step 2, line 2. int3 transfers control through breakpoint-exception handling. The saved instruction pointer is after int3; the later sub has not run.
ammo = 6; action = pause; eax = 6; state = breakpoint exception; saved next instruction = sub eax, 1.
Step 3, line 2. Inspection shows 6 ammo, not 5. Exception-entry state and the operating-system handler are outside this small model.
ammo = 6; action = pause; eax = 6; state = paused for inspection; saved next instruction = sub eax, 1.
Takeaway: a breakpoint is an exception with a continuation decision, not an automatic no-op.
Cheatsheet
Section titled “Cheatsheet”| Question | Useful operation or rule |
|---|---|
| Equal or affordable? | cmp sets subtraction flags; pick signed or unsigned jcc deliberately. |
| Is this bit present? | test value, mask keeps the original value. |
| Where does execution go? | jmp always redirects; jcc reads a condition; neither creates a return address. |
| Where does the helper return? | Near call saves the continuation; ret consumes it. |
| How is a temporary value preserved? | Balance push/pop and the convention’s other stack work. |
| How do bits move? | shl fills low bits with zero; shr fills high bits with zero; sar repeats the sign. |
| Which bit operation is needed? | and keeps, or sets, xor toggles, not inverts. |
| Why did execution stop? | nop continues; int3 enters breakpoint-exception handling. |
Next, read the scalar floating-point reference. For debugger handling rather than intentional breakpoint instructions, see Debug Events and Software Breakpoints.
Compare before changing
A shop checks a gold amount against a cost using a comparison. What changes during that comparison alone?
An integer comparison computes subtraction flags and discards the numeric difference. Spending requires a separate write or arithmetic update.
Finished Lesson 5.5?
Mark it done and the lesson list shows a beside it. A chapter whose lessons are all done is marked done too. Progress is saved in this browser only.
Plain Markdown, saved in this browser.
This book grew out of the original Game Hacking Academy by attilathedud and has been rewritten as a beginner-first course. Like the original repository, its source is published under the Apache License 2.0.