How SLEIGH Turns Bytes into Meaning

This chapter focuses on the internal contract of a SLEIGH processor specification: how it turns instruction bits and decoder context into an assembly statement and an explicit account of machine state changes. The useful outcome is not memorizing SLEIGH syntax. It is being able to trace one instruction through the model and see why constructor priority, context updates, operand widths, and side effects are all part of correctness.

The distinction matters because SLEIGH and p-code are related but not identical. SLEIGH describes the translation; p-code is the intermediate language into which instruction behavior is translated. One is the specification language, the other is the representation consumed by analysis and emulation. Keeping that boundary clear makes it easier to understand both what InSPECtor can enumerate from a specification and what still has to be checked against execution.

P-code is the common semantic layer

A processor has registers, memory, control flow, arithmetic rules, flags, and special architectural state. P-code gives Ghidra a common way to describe those effects even when the original instruction sets look very different. SLEIGH supplies the translation from a machine instruction to p-code; the p-code operations then provide the material on which data-flow analysis and decompilation operate.

The model starts with address spaces. An address space is an indexed sequence of memory-like locations. One can represent ordinary RAM, but a specification can also define a space for registers or other processor resources. A register is not magic in this model: it is represented as bytes at a location in an address space. This uniform treatment lets the same intermediate language describe a memory load, a register transfer, or a temporary value without requiring the analysis engine to understand every processor’s native naming scheme.

A varnode is a contiguous range of bytes in one of those spaces. Its identity comes from the address of its first byte and its size. Varnodes can overlap, and their meaning is imposed by the operation that uses them. The same bytes may be interpreted as an integer in one operation, a Boolean in another, or a floating-point value in a third, subject to the specification’s endianness and the operation’s rules. This flexibility is useful for modeling real instruction sets, where a register may be accessed at several widths. It also makes width and interpretation decisions consequential: a semantically wrong size can change which bytes are read or written while leaving the surrounding p-code superficially plausible.

A p-code operation names its inputs and, when it writes a value, its output. The operation is not supposed to alter some hidden location as a side effect. Data movement, arithmetic, comparisons, loads, stores, branches, extensions, and truncations are expressed as explicit operations. An instruction usually becomes a sequence rather than a single operation. An addition, for example, may calculate a carry or overflow condition, produce a result, set a zero flag, and set a negative flag through separate steps. The sequence is the semantic claim.

That explicitness is what makes p-code useful for retargetable analysis. The decompiler can reason about common operations without having a separate analysis implementation for every processor family. But the common representation does not make the source model true by itself. If the SLEIGH specification omits a flag update, uses a word-sized store where the architecture performs a byte store, or writes a destination before reading all of an aliased source, p-code will express the wrong behavior with complete formal clarity.

The distinction between language and representation is therefore practical. SLEIGH has to answer questions about which bytes form an instruction, how operands are extracted, how a mode or prefix changes decoding, and how the instruction is displayed. P-code has to answer questions about what the selected instruction does. A validation method that looks only at p-code execution can miss an instruction that was decoded incorrectly; a method that checks only whether a constructor matches can miss an incorrect semantic sequence.

Decoding is a sequence of decisions, not a lookup table

SLEIGH describes decoding through constructors organized into tables. A constructor is a rule that can connect several stages of interpretation. Its pattern section constrains instruction bits and decoder context. Its optional decode actions can update context for later decisions. Its display section describes the assembly form. Its semantic section translates the selected instruction into p-code. Constructors in a subtable can decode a particular operand or part of a multi-stage instruction, while root constructors provide the starting points.

The most useful way to understand this is as a stateful path. The input is not just a byte string. It is a byte string together with context bits that may represent information such as the current processor mode or a fact established by an earlier decoding step. The decoder consults a table, selects a constructor whose pattern is compatible with that state, applies any decode action, and follows referenced subtables. The path continues until all required operands and semantic pieces have been resolved.

A memory operand shows why the stages cannot be collapsed into one label. A subtable may decode a register field and an immediate into a reference to a location, rather than into the contents of that location. The parent constructor can use that reference in its display section to print an addressing form, while its semantic section uses it to express a load or store. The same instruction therefore has a textual operand and an operational address, and the two must remain connected without being confused. A specification that displays the right bracketed expression but reads from the wrong address has succeeded at presentation while failing at semantics.

The decoder also has to keep different kinds of state separate. Instruction bits are the material being matched. Decoder context records facts that affect how later bits are interpreted. Architectural state—register contents, memory, flags, and control-flow state—is what the selected p-code will read or change during execution. Context may influence which constructor is selected, while architectural state may influence the result of the selected operation; treating one as a substitute for the other can produce a plausible but invalid model. This separation later becomes important to validation: a generator can solve for a byte pattern and context that follow a path, but a behavioral test still needs an initial architectural state.

Within an instruction, p-code operations normally execute in order, with fall-through control flow unless a branch changes it. That ordering is not an incidental artifact of the intermediate representation. It is how a specification records when a value is read, when a temporary is written, and when a flag is derived. If a destination is updated before a source has been fully consumed, an instruction whose operands name different registers may appear correct while an aliased form behaves differently. The sequence consequently carries information that a summary such as “adds one register to another” would lose.

flowchart LR A[Instruction fields] --> C[Root constructor] X[Decoder context] --> C C --> P[Pattern constraints] P --> S[Referenced subtables] S --> U[Decode actions update context] U --> D[Decoded operands] D --> V[Display section] D --> M[Semantic section] M --> Q[P-code operations] Q --> R[Register and memory effects]

Imagine a simplified variable-length instruction with an operation field, two register fields, and an immediate field. A root rule may establish that the operation field denotes an arithmetic family. A subtable can then choose whether the immediate is a special zero form or a general immediate form. Another rule can use the decoded fields to produce a readable instruction and a semantic expression. The important point is not the particular mnemonic. It is that the final interpretation is assembled from a chain of compatible choices, not selected by one isolated opcode lookup.

Constructor matching also has precedence. The decoder considers constructors from more specific to less specific, using the number of constrained bits as a measure of specificity in the paper’s model. If no constructor matches, the encoding is treated as undecodable or invalid. When one does match, the decoder applies its actions and continues. Declaration order can break ties when constructors are not cleanly separated, so the behavior of the table includes relationships among rules that may look independent when read one at a time.

Overlaps make those relationships visible. A general constructor may recognize a family of encodings, while a more specific constructor gives one sub-encoding a special display or semantic treatment. To generate or reason about the general case, it is not enough to satisfy the general constructor’s positive constraints. The encoding must also avoid matching the earlier special case. In effect, the less-specific path inherits a negative constraint from the more-specific path. If that exclusion is missing or modeled incorrectly, the decoder may select the wrong display and p-code even though both constructors individually describe legal-looking patterns.

This is also why a constructor can be unreachable. A rule may appear in the specification, but every encoding that satisfies it may be captured first by another overlapping rule. An unreachable constructor is not automatically a bug: it may be redundant by design. But if the earlier rule has different semantics, the unreachable path can indicate that the specification has encoded precedence incorrectly. Reachability is consequently a property of the constructor network, not of a rule considered in isolation.

Subtables can use attachments to give bit-field values meanings such as register names or to exclude values that do not correspond to a valid operand. They can also export a value or reference for a parent constructor to use. Those conveniences keep a specification compact, but they add dependencies between a field’s numerical value, its mapped architectural object, and the semantic section that consumes it. An unconstrained register field does not necessarily mean “any integer”: its attachment may rule out a sentinel value or map the remaining values to particular registers.

Context makes the path stateful across time. A decode action can write a context field that changes the constructors eligible in a later phase. This supports patterns such as split immediate fields or mode-dependent interpretation. A self-referencing constructor can consume a prefix, update context, and then cause the decoder to examine the instruction again under the new state. Optional and repeated prefixes are particularly demanding because they change the offset at which later fields are read and can overlap with ordinary opcode decoding.

The context is not always a fixed switch. If a decode action derives a later context value from unconstrained instruction bits, then the later context is symbolic until those bits are solved. A validator must carry the relationship forward rather than replacing it prematurely with an arbitrary value. Otherwise it may generate a byte sequence that satisfies one local rule but could not actually follow the full decode path.

Backtracking handles another source of dependency. A constructor may match at the current table but lead to no valid choice in a referenced subtable. In that case, the decoder can return to an earlier decision and try another constructor. This behavior means that a successful path is the result of a global compatibility relation. A locally attractive choice is not necessarily the final choice, and a generator that assumes every earlier selection is permanent will either miss valid forms or report paths the real decoder would never complete.

The same mechanisms explain why correctness cannot be reduced to a set of mnemonic spellings. A processor specification has to preserve the conditions that select an instruction, the context in which it is legal, the operands that the fields denote, the order in which rules are resolved, and the effects represented after selection. A mistake in any one of these can change the meaning delivered to analysis. A wrong context bit can make a valid form look invalid. An over-constrained pattern can exclude a mode in which hardware accepts the instruction. A misplaced operation in the semantic sequence can change the result only when operands alias or a special register discards a write.

What this structure makes testable

SLEIGH’s structure gives InSPECtor a valuable starting point. Constructors, tables, fields, context transitions, attachments, and semantic sections expose a space of paths that can be traversed rather than guessed. The generator can ask which constraints must hold for a particular path, carry the context changes that path induces, and eventually produce an encoding that the specification itself considers valid.

But the structure does not settle every question. A constructor path establishes how the specification recognizes and represents a case. It does not, by itself, establish that the represented p-code matches physical execution for every register value, memory condition, exception, or architectural corner case. Nor does reaching all visible paths prove that the specification contains every instruction in the processor’s ISA. Those remaining questions motivate the next part of InSPECtor: symbolic generation of instruction encodings, targeted initial states, and differential comparison with hardware.

The central distinction is now concrete. SLEIGH is the language that binds decoding and semantic description together; p-code is the explicit intermediate account of effects; constructors and context determine which account applies. Because the contract is structured, it can be explored systematically. Because the contract is still a model, its behavior must be tested outside the model as well.

References