Reverse AMD microcode updates inside Binary Ninja.
Open a microcode .bin, run one command, and the update container is typed
and labelled. K8 and K10 patches (families 0x0F–0x12) have their obfuscated triads
decoded and disassembled. Zen 1 and Zen 2 payloads are disassembled and lifted to
LLIL and HLIL. Zen 5 updates get their real register geometry and every 36-byte op-quad
decoded in the Linear view.
AMD ships CPU microcode as small blobs. Zen updates share one signed container (header, RSA material, options, match registers, then the microcode itself) while the payload changes from generation to generation. The older K8 and K10 patches use a plain 0x40-byte container: a header, eight match registers and an array of obfuscated 28-byte triads. Zenella reads the header, picks the right profile and applies it to the bytes in front of you.
Nothing is executed, decrypted or signed. The plugin only defines types, data variables, symbols and comments in the current database, and every apply runs in one undoable transaction, so a wrong guess costs you a Ctrl‑Z.
The decoders live in zenella_core.py (Zen) and prezen_core.py
(K8/K10) and have no Binary Ninja dependency. The same code drives
zenella_inspect.py and the prezen_core.py command line, which
print the structural report, the triad listing or JSON for a file without opening the GUI.
The K8/K10 decoder renders the same text as the GhidraAmdMicrocode SLEIGH specification,
and its triad de-obfuscation is a port of the amd-ucode-patch reference parser.
0x40 + 28·N-byte patches with loader 0x8000 or 0x8003. The N triads, three 64-bit
micro-ops and a sequence word each, are stored scrambled; the plugin de-obfuscates them,
mirrors them into an executable section at patch RAM address uc_0c00 and
disassembles them under the amd_prezen_ucode architecture. Match registers
become handler functions, jcc and sequence-word branches become edges. The text matches
Ghidra's AMDuCode K8/K10 languages instruction for instruction.
0xC80-byte updates. The 64 instruction packages become a Binary Ninja architecture: disassembly in the ZenUtils style, LLIL lifting, and MLIL/HLIL from there. Cross references and graph view work as for any other target.
0x3820-byte updates. The loader ID selects the geometry. For loader 0x8015 the register area holds 31 match and 31 mask words, so the op-quads start at 0x420 and run to the zero padding. Each op-quad is four 64-bit micro-ops plus a sequence word, rendered with the opcode enum names. An experimental report follows the sequence words and lists the micro-ops in execution order, assembly or raw hex.
zenella_inspect.py prints the same regions, op-quads, sequence words
and alignment diagnostics as text or JSON, and with --flow the
sequence-ordered micro-op stream. Useful for diffing updates or checking a
layout before touching a database.
Every Zen 5 update is 0x3820 bytes. The map below is the layout applied for loader 0x8015 (Zen 5c). Segment heights follow the byte sizes, with a minimum so the small ones stay readable.
| Symbol | Offset | Size | Type | Contents |
|---|---|---|---|---|
amd_mc_header |
0x0000 |
0x20 | AMD_MC_Header |
Date, revision, loader ID, patch size, CPUID, flags |
amd_mc_signature |
0x0020 |
0x100 | u8[256] |
RSA signature |
amd_mc_modulus |
0x0120 |
0x100 | u8[256] |
Public key modulus |
amd_mc_check |
0x0220 |
0x100 | u8[256] |
Check block, likely a Montgomery constant |
amd_mc_options |
0x0320 |
0x04 | AMD_MC_UcodeOptions |
autorun, encrypted flag, loader ID. This copy of the loader ID selects the layout |
amd_mc_rev |
0x0324 |
0x04 | u32 |
Second copy of the update revision |
amd_mc_match_regs |
0x0328 |
0x7C | AMD_MC_MatchRegisterBlock |
31 match registers |
amd_mc_mask_regs |
0x03A4 |
0x7C | AMD_MC_MaskRegisterBlock |
31 mask registers |
amd_ucode_body |
0x0420 |
0x2A30 | AMD_Zen5_OpQuad[300] |
Op-quads up to the zero padding. The NOP run and the finalization sequence are carved out as
amd_mc_nop_section and amd_mc_finalization_section |
amd_mc_zero_padding |
0x2E50 |
0x9D0 | u8[2512] |
Trailing zeros |
The op-quad count follows the padding, so it can differ between updates. 300 is what the two reference updates (revisions 0x0B10104E and 0x0B101054) contain. Loader 0x8010 keeps a searched equal match/mask split; 0x8004 and 0x8005 use zentool's fixed tables.
| Symbol | Offset | Size | Type | Contents |
|---|---|---|---|---|
amd_mc_header | 0x0000 | 0x20 | AMD_MC_Header | Same header as Zen 5 |
| signature, modulus, check | 0x0020 | 0x300 | u8[256] × 3 | Same crypto blocks as Zen 5 |
amd_mc_options | 0x0320 | 0x04 | AMD_Zen12_UcodeOptions | autorun, encrypted, two unknown bytes |
amd_mc_revision_copy | 0x0324 | 0x04 | u32 | Second copy of the revision |
amd_zen12_match_entries | 0x0328 | 0x58 | AMD_Zen12_MatchEntry[22] | Two 13-bit match addresses per word plus enable bits |
amd_zen1_ucode / amd_zen2_ucode | 0x0380 | 0x900 | AMD_Zen12_InstructionPackage[64] | 64 packages of four 64-bit instructions and a sequence word; mapped as code and lifted |
| Symbol | Offset | Size | Type | Contents |
|---|---|---|---|---|
amd_prezen_header | 0x0000 | 0x20 | AMD_PreZen_Header | BCD date, patch level, loader ID (0x8000 K8/K10, 0x8003 family 0x12), triad count, init flag, triad checksum, cpuid equivalence ID |
amd_prezen_match_regs | 0x0020 | 0x20 | AMD_PreZen_MatchRegister[8] | Low 16 bits name the ROM triad that register i redirects to patch triad uc_0c00 + 2i; 0xFFFF means unused |
amd_prezen_triads_raw | 0x0040 | 28 × N | AMD_PreZen_TriadObfuscated[N] | N obfuscated triads as stored in the file; each carries the decoded text as a comment |
uc_0c00 … uc_0c00+N | region + 0x15000 | 28 × N | .zenella_prezen_ucode_<base> | Executable mirror of the triads at patch RAM address 0xC00 × 28; functions at every match-register handler and branch target |
The file keeps the scrambled bytes. Each 32-bit word of a triad is rotated by an amount that depends on the triad index, swapped with its neighbour and inverted; the architecture undoes this per micro-op from its own eight bytes and the address, so a saved database keeps disassembling after it is reopened. Patch size is always 0x40 + 28·N and the header checksum is the 32-bit word sum of the triad array.
Everything sits under Plugins → AMD Microcode. Each apply command comes in two forms: at file start for a standalone update and at cursor for an update embedded in a larger image, where the cursor marks the header.
Reads the header, decides between K8/K10, Zen 1, Zen 2 and Zen 5, and runs the matching apply. This is the one to start with.
Types the 0x40-byte container and the obfuscated triad array, then mirrors the triads into
an executable section at uc_0c00 under the amd_prezen_ucode
architecture. Match registers get handler functions, jcc and sequence-word branches get
edges; micro-ops are disassembled, not lifted.
Prints the header, the match registers and every de-obfuscated triad (three micro-ops and the sequence word, with the raw words) as a report tab, in the same text Ghidra's AMDuCode languages produce.
Types the container, maps the 64 packages as executable code under the Zen 1 or Zen 2 architecture and lets Binary Ninja lift it. Pick the generation yourself if auto-detect has no opinion.
Prints a plain-text listing of the match registers and all packages in the format ZenUtils users know, as a report tab.
Applies the confirmed loader 0x8015 layout: 31 match and 31 mask registers at 0x328, op-quads from 0x420 to the zero padding. Defines the types, creates the data variables and symbols from the table above, and verifies the result against the bytes before the transaction commits. Other loader IDs get their own geometry.
Four alternative Zen 5 geometries for research. None of them is the default and none carries documented evidence; they exist so a hypothesis can be applied and compared quickly.
Two further commands are read-only reports that follow the sequence words instead of the file order. They never touch the database or the structural layout; the rules below are inferred from decrypted updates and labelled as such in the report header.
0x1f00 … 0x1f00 + quads
continues in the patch (the patch RAM base observed in every decrypted sample), a
target equal to a match-register address re-enters at that hook's quad (m1 → quad 2N,
m2 → quad 2N+1), and any other ROM target ends the run with a comment line. Every
op-quad is printed exactly once.Expect many short runs rather than one program: a Zen 5 update is a set of hooks (two per match register, 58 in a typical patch), and most of them end by jumping back into the CPU's ROM microcode, which is not part of the file.
The report the plugin builds is also available from a shell. Only the standard library is used.
python3 zenella_inspect.py update.bin # regions, op-quads, sequence words python3 zenella_inspect.py update.bin --json # everything as JSON python3 zenella_inspect.py update.bin --layout exact # force an alternative geometry python3 zenella_inspect.py image.bin --base 0x1000 # embedded update python3 zenella_inspect.py update_dec.bin --flow # experimental: micro-ops in sequence-word order (decrypted Zen 5) python3 zenella_inspect.py update_dec.bin --flow-detail # same order as chains with headers and end reasons python3 zenella_inspect.py update_dec.bin --flow --patch-base none # or auto / 0x1f00: how ROM targets map into the patch python3 prezen_core.py k10_patch.bin # K8/K10: header, match registers, decoded triads
All structures are packed. Names from Zenella 1.2 are kept, so scripts and existing databases
keep working; new names are prefixed with the generation, and the K8/K10 types with
AMD_PreZen_.
AMD_MC_Header 0x20 bytes, shared by all generationsu16 | year |
u8 | day |
u8 | month |
u32 | update_revision |
AMD_MC_LoaderIdTag | loader_id |
u16 | size_of_patch |
u32 | minimum_patch_level |
u16 | nb_ven |
u16 | nb_dev |
u16 | sb_ven |
u16 | sb_dev |
AMD_MC_CpuId | proc_sig |
u8 | bios_revision |
u8 | flags |
u8 | reserved |
u8 | reserved2 |
The CPUID in proc_sig is expanded and commented with the matching
processor description from cpuid_descriptions.json.
AMD_Zen5_MicroOp64 8 bytes, bitfields| Bits | Field |
|---|---|
0–15 | imm16 |
16–20 | imm_flags |
21–25 | rt |
26–30 | rs |
31–35 | rd |
36–41 | flags |
42–44 | size |
45 | load |
46 | store |
47–54 | opcode (AMD_Zen_Opcode) |
55–58 | mid |
59–61 | exec_unit (spec, br, ld, stn, st, regx, reg) |
62–63 | hi |
AMD_Zen5_OpQuad 36 bytesAMD_Zen5_MicroOp64 | uop0 … uop3 |
u32 | sequence_word |
The Linear view shows each micro-op with its opcode name, class and a zentool-style operand projection. The sequence word is annotated, not lifted.
AMD_MC_Patch 0x3820 bytes, Zen 5 containerAMD_MC_Header | header |
u8[256] | signature |
u8[256] | modulus |
u8[256] | check |
AMD_MC_UcodeOptions | options |
u32 | rev |
AMD_MC_MatchRegisterBlock | match_regs |
AMD_MC_MaskRegisterBlock | mask_regs |
AMD_Zen5_OpQuad[300] | body |
u8[2512] | zero_padding |
Also registered under the generation-specific name
AMD_Zen5_Patch. The array length and padding size follow the update.
AMD_MC_UcodeOptions 4 bytesu8 | autorun |
u8 | encrypted |
u16 | loaderid |
AMD_MC_LoaderIdTag u16 enum0x8004 | AMD_MC_LOADER_8004 |
0x8005 | AMD_MC_LOADER_8005 |
0x8010 | AMD_MC_LOADER_8010 |
0x8015 | AMD_MC_LOADER_8015 |
0x8016 | AMD_MC_LOADER_8016 |
AMD_MC_MatchRegisterBlock 124 bytesu32[31] | match_reg |
AMD_MC_MaskRegisterBlock 124 bytesu32[31] | mask_reg |
Sizes shown are for loader 0x8015. Other loaders get scoped block types of their own width.
AMD_Zen12_InstructionPackage 36 bytesu64 | uop0 … uop3 |
u32 | sequence_word |
AMD_Zen12_MatchEntry 4 bytesu32 | raw |
AMD_Zen12_Patch 0xC80 bytesAMD_MC_Header | header |
u8[256] × 3 | signature, modulus, check |
AMD_Zen12_UcodeOptions | options |
u32 | revision_copy |
AMD_Zen12_MatchEntry[22] | match_entries |
AMD_Zen12_ExecutablePayload | payload |
AMD_PreZen_Header 0x20 bytes, K8/K10u32 | date (BCD 0xMMDDYYYY) |
u32 | patch_level (family = 0xF + top byte) |
AMD_PreZen_LoaderIdTag | loader_id |
u8 | op_triad_count |
u8 | init_flag |
u32 | op_triad_checksum |
u32 | nb_dev_id |
u8[4] | unknown0 |
u16 | cpuid (equivalence ID) |
u8[6] | unknown1 |
AMD_PreZen_LoaderIdTag u16 enum0x8000 | PREZEN_K8_K10_0x8000 |
0x8003 | PREZEN_FAMILY12_0x8003 |
AMD_PreZen_MatchRegister 4 bytesu32 | raw (low 16 bits = ROM triad, 0xFFFF unused) |
AMD_PreZen_TriadObfuscated 28 bytes, as storedu32[7] | obfuscated_word |
AMD_PreZen_Triad 28 bytes, decodedu64 | op0, op1, op2 |
u32 | sequence_word |
54–62 | operation (9 bits) |
59–62 | hi4 (5 marks a jcc) |
54–58 | cc (32 condition codes) |
52 | 3op (three-operand form) |
46–51 | dst (64 register names) |
30–31 | size (.b .w .d .q) |
24–29 | src1 |
23 | regmode (1 = register, 0 = imm16) |
9–14 | src2reg |
0–15 | imm |
0–11 | braddr (jcc target triad) |
0 | sw_next fall through to the next triad |
1 | sw_complete return |
2 | sw_branch to triad in bits 0–11 |
| other | sw_raw shown as stored |
Named operations: add, or, adc, sbb, and, sub, xor, cmp, test, rll, rrl,
sll, srl, writepc, mov, mul, imul, muleh, mulel, div1, div2, ld, st, lea, bswap, not and
crash; every other operation number is shown as unk_op. Registers are
eax…edi, t1d…t56d, the regmd result registers and pcd. Names, fields and text follow the
GhidraAmdMicrocode AMDuCodePreZen.sinc specification.
AMD_Zen_Opcode u16 enum, bits 47–54 of a micro-opNames come from the published ZenUtils research. Values above 0xFF are synthetic: a load/store op is identified by its class, not by the opcode bits.
| Class dependent | |
|---|---|
0x00 | AMD_ZEN_UOP_LD_ST_00 |
0x100 | AMD_ZEN_LD |
0x101 | AMD_ZEN_ST |
0x05 | AMD_ZEN_BR_JMP |
| Special | |
0xFF | AMD_ZEN_SPEC_NOP |
0xDE | AMD_ZEN_TYPE5_READ |
| Register ops | |
|---|---|
0x19 | AMD_ZEN_REG_NSUB |
0x30 | AMD_ZEN_REG_AND |
0x40 | AMD_ZEN_REG_SHL |
0x41 | AMD_ZEN_REG_BLL |
0x42 | AMD_ZEN_REG_ROL |
0x44 | AMD_ZEN_REG_RLC |
0x46 | AMD_ZEN_REG_RRD |
0x47 | AMD_ZEN_REG_SRC |
0x48 | AMD_ZEN_REG_SHR |
0x4A | AMD_ZEN_REG_ROR |
0x4C | AMD_ZEN_REG_RRC |
0x4F | AMD_ZEN_REG_SRD |
0x50 | AMD_ZEN_REG_SUB |
0x52 | AMD_ZEN_REG_SBB |
| Register ops, continued | |
|---|---|
0x55 | AMD_ZEN_REG_NADD |
0x5C | AMD_ZEN_REG_ADD2 |
0x5D | AMD_ZEN_REG_ADC |
0x5E | AMD_ZEN_REG_ADD3 |
0x5F | AMD_ZEN_REG_ADD |
0x6F | AMD_ZEN_REG_VZEROUPPER_64B |
0x70 | AMD_ZEN_REG_POPCNT |
0x72 | AMD_ZEN_REG_SBIT |
0x7F | AMD_ZEN_REG_VZEROUPPER_32B |
0x93 | AMD_ZEN_REG_MOV2 |
0xA0 | AMD_ZEN_REG_MOV_SREG |
0xA9 | AMD_ZEN_REG_BSWAP |
0xB5 | AMD_ZEN_REG_XOR |
0xBE | AMD_ZEN_REG_OR |
Zenella is a plugin folder, not a single file. It needs nothing beyond the Python that ships with Binary Ninja.
In Binary Ninja choose Plugins → Open Plugin Folder…, or go straight there:
~/Library/Application Support/Binary Ninja/plugins/ # macOS %APPDATA%\Binary Ninja\plugins\ # Windows ~/.binaryninja/plugins/ # Linux
Clone or download ercihan/zenella
and place the whole directory in the plugin folder. Remove any older single-file copy of
amd_zen_ucode.py first; two copies would both register the menu.
cd ~/Library/Application\ Support/Binary\ Ninja/plugins/ git clone https://github.com/ercihan/zenella.git
The AMD Microcode submenu appears under Plugins. The log shows the loaded version and the path it was loaded from, which is handy if a stale copy is still around.
Load the .bin as a raw file. For an update inside a firmware image, open the
image and place the cursor on the update header.
Plugins → AMD Microcode → Auto-detect and apply at file start, or the cursor variant. The log tells you which profile was chosen and why.
In Linear view amd_mc_header shows date, revision, loader ID and the expanded
CPUID with the processor name as a comment.
K8 and K10: jump to uc_0c00 or follow the handler addresses in the
match-register comments, and read the triads in Disassembly or graph view (HLIL shows
micro-ops as unimplemented because only control flow is lifted). Zen 1 and
Zen 2: jump into the lifted packages and use graph or HLIL view. Zen 5:
scroll amd_ucode_body; every op-quad is decoded, and the NOP run and the
finalization sequence are labelled so you can skip them. For execution order instead of
file order, run the experimental Zen5 → Experimental → Show sequence-ordered
op-quad listing on a decrypted update.
Zen 5 micro-op semantics are undocumented. The plugin names opcodes and fields from the published research and shows sequence words as stored; it does not claim to execute or lift Zen 5 code. The experimental menu exists precisely because some of the geometry is still being worked out. K8 and K10 micro-ops are likewise disassembled but not lifted: the text and the control flow follow Ghidra's AMDuCode specification, which carries no data-flow semantics either.