How x86 Conditional Jumps Really Work — EFLAGS Truth with GDB + pwndbg

How x86 Conditional Jumps Really Work — EFLAGS Truth with GDB + pwndbg

Field notes on x86 conditional jumps. `ja`, `jb`, `je`, `jl` — these aren't independent instructions reading different state. They're all checking different bits of EFLAGS, set by whatever ALU instruction ran most recently. Once you see this in GDB, every malware analysis and reverse-engineering session reads differently.

May 12, 2026
Harrison Guo
3 min read
Kernel Debug Field Notes Reverse Engineering

Status: Companion blog draft for the existing video. Long-form transcript + bridge framing TBD.

Companion assets

  • Original video:

    How x86 Jumps REALLY Work: EFLAGS Truth with GDB + pwndbg | Malware, RE, Debugging

  • GitHub: harrison001/RETAlchemy — return-address manipulation lab; related material for x86 control flow

TL;DR

All x86 conditional jumps (ja, jb, je, jge, js, …) read from one place: EFLAGS. The mnemonics suggest they read operand values; they don’t. They read bits that the previous ALU instruction (cmp, sub, test, add, etc.) wrote into EFLAGS. Once you watch this in GDB’s info registers eflags between every step, malware analysis and reverse engineering stop being mysterious — and you stop misreading branch conditions in production crash dumps.

The setup

  • A small x86 program with several conditional branches
  • GDB with pwndbg (or peda) for live EFLAGS display
  • Step instruction by instruction; watch cmp set flags; watch the next j* read them

Debug command transcript

# TODO: paste actual gdb + pwndbg sequence from the video
# gdb ./demo
# pwndbg> start
# pwndbg> nexti
# pwndbg> info registers eflags
# pwndbg> nexti  # cmp instruction
# pwndbg> info registers eflags  # now flags reflect cmp result
# pwndbg> nexti  # ja / jb / je / etc.
# pwndbg> info registers eflags  # unchanged; branch decision was made from these bits

What broke (or rather, what was misread)

A common bug class in reverse engineering and crash analysis: looking at a sequence like

cmp eax, ebx
ja .target

and reading ja as “jump if eax > ebx.” That’s mostly true — but ja is actually checking the carry flag and the zero flag set by cmp. If anything between cmp and ja modifies EFLAGS (a function call, a system call, even a test on an unrelated register), the branch will read stale or wrong flags.

This is the source of “but the values look right, why did the branch take the wrong path?” debugging.

What fixed it

TODO: write up the rule:

  1. EFLAGS is the state actually read by j* instructions.
  2. Many instructions write to EFLAGS — not just cmp/test/arithmetic. Function returns can; some syscalls can.
  3. To know what the branch will do, read EFLAGS at the moment of the branch — not at the moment of the previous comparison.

What this teaches backend / AI infra engineers

You don’t read raw x86 in production. But you do:

  • Read coredumps with j* instructions where the wrong branch was taken
  • Understand compiled output of Go’s if x < y to know if it’s actually a single comparison or two (compiler may emit jb + fallback)
  • Audit security-critical code where flags are used for constant-time comparisons (cryptography, timing-safe equality)

The deeper lesson: CPU state is global mutable state, and instructions you don’t realize touch EFLAGS can change branch behavior. Senior systems engineers know which instructions are pure and which have side effects on flags. This separates “I can debug the assembly” from “I can read assembly fluently.”


🎧 More Ways to Consume This Content

I occasionally advise small teams on backend reliability, Go performance, and production AI systems. Learn more: /services

Comments

This space is waiting for your voice.

Comments will be supported shortly. Stay connected for updates!

Preview of future curated comments

This section will display user comments from various platforms like X, Reddit, YouTube, and more. Comments will be curated for quality and relevance.