Machine code, assembly and relocatable meaning¶
Bytes acquire meaning through a contract¶
A byte is an integer from zero through 255. Memory does not mark a particular byte as an opcode, character, address or pixel. A consumer supplies that interpretation. For instruction execution, interpretation depends on the instruction set, execution mode and starting address. The same bytes can denote different instructions under different modes; beginning decode in the middle of a variable-length instruction can produce a different sequence. A disassembly is therefore a claim about context, not merely a decorative hexadecimal dump.
The prerequisite datapath chapter describes execution as a state transition. Machine code supplies the encoded operation and operands for that transition. Syntax determines where the fields begin and end; semantics determines their effects. Recognizing a byte sequence does not prove that the instruction is supported by an emulator, allowed at the current privilege level, or able to access its operands. These questions belong to different stages and need separate failure reporting.
Assembly language makes those bytes manageable using mnemonics, register names, symbolic addresses and directives. An assembler selects encodings and records unresolved relationships. A linker places sections and resolves relationships across objects. A loader establishes memory mappings and initial process state. Instruction execution is the final consumer of this chain. Confusing these responsibilities makes an assembler appear responsible for stack initialization or makes a decoder appear responsible for locating external functions.
A complete small byte sequence¶
Consider these literal bytes in a 64-bit execution environment:
| Offset | Bytes | Interpretation | Length |
|---|---|---|---|
| 0 | b8 2a 00 00 00 |
Move immediate 42 into EAX | 5 |
| 5 | 83 c0 01 |
Add sign-extended immediate 1 to EAX | 3 |
| 8 | c3 |
Near return | 1 |
The immediate 42 is encoded least-significant byte first. Opcode B8 identifies the register-immediate form for the low register selected by the opcode. With this form and no width-changing prefix, the operand is 32 bits. Writing EAX in 64-bit mode also clears the upper half of RAX. The second instruction performs a 32-bit addition, so its flags are those of a 32-bit operation even though the host C implementation stores registers in 64-bit fields.
After the first two instructions, EAX is 43. The return does not mean that the CPU stops or prints 43. It obtains a return address from the stack and transfers control there. A caller, ABI and valid stack are prerequisites for interpreting this sequence as a returning function. A raw nine-byte buffer has no executable header, import resolution, memory permissions or initial stack by itself. Those missing properties cannot be inferred from valid opcode bytes.
These fixtures are deliberately written as literal bytes rather than generated by the assembler under test. A round trip through an encoder and decoder can hide a shared error: both implementations may agree on the same wrong interpretation. Independent known bytes make the intended contract explicit. They still cover selected forms only and do not constitute a complete x86 conformance suite.
Instruction selection and semantic equivalence¶
One assembly operation can have several encodings. A register constant may fit a short immediate, a sign-extended immediate or a full-width immediate. The assembler must preserve the requested width and value while choosing among supported forms. Shorter is not automatically correct: a sign-extended negative immediate and a zero-extended 32-bit register write produce different 64-bit values. Width is part of meaning, not solely a storage optimization.
Similarly, two sequences yielding the same final general register value need not be interchangeable. They may differ in flags, memory accesses, exceptions, atomicity or instruction length. Replacing a move of zero with a logical operation that clears a register changes status flags. Replacing a memory operation with several instructions can expose intermediate states. A compiler optimizer must prove equivalence for the observations allowed by its language and target model, rather than compare one output value.
Assembler syntax is another independent layer. Destination-first notation expresses an operand convention for text. It does not mean the destination is always encoded first in the byte stream. Opcode direction bits and ModR/M fields select different operand roles. ChrisCPU represents that distinction with operand-form fields, allowing its execution helpers to find the old destination and source without reparsing textual assembly.
Immediate, displacement and relative origin¶
An immediate is a value embedded in the instruction. A displacement contributes to an address or control-flow target. Both occupy bytes, but their interpretation differs. chris_imm_sx sign-extends supported immediate widths when execution semantics require it. A decoder should preserve encoded width as well as numeric contents; otherwise it loses the information needed to distinguish a byte value 254 from the signed relative offset −2.
The two-byte sequence eb fe at address 0x1000 illustrates relative control flow. Its displacement is signed −2. The relative origin is the address after the instruction, 0x1002, so the target is 0x1000. Adding −2 to the starting address instead would produce the wrong target. This origin convention also explains why relocating a displacement needs to account for the location and width of the displacement field.
For a four-byte PC-relative relocation, write the relationship as value = S + A − P, where S is the target symbol address, A the relocation addend and P the address of the patched field. A call beginning at 0x1000 has its four-byte field at 0x1001 and next instruction at 0x1005. For target 0x1100, addend −4 gives 0x1100 − 4 − 0x1001 = 0xfb. The CPU then adds 0xfb to 0x1005 and reaches 0x1100.
ChrisASM's representations and ownership¶
chrisasm_assemble receives source text and a ChrisoImage output. It initializes the output, resets assembler counters and parses lines while accumulating sections, symbols and fixups. Three static byte buffers hold initialized TEXT, RODATA and DATA, each with capacity 65,536 bytes. Four length counters include BSS, whose size is meaningful without an initialized byte buffer. This represents the distinction between bytes stored in an object and storage to be allocated and initialized later.
ChrisoImage contains four section pointers and sizes, a bounded array of 256 symbols and a bounded array of 512 relocations. A ChrisoSym records a name, section, offset, size, binding and kind. A ChrisoRel records section, offset, symbol index, signed addend and relocation type. Static assertions constrain symbol and relocation record sizes to 80 and 20 bytes respectively. The in-memory image also contains pointers; its C structure is not itself a directly portable disk image.
emit_u32 decomposes an integer into four little-endian bytes using shifts and masks. This emission method is independent of host byte order. emit_u8 checks the current initialized section and its capacity; an invalid target or full buffer sets g_overflow. The top-level function rejects overflow before publication. The distinction between setting an error flag and immediately returning an error matters when inspecting the remaining parse path after capacity is reached.
publish_secs allocates storage for each nonempty initialized section using malloc or kmalloc, then copies from the static buffers. BSS receives a null pointer and a size. These copies detach successful output bytes from subsequent reuse of the static emission buffers. However, if a later section allocation fails, this function returns without visibly freeing earlier allocated sections. Callers and a future ownership audit must account for partially populated output; a successful-path copy is not proof of rollback on every failure.
Local fixups and external relocations¶
Same-section labels beginning with .L use a separate local-label table. Each local entry stores a bounded name, section, offset and defined flag. Forward references produce fixup entries containing name, section and patch offset. Keeping these labels separate avoids consuming a public object-symbol entry for each local branch, an explicit concern recorded in the source comment.
patch_fixups looks up each target, rejects an undefined label or section mismatch, verifies that four patch bytes fit in the section, computes target offset minus patch offset plus four, and writes the displacement little-endian. The exact arithmetic is target_offset - (patch_offset + 4). Because both offsets refer to the same section, the final section load address cancels. A cross-section reference cannot use that cancellation before placement is known.
The local table permits 1,024 entries and the fixup table 4,096 entries. local_find performs a linear scan. For F fixups and L local labels, resolution performs O(F × L) name comparisons in the worst case, with bounded name length. Memory usage is fixed by those array capacities, and appending a fixup is constant time once capacity is available. A hash table could improve expected lookup time for larger inputs, but would add representation and collision-policy complexity; it is an alternative, not the current algorithm.
emit_call_or_jmp records an unresolved symbol, emits an opcode and zero displacement, and attaches an R_X86_64_PLT32 relocation with addend −4. The named relocation type does not by itself prove a dynamic Procedure Linkage Table is implemented. It describes a relocation record consumed by the later linking path. Any claim about dynamic loading must inspect that consumer and its runtime support separately.
Parsing failures, reentrancy and observability¶
The top-level parser uses a 512-byte line buffer. Characters beyond its retained capacity are consumed without being appended; this is not an explicit diagnostic rejecting every overlong line. The resulting truncated prefix is what parsing sees. Identifier storage is also bounded. Documentation must expose these limits because accepting a shortened token or line can differ from rejecting the original source with a useful diagnostic.
Assembler state lives in static globals. Two concurrent calls can overwrite each other's buffers and counters, and recursive use is not safe merely because each caller supplies a different output image. A reentrant design would place emission buffers, local tables, counters and diagnostic state in a per-invocation context. Serializing callers is another possible containment strategy. Neither a lock nor such a context is supplied by the inspected entry point.
The public result is success or failure; it does not provide a structured error containing source span, expected grammar and recovery action. This limits tooling that wants precise editor feedback. Improving diagnostics would require maintaining source position through tokenization and distinguishing syntax errors, capacity exhaustion, unresolved labels and allocation failure. Describing those as separate concepts does not imply the existing return code distinguishes them.
Reproducible evidence and boundaries¶
make host-chrisasm-test builds and runs the repository's assembler test. It checks basic MOV/RET assembly, rejection of an unknown mnemonic, an external call relocation with addend −4 and encoding of PUSH R8. It passed on the declared revision. This test does not enumerate all mnemonics, capacity boundaries, malformed operands or allocation-failure paths.
python scripts/check_instruction_contracts.py --source .source separately compiles the real decoder and operand helpers. Its 20 literal instruction fixtures, 69 truncations, 80 register cases, two invalid indices, four effective addresses and one unsupported-opcode classification passed. Its memory callbacks reject accesses, so this is deliberately not a guest execution or boot test. The relationship between assembler output, linker relocation, executable loading and full instruction execution remains a chain of contracts that needs evidence at each boundary.
Normative encodings belong to the Intel architecture manuals. The bounded buffers, local-label algorithm, relocation emission and parser limitations above are observations of the declared ChrisOS source revision. Future improvements should preserve that distinction while adding structured diagnostics, explicit ownership cleanup and broader independently specified encoding fixtures.