dexutil: take opcode names and widths from the format specification

The opcode table was shifted by one from 0x16 to 0x2C, and nine widths were wrong with it: 0x1C was named monitor-enter and given one unit where the specification has const-class at two; 0x17 was const-wide/16 at 2 where the spec has const-wide/32 at 3; 0x18 was 3 where the spec has const-wide at 5; 0x26 was throw at 1 where the spec has fill-array-data at 3. Each wrong entry shifts every instruction after it in the same method, and the decode keeps producing plausible instructions, so nothing looks broken until an operand index walks off its table.

Measured on a real 8.9 MB dex before the fix: 65 of 50,791 method bodies did not decode to their declared boundary, and 32 carried an operand index outside its table. After: 50,791 of 50,791 land exactly on insns_off + insns_size*2, with 0 illegal indices. Cross-checked against androguard 4.1.4, which shares no code with this project: identical per-instruction width sequences for all 50,791 bodies.

Names and widths now come from the format specification table, and width is a lookup (OP_UNITS) instead of a chain of range tests -- the shape that let a one-observation-at-a-time repair look complete. The goto family moves with it in branch_target() and describe(), and in dex_find_insn.py kind map, where a search for a branch was matching throw and goto/32.
This commit is contained in:
inliver233
2026-09-21 18:19:16 +08:00
parent ab0a5ae0e2
commit 03485faa2a
2 changed files with 169 additions and 105 deletions
+2 -2
View File
@@ -64,8 +64,8 @@ _KIND_MAP = {
"move-result": MOVE_RESULT_OPS,
"const4": (0x12,),
"const16": (0x13,),
"goto": (0x27, 0x28, 0x29),
"switch": (0x2A, 0x2B),
"goto": (0x28, 0x29, 0x2A),
"switch": (0x2B, 0x2C),
"nop": (0x00,),
}
_FIELD_READ = tuple(range(0x52, 0x59))
+167 -103
View File
@@ -84,37 +84,129 @@ OFFSETS = {
# on against a second decoder.
OP_NAMES = {
0x00: "nop", 0x01: "move", 0x02: "move/from16", 0x03: "move/16",
0x04: "move-wide", 0x05: "move-wide/from16", 0x06: "move-wide/16",
0x07: "move-object", 0x08: "move-object/from16", 0x09: "move-object/16",
0x0A: "move-result", 0x0B: "move-result-wide", 0x0C: "move-result-object",
0x0D: "move-exception", 0x0E: "return-void", 0x0F: "return",
0x10: "return-wide", 0x11: "return-object", 0x12: "const/4",
0x13: "const/16", 0x14: "const", 0x15: "const/high16",
0x16: "const-wide/high16", 0x17: "const-wide/16", 0x18: "const-wide/32",
0x19: "const-string", 0x1A: "const-string/jumbo", 0x1B: "const-class",
0x1C: "monitor-enter", 0x1D: "monitor-exit", 0x1E: "check-cast",
0x1F: "instance-of", 0x20: "array-length", 0x21: "new-instance",
0x22: "new-array", 0x23: "filled-new-array", 0x24: "filled-new-array/range",
0x25: "fill-array-data", 0x26: "throw", 0x27: "goto",
0x28: "goto/16", 0x29: "goto/32", 0x2A: "packed-switch",
0x2B: "sparse-switch",
0x32: "if-eq", 0x33: "if-ne", 0x34: "if-lt", 0x35: "if-ge",
0x36: "if-gt", 0x37: "if-le",
0x04: "move-wide", 0x05: "move-wide/from16", 0x06: "move-wide/16", 0x07: "move-object",
0x08: "move-object/from16", 0x09: "move-object/16", 0x0A: "move-result", 0x0B: "move-result-wide",
0x0C: "move-result-object", 0x0D: "move-exception", 0x0E: "return-void", 0x0F: "return",
0x10: "return-wide", 0x11: "return-object", 0x12: "const/4", 0x13: "const/16",
0x14: "const", 0x15: "const/high16", 0x16: "const-wide/16", 0x17: "const-wide/32",
0x18: "const-wide", 0x19: "const-wide/high16", 0x1A: "const-string", 0x1B: "const-string/jumbo",
0x1C: "const-class", 0x1D: "monitor-enter", 0x1E: "monitor-exit", 0x1F: "check-cast",
0x20: "instance-of", 0x21: "array-length", 0x22: "new-instance", 0x23: "new-array",
0x24: "filled-new-array", 0x25: "filled-new-array/range", 0x26: "fill-array-data", 0x27: "throw",
0x28: "goto", 0x29: "goto/16", 0x2A: "goto/32", 0x2B: "packed-switch",
0x2C: "sparse-switch", 0x2D: "cmpl-float", 0x2E: "cmpg-float", 0x2F: "cmpl-double",
0x30: "cmpg-double", 0x31: "cmp-long", 0x32: "if-eq", 0x33: "if-ne",
0x34: "if-lt", 0x35: "if-ge", 0x36: "if-gt", 0x37: "if-le",
0x38: "if-eqz", 0x39: "if-nez", 0x3A: "if-ltz", 0x3B: "if-gez",
0x3C: "if-gtz", 0x3D: "if-lez",
0x3C: "if-gtz", 0x3D: "if-lez", 0x44: "aget", 0x45: "aget-wide",
0x46: "aget-object", 0x47: "aget-boolean", 0x48: "aget-byte", 0x49: "aget-char",
0x4A: "aget-short", 0x4B: "aput", 0x4C: "aput-wide", 0x4D: "aput-object",
0x4E: "aput-boolean", 0x4F: "aput-byte", 0x50: "aput-char", 0x51: "aput-short",
0x52: "iget", 0x53: "iget-wide", 0x54: "iget-object", 0x55: "iget-boolean",
0x56: "iget-byte", 0x57: "iget-char", 0x58: "iget-short",
0x59: "iput", 0x5A: "iput-wide", 0x5B: "iput-object", 0x5C: "iput-boolean",
0x5D: "iput-byte", 0x5E: "iput-char", 0x5F: "iput-short",
0x60: "sget", 0x61: "sget-wide", 0x62: "sget-object", 0x63: "sget-boolean",
0x64: "sget-byte", 0x65: "sget-char", 0x66: "sget-short",
0x67: "sput", 0x68: "sput-wide", 0x69: "sput-object", 0x6A: "sput-boolean",
0x6B: "sput-byte", 0x6C: "sput-char", 0x6D: "sput-short",
0x6E: "invoke-virtual", 0x6F: "invoke-super", 0x70: "invoke-direct",
0x71: "invoke-static", 0x72: "invoke-interface",
0x74: "invoke-virtual/range", 0x75: "invoke-super/range",
0x76: "invoke-direct/range", 0x77: "invoke-static/range",
0x78: "invoke-interface/range",
0x56: "iget-byte", 0x57: "iget-char", 0x58: "iget-short", 0x59: "iput",
0x5A: "iput-wide", 0x5B: "iput-object", 0x5C: "iput-boolean", 0x5D: "iput-byte",
0x5E: "iput-char", 0x5F: "iput-short", 0x60: "sget", 0x61: "sget-wide",
0x62: "sget-object", 0x63: "sget-boolean", 0x64: "sget-byte", 0x65: "sget-char",
0x66: "sget-short", 0x67: "sput", 0x68: "sput-wide", 0x69: "sput-object",
0x6A: "sput-boolean", 0x6B: "sput-byte", 0x6C: "sput-char", 0x6D: "sput-short",
0x6E: "invoke-virtual", 0x6F: "invoke-super", 0x70: "invoke-direct", 0x71: "invoke-static",
0x72: "invoke-interface", 0x74: "invoke-virtual/range", 0x75: "invoke-super/range", 0x76: "invoke-direct/range",
0x77: "invoke-static/range", 0x78: "invoke-interface/range", 0x7B: "neg-int", 0x7C: "not-int",
0x7D: "neg-long", 0x7E: "not-long", 0x7F: "neg-float", 0x80: "neg-double",
0x81: "int-to-long", 0x82: "int-to-float", 0x83: "int-to-double", 0x84: "long-to-int",
0x85: "long-to-float", 0x86: "long-to-double", 0x87: "float-to-int", 0x88: "float-to-long",
0x89: "float-to-double", 0x8A: "double-to-int", 0x8B: "double-to-long", 0x8C: "double-to-float",
0x8D: "int-to-byte", 0x8E: "int-to-char", 0x8F: "int-to-short", 0x90: "add-int",
0x91: "sub-int", 0x92: "mul-int", 0x93: "div-int", 0x94: "rem-int",
0x95: "and-int", 0x96: "or-int", 0x97: "xor-int", 0x98: "shl-int",
0x99: "shr-int", 0x9A: "ushr-int", 0x9B: "add-long", 0x9C: "sub-long",
0x9D: "mul-long", 0x9E: "div-long", 0x9F: "rem-long", 0xA0: "and-long",
0xA1: "or-long", 0xA2: "xor-long", 0xA3: "shl-long", 0xA4: "shr-long",
0xA5: "ushr-long", 0xA6: "add-float", 0xA7: "sub-float", 0xA8: "mul-float",
0xA9: "div-float", 0xAA: "rem-float", 0xAB: "add-double", 0xAC: "sub-double",
0xAD: "mul-double", 0xAE: "div-double", 0xAF: "rem-double", 0xB0: "add-int/2addr",
0xB1: "sub-int/2addr", 0xB2: "mul-int/2addr", 0xB3: "div-int/2addr", 0xB4: "rem-int/2addr",
0xB5: "and-int/2addr", 0xB6: "or-int/2addr", 0xB7: "xor-int/2addr", 0xB8: "shl-int/2addr",
0xB9: "shr-int/2addr", 0xBA: "ushr-int/2addr", 0xBB: "add-long/2addr", 0xBC: "sub-long/2addr",
0xBD: "mul-long/2addr", 0xBE: "div-long/2addr", 0xBF: "rem-long/2addr", 0xC0: "and-long/2addr",
0xC1: "or-long/2addr", 0xC2: "xor-long/2addr", 0xC3: "shl-long/2addr", 0xC4: "shr-long/2addr",
0xC5: "ushr-long/2addr", 0xC6: "add-float/2addr", 0xC7: "sub-float/2addr", 0xC8: "mul-float/2addr",
0xC9: "div-float/2addr", 0xCA: "rem-float/2addr", 0xCB: "add-double/2addr", 0xCC: "sub-double/2addr",
0xCD: "mul-double/2addr", 0xCE: "div-double/2addr", 0xCF: "rem-double/2addr", 0xD0: "add-int/lit16",
0xD1: "rsub-int", 0xD2: "mul-int/lit16", 0xD3: "div-int/lit16", 0xD4: "rem-int/lit16",
0xD5: "and-int/lit16", 0xD6: "or-int/lit16", 0xD7: "xor-int/lit16", 0xD8: "add-int/lit8",
0xD9: "rsub-int/lit8", 0xDA: "mul-int/lit8", 0xDB: "div-int/lit8", 0xDC: "rem-int/lit8",
0xDD: "and-int/lit8", 0xDE: "or-int/lit8", 0xDF: "xor-int/lit8", 0xE0: "shl-int/lit8",
0xE1: "shr-int/lit8", 0xE2: "ushr-int/lit8", 0xFA: "invoke-polymorphic", 0xFB: "invoke-polymorphic/range",
0xFC: "invoke-custom", 0xFD: "invoke-custom/range", 0xFE: "const-method-handle", 0xFF: "const-method-type",
}
# Instruction length in 16-bit code units, one entry per valid opcode.
#
# Keyed by opcode from the format specification, NOT inferred from a mnemonic
# and NOT copied from a run of neighbours. 0x16-0x2C alternates
# 2,3,5,2,2,3,2,1,1,2,2,1,2,2,3,3,3,1,1,2,3,3,3 -- an entire run where the
# plausible-looking "same family, same width" assumption is wrong at nine
# opcodes. A single wrong entry shifts every instruction after it in the same
# method, and the decode keeps producing plausible instructions, so nothing
# looks broken until an operand index walks off its table.
OP_UNITS = {
0x00: 1, 0x01: 1, 0x02: 2, 0x03: 3,
0x04: 1, 0x05: 2, 0x06: 3, 0x07: 1,
0x08: 2, 0x09: 3, 0x0A: 1, 0x0B: 1,
0x0C: 1, 0x0D: 1, 0x0E: 1, 0x0F: 1,
0x10: 1, 0x11: 1, 0x12: 1, 0x13: 2,
0x14: 3, 0x15: 2, 0x16: 2, 0x17: 3,
0x18: 5, 0x19: 2, 0x1A: 2, 0x1B: 3,
0x1C: 2, 0x1D: 1, 0x1E: 1, 0x1F: 2,
0x20: 2, 0x21: 1, 0x22: 2, 0x23: 2,
0x24: 3, 0x25: 3, 0x26: 3, 0x27: 1,
0x28: 1, 0x29: 2, 0x2A: 3, 0x2B: 3,
0x2C: 3, 0x2D: 2, 0x2E: 2, 0x2F: 2,
0x30: 2, 0x31: 2, 0x32: 2, 0x33: 2,
0x34: 2, 0x35: 2, 0x36: 2, 0x37: 2,
0x38: 2, 0x39: 2, 0x3A: 2, 0x3B: 2,
0x3C: 2, 0x3D: 2, 0x44: 2, 0x45: 2,
0x46: 2, 0x47: 2, 0x48: 2, 0x49: 2,
0x4A: 2, 0x4B: 2, 0x4C: 2, 0x4D: 2,
0x4E: 2, 0x4F: 2, 0x50: 2, 0x51: 2,
0x52: 2, 0x53: 2, 0x54: 2, 0x55: 2,
0x56: 2, 0x57: 2, 0x58: 2, 0x59: 2,
0x5A: 2, 0x5B: 2, 0x5C: 2, 0x5D: 2,
0x5E: 2, 0x5F: 2, 0x60: 2, 0x61: 2,
0x62: 2, 0x63: 2, 0x64: 2, 0x65: 2,
0x66: 2, 0x67: 2, 0x68: 2, 0x69: 2,
0x6A: 2, 0x6B: 2, 0x6C: 2, 0x6D: 2,
0x6E: 3, 0x6F: 3, 0x70: 3, 0x71: 3,
0x72: 3, 0x74: 3, 0x75: 3, 0x76: 3,
0x77: 3, 0x78: 3, 0x7B: 1, 0x7C: 1,
0x7D: 1, 0x7E: 1, 0x7F: 1, 0x80: 1,
0x81: 1, 0x82: 1, 0x83: 1, 0x84: 1,
0x85: 1, 0x86: 1, 0x87: 1, 0x88: 1,
0x89: 1, 0x8A: 1, 0x8B: 1, 0x8C: 1,
0x8D: 1, 0x8E: 1, 0x8F: 1, 0x90: 2,
0x91: 2, 0x92: 2, 0x93: 2, 0x94: 2,
0x95: 2, 0x96: 2, 0x97: 2, 0x98: 2,
0x99: 2, 0x9A: 2, 0x9B: 2, 0x9C: 2,
0x9D: 2, 0x9E: 2, 0x9F: 2, 0xA0: 2,
0xA1: 2, 0xA2: 2, 0xA3: 2, 0xA4: 2,
0xA5: 2, 0xA6: 2, 0xA7: 2, 0xA8: 2,
0xA9: 2, 0xAA: 2, 0xAB: 2, 0xAC: 2,
0xAD: 2, 0xAE: 2, 0xAF: 2, 0xB0: 1,
0xB1: 1, 0xB2: 1, 0xB3: 1, 0xB4: 1,
0xB5: 1, 0xB6: 1, 0xB7: 1, 0xB8: 1,
0xB9: 1, 0xBA: 1, 0xBB: 1, 0xBC: 1,
0xBD: 1, 0xBE: 1, 0xBF: 1, 0xC0: 1,
0xC1: 1, 0xC2: 1, 0xC3: 1, 0xC4: 1,
0xC5: 1, 0xC6: 1, 0xC7: 1, 0xC8: 1,
0xC9: 1, 0xCA: 1, 0xCB: 1, 0xCC: 1,
0xCD: 1, 0xCE: 1, 0xCF: 1, 0xD0: 2,
0xD1: 2, 0xD2: 2, 0xD3: 2, 0xD4: 2,
0xD5: 2, 0xD6: 2, 0xD7: 2, 0xD8: 2,
0xD9: 2, 0xDA: 2, 0xDB: 2, 0xDC: 2,
0xDD: 2, 0xDE: 2, 0xDF: 2, 0xE0: 2,
0xE1: 2, 0xE2: 2, 0xFA: 4, 0xFB: 4,
0xFC: 3, 0xFD: 3, 0xFE: 2, 0xFF: 2,
}
# Opcode groups whose operands are a pair of registers plus an int16 offset.
@@ -159,22 +251,24 @@ def read_uleb(b, o):
def insn_units(op, data, pos, end):
"""Instruction length in 16-bit code units.
One wrong width desynchronises everything after it, and the failure is quiet:
the decode keeps producing plausible-looking instructions, just shifted, so
every offset you derive from that point is wrong. Two width traps are common
enough to call out:
Widths come from OP_UNITS, the format specification's table -- not from a
mnemonic, and not from a run of neighbours. The failure that replaces was
quiet in the worst way: a wrong width keeps producing plausible instructions,
just shifted, so every offset derived after that point is wrong and nothing in
the output says so. The traps worth naming:
* `0x32`-`0x3D` (if-test 22t / if-testz 21t) are **2** units, not 1. Treating
them as 1 invents a fake second instruction at every branch.
* `0x1A` (const-string/jumbo) is emitted as a **4-byte** form (op, register,
uint16 string index) by real toolchains and read that way by ART and
baksmali, even though the reference format for the opcode is 31c. Counting
it as 3 units is the single most costly width error -- it shifts the rest
of the method by one unit per occurrence.
* The `goto` family is one slot off in the obvious reading: `0x27` is
`goto` (10t, **1** unit), `0x28` is `goto/16` (20t, **2** units) and
`0x29` is `goto/32` (30t, **3** units). Reading the offset width from the
mnemonic instead of the opcode desynchronises every branch of this shape.
* Width is not a family property. `const-wide/16` (0x16) is 2 units,
`const-wide/32` (0x17) is 3 and `const-wide` (0x18) is 5; `const-class`
(0x1C) is 2 while `monitor-exit` (0x1E) is 1; `instance-of` (0x20) is 2
while `array-length` (0x21) is 1. Nine opcodes in 0x16-0x2C break the
"same family, same width" reading.
* `0x32`-`0x3D` (if-test 22t / if-testz 21t) are **2** units, not 1.
Treating them as 1 invents a fake second instruction at every branch.
* `0x1A` (const-string, 21c) and `0x1B` (const-string/jumbo, 31c) are
different widths; only the jumbo form carries a 32-bit string index.
* The `goto` family sits at `0x28` (10t, **1** unit), `0x29` (20t, **2**)
and `0x2A` (30t, **3**) -- one slot later than the obvious reading, which
puts `throw`, `goto`, `goto/16` at 0x27/0x28/0x29.
Whenever a decode is used to derive a patch offset, assert that the walk ends
exactly on `insns_off + insns_size*2`. See `decode_all`.
@@ -212,54 +306,10 @@ def insn_units(op, data, pos, end):
count = u32(data, pos + 4)
return 4 + (count * width + 1) // 2
return 1
# -- 1 unit -------------------------------------------------------------
if op in (0x01, 0x04, 0x07, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10,
0x11, 0x12, # 12x / 11x / 11n / 10x
0x1C, 0x1D, # monitor-enter / monitor-exit
0x20, # array-length is 12x, NOT 22c
0x26, 0x27): # throw, goto
return 1
if 0x7B <= op <= 0x8F: # 12x unop
return 1
if 0xB0 <= op <= 0xCF: # 12x binop/2addr
return 1
if 0xE3 <= op <= 0xFC: # 10x family
return 1
# -- 2 units ------------------------------------------------------------
if op in (0x02, 0x05, 0x08): # 22x move/from16 variants
return 2
if op in (0x13, 0x15, 0x16, 0x17, 0x19, 0x1A, 0x1B, 0x1E, 0x1F,
0x21, 0x22): # 21s/21h/21c/22c
return 2
if 0x2C <= op <= 0x31: # 23x cmp
return 2
if op in IF_TEST or op in IF_TESTZ: # 22t / 21t <-- 2, not 1
return 2
if 0x44 <= op <= 0x51: # 23x aget/aput
return 2
if 0x52 <= op <= 0x5F: # 22c iget/iput
return 2
if 0x60 <= op <= 0x6D: # 21c sget/sput
return 2
if 0x90 <= op <= 0xAF: # 23x binop
return 2
if 0xD0 <= op <= 0xD7: # 22s binop/lit16
return 2
if 0xD8 <= op <= 0xE2: # 22b binop/lit8
return 2
if op == 0x29: # measured: this slot is goto/16 (20t)
return 2
# -- 3 units ------------------------------------------------------------
if op in (0x03, 0x06, 0x09, # 32x
0x14, 0x18, # 31i const, const-wide/32
0x23, 0x24, # 35c / 3rc filled-new-array
0x25, # 31t fill-array-data
0x2A, 0x2B, # 31t packed/sparse-switch
0x6E, 0x6F, 0x70, 0x71, 0x72, # 35c invoke
0x74, 0x75, 0x76, 0x77, 0x78): # 3rc invoke/range
return 3
# -- payloads / unknown -------------------------------------------------
return 1
# Unallocated opcodes (0x3E-0x43, 0x73, 0x79-0x7A, 0xE3-0xF9) never appear in
# a valid dex; anything unlisted is treated as one unit so a corrupt stream
# still terminates rather than running off the end.
return OP_UNITS.get(op, 1)
class Dex(object):
@@ -347,7 +397,17 @@ class Dex(object):
cd = self.find_class(fqcn)
if cd is None:
raise KeyError("class not found: %s" % fqcn)
p = u32(self.d, cd + 24)
for item in self.methods_at(cd):
yield item
def methods_at(self, class_def_off):
"""Same as methods_of(), from a class_def_item offset you already have.
methods_of() calls find_class(), which is a linear scan of class_defs, so
walking every class in a dex through it costs O(n^2). A caller that is
already iterating class_defs should come through here instead.
"""
p = u32(self.d, class_def_off + 24)
if p == 0:
return
sf, p = read_uleb(self.d, p)
@@ -441,14 +501,14 @@ class Dex(object):
op, pos, raw = insn["op"], insn["off"], insn["raw"]
if op in IF_TEST or op in IF_TESTZ:
return pos + s16(u16(self.d, pos + 2)) * 2
if op == 0x27: # goto (10t, signed byte)
if op == 0x28: # goto (10t, signed byte)
off = raw[1]
if off > 127:
off -= 256
return pos + off * 2
if op == 0x28:
if op == 0x29: # goto/16 (20t, signed int16)
return pos + s16(u16(self.d, pos + 2)) * 2
if op == 0x29:
if op == 0x2A: # goto/32 (30t, signed int32)
off = u32(self.d, pos + 2)
if off > 0x7FFFFFFF:
off -= 0x100000000
@@ -482,7 +542,7 @@ class Dex(object):
self.branch_target(insn))
elif op in IF_TESTZ:
text += " v%d -> 0x%x" % (raw[1] & 0xF, self.branch_target(insn))
elif op in (0x27, 0x28, 0x29):
elif op in (0x28, 0x29, 0x2A):
text += " -> 0x%x" % self.branch_target(insn)
elif op in IFIELD_OPS:
cls, nm, ty = self.field(u16(self.d, pos + 2))
@@ -496,10 +556,13 @@ class Dex(object):
elif op in INVOKE_OPS:
cls, nm, ds = self.method(u16(self.d, pos + 2))
text += " %s.%s%s" % (cls, nm, ds)
elif op in (0x19, 0x1A):
# Both forms carry the string index as a uint16 at byte offset 2.
# Reading a uint32 here gives a huge index that walks off the table.
elif op == 0x1A:
# const-string (21c): uint16 string index at byte offset 2.
text += ' "%s"' % self.string_safe(u16(self.d, pos + 2))
elif op == 0x1B:
# const-string/jumbo (31c): the index is a uint32, so reading it as a
# uint16 would silently name a different string.
text += ' "%s"' % self.string_safe(u32(self.d, pos + 2))
elif op == 0x12:
lit = raw[1] & 0xF
text += " v%d, %d" % (raw[1] >> 4, lit - 16 if lit > 7 else lit)
@@ -634,9 +697,10 @@ def _max_reg(insn):
return raw[1] & 0xF
if op == 0x12:
return raw[1] >> 4
if op in (0x01, 0x04, 0x07, 0x0A, 0x0B, 0x0C, 0x0D, 0x0F, 0x10, 0x11,
0x1C, 0x1D, 0x1E):
return raw[1] & 0xF
if op in (0x01, 0x04, 0x07, 0x0F, 0x10, 0x11):
return raw[1] & 0xF # 12x: two 4-bit registers
if op in (0x0A, 0x0B, 0x0C, 0x0D, 0x1C, 0x1D, 0x1E, 0x1F, 0x22):
return raw[1] # 11x / 21c: one 8-bit register
return 0