#!/usr/bin/python3 # SPDX-License-Identifier: MIT License # Copyright (C) 2020 Advanced Micro Devices, Inc. """ Parse an amd-ucode container file and print the family, model, stepping number, and patch level for each patch in the file. The --extract option will dump the raw microcode patches to a provided directory. """ import argparse import sys import os from collections import namedtuple from collections import OrderedDict EQ_TABLE_ENTRY_SIZE = 16 EQ_TABLE_LEN_OFFSET = 8 EQ_TABLE_OFFSET = 12 EQ_TABLE_TYPE = 0 PATCH_TYPE = 1 VERBOSE_DEBUG = 2 FMS = namedtuple("FMS", ("family", "model", "stepping")) EquivTableEntry = namedtuple("EquivTableEntry", ("cpuid", "equiv_id", "data", "offset")) PatchEntry = namedtuple("PatchEntry", ("file", "offset", "size", "equiv_id", "level")) def read_int32(ucode_file): """ Read four bytes of binary data and return as a 32 bit int """ return int.from_bytes(ucode_file.read(4), 'little') def read_int16(ucode_file): """ Read two bytes of binary data and return as a 16 bit int """ return int.from_bytes(ucode_file.read(2), 'little') def read_int8(ucode_file): """ Read one byte of binary data and return as a 8 bit int """ return int.from_bytes(ucode_file.read(1), 'little') def cpuid2fms(cpu_id): family = (cpu_id >> 8) & 0xf family += (cpu_id >> 20) & 0xff model = (cpu_id >> 4) & 0xf model |= (cpu_id >> 12) & 0xf0 stepping = cpu_id & 0xf return FMS(family, model, stepping) def fms2str(fms): return "Family=%#04x Model=%#04x Stepping=%#04x" % \ (fms.family, fms.model, fms.stepping) def parse_equiv_table(opts, ucode_file, start_offset, eq_table_len): """ Read equivalence table and return a list of the equivalence ids contained """ table = {} raw_table = [] # For sanity check only cpuid_map = {} table_item = start_offset + EQ_TABLE_OFFSET table_stop = start_offset + EQ_TABLE_OFFSET + eq_table_len while table_item < table_stop: ucode_file.seek(table_item, 0) data = ucode_file.read(EQ_TABLE_ENTRY_SIZE) ucode_file.seek(table_item, 0) cpu_id = read_int32(ucode_file) if opts.verbose >= VERBOSE_DEBUG: errata_mask = read_int32(ucode_file) errata_compare = read_int32(ucode_file) else: # Skip errata mask and compare fields ucode_file.seek(8, 1) equiv_id = read_int16(ucode_file) if opts.verbose >= VERBOSE_DEBUG: res = read_int16(ucode_file) if equiv_id != 0: if equiv_id not in table: table[equiv_id] = OrderedDict() if cpu_id in table[equiv_id]: print("WARNING: Duplicate CPUID %#010x (%s) in the equivalence table for equiv_id %#06x " % (fms2str(cpuid2fms(cpu_id)), equiv_id)) if cpu_id in cpuid_map: if equiv_id != cpuid_map[cpu_id]: print("WARNING: Different equiv_id's (%#06x and %#06x) are present in the equivalence table for CPUID %#010x (%s)" % (equiv_id, cpuid_map[cpu_id], cpu_id, fms2str(cpuid2fms(cpu_id)))) else: cpuid_map[cpu_id] = equiv_id entry = EquivTableEntry(cpu_id, equiv_id, data, table_item) table[equiv_id][cpu_id] = entry raw_table.append(entry) if opts.verbose >= VERBOSE_DEBUG: print(" [equiv entry@%#010x: cpuid %#010x, equiv id %#06x, errata mask %#010x, errata compare %#010x, res %#06x]" % (table_item, cpu_id, equiv_id, errata_mask, errata_compare, res)) table_item += EQ_TABLE_ENTRY_SIZE return (table, raw_table) def extract_patch(opts, out_dir, ucode_file, patch, equiv_table=None): """ Extract raw microcode patch starting at patch_start to the directory provided by the -o option or the current directory if not specified. Directory will be created if it doesn't already exist. """ cwd = os.getcwd() if not os.path.exists(out_dir): os.makedirs(out_dir) os.chdir(out_dir) if equiv_table is None: # Raw patch out_file_name = "mc_patch_0%x.bin" % patch.level else: out_file_name = "mc_equivid_%#06x" % patch.equiv_id for cpuid in equiv_table[patch.equiv_id]: out_file_name += '_cpuid_%#010x' % cpuid out_file_name += "_patch_%#010x.bin" % patch.level out_path = "%s/%s" % (os.getcwd(), out_file_name) out_file = open(out_file_name, "wb") os.chdir(cwd) if equiv_table is not None: cpuids = equiv_table[patch.equiv_id].values() if patch.equiv_id in equiv_table else [] else: cpuids = None write_mc(opts, out_file, [patch], ucode_file, cpuids) out_file.close() print(" Patch extracted to %s" % out_path) def merge_mc(opts, out_path, table, patches): # Do some sanity checks, ut only warn about the issues equivid_map = {} cpuid_map = {} for entry in table: if entry.equiv_id not in equivid_map: equivid_map[entry.equiv_id] = dict() if entry.cpuid in equivid_map[entry.equiv_id]: print("WARNING: Duplicate CPUID %#010x (%s) in the equivalence table for equiv_id %#06x " % (fms2str(cpuid2fms(entry.cpuid)), entry.equiv_id)) else: equivid_map[entry.equiv_id][entry.cpuid] = entry if entry.cpuid in cpuid_map: if entry.equiv_id != cpuid_map[entry.cpuid]: print("WARNING: Different equiv_id's (%#06x and %#06x) are present in the equivalence table for CPUID %#010x (%s)" % (entry.equiv_id, cpuid_map[entry.cpuid], entry.cpuid, fms2str(cpuid2fms(entry.cpuid)))) else: cpuid_map[entry.cpuid] = entry.equiv_id with open(out_path, "wb") as out_file: write_mc(opts, out_file, patches, equiv_table=table) print("Microcode written to %s" % out_path) def write_mc(opts, out_file, patches, ucode_file=None, equiv_table=None): """ Writes microcode data to the specified file. """ if equiv_table is not None: # Container header out_file.write(b'DMA\x00') # Equivalence table header out_file.write(EQ_TABLE_TYPE.to_bytes(4, 'little')) table_size = EQ_TABLE_ENTRY_SIZE * (len(equiv_table) + 1) out_file.write(table_size.to_bytes(4, 'little')) # Equivalence table for cpuid in equiv_table: out_file.write(cpuid.data) out_file.write(b'\0' * EQ_TABLE_ENTRY_SIZE) for patch in patches: # Patch header if equiv_table is not None: out_file.write(PATCH_TYPE.to_bytes(4, 'little')) out_file.write(patch.size.to_bytes(4, 'little')) if ucode_file is None: in_file = open(patch.file, "rb") else: in_file = ucode_file in_file.seek(patch.offset, 0) out_file.write(in_file.read(patch.size)) if ucode_file is None: in_file.close() def parse_ucode_file(opts, path, start_offset): """ Scan through microcode container file printing the microcode patch level for each model contained in the file. """ table = None patches = [] with open(path, "rb") as ucode_file: print("Microcode patches in %s%s:" % (path, "+%#x" % start_offset if start_offset else "")) # Seek to end of file to determine file size ucode_file.seek(0, 2) end_of_file = ucode_file.tell() # Check magic number ucode_file.seek(start_offset, 0) if ucode_file.read(4) != b'DMA\x00': print("ERROR: Missing magic number at beginning of container") return (None, None, None) # Check the equivalence table type eq_table_type = read_int32(ucode_file) if eq_table_type != EQ_TABLE_TYPE: print("ERROR: Invalid equivalence table identifier: %#010x" % eq_table_type) return (None, None, None) # Read the equivalence table length eq_table_len = read_int32(ucode_file) ids, table = parse_equiv_table(opts, ucode_file, start_offset, eq_table_len) cursor = start_offset + EQ_TABLE_OFFSET + eq_table_len while cursor < end_of_file: # Seek to the start of the patch information ucode_file.seek(cursor, 0) patch_start = cursor + 8 patch_type_bytes = ucode_file.read(4) # Beginning of a new container if patch_type_bytes == b'DMA\x00': return (cursor, table, patches) patch_type = int.from_bytes(patch_type_bytes, 'little') if patch_type != PATCH_TYPE: print("Invalid patch identifier: %#010x" % (patch_type)) return (None, table, patches) patch_length = read_int32(ucode_file) if opts.verbose: data_code = read_int32(ucode_file) else: ucode_file.seek(4, 1) ucode_level = read_int32(ucode_file) if opts.verbose >= VERBOSE_DEBUG: mc_patch_data_id = read_int16(ucode_file) mc_patch_data_len = read_int8(ucode_file) init_flag = read_int8(ucode_file) mc_patch_data_checksum = read_int32(ucode_file) nb_dev_id = read_int32(ucode_file) sb_dev_id = read_int32(ucode_file) else: ucode_file.seek(16, 1) equiv_id = read_int16(ucode_file) if opts.verbose >= VERBOSE_DEBUG: nb_rev_id = read_int8(ucode_file) sb_rev_id = read_int8(ucode_file) bios_api_rev = read_int8(ucode_file) reserved1 = [read_int8(ucode_file) for _ in range(3)] match_reg = [read_int32(ucode_file) for _ in range(8)] if opts.verbose: add_info = " Start=%u bytes Date=%04x-%02x-%02x Equiv_id=%#06x" % \ (patch_start, data_code & 0xffff, data_code >> 24, (data_code >> 16) & 0xff, equiv_id) else: add_info = "" if equiv_id not in ids: print("Patch equivalence id not present in equivalence table (%#06x)" % (equiv_id)) print(" Family=???? Model=???? Stepping=????: Patch=%#010x Length=%u bytes%s" % (ucode_level, patch_length, add_info)) # The cpu_id is the equivalent to CPUID_Fn00000001_EAX for cpuid in ids[equiv_id]: print(" %s: Patch=%#010x Length=%u bytes%s" % (fms2str(cpuid2fms(cpuid)), ucode_level, patch_length, add_info)) if opts.verbose >= VERBOSE_DEBUG: print(" [data_code=%#010x, mc_patch_data_id=%#06x, mc_patch_data_len=%#04x, init_flag=%#04x, mc_patch_data_checksum=%#010x]" % (data_code, mc_patch_data_id, mc_patch_data_len, init_flag, mc_patch_data_checksum)) print(" [nb_dev_id=%#010x, sb_dev_id=%#010x, nb_rev_id=%#04x, sb_rev_id=%#04x, bios_api_rev=%#04x, reserved=[%#04x, %#04x, %#04x]]" % (nb_dev_id, sb_dev_id, nb_rev_id, sb_rev_id, bios_api_rev, reserved1[0], reserved1[1], reserved1[2])) patch = PatchEntry(path, patch_start, patch_length, equiv_id, ucode_level) patches.append(patch) if opts.extract: extract_patch(opts, opts.extract, ucode_file, patch) if opts.split: extract_patch(opts, opts.split, ucode_file, patch, ids) cursor = cursor + patch_length + 8 return (None, table, patches) def parse_ucode_files(opts): all_tables = [] all_patches = [] for f in opts.container_file: offset = 0 while offset is not None: offset, table, patches = parse_ucode_file(opts, f, offset) if opts.merge: if table is not None: all_tables += table if patches is not None: all_patches += patches if opts.merge: merge_mc(opts, opts.merge, all_tables, all_patches) def parse_options(): """ Parse options """ parser = argparse.ArgumentParser(description="Print information about an amd-ucode container") parser.add_argument("container_file", nargs='+') parser.add_argument("-e", "--extract", help="Dump each patch in container to the specified directory") parser.add_argument("-s", "--split", help="Split out each patch in a separate container to the specified directory") parser.add_argument("-m", "--merge", help="Write a merged container to the specified file") parser.add_argument("-v", "--verbose", action="count", default=0, help="Be verbose about the information in the container file") opts = parser.parse_args() for f in opts.container_file: if not os.path.isfile(f): parser.print_help() print() print("ERROR: Container file \"%s\" does not exist" % f) sys.exit() return opts def main(): """ main """ opts = parse_options() parse_ucode_files(opts) if __name__ == "__main__": main()