├── tests ├── __init__.py ├── test_asm_disasm.py ├── test_eval.py └── test_arch_registry.py ├── Makefile ├── zenutils ├── arch │ ├── __init__.py │ ├── zen1.yaml │ ├── zen2.yaml │ ├── eval.py │ ├── registry.py │ └── zen_base.yaml ├── __init__.py ├── asm.py ├── disasm.py └── masm.py ├── samples ├── ucode │ ├── strlen.s │ ├── strlen_zen1.s │ └── subleq.s └── mcode │ ├── call_strlen_zen1.asm │ └── call_subleq_example.asm ├── .gitignore ├── pyproject.toml ├── README.md └── uv.lock /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | 3 | test: 4 | pytest -------------------------------------------------------------------------------- /zenutils/arch/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Architecture specification and registry module for ZenUtils. 3 | """ 4 | 5 | from .registry import Registry, ArchSpec 6 | from .eval import ConditionEvaluator 7 | 8 | __all__ = ["Registry", "ArchSpec", "ConditionEvaluator"] 9 | -------------------------------------------------------------------------------- /samples/ucode/strlen.s: -------------------------------------------------------------------------------- 1 | .arch Zen2 2 | .date 0x07112025 3 | .revision 0x08701040 4 | .format 0x8004 5 | .cpuid 0x00008710 6 | 7 | ; implements rbx = strlen(rax) 8 | 9 | .match_reg 0, 0x420 10 | mov reg15, rax 11 | 12 | loop: 13 | mov.b reg14, ls:[reg15] 14 | and.Z reg0, reg14, reg14 15 | jz.z end 16 | add reg15, reg15, 1 17 | .sw_branch loop 18 | 19 | end: 20 | sub rbx, reg15, rax 21 | .sw_complete -------------------------------------------------------------------------------- /samples/ucode/strlen_zen1.s: -------------------------------------------------------------------------------- 1 | .arch Zen1 2 | .date 0x10102025 3 | .revision 0x0800820E 4 | .format 0x8004 5 | .cpuid 0x00008082 6 | 7 | ; implements rbx = strlen(rax) 8 | 9 | .match_reg 0, 0xc98 10 | mov reg15, rax 11 | 12 | loop: 13 | mov.b reg14, ls:[reg15] 14 | and.Z reg0, reg14, reg14 15 | jz.z end 16 | add reg15, reg15, 1 17 | .sw_branch loop 18 | 19 | end: 20 | sub rbx, reg15, rax 21 | .sw_complete -------------------------------------------------------------------------------- /zenutils/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | ZenUtils - Collection of tools to create, dump and manipulate Zen microcode updates. 3 | 4 | This package provides tools for assembling, disassembling, and manipulating 5 | Zen microcode updates for AMD Zen architectures. 6 | """ 7 | 8 | from .arch.registry import Registry, ArchSpec 9 | from .asm import assemble_single 10 | from .disasm import disassemble_single 11 | 12 | __version__ = "0.1.0" 13 | __all__ = ["Registry", "ArchSpec", "assemble_single", "disassemble_single"] 14 | -------------------------------------------------------------------------------- /zenutils/arch/zen1.yaml: -------------------------------------------------------------------------------- 1 | # Zen1 ISA version delta: inherits all from base Zen 2 | inherits: ZenBase 3 | name: Zen1 4 | 5 | # Override default header values 6 | object_format: 7 | header: 8 | defaults: 9 | date: 0x07112025 10 | revision: 0x08001150 11 | format: 0x8004 12 | cpuid: 0x00008011 13 | rev: 0x08001150 14 | 15 | # Override the number of match registers 16 | match_registers: 17 | entry_count: 22 18 | regs_per_entry: 2 19 | 20 | # Override the number of instruction packages 21 | packages: 22 | start_address: 0x1fc0 23 | count: 64 -------------------------------------------------------------------------------- /zenutils/arch/zen2.yaml: -------------------------------------------------------------------------------- 1 | # Zen2 ISA version delta: inherits all from base Zen 2 | inherits: ZenBase 3 | name: Zen2 4 | 5 | # Override default header values 6 | object_format: 7 | header: 8 | defaults: 9 | date: 0x07112025 10 | revision: 0x08701050 11 | format: 0x8004 12 | cpuid: 0x00008710 13 | rev: 0x08701050 14 | 15 | # Override the number of match registers 16 | match_registers: 17 | entry_count: 22 18 | regs_per_entry: 2 19 | 20 | # Override the number of instruction packages 21 | packages: 22 | start_address: 0x1fc0 23 | count: 64 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | build/ 8 | develop-eggs/ 9 | dist/ 10 | downloads/ 11 | eggs/ 12 | .eggs/ 13 | lib/ 14 | lib64/ 15 | parts/ 16 | sdist/ 17 | var/ 18 | wheels/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | MANIFEST 23 | 24 | # Virtual environments 25 | venv/ 26 | env/ 27 | ENV/ 28 | env.bak/ 29 | venv.bak/ 30 | test-env/ 31 | 32 | # IDE 33 | .vscode/ 34 | .idea/ 35 | *.swp 36 | *.swo 37 | 38 | # OS 39 | .DS_Store 40 | Thumbs.db 41 | 42 | # Testing 43 | .pytest_cache/ 44 | .coverage 45 | htmlcov/ 46 | .tox/ 47 | .nox/ -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "zenutils" 7 | version = "0.1.0" 8 | description = "Collection of tools to create, dump and manipulate Zen microcode updates" 9 | readme = "README.md" 10 | requires-python = ">=3.8" 11 | dependencies = [ 12 | "pyyaml>=6.0", 13 | "pytest>=7.0", 14 | ] 15 | authors = [ 16 | {name = "ZenUtils Contributors"}, 17 | ] 18 | 19 | [project.scripts] 20 | zenasm = "zenutils.asm:main" 21 | zendisasm = "zenutils.disasm:main" 22 | zenmasm = "zenutils.masm:main" 23 | 24 | [project.urls] 25 | Homepage = "https://github.com/AngryUEFI/ZenUtils" 26 | Repository = "https://github.com/AngryUEFI/ZenUtils" 27 | 28 | [tool.pytest.ini_options] 29 | testpaths = ["tests"] 30 | python_files = ["test_*.py"] 31 | python_classes = ["Test*"] 32 | python_functions = ["test_*"] 33 | 34 | [tool.hatch.build.targets.wheel] 35 | packages = ["zenutils"] 36 | -------------------------------------------------------------------------------- /samples/mcode/call_strlen_zen1.asm: -------------------------------------------------------------------------------- 1 | ; call_strlen.asm 2 | ; Uses the strlen microcode patch (hooked on FCOS) to compute the length of str_input. 3 | ; Contract: RBX <- strlen(RAX) 4 | 5 | bits 64 6 | default rel 7 | 8 | section .text 9 | global _start 10 | 11 | _start: 12 | ; Preserve CoreContext* and locate result buffer. 13 | mov r10, rax 14 | mov rdi, [r10] 15 | 16 | ; Prepare inputs for the microcode patch. 17 | lea rax, [rel str_input] 18 | xor rbx, rbx 19 | 20 | fninit ; ensure x87 is in a known state 21 | fcos ; patched microcode overwrites RBX with strlen(RAX) 22 | 23 | ; Write the computed length to the result buffer. 24 | mov [rdi], rbx 25 | 26 | xor eax, eax ; AngryUEFI expects RAX cleared on return 27 | ret 28 | 29 | section .data 30 | str_input: 31 | db "hello world omg it workiedd", 0 32 | -------------------------------------------------------------------------------- /tests/test_asm_disasm.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | # Support both standard imports (when package is installed) and relative imports (when running outside uv environment) 4 | try: 5 | from zenutils.arch.registry import Registry, ArchSpec 6 | from zenutils.asm import assemble_single 7 | from zenutils.disasm import disassemble_single 8 | except ImportError: 9 | from arch.registry import Registry, ArchSpec 10 | from asm import assemble_single 11 | from disasm import disassemble_single 12 | 13 | data = [ 14 | (0x382F9E108CE00000, "add.n reg1, reg3, reg7"), 15 | (0x382F9C1000000000, "add reg0, reg0, reg0"), 16 | (0x382F9C108C080042, "add reg1, reg3, 0x42"), 17 | (0x382F9E108C080042, "add.n reg1, reg3, 0x42"), 18 | (0x385A8FF08C080042, "xor.zcZCnd reg1, reg3, 0x42"), 19 | (0x20021C2000081FC0, "jz.z 0x1fc0"), 20 | (0x20049C0000081FE2, "jge 0x1fe2"), 21 | (0x286F3C1EF0008400, "mov r13, cpuid:[r12]"), 22 | (0x18505C073C07B000, "mov msr2:[reg15], reg14"), 23 | (0x286F20173DC09820, "mov.b reg14, ls:[reg15 + reg14 + 0x20]"), 24 | (0x38501C1080E00000, "mov reg1, reg7"), 25 | (0x38501C1080084242, "mov reg1, 0x4242") 26 | ] 27 | 28 | def test_assemble_single(): 29 | reg = Registry() 30 | zen2 = reg.get('Zen2') 31 | 32 | for expected, assembly in data: 33 | assert assemble_single(zen2, assembly) == expected 34 | 35 | def test_disassemble_single(): 36 | reg = Registry() 37 | zen2 = reg.get('Zen2') 38 | 39 | for word, expected in data: 40 | assert disassemble_single(zen2, word) == expected 41 | -------------------------------------------------------------------------------- /tests/test_eval.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | # Support both standard imports (when package is installed) and relative imports (when running outside uv environment) 4 | try: 5 | from zenutils.arch.eval import ConditionEvaluator 6 | except ImportError: 7 | from arch.eval import ConditionEvaluator 8 | 9 | def test_eval(): 10 | fields = {'foo': {'value': 1}, 'bar': {'value': 2000}} 11 | evaluator = ConditionEvaluator(fields) 12 | assert evaluator.evaluate("foo < 10 && bar == 2000") 13 | assert evaluator.evaluate("(foo < 10 || foor >= 20) && bar == 2000") 14 | assert not evaluator.evaluate("foo > 10") 15 | 16 | def test_extract(): 17 | valid = "operation == 0xa0 && imm_mode == 1" 18 | extracted = ConditionEvaluator.extract_defs_from_condition(valid) 19 | assert len(extracted) == 2 20 | assert extracted['operation'] == 0xa0 21 | assert extracted['imm_mode'] == 1 22 | 23 | # For extraction only expr of the form 'var == val [&& var_n == val_n]*' are allowed 24 | invalid_form = "operation == 0xa0 || imm_mode == 1" 25 | form_error_occured = False 26 | try: 27 | extracted = ConditionEvaluator.extract_defs_from_condition(invalid_form) 28 | except ValueError as e: 29 | form_error_occured = True 30 | 31 | assert form_error_occured 32 | 33 | # For extraction each variable may only occur once 34 | invalid_multi_var = "operation == 0xa0 && operation == 1" 35 | multi_var_error_occured = False 36 | try: 37 | extracted = ConditionEvaluator.extract_defs_from_condition(invalid_multi_var) 38 | except ValueError as e: 39 | multi_var_error_occured = True 40 | 41 | assert multi_var_error_occured -------------------------------------------------------------------------------- /samples/mcode/call_subleq_example.asm: -------------------------------------------------------------------------------- 1 | ; call_fcos_mem.asm 2 | ; Invokes the FCOS load/store microcode patch and returns the value written by it. 3 | 4 | bits 64 5 | default rel 6 | 7 | section .text 8 | global _start 9 | 10 | _start: 11 | ; Setup Subleq program in result buffer memory. 12 | mov rdi, [rax] 13 | ; Instr 0: subleq 0, 8, 0 14 | mov qword [rdi+0], 24 15 | mov qword [rdi+8], 8 16 | mov qword [rdi+16], 0 17 | ; Instr 1: subleq 0, 8, 0 18 | mov qword [rdi+24], 16 19 | mov qword [rdi+32], 8 20 | mov qword [rdi+40], 0 21 | ; Instr 2: subleq 0,0,0xFFFFFFFFFFFFFFFF (to halt) 22 | mov qword [rdi+48], 488 23 | mov qword [rdi+56], 496 24 | mov qword [rdi+64], -1 25 | 26 | ; Setup Subleq data in result buffer memory. 27 | mov qword [rdi+520], 40 ; 8 = 40 28 | mov qword [rdi+528], 5 ; 16 = 5 29 | mov qword [rdi+536], 10 ; 24 = 10 30 | 31 | ; The CoreContext pointer is in rax. The microcode patch will use this. 32 | ; We don't need to do anything with it here, just preserve it across the fcos call. 33 | push rax 34 | 35 | ; Trigger the microcode patch. 36 | fninit 37 | fcos 38 | 39 | pop rax 40 | 41 | ; The microcode writes to the result buffer. The pointer to that buffer 42 | ; is the first field in CoreContext (pointed to by rax). 43 | mov rdi, [rax] ; rdi = result buffer pointer 44 | 45 | ; The microcode wrote 0x1337 to offset 0 and the CoreContext* to offset 8. 46 | ; Let's return the value from offset 0. 47 | mov rax, [rdi+0] 48 | 49 | ret -------------------------------------------------------------------------------- /samples/ucode/subleq.s: -------------------------------------------------------------------------------- 1 | .arch Zen1 2 | .date 0x10102025 3 | .revision 0x08008211 4 | .format 0x8004 5 | .cpuid 0x00008082 6 | 7 | .match_reg 0, 0xc98 8 | 9 | ; Subleq Register Layout: 10 | ; reg0: Zero Register 11 | ; r8: mem_a/op_a 12 | ; r9: mem_b 13 | ; r10: op_b 14 | ; r11: jmp_c 15 | ; r12: halt_check 16 | ; reg9: program_counter 17 | ; reg10: base address of data memory (result buffer + 512 bytes) 18 | 19 | ; Get the result buffer address from CoreContext (passed in rax) 20 | mov rbx, ls:[rax+reg0+0] ; Dereference CoreContext->ResultBuffer into rbx, rbx now holds the address 21 | 22 | mov reg9, rbx ; Initialise program counter to start of result buffer, where program will exist 23 | add reg10, reg9, 512 ; reg10 = [rax + 512] , start of result data buffer 24 | 25 | mov r8, reg0 ; Clear op_a/mem_a 26 | mov r9, reg0 ; Clear mem_b 27 | mov r10, reg0 ; Clear op_b 28 | mov r11, reg0 ; Clear jmp_c 29 | mov r12, reg0 ; Clear halt check register 30 | sub r12, reg0, 1 ; r12 = -1 31 | mov r13, reg0 ; time stamp register, starts from 0 32 | 33 | ; Start of Subleq Loop 34 | SUBLEQ_LOOP: 35 | ; Increment time stamp 36 | add r13, r13, 1 37 | ; Load operands from memory into registers 38 | ; Load op_a 39 | mov r8, ls:[reg9+reg0+0] 40 | ; Load op_b 41 | mov r10, ls:[reg9+reg0+8] 42 | ; Load mem_a 43 | mov r8, ls:[reg10+r8] 44 | ; Load mem_b 45 | mov r9, ls:[reg10+r10] 46 | 47 | ; Load jmp_c 48 | mov r11, ls:[reg9+reg0+16] 49 | 50 | ; Perform subleq operation 51 | ; mem_b = mem_b - mem_a 52 | sub.CZ r9, r9, r8 53 | 54 | ; Store the result back to memory 55 | mov ls:[reg10+r10], r9 56 | 57 | ; If mem_b <= 0, jump to BRANCH_TAKEN 58 | jbe.cz BRANCH_TAKEN 59 | 60 | ; If mem_b > 0, increment program counter to next instruction (3 * 8 bytes) 61 | add reg9, reg9, 24 62 | ; Repeat the loop 63 | .sw_branch SUBLEQ_LOOP 64 | 65 | ; Subleq branch is taken 66 | BRANCH_TAKEN: 67 | ; Check for halt condition 68 | ; Halt if branch is taken and jmp_c is -1 69 | sub.Z reg0, r11, r12 70 | ; If jmp_c == -1, halt the program 71 | je.z END_SUBLEQ_LOOP 72 | 73 | ; Set data pointer to jmp_c 74 | ; r11 (jmp_c) is the slot offset 75 | ; Add slot offset to base address 76 | add reg9, rbx, r11 77 | 78 | ; next loop iteration 79 | .sw_branch SUBLEQ_LOOP 80 | 81 | END_SUBLEQ_LOOP: 82 | ; Cleanup and exit 83 | ; Clear op_a/mem_a 84 | mov r8, reg0 85 | ; Clear mem_b 86 | mov r9, reg0 87 | ; Clear op_b 88 | mov r10, reg0 89 | ; Clear jmp_c 90 | mov r11, reg0 91 | 92 | ; Return 1 in result buffer + 504 (last variable) to indicate success 93 | mov.q r12, 1 94 | mov ls:[reg10+504], r12 95 | ; Clear halt check register 96 | mov r12, reg0 97 | 98 | mov ls:[reg10+496], r13 ; Store time stamp in result buffer + 496 99 | .sw_complete 100 | -------------------------------------------------------------------------------- /zenutils/arch/eval.py: -------------------------------------------------------------------------------- 1 | import ast 2 | 3 | class ConditionEvaluator: 4 | """ 5 | Evaluate simple boolean expressions with &&, ||, comparison ops, 6 | variables and numeric literals (dec, hex, bin). Also extracts all 7 | equality tests of the form `var == const`. 8 | """ 9 | # AST node types allowed in expressions 10 | _ALLOWED_NODES = { 11 | ast.Expression, ast.BoolOp, ast.UnaryOp, ast.Compare, 12 | ast.Name, ast.Load, ast.Constant, 13 | ast.And, ast.Or, ast.Not, 14 | ast.Gt, ast.Lt, ast.GtE, ast.LtE, ast.Eq, ast.NotEq, 15 | ast.USub, ast.UAdd 16 | } 17 | 18 | _ALLOWED_NODES_FOR_EXTRACT = { 19 | ast.Expression, ast.BoolOp, ast.Compare, 20 | ast.Name, ast.Load, ast.Constant, 21 | ast.And, ast.Eq 22 | } 23 | 24 | def __init__(self, variables): 25 | """ 26 | variables: dict[str -> obj] where obj.value is the integer value 27 | """ 28 | # build evaluation namespace: var_name -> integer 29 | self._env = {name: obj['value'] for name, obj in variables.items()} 30 | 31 | @staticmethod 32 | def _sanitize(expr): 33 | # convert && → and, || → or 34 | return expr.replace('&&', ' and ').replace('||', ' or ') 35 | 36 | @staticmethod 37 | def _check_node(node): 38 | if type(node) not in ConditionEvaluator._ALLOWED_NODES: 39 | raise ValueError(f"Unsupported expression element: {type(node).__name__}") 40 | for child in ast.iter_child_nodes(node): 41 | ConditionEvaluator._check_node(child) 42 | 43 | @staticmethod 44 | def _check_node_for_extract(node): 45 | if type(node) not in ConditionEvaluator._ALLOWED_NODES_FOR_EXTRACT: 46 | raise ValueError(f"Unsupported expression element: {type(node).__name__}") 47 | for child in ast.iter_child_nodes(node): 48 | ConditionEvaluator._check_node_for_extract(child) 49 | 50 | def evaluate(self, expr): 51 | """ 52 | Parse and evaluate the boolean expression. 53 | Returns (result: bool) 54 | """ 55 | src = self._sanitize(expr) 56 | tree = ast.parse(src, mode='eval') 57 | # validate AST 58 | self._check_node(tree) 59 | # safe evaluation 60 | code = compile(tree, '', 'eval') 61 | result = eval(code, {'__builtins__': None}, self._env) 62 | return bool(result) 63 | 64 | # For extraction only expr of the form 'var == val [&& var_n == val_n]*' are allowed 65 | # For extraction each variable may only occur once 66 | @staticmethod 67 | def extract_defs_from_condition(expr): 68 | """ 69 | Parse and extract defs from boolean expression. 70 | Returns (eq_tests: dict[var_name -> expected_value]) 71 | """ 72 | src = ConditionEvaluator._sanitize(expr) 73 | tree = ast.parse(src, mode='eval') 74 | # validate AST 75 | ConditionEvaluator._check_node_for_extract(tree) 76 | # extract unique equality tests 77 | eq_tests = {} 78 | class EqVisitor(ast.NodeVisitor): 79 | def visit_Compare(self, node): 80 | if len(node.ops) == 1 and isinstance(node.ops[0], ast.Eq): 81 | left, right = node.left, node.comparators[0] 82 | if isinstance(left, ast.Name) and isinstance(right, ast.Constant): 83 | if left.id in eq_tests: 84 | raise ValueError(f"Multiple occurences of var: {left.id}") 85 | eq_tests[left.id] = right.value 86 | elif isinstance(right, ast.Name) and isinstance(left, ast.Constant): 87 | if right.id in eq_tests: 88 | raise ValueError(f"Multiple occurences of var: {right.id}") 89 | eq_tests[right.id] = left.value 90 | self.generic_visit(node) 91 | EqVisitor().visit(tree) 92 | return eq_tests 93 | -------------------------------------------------------------------------------- /tests/test_arch_registry.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | # Support both standard imports (when package is installed) and relative imports (when running outside uv environment) 4 | try: 5 | from zenutils.arch.registry import Registry, ArchSpec 6 | except ImportError: 7 | from arch.registry import Registry, ArchSpec 8 | 9 | 10 | def test_registry_loads_all_arch_specs(): 11 | # Load registry from default arch/specs directory 12 | reg = Registry() 13 | 14 | # Check that Zen and Zen1 are present 15 | specs = set(reg._specs.keys()) 16 | assert 'ZenBase' in specs, "Base spec 'Zen' should be loaded" 17 | assert 'Zen1' in specs, "Derived spec 'Zen1' should be loaded" 18 | 19 | # Validate properties of base 'Zen' spec 20 | zen = reg.get('ZenBase') 21 | assert zen.name == 'ZenBase' 22 | assert isinstance(zen.word_size, int) and zen.word_size == 64 23 | # Verify register list is non-empty 24 | assert hasattr(zen, 'registers') and isinstance(zen.registers, list) 25 | assert 'reg0' in zen.registers 26 | 27 | # Validate package configuration 28 | pkg = zen.packages 29 | assert isinstance(pkg.get('instructions_per_package'), int) and pkg['instructions_per_package'] == 4 30 | seq = pkg.get('sequence_word_encoding', {}) 31 | # Sequence word encoding should define continue, branch, complete 32 | for action in ['continue', 'branch', 'complete']: 33 | assert action in seq, f"'{action}' must be defined in sequence_word_encoding" 34 | 35 | # Validate 'Zen1' spec inherits defaults and overrides counts 36 | zen1 = reg.get('Zen1') 37 | assert zen1.name == 'Zen1' 38 | # Inherited: word_size 39 | assert zen1.word_size == zen.word_size 40 | # Inherited package size but overridden count 41 | assert zen1.packages.get('instructions_per_package') == 4 42 | assert zen.packages.get('instructions_per_package') == 4 43 | assert zen1.packages.get('count') == 64 44 | # Overridden match register count 45 | assert zen1.match_registers.get('entry_count') == 22 46 | assert zen1.match_registers.get('regs_per_entry') == 2 47 | 48 | def test_spec_get_field_values(): 49 | reg = Registry() 50 | zen2 = reg.get('Zen2') 51 | 52 | # Test instruction: add.n reg1, reg3, reg7 53 | common_fields = zen2.get_field_values(0x382F9E108CE00000, zen2.common_fields) 54 | assert common_fields['operation']['value'] == 0x5f 55 | assert common_fields['native_flags']['value'] == 1 56 | assert common_fields['rd']['value'] == 1 57 | assert common_fields['rs']['value'] == 3 58 | assert common_fields['rt']['value'] == 7 59 | 60 | def test_instruction_class(): 61 | reg = Registry() 62 | zen2 = reg.get('Zen2') 63 | 64 | # Test instruction: add reg1, reg3, 0x42 65 | word = 0x382F9C108E280042 66 | fields = zen2.get_common_field_values(word) 67 | class_name = zen2.get_class(fields) 68 | assert class_name == 'regop' 69 | 70 | class_fields = zen2.get_class_field_values(word, class_name) 71 | fields.update(class_fields) 72 | assert fields['imm16']['value'] == 0x42 73 | 74 | class_fields2 = zen2.decode_instruction(word) 75 | assert fields['imm16']['value'] == 0x42 76 | 77 | def test_get_instruction_spec(): 78 | reg = Registry() 79 | zen2 = reg.get('Zen2') 80 | 81 | # Test instruction: add reg1, reg3, 0x42 82 | word = 0x382F9C108E280042 83 | 84 | fields = zen2.decode_instruction(word) 85 | insn = zen2.get_instruction_spec(fields) 86 | assert insn['template'].startswith('add') 87 | 88 | def test_get_flags(): 89 | reg = Registry() 90 | zen2 = reg.get('Zen2') 91 | 92 | # Test instruction: add.n reg1, reg3, reg7 93 | word = 0x382F9E108CE00000 94 | 95 | fields = zen2.decode_instruction(word) 96 | flags = zen2.get_flags(fields) 97 | assert len(flags) == 2 98 | assert 'n' in flags 99 | assert 'q' in flags 100 | 101 | def test_encode_instruction(): 102 | reg = Registry() 103 | zen2 = reg.get('Zen2') 104 | 105 | # Test instruction: add reg1, reg3, 0x42 106 | word = 0x382F9C108E280042 107 | fields = zen2.decode_instruction(word) 108 | 109 | word_out = zen2.encode_instruction(fields) 110 | assert word_out == word 111 | 112 | def test_get_default_fields_for_instruction(): 113 | reg = Registry() 114 | zen2 = reg.get('Zen2') 115 | 116 | insn_spec = { 117 | 'template': 'add rd, rs, rt', 118 | 'condition': 'operation == 0x5f && imm_mode == 0' 119 | } 120 | 121 | fields = zen2.get_default_fields_for_instruction(insn_spec, 'regop') 122 | word = zen2.encode_instruction(fields) 123 | 124 | # Test instruction: add reg0, reg0, reg0 125 | expected = 0x382F9C1000000000 126 | 127 | assert word == expected -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ZenUtils 2 | 3 | Collection of tools to create, dump and manipulate Zen microcode updates. Currently supported are Zen1 and Zen2 microcode variants, and basic instructions: arithmetic, logic, shift, load, store, conditional microcode branch. See [zen_base.yaml](arch/zen_base.yaml) for field descriptions, operations and flags. 4 | 5 | ```nasm 6 | mov reg1, 0x4242 7 | add reg1, reg3, 0x42 8 | xor.zcZCnd reg1, reg3, 0x42 9 | jz.z 0x1fc0 10 | mov msr2:[reg15], reg14 11 | mov.b reg14, ls:[reg15 + reg14 + 0x20] 12 | ``` 13 | 14 | ## Samples 15 | - `samples/ucode` contains microcode patch sources for the macro assembler and disassembler. 16 | - `samples/mcode` hosts companion x86-64 code that calls into the patches, useful when testing end-to-end flows. 17 | 18 | ## Install 19 | To install ZenUtils, use uv: 20 | 21 | ```sh 22 | uv pip install git+https://github.com/AngryUEFI/ZenUtils 23 | ``` 24 | 25 | This will install the command line tools and Python package. 26 | To add ZenUtils as a dependency to your own project using uv, run: 27 | 28 | ```sh 29 | uv pip add git+https://github.com/AngryUEFI/ZenUtils 30 | ``` 31 | 32 | This will add ZenUtils to your `pyproject.toml` for use as a library in your project. 33 | 34 | 35 | 36 | ## Tools 37 | 38 | ### Assembler 39 | Usage: 40 | ``` 41 | $ python zenutils/asm.py --arch Zen2 "add reg1, reg3, 0x42" 42 | 43 | ; 0x382F9C108C080042 0011100000101111100111000001000010001100000010000000000001000010 44 | ``` 45 | 46 | Inspect fields: 47 | ``` 48 | $ python zenutils/asm.py --arch Zen2 "add reg1, reg3, 0x42" -i 49 | 50 | ; 0x382F9C108C080042 0011100000101111100111000001000010001100000010000000000001000010 51 | ; imm16 : 66 0000000001000010 52 | ; imm_signed : 0 0 53 | ; imm_mode : 1 1 54 | ; rt : 0 00000 55 | ; rs : 3 00011 56 | ; rd : 1 00001 57 | ; rmod : 1 1 58 | ; read_zf : 0 0 59 | ; read_cf : 0 0 60 | ; write_zf : 0 0 61 | ; write_cf : 0 0 62 | ; native_flags: 0 0 63 | ; size_flags : 7 111 64 | ; load : 0 0 65 | ; store : 0 0 66 | ; operation : 95 01011111 67 | ; exec_unit : 7 111 68 | ``` 69 | 70 | Python usage: 71 | ```python 72 | from zenutils.arch.registry import Registry 73 | from zenutils.asm import assemble_single 74 | 75 | zen2_spec = Registry().get('Zen2') 76 | word = assemble_single(zen2_spec, "add reg1, reg3, 0x42", label_addrs = {}) 77 | print(f"0x{word:016X}") 78 | 79 | ``` 80 | 81 | ### Macro Assembler 82 | The macro assembler converts microcode assembly files to microcode updates binaries. It supports labels and takes care of placing instructions and quads as needed. Use the `-d` flag to get text-based debug output instead. 83 | 84 | > Note 85 | > The microcode updates must be signed (f.i., with Zentool) before they are accepted by Zen CPUs. 86 | 87 | For microcode examples see `samples/ucode`. Usage: 88 | 89 | ``` 90 | $ python zenutils/masm.py samples/ucode/strlen.s -o cpu00870F10_strlen_shld.bin 91 | 92 | ; Packages used: 3 of 64 93 | 94 | $ zentool resign cpu00870F10_strlen_shld.bin 95 | ``` 96 | 97 | ### Disassembler 98 | Usage: 99 | 100 | ``` 101 | $ python zenutils/disasm.py --arch Zen2 0x382F9C108C080042 102 | 103 | add reg1, reg3, 0x42 104 | ``` 105 | 106 | Inspect fields: 107 | 108 | ``` 109 | $ python zenutils/disasm.py --arch Zen2 0x382F9C108C080042 -i 110 | 111 | ; 0x382F9C108C080042 0011100000101111100111000001000010001100000010000000000001000010 112 | ; imm16 : 66 0000000001000010 113 | ; imm_signed : 0 0 114 | ; imm_mode : 1 1 115 | ; rt : 0 00000 116 | ; rs : 3 00011 117 | ; rd : 1 00001 118 | ; rmod : 1 1 119 | ; read_zf : 0 0 120 | ; read_cf : 0 0 121 | ; write_zf : 0 0 122 | ; write_cf : 0 0 123 | ; native_flags: 0 0 124 | ; size_flags : 7 111 125 | ; load : 0 0 126 | ; store : 0 0 127 | ; operation : 95 01011111 128 | ; exec_unit : 7 111 129 | add reg1, reg3, 0x42 130 | ``` 131 | 132 | Disassemble a whole binary microcode update file: 133 | ``` 134 | $ python zenutils/disasm.py --arch Zen2 -u cpu00870F10_strlen_shld.bin 135 | 136 | ; Header 137 | .date 0x07112025 138 | .revision 0x08701040 139 | .format 0x8004 140 | ... 141 | 142 | ; Match Register 143 | .match_reg 0 0x00000420 144 | .match_reg 1 0x00000000 145 | .match_reg 2 0x00000000 146 | ... 147 | 148 | ; Instruction Packages 149 | ; Slot 0 @ 0x1fc0 (0x38501c1782000000 0x387f9c1000000000 0x387f9c1000000000 0x387f9c1000000000 0x00000001) 150 | mov reg15, rax 151 | nop 152 | nop 153 | nop 154 | .sw_continue 155 | 156 | ; Slot 1 @ 0x1fc1 (0x286f20173c009800 0x38581c9039c00000 0x20021c2000081fc2 0x382f9c17bc080001 0x00121fc1) 157 | mov.b reg14, ls:[reg15] 158 | and.Z reg0, reg14, reg14 159 | jz.z 0x1fc2 160 | add reg15, reg15, 0x1 161 | .sw_branch 0x1fc1 ; (immediately) 162 | 163 | ; Slot 2 @ 0x1fc2 (0x38281c19be000000 0x387f9c1000000000 0x387f9c1000000000 0x387f9c1000000000 0x03100082) 164 | sub rbx, reg15, rax 165 | nop 166 | nop 167 | nop 168 | .sw_complete ; (immediately) 169 | ... 170 | ``` 171 | 172 | ## Ack 173 | - [Zentool](https://github.com/google/security-research/tree/master/pocs/cpus/entrysign/zentool) 174 | - [AngryUEFI](https://github.com/AngryUEFI/AngryUEFI), [AngryCAT](https://github.com/AngryUEFI/AngryCAT) 175 | -------------------------------------------------------------------------------- /zenutils/asm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import argparse 4 | import re 5 | # Support both standard imports (when package is installed) and relative imports (when running as script) 6 | try: 7 | from .arch.registry import Registry 8 | except ImportError: 9 | from arch.registry import Registry 10 | 11 | # Parse instruction or template into mnemonic and operands 12 | # Preserves memory operands as one operand (e.g., `msr2:[reg15 + 8]`) 13 | def parse_instruction(assembly: str) -> tuple: 14 | parts = assembly.strip().split(None, 1) 15 | mnemonic = parts[0] 16 | mnemonic = mnemonic.split('.', 1)[0].lower() 17 | operands = parts[1] if len(parts) > 1 else "" 18 | operands = [op.strip() for op in operands.split(',')] if operands else [] 19 | return (mnemonic, operands) 20 | 21 | # Parses memory operand into sub operands 22 | def parse_memory_operand(operand: str) -> list: 23 | parts = [p for p in re.split(r' |\t|:|\[|\]|\+|-', operand) if p != ''] 24 | return parts 25 | 26 | # Checks if string parses to a number 27 | def can_parse_number(string: str) -> bool: 28 | try: 29 | int(string, 0) 30 | except Exception as e: 31 | return False 32 | return True 33 | 34 | # Get types for instruction operands 35 | def get_op_types_from_inst(spec, instr_ops: list) -> list: 36 | types = [] 37 | for op in instr_ops: 38 | if '[' in op and ']' in op: 39 | types.append('memory') 40 | elif op in spec.registers: 41 | types.append('register') 42 | elif can_parse_number(op): 43 | types.append('immediate') 44 | elif op in spec.segment_to_code: 45 | types.append('immediate') 46 | elif re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', op): 47 | types.append('immediate') # mark labels as immediate for now 48 | else: 49 | raise ValueError("Unknown type for operand: {}".format(op)) 50 | return types 51 | 52 | # Get types for operands from template 53 | def get_op_types_from_template(spec, class_name:str, ops: list) -> list: 54 | types = [] 55 | for op in ops: 56 | if '[' in op and ']' in op: 57 | types.append('memory') 58 | continue 59 | 60 | if op in spec.common_fields: 61 | types.append(spec.common_fields[op]['type']) 62 | elif op in spec.instruction_classes[class_name]['fields']: 63 | types.append(spec.instruction_classes[class_name]['fields'][op]['type']) 64 | else: 65 | raise ValueError("Unknown type for operand: {}".format(op)) 66 | return types 67 | 68 | # Check if instruction memory op matches template memory op 69 | def memory_operand_matching_template(spec, class_name:str, tpl_mem_op: str, instr_mem_op: str) -> bool: 70 | tpl_sub_ops = parse_memory_operand(tpl_mem_op) 71 | instr_sub_ops = parse_memory_operand(instr_mem_op) 72 | if len(tpl_sub_ops) != len(instr_sub_ops): 73 | return False 74 | 75 | tpl_sub_ops_types = get_op_types_from_template(spec, class_name, tpl_sub_ops) 76 | instr_sub_ops_types = get_op_types_from_inst(spec, instr_sub_ops) 77 | return tpl_sub_ops_types == instr_sub_ops_types 78 | 79 | # Search matching template for instruction 80 | def find_instr_spec(spec, assembly: str) -> tuple: 81 | instr_mnem, instr_ops = parse_instruction(assembly) 82 | instr_op_types = get_op_types_from_inst(spec, instr_ops) 83 | 84 | for class_name, instr_specs in spec.instructions.items(): 85 | for instr_spec in instr_specs: 86 | tpl_mnem, tpl_ops = parse_instruction(instr_spec['template']) 87 | if tpl_mnem != instr_mnem or len(tpl_ops) != len(instr_ops): 88 | continue 89 | 90 | tpl_op_types = get_op_types_from_template(spec, class_name, tpl_ops) 91 | if tpl_op_types != instr_op_types: 92 | continue 93 | 94 | if 'memory' in instr_op_types: 95 | # Todo: expand to consider multiple memory operands 96 | index = instr_op_types.index('memory') 97 | if not memory_operand_matching_template(spec, class_name, tpl_ops[index], instr_ops[index]): 98 | continue 99 | 100 | return (class_name, instr_spec) 101 | raise ValueError("No inst spec found for instruction: {}".format(assembly)) 102 | 103 | def insert_operand_values(spec, fields: dict, instr_ops: list, field_names: list, assembly: str, label_addrs: dict): 104 | for op, field_name in zip(instr_ops, field_names): 105 | # Handle memory operand 106 | if '[' in op and ']' in op: 107 | sub_ops = parse_memory_operand(op) 108 | sub_field_names = parse_memory_operand(field_name) 109 | insert_operand_values(spec, fields, sub_ops, sub_field_names, assembly, label_addrs) 110 | continue 111 | 112 | field = fields[field_name] 113 | if field['type'] == 'register': 114 | field['value'] = spec.register_to_idx[op] 115 | # Todo: Register custom types in a yaml structure making special handling superfluous 116 | elif field_name == 'segment': 117 | segment_code = spec.segment_to_code.get(op, None) 118 | if segment_code == None: 119 | segment_code = int(op, 0) 120 | field['value'] = segment_code 121 | elif field['type'] == 'immediate' and can_parse_number(op): 122 | field['value'] = int(op, 0) 123 | elif field['type'] == 'immediate' and re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', op): 124 | if op not in label_addrs: 125 | raise ValueError("Use of undefined label '{}' in '{}'.".format(op, assembly)) 126 | field['value'] = label_addrs[op] 127 | else: 128 | raise ValueError("Unknown field type '{}' in '{}'.".format(field['type'], assembly)) 129 | 130 | def assemble_single(spec, assembly: str, label_addrs: dict = {}) -> int: 131 | # Get matching instruction spec and default field values 132 | class_name, instr_spec = find_instr_spec(spec, assembly) 133 | fields = spec.get_default_fields_for_instruction(instr_spec, class_name) 134 | 135 | # Insert values for operands 136 | _, instr_ops = parse_instruction(assembly) 137 | _, field_names = parse_instruction(instr_spec['template']) 138 | insert_operand_values(spec, fields, instr_ops, field_names, assembly, label_addrs) 139 | 140 | # Insert values for flags 141 | parts = assembly.split(None, 1)[0].split('.', 1) 142 | if len(parts) == 2: 143 | flags = parts[1] 144 | for flag in flags: 145 | flag_spec = spec.instruction_flags[flag] 146 | fields[flag_spec['field']]['value'] = flag_spec['value'] 147 | 148 | return spec.encode_instruction(fields) 149 | 150 | def main(): 151 | parser = argparse.ArgumentParser(description="Assemble instruction to word.") 152 | parser.add_argument('--arch', required=True, help='Architecture spec name (e.g., Zen1, Zen2)') 153 | parser.add_argument('-i','--inspect', action='store_true', help="Verbose field breakdown") 154 | parser.add_argument('instr', help='Instruction text (in quotes)') 155 | args = parser.parse_args() 156 | 157 | # Load architecture spec 158 | reg = Registry() 159 | try: 160 | spec = reg.get(args.arch) 161 | except KeyError: 162 | sys.exit(f"Error: Unknown architecture '{args.arch}'") 163 | 164 | # Assemble instruction 165 | try: 166 | word = assemble_single(spec, args.instr) 167 | except Exception as e: 168 | sys.exit(f"Error: {e}") 169 | print(f"; 0x{word:016X} {word:064b}") 170 | 171 | # Inspect fields 172 | if args.inspect: 173 | fields = spec.decode_instruction(word) 174 | for name, field in sorted(fields.items(), key=lambda kv: kv[1]['bits'][0]): 175 | lo, hi = field['bits'] 176 | width = hi - lo + 1 177 | val = field.get('value', 0) 178 | bin_str = f"{val:0{width}b}".rjust(64 - lo) 179 | print(f"; {name:<12s}:{val:>5d} {bin_str}") 180 | 181 | if __name__ == '__main__': 182 | main() 183 | -------------------------------------------------------------------------------- /zenutils/disasm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import argparse 4 | import re 5 | import struct 6 | # Support both standard imports (when package is installed) and relative imports (when running as script) 7 | try: 8 | from .arch.registry import Registry 9 | except ImportError: 10 | from arch.registry import Registry 11 | 12 | def disassemble_single(spec, word: int, prev_word:int = 0) -> str: 13 | # Decode fields 14 | fields = spec.decode_instruction(word) 15 | if fields is None: 16 | return None 17 | 18 | # Get disassembly template 19 | insn = spec.get_instruction_spec(fields) 20 | if insn is None: 21 | return None 22 | assembly = insn['template'] 23 | parts = [p for p in re.split(r' |\t|:|\[|\]|\+|-|,', assembly) if p != ''] 24 | 25 | # Insert operands 26 | # Iterate operands from right to left, replace only first occurrence 27 | # Todo: str.replace could go terribly wrong, find + splice is better 28 | # Todo: Register custom types in a yaml structure making special handling superfluous 29 | for part in reversed(parts[1:]): 30 | field = fields[part] 31 | if field['type'] == 'register': 32 | new_part = spec.registers[field.get('value', 0)] 33 | elif part == 'segment': 34 | segment_index = field.get('value', 0) 35 | new_part = spec.code_to_segment.get(segment_index, hex(segment_index)) 36 | elif field['type'] == 'immediate': 37 | new_part = hex(field.get('value', 0)) 38 | else: 39 | raise ValueError("Unknown field type {} in {}".format(field['type'], assembly)) 40 | assembly = assembly.replace(part, new_part, 1) 41 | 42 | # Insert flags, in disassembly .q is optional 43 | flags = spec.get_flags(fields) 44 | if 'q' in flags: 45 | del flags['q'] 46 | if len(flags): 47 | assembly = assembly.replace(parts[0], parts[0] + '.' + ''.join(flags.keys())) 48 | 49 | # Print 32-bit immediate mode 50 | if 'imm32_mode' in fields and fields['imm32_mode']['value'] == 1: 51 | value = ((prev_word & 0xffff) << 16) | fields['imm16']['value'] 52 | assembly += f", imm32:0x{value:x}" 53 | 54 | return assembly 55 | 56 | def detect_zen_arch(cpuid: int) -> str: 57 | part = (cpuid >> 8) & 0xFF 58 | if part == 0x80: 59 | return "Zen1" 60 | elif part == 0x87: 61 | return "Zen2" 62 | elif part == 0xA2: 63 | return "Zen3" 64 | elif part == 0xA6: 65 | return "Zen4" 66 | elif part == 0xB4: 67 | return "Zen5" 68 | else: 69 | raise ValueError(f"Auto detect of architecture failed with cpuid '0x{cpuid:x}'.") 70 | 71 | def disassemble_object(reg, spec, data: bytes, auto_detect_arch: bool) -> str: 72 | result = [] 73 | offset = 0 74 | 75 | # Print object header 76 | result.append("; Header") 77 | fields = spec.object_format['header']['fields'] 78 | cpuid = None 79 | for field in fields: 80 | name = field['name'] 81 | ftype = field['type'] 82 | if ftype == 'uint32': 83 | val, = struct.unpack_from('5d} {bin_str}") 207 | 208 | # Disassemble instruction 209 | asm = disassemble_single(spec, word) 210 | print(asm) 211 | 212 | if __name__ == '__main__': 213 | main() 214 | -------------------------------------------------------------------------------- /zenutils/arch/registry.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | from pathlib import Path 3 | from copy import deepcopy 4 | from .eval import ConditionEvaluator 5 | 6 | def deep_merge(base: dict, override: dict) -> dict: 7 | """ 8 | Recursively merge override into base, returning a new dict. 9 | Values in override take precedence. Nested dicts are merged. 10 | """ 11 | merged = deepcopy(base) 12 | for key, val in override.items(): 13 | if key in merged and isinstance(merged[key], dict) and isinstance(val, dict): 14 | merged[key] = deep_merge(merged[key], val) 15 | else: 16 | merged[key] = deepcopy(val) 17 | return merged 18 | 19 | 20 | class ArchSpec: 21 | """ 22 | Represents a fully-resolved architecture specification. 23 | """ 24 | def __init__(self, raw: dict): 25 | # Store raw merged dict for introspection 26 | self._raw = raw 27 | # Basic fields 28 | self.name = raw.get('name') 29 | self.word_size = raw.get('word_size') 30 | self.endianness = raw.get('endianness') 31 | # Registers 32 | self.registers = [r for r in raw.get('registers', [])] 33 | self.register_to_idx = {r: i for i, r in enumerate(self.registers)} 34 | # Size flags 35 | self.size_flag_to_code = raw.get('size_flags', {}) 36 | self.code_to_size_flag = {v: k for k, v in self.size_flag_to_code.items()} 37 | # Segments 38 | self.code_to_segment = raw.get('segments', {}) 39 | self.segment_to_code = {v: k for k, v in self.code_to_segment.items()} 40 | # Common fields 41 | self.common_fields = raw.get('common_fields', {}) 42 | # Instruction classes 43 | self.instruction_classes = raw.get('instruction_classes', {}) 44 | # Instruction flags 45 | self.instruction_flags = raw.get('instruction_flags', {}) 46 | # Instructions 47 | self.instructions = raw.get('instructions', {}) 48 | # Object format 49 | self.object_format = raw.get('object_format', {}) 50 | # Match registers 51 | self.match_registers = raw.get('match_registers', {}) 52 | # Instruction packages 53 | self.packages = raw.get('packages', {}) 54 | 55 | @staticmethod 56 | def get_field_values(word: int, field_spec: dict) -> dict: 57 | result = deepcopy(field_spec) 58 | for field in result.values(): 59 | lo, hi = field['bits'] 60 | width = hi - lo + 1 61 | mask = (1 << width) - 1 62 | field['value'] = (word >> lo) & mask 63 | return result 64 | 65 | def get_common_field_values(self, word: int) -> dict: 66 | return self.get_field_values(word, self.common_fields) 67 | 68 | def get_class_field_values(self, word: int, class_name: str) -> dict: 69 | return self.get_field_values(word, self.instruction_classes[class_name]['fields']) 70 | 71 | def get_class(self, fields: dict) -> str: 72 | evaluator = ConditionEvaluator(fields) 73 | for name, insn_class in self.instruction_classes.items(): 74 | if evaluator.evaluate(insn_class['condition']): 75 | return name 76 | return None 77 | 78 | def decode_instruction(self, word: int) -> dict: 79 | fields = self.get_common_field_values(word) 80 | class_name = self.get_class(fields) 81 | if class_name is None: 82 | return None 83 | 84 | fields.update(self.get_class_field_values(word, class_name)) 85 | return fields 86 | 87 | def get_instruction_spec(self, fields: dict, class_name: str = None) -> dict: 88 | if class_name == None: 89 | class_name = self.get_class(fields) 90 | evaluator = ConditionEvaluator(fields) 91 | for insn in self.instructions[class_name]: 92 | if evaluator.evaluate(insn['condition']): 93 | return insn 94 | return None 95 | 96 | def get_flags(self, fields: dict) -> dict: 97 | flags = {} 98 | for flag, spec in self.instruction_flags.items(): 99 | if spec['field'] in fields: 100 | if fields[spec['field']].get('value', 0) == spec['value']: 101 | flags[flag] = spec 102 | return flags 103 | 104 | @staticmethod 105 | def encode_instruction(fields: dict) -> int: 106 | word = 0 107 | for field in fields.values(): 108 | lo, hi = field['bits'] 109 | width = hi - lo + 1 110 | mask = (1 << width) - 1 111 | word |= (field.get('value', 0) & mask) << lo 112 | return word 113 | 114 | def get_default_fields_for_instruction(self, instruction: dict, class_name: str) -> dict: 115 | fields = deepcopy(self.common_fields) 116 | fields.update(deepcopy(self.instruction_classes[class_name]['fields'])) 117 | defaults = deepcopy(self.instruction_classes[class_name]['defaults']) 118 | defaults.update(ConditionEvaluator.extract_defs_from_condition(instruction['condition'])) 119 | for field, val in defaults.items(): 120 | fields[field]['value'] = val 121 | return fields 122 | 123 | def encode_sequence_word(self, action: str, target: int = 0) -> int: 124 | seq = self.packages.get('sequence_word_encoding', {}) 125 | enc = seq.get(action.lower()) 126 | if enc is None: 127 | raise KeyError(f"Unknown sequence word action: {action}") 128 | if isinstance(enc, int): 129 | return enc 130 | encoding = enc.get('encoding', 0) 131 | lo, hi = enc['target_address_bits'] 132 | mask = (1 << (hi - lo + 1)) - 1 133 | return encoding | ((target & mask) << lo) 134 | 135 | def decode_sequence_word(self, word: int) -> str: 136 | # Todo: improve spec and generalize this function 137 | if word & 0x00020000 != 0: 138 | lo, hi = self.packages['sequence_word_encoding']['branch']['target_address_bits'] 139 | mask = (1 << (hi - lo + 1)) - 1 140 | val = (word >> lo) & mask 141 | imm_str = " ; (immediately)" if word & 0x00100000 else "" 142 | return f".sw_branch 0x{val:x}{imm_str}" 143 | 144 | if word & 1: 145 | return ".sw_continue" 146 | 147 | if word & 2: 148 | imm_str = " ; (immediately)" if word & 0x00100000 else "" 149 | return f".sw_complete{imm_str}" 150 | 151 | return f".sw 0x{word:08x}" 152 | 153 | def encode_match_registers(self, match_regs: dict) -> list: 154 | result = [] 155 | fields = deepcopy(self.match_registers['fields']) 156 | for i in range(self.match_registers['entry_count']): 157 | fields['m1']['value'] = match_regs.get(i * 2, 0) 158 | fields['u1']['value'] = int(fields['m1']['value'] != 0) 159 | fields['m2']['value'] = match_regs.get(i * 2 + 1, 0) 160 | fields['u2']['value'] = int(fields['m2']['value'] != 0) 161 | 162 | word = 0 163 | for field in fields.values(): 164 | val = field.get('value', 0) 165 | lo, hi = field['bits'] 166 | mask = (1 << (hi - lo + 1)) - 1 167 | word |= (val & mask) << lo 168 | 169 | result.append(word) 170 | return result 171 | 172 | def decode_match_registers(self, data: bytes) -> list: 173 | result = [] 174 | fields = deepcopy(self.match_registers['fields']) 175 | for i in range(len(data) // 4): 176 | word = int.from_bytes(data[i*4:i*4+4], 'little') 177 | 178 | for field in fields.values(): 179 | lo, hi = field['bits'] 180 | mask = (1 << (hi - lo + 1)) - 1 181 | field['value'] = (word >> lo) & mask 182 | 183 | result.append(deepcopy(fields)) 184 | return result 185 | 186 | class Registry: 187 | """ 188 | Loads architecture specs from default 'arch/specs' relative path or a custom directory. 189 | Resolves inheritance and provides ArchSpec objects. 190 | """ 191 | def __init__(self, spec_dir: Path = None): 192 | # Default to 'specs' directory next to this file 193 | if spec_dir is None: 194 | spec_dir = Path(__file__).parent 195 | self._load_specs(spec_dir) 196 | 197 | def _load_specs(self, spec_dir: Path): 198 | raw_specs = {} 199 | for path in spec_dir.glob('*.yaml'): 200 | data = yaml.safe_load(path.read_text()) 201 | name = data.get('name') 202 | if not name: 203 | raise ValueError(f"Spec file {path} missing 'name'") 204 | raw_specs[name] = data 205 | # Resolve inheritance 206 | self._specs = {} 207 | 208 | def resolve(name: str) -> ArchSpec: 209 | if name in self._specs: 210 | return self._specs[name] 211 | data = raw_specs.get(name) 212 | if data is None: 213 | raise KeyError(f"Spec '{name}' not found in {spec_dir}") 214 | parent_name = data.get('inherits') 215 | if parent_name: 216 | parent = resolve(parent_name) 217 | merged = deep_merge(parent._raw, data) 218 | else: 219 | merged = data 220 | spec = ArchSpec(merged) 221 | self._specs[name] = spec 222 | return spec 223 | 224 | for spec_name in raw_specs: 225 | resolve(spec_name) 226 | 227 | def get(self, name: str) -> ArchSpec: 228 | """ 229 | Retrieve a resolved ArchSpec by name. Example: 230 | spec = Registry().get('Zen2') 231 | """ 232 | spec = self._specs.get(name) 233 | if spec is None: 234 | raise KeyError(f"Spec '{name}' not loaded. Available: {list(self._specs)}") 235 | return spec 236 | -------------------------------------------------------------------------------- /zenutils/arch/zen_base.yaml: -------------------------------------------------------------------------------- 1 | # Base specification for Zen arch 2 | 3 | name: ZenBase 4 | word_size: 64 5 | endianness: little 6 | 7 | # Microcode and general-purpose registers 8 | registers: [reg0, reg1, reg2, reg3, reg4, reg5, reg6, reg7, reg8, reg9, reg10, reg11, reg12, reg13, reg14, reg15, 9 | rax, rcx, rdx, rbx, rsp, rbp, rsi, rdi, r8, r9, r10, r11, r12, r13, r14, r15] 10 | 11 | # Size flags encoding 12 | size_flags: 13 | b: 0b000 # byte (1-byte) 14 | w: 0b001 # word (2-byte) 15 | d: 0b011 # doubleword (4-byte) 16 | q: 0b111 # quadword (8-byte) 17 | 18 | # Segment encoding 19 | segments: 20 | 0: vs 21 | 1: cpuid 22 | 5: msr1 23 | 6: ls 24 | 9: ucode 25 | 12: msr2 26 | 27 | # Common fields of micro instructions 28 | common_fields: 29 | rt: # 2nd source register operand 30 | type: register 31 | bits: [21, 25] 32 | rs: # 1st source register operand 33 | type: register 34 | bits: [26, 30] 35 | rd: # Destination register operand 36 | type: register 37 | bits: [31, 35] 38 | rmod: # Unknown, must be set for RegOp, LdOp 39 | type: flag 40 | bits: [36, 36] 41 | read_zf: # If set read zero flag 42 | type: flag 43 | bits: [37, 37] 44 | read_cf: # If set read carry flag 45 | type: flag 46 | bits: [38, 38] 47 | write_zf: # If set write zero flag 48 | type: flag 49 | bits: [39, 39] 50 | write_cf: # If set write carry flag 51 | type: flag 52 | bits: [40, 40] 53 | native_flags: # If set use x86_64 cf/zf, if clear use microcode cf/zf 54 | type: flag 55 | bits: [41, 41] 56 | size_flags: # Operation size 57 | type: immediate 58 | bits: [42, 44] 59 | load: # If set performs memory load 60 | type: flag 61 | bits: [45, 45] 62 | store: # If set performs memory store 63 | type: flag 64 | bits: [46, 46] 65 | operation: # Operation ALU (add, shl, and, ...) load, store, cond. microcode branch 66 | type: immediate 67 | bits: [47, 54] 68 | exec_unit: # Execution unit 69 | type: immediate 70 | bits: [59, 61] 71 | 72 | # Instruction classes 73 | instruction_classes: 74 | # RegOp - ALU operations 75 | regop: 76 | fields: 77 | imm16: # Immediate 78 | type: immediate 79 | bits: [0, 15] 80 | imm_signed: # If set treat imm16 as signed immediate 81 | type: flag 82 | bits: [16, 16] 83 | imm32_mode: # If set loads 32bit immediate, combining imm16 fields from last and current instruction 84 | type: flag 85 | bits: [17, 17] 86 | imm_mode: # If set use imm16 as 2nd source operand instead of rt 87 | type: flag 88 | bits: [19, 19] 89 | cond: # Lower 4 bit of operation encode condition for some instructions 90 | type: immediate 91 | bits: [47, 50] 92 | defaults: 93 | rmod: 1 94 | size_flags: 7 95 | exec_unit: 7 96 | condition: operation >= 0x20 && load == 0 && store == 0 97 | # LdOp - memory load operation 98 | ldop: 99 | fields: 100 | offset10: # Offset 101 | type: immediate 102 | bits: [0, 9] 103 | segment: # Memory segment 104 | type: immediate 105 | bits: [10, 13] 106 | nop3: # Unknown, one bit must be set for load to work 107 | type: immediate 108 | bits: [15, 16] 109 | qwsz: # If set left-shift offset by 3 (qword sized elements) 110 | type: flag 111 | bits: [19, 19] 112 | defaults: 113 | nop3: 1 114 | rmod: 1 115 | size_flags: 7 116 | load: 1 117 | operation: 0xde 118 | exec_unit: 5 119 | condition: load == 1 && operation == 0xde && store == 0 120 | # StOp - memory store operation 121 | stop: 122 | fields: 123 | offset10: # Offset 124 | type: immediate 125 | bits: [0, 9] 126 | segment: # Memory segment 127 | type: immediate 128 | bits: [10, 13] 129 | nop3: # Unknown, both bits must be set for store to work 130 | type: immediate 131 | bits: [15, 16] 132 | mode: # Unknown, both bits must be set for store to work 133 | type: immediate 134 | bits: [17, 18] 135 | qwsz: # If set left-shift offset by 3 (qword sized elements) 136 | type: flag 137 | bits: [19, 19] 138 | defaults: 139 | nop3: 3 140 | mode: 3 141 | size_flags: 7 142 | store: 1 143 | operation: 0xa0 144 | exec_unit: 3 145 | condition: store == 1 && operation == 0xa0 && load == 0 146 | # BrOp - microcode conditional branch 147 | brop: 148 | fields: 149 | address13: # Absolute microcode ROM/RAM address 150 | type: immediate 151 | bits: [0, 12] 152 | mode3: # Unknown, must be set for branch to work 153 | type: flag 154 | bits: [19, 19] 155 | cond: # Lower 4 bit of operation encode branch condition (je, jne, jb, ja, etc.) 156 | type: immediate 157 | bits: [47, 50] 158 | defaults: 159 | mode3: 1 160 | size_flags: 7 161 | exec_unit: 4 162 | condition: operation < 0x10 && load == 0 && store == 0 163 | 164 | # Instruction flags 165 | instruction_flags: 166 | z: 167 | field: read_zf 168 | value: 1 169 | c: 170 | field: read_cf 171 | value: 1 172 | Z: 173 | field: write_zf 174 | value: 1 175 | C: 176 | field: write_cf 177 | value: 1 178 | n: 179 | field: native_flags 180 | value: 1 181 | b: 182 | field: size_flags 183 | value: 0b000 184 | w: 185 | field: size_flags 186 | value: 0b001 187 | d: 188 | field: size_flags 189 | value: 0b011 190 | q: 191 | field: size_flags 192 | value: 0b111 193 | 194 | instructions: 195 | # ALU ops 196 | regop: 197 | - template: nop 198 | condition: operation == 0xff 199 | - template: mov rd, rt 200 | condition: operation == 0xa0 && imm_mode == 0 201 | - template: mov rd, imm16 202 | condition: operation == 0xa0 && imm_mode == 1 203 | # Arithmetic ops 204 | - template: add rd, rs, rt 205 | condition: operation == 0x5f && imm_mode == 0 206 | - template: add rd, rs, imm16 207 | condition: operation == 0x5f && imm_mode == 1 208 | - template: adc rd, rs, rt 209 | condition: operation == 0x5d && imm_mode == 0 210 | - template: adc rd, rs, imm16 211 | condition: operation == 0x5d && imm_mode == 1 212 | - template: sub rd, rs, rt 213 | condition: operation == 0x50 && imm_mode == 0 214 | - template: sub rd, rs, imm16 215 | condition: operation == 0x50 && imm_mode == 1 216 | - template: sub2 rd, rs, rt 217 | condition: operation >= 0x20 && operation <= 0x2f && imm_mode == 0 218 | - template: sub2 rd, rs, imm16 219 | condition: operation >= 0x20 && operation <= 0x2f && imm_mode == 1 220 | - template: sbb rd, rs, rt 221 | condition: operation == 0x52 && imm_mode == 0 222 | - template: sbb rd, rs, imm16 223 | condition: operation == 0x52 && imm_mode == 1 224 | - template: mul rd, rs, rt 225 | condition: operation == 0x60 && imm_mode == 0 226 | - template: mul rd, rs, imm16 227 | condition: operation == 0x60 && imm_mode == 1 228 | # Logic ops 229 | - template: and rd, rs, rt 230 | condition: operation == 0xb0 && imm_mode == 0 231 | - template: and rd, rs, imm16 232 | condition: operation == 0xb0 && imm_mode == 1 233 | - template: xor rd, rs, rt 234 | condition: operation == 0xb5 && imm_mode == 0 235 | - template: xor rd, rs, imm16 236 | condition: operation == 0xb5 && imm_mode == 1 237 | - template: or rd ,rs ,rt 238 | condition: operation == 0xbe && imm_mode == 0 239 | - template: or rd ,rs ,imm16 240 | condition: operation == 0xbe && imm_mode == 1 241 | # Shift/rotate ops 242 | - template: shl rd, rs, rt 243 | condition: operation == 0x40 && imm_mode == 0 244 | - template: shl rd, rs, imm16 245 | condition: operation == 0x40 && imm_mode == 1 246 | - template: scl rd, rs, rt 247 | condition: operation == 0x41 && imm_mode == 0 248 | - template: scl rd, rs, imm16 249 | condition: operation == 0x41 && imm_mode == 1 250 | - template: rol rd, rs, rt 251 | condition: operation == 0x42 && imm_mode == 0 252 | - template: rol rd, rs, imm16 253 | condition: operation == 0x42 && imm_mode == 1 254 | - template: rcl rd, rs, rt 255 | condition: operation == 0x44 && imm_mode == 0 256 | - template: rcl rd, rs, imm16 257 | condition: operation == 0x44 && imm_mode == 1 258 | - template: shr rd, rs, rt 259 | condition: operation == 0x48 && imm_mode == 0 260 | - template: shr rd, rs, imm16 261 | condition: operation == 0x48 && imm_mode == 1 262 | - template: scr rd, rs, rt 263 | condition: operation == 0x49 && imm_mode == 0 264 | - template: scr rd, rs, imm16 265 | condition: operation == 0x49 && imm_mode == 1 266 | - template: ror rd, rs, rt 267 | condition: operation == 0x4a && imm_mode == 0 268 | - template: ror rd, rs, imm16 269 | condition: operation == 0x4a && imm_mode == 1 270 | - template: rcr rd, rs, rt 271 | condition: operation == 0x4c && imm_mode == 0 272 | - template: rcr rd, rs, imm16 273 | condition: operation == 0x4c && imm_mode == 1 274 | - template: sar rd, rs, rt 275 | condition: operation == 0x4e && imm_mode == 0 276 | - template: sar rd, rs, imm16 277 | condition: operation == 0x4e && imm_mode == 1 278 | # Conditional ops 279 | - template: movxy_x rd, rs, rt 280 | condition: operation == 0x90 && imm_mode == 0 281 | - template: movxy_x rd, rs, imm16 282 | condition: operation == 0x90 && imm_mode == 1 283 | - template: movxy_y rd, rs, rt 284 | condition: operation == 0x91 && imm_mode == 0 285 | - template: movxy_y rd, rs, imm16 286 | condition: operation == 0x91 && imm_mode == 1 287 | - template: movxy_b rd, rs, rt 288 | condition: operation == 0x92 && imm_mode == 0 289 | - template: movxy_b rd, rs, imm16 290 | condition: operation == 0x92 && imm_mode == 1 291 | - template: movxy_nb rd, rs, rt 292 | condition: operation == 0x93 && imm_mode == 0 293 | - template: movxy_nb rd, rs, imm16 294 | condition: operation == 0x93 && imm_mode == 1 295 | - template: movxy_z rd, rs, rt 296 | condition: operation == 0x94 && imm_mode == 0 297 | - template: movxy_z rd, rs, imm16 298 | condition: operation == 0x94 && imm_mode == 1 299 | - template: movxy_nz rd, rs, rt 300 | condition: operation == 0x95 && imm_mode == 0 301 | - template: movxy_nz rd, rs, imm16 302 | condition: operation == 0x95 && imm_mode == 1 303 | - template: movxy_be rd, rs, rt 304 | condition: operation == 0x96 && imm_mode == 0 305 | - template: movxy_be rd, rs, imm16 306 | condition: operation == 0x96 && imm_mode == 1 307 | - template: movxy_a rd, rs, rt 308 | condition: operation == 0x97 && imm_mode == 0 309 | - template: movxy_a rd, rs, imm16 310 | condition: operation == 0x97 && imm_mode == 1 311 | - template: movxy_l rd, rs, rt 312 | condition: operation == 0x98 && imm_mode == 0 313 | - template: movxy_l rd, rs, imm16 314 | condition: operation == 0x98 && imm_mode == 1 315 | - template: movxy_ge rd, rs, rt 316 | condition: operation == 0x99 && imm_mode == 0 317 | - template: movxy_ge rd, rs, imm16 318 | condition: operation == 0x99 && imm_mode == 1 319 | - template: movxy_le rd, rs, rt 320 | condition: operation == 0x9a && imm_mode == 0 321 | - template: movxy_le rd, rs, imm16 322 | condition: operation == 0x9a && imm_mode == 1 323 | - template: movxy_g rd, rs, rt 324 | condition: operation == 0x9b && imm_mode == 0 325 | - template: movxy_g rd, rs, imm16 326 | condition: operation == 0x9b && imm_mode == 1 327 | - template: movxy_s rd, rs, rt 328 | condition: operation == 0x9c && imm_mode == 0 329 | - template: movxy_s rd, rs, imm16 330 | condition: operation == 0x9c && imm_mode == 1 331 | - template: movxy_ns rd, rs, rt 332 | condition: operation == 0x9e && imm_mode == 0 333 | - template: movxy_ns rd, rs, imm16 334 | condition: operation == 0x9e && imm_mode == 1 335 | # Memory load 336 | ldop: 337 | - template: mov rd, segment:[rs] 338 | condition: load == 1 && operation == 0xde && rt == 0 && offset10 == 0 339 | - template: mov rd, segment:[rs + rt] 340 | condition: load == 1 && operation == 0xde && offset10 == 0 341 | - template: mov rd, segment:[rs + offset10] 342 | condition: load == 1 && operation == 0xde && rt == 0 343 | - template: mov rd, segment:[rs + rt + offset10] 344 | condition: load == 1 && operation == 0xde 345 | # Memory store 346 | stop: 347 | - template: mov segment:[rs], rd 348 | condition: store == 1 && operation == 0xa0 && rt == 0 && offset10 == 0 349 | - template: mov segment:[rs + rt], rd 350 | condition: store == 1 && operation == 0xa0 && offset10 == 0 351 | - template: mov segment:[rs + offset10], rd 352 | condition: store == 1 && operation == 0xa0 && rt == 0 353 | - template: mov segment:[rs + rt + offset10], rd 354 | condition: store == 1 && operation == 0xa0 355 | # Microcode conditional branch 356 | brop: 357 | - template: jmp address13 358 | condition: cond == 1 359 | - template: jb address13 360 | condition: cond == 2 361 | - template: jnb address13 362 | condition: cond == 3 363 | - template: jz address13 364 | condition: cond == 4 365 | - template: jnz address13 366 | condition: cond == 5 367 | - template: je address13 368 | condition: cond == 4 369 | - template: jne address13 370 | condition: cond == 5 371 | - template: jbe address13 372 | condition: cond == 6 373 | - template: ja address13 374 | condition: cond == 7 375 | - template: jl address13 376 | condition: cond == 8 377 | - template: jge address13 378 | condition: cond == 9 379 | - template: jle address13 380 | condition: cond == 10 381 | - template: jg address13 382 | condition: cond == 11 383 | - template: js address13 384 | condition: cond == 12 385 | - template: jns address13 386 | condition: cond == 13 387 | 388 | # Binary object format for Zen microcode updates 389 | object_format: 390 | header: 391 | defaults: 392 | date: 0 393 | revision: 0 394 | format: 0x8004 395 | cpuid: 0 396 | rev: 0 397 | # Taken from Zentool 398 | modulus: "80000000000000000000000000000000AE4634B83805EA28D7ECAC0053A6AB6C0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000013" 399 | check: "17C1987722BF77D849C02D62921546347A747E5F8904FAF9BB1A2F479FC9DB9979435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E50D79435E5" 400 | fields: 401 | - name: date 402 | type: uint32 403 | - name: revision 404 | type: uint32 405 | - name: format 406 | type: uint16 407 | - name: patchlen 408 | type: uint8 409 | - name: init 410 | type: uint8 411 | - name: checksum 412 | type: uint32 413 | - name: nbvid 414 | type: uint16 415 | - name: nbdid 416 | type: uint16 417 | - name: sbvid 418 | type: uint16 419 | - name: sbdid 420 | type: uint16 421 | - name: cpuid 422 | type: uint32 423 | - name: biosrev 424 | type: uint8 425 | - name: flags 426 | type: uint8 427 | - name: reserved 428 | type: uint8 429 | - name: reserved2 430 | type: uint8 431 | - name: signature 432 | type: bytearray 433 | length: 256 434 | - name: modulus 435 | type: bytearray 436 | length: 256 437 | - name: check 438 | type: bytearray 439 | length: 256 440 | - name: autorun 441 | type: uint8 442 | - name: encrypted 443 | type: uint8 444 | - name: unknown1 445 | type: uint8 446 | - name: unknown2 447 | type: uint8 448 | - name: rev 449 | type: uint32 450 | 451 | # Match register encoding 452 | match_registers: 453 | entry_count: 0 454 | regs_per_entry: 2 455 | type: match_t 456 | fields: 457 | m1: 458 | bits: [0, 12] 459 | type: immediate 460 | u1: # match register 1 enable flag 461 | bits: [13, 13] 462 | type: flag 463 | m2: 464 | bits: [14, 26] 465 | type: immediate 466 | u2: # match register 2 enable flag 467 | bits: [27, 27] 468 | type: flag 469 | pad: 470 | bits: [28, 31] 471 | type: reserved 472 | 473 | # Instruction package (quad) specification 474 | packages: 475 | start_address: 0 476 | count: 0 477 | instructions_per_package: 4 478 | instruction_size: 64 # bits 479 | sequence_word_size: 32 # bits 480 | sequence_word_encoding: 481 | continue: 0x00000001 482 | branch: 483 | encoding: 0x00120000 484 | target_address_bits: [0, 12] 485 | complete: 0x03100082 -------------------------------------------------------------------------------- /zenutils/masm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Macro Assembler for Zen architectures with labels, match register handling 4 | and automated placement of instructions. 5 | """ 6 | import sys 7 | import argparse 8 | import re 9 | import struct 10 | # Support both standard imports (when package is installed) and relative imports (when running as script) 11 | try: 12 | from .arch.registry import Registry 13 | from .asm import assemble_single, can_parse_number 14 | except ImportError: 15 | from arch.registry import Registry 16 | from asm import assemble_single, can_parse_number 17 | 18 | class HeaderEmitter: 19 | """Emit object header fields by walking spec.object_format.header.fields.""" 20 | def __init__(self, spec, header_values): 21 | hdr = spec._raw['object_format']['header'] 22 | self.fields = hdr.get('fields', []) 23 | self.defaults = hdr.get('defaults', {}) 24 | self.values = header_values 25 | 26 | def emit(self, out_stream, debug): 27 | if debug: 28 | self.emit_ascii(out_stream) 29 | else: 30 | self.emit_binary(out_stream) 31 | 32 | def emit_ascii(self, out_stream): 33 | out_stream.write(bytes(f"; Header\n", 'ascii')) 34 | 35 | for field in self.fields: 36 | name = field['name'] 37 | ftype = field['type'] 38 | # priority: directive → default → zero 39 | val = self.values.get(name, self.defaults.get(name, 0)) 40 | 41 | if ftype == 'uint32': 42 | out_stream.write(bytes(f".{name} 0x{val:08x}\n", 'ascii')) 43 | elif ftype == 'uint16': 44 | out_stream.write(bytes(f".{name} 0x{val:04x}\n", 'ascii')) 45 | elif ftype == 'uint8': 46 | out_stream.write(bytes(f".{name} 0x{val:02x}\n", 'ascii')) 47 | elif ftype in ('bytes', 'bytearray'): 48 | val = self.values.get(name, self.defaults.get(name, "")) 49 | length = field['length'] 50 | arr = val if isinstance(val, (bytes, bytearray)) else bytes.fromhex(val) 51 | if len(arr) == 0: 52 | arr = bytes([0] * length) 53 | if len(arr) != length: 54 | raise ValueError(f"Length of bytes field {name} is {len(arr)}, expected {length}.") 55 | hex = ' '.join([f"{b:02x}" for b in arr]) 56 | out_stream.write(bytes(f".{name} {hex}\n", 'ascii')) 57 | else: 58 | raise NotImplementedError(f"Unsupported header field type: {ftype}") 59 | 60 | def emit_binary(self, out_stream): 61 | for field in self.fields: 62 | name = field['name'] 63 | ftype = field['type'] 64 | # priority: directive → default → zero 65 | val = self.values.get(name, self.defaults.get(name, 0)) 66 | 67 | if ftype == 'uint32': 68 | out_stream.write(struct.pack('\w+)(?:\s+(?P