From 4330ff4351d2df7feb2a0f025596148e125a9aba Mon Sep 17 00:00:00 2001 From: Mark Langen Date: Tue, 20 May 2025 19:42:51 -0700 Subject: [PATCH 1/3] Implement Ghidra Importer * Imports large amounts of function, variable, and structure information into a Ghidra project for the game. * General approach: Configure with --debug to include full symbols information, run a build, and extract the DWARF debug information from the build to get information about functions / structs. * If a compilation unit has debugging information present in the build executable (because it is marked as Matching in configure.py and gets linked), then full parameter name, return type, global variable, etc. information is imported for that compilation unit. * All the structs, enums, and unions refenced in these Matching compilation units get imported. Even with only a small subset of the CIs matching, any given CI will generally reference a significant subset of the total struct types in the game, so we still get good coverage of the total set of structs. * If a compilation unit is not marked as Matching, we fall back to demangling the symbol names in symbols.txt, and using that to generate function name and parameter type information to import. * If a symbol in symbols.txt cannot be demangled, then we finally fall back to simply importing a label for the address in question. * TODO: Function types for callback parameters. Bitfields in structs. --- configure.py | 3 +- ghidra_scripts/README.md | 67 +++ ghidra_scripts/bfbb_import.py | 7 + ghidra_scripts/gimport/__init__.py | 0 ghidra_scripts/gimport/demangle.py | 207 ++++++++ ghidra_scripts/gimport/dwarf.py | 321 ++++++++++++ ghidra_scripts/gimport/dwarfone.py | 464 ++++++++++++++++++ ghidra_scripts/gimport/extract_info.py | 439 +++++++++++++++++ .../gimport/function_with_paramn.png | Bin 0 -> 10335 bytes .../gimport/function_with_return.png | Bin 0 -> 3538 bytes ghidra_scripts/gimport/gtypes.py | 105 ++++ ghidra_scripts/gimport/import_info.py | 231 +++++++++ .../gimport/manage_script_directories.png | Bin 0 -> 16366 bytes ghidra_scripts/gimport/struct_import.png | Bin 0 -> 9358 bytes 14 files changed, 1843 insertions(+), 1 deletion(-) create mode 100644 ghidra_scripts/README.md create mode 100644 ghidra_scripts/bfbb_import.py create mode 100644 ghidra_scripts/gimport/__init__.py create mode 100644 ghidra_scripts/gimport/demangle.py create mode 100644 ghidra_scripts/gimport/dwarf.py create mode 100644 ghidra_scripts/gimport/dwarfone.py create mode 100644 ghidra_scripts/gimport/extract_info.py create mode 100644 ghidra_scripts/gimport/function_with_paramn.png create mode 100644 ghidra_scripts/gimport/function_with_return.png create mode 100644 ghidra_scripts/gimport/gtypes.py create mode 100644 ghidra_scripts/gimport/import_info.py create mode 100644 ghidra_scripts/gimport/manage_script_directories.png create mode 100644 ghidra_scripts/gimport/struct_import.png diff --git a/configure.py b/configure.py index f3f11d844..a260f713e 100644 --- a/configure.py +++ b/configure.py @@ -167,6 +167,7 @@ ] if args.debug: config.ldflags.append("-g") # Or -gdwarf-2 for Wii linkers + config.ldflags.append("-sym full") if args.map: config.ldflags.append("-mapunused") # config.ldflags.append("-listclosure") # For Wii linkers @@ -208,7 +209,7 @@ # Debug flags if args.debug: # Or -sym dwarf-2 for Wii compilers - cflags_base.extend(["-sym on", "-DDEBUG=1"]) + cflags_base.extend(["-sym full", "-DDEBUG=1"]) else: cflags_base.append("-DNDEBUG=1") diff --git a/ghidra_scripts/README.md b/ghidra_scripts/README.md new file mode 100644 index 000000000..69c0bfe81 --- /dev/null +++ b/ghidra_scripts/README.md @@ -0,0 +1,67 @@ +# Ghidra Importer + +## What do you get from using the importer? + +`bfbb_import` is a script which take basic symbols from the original game (in symbols.txt), and more detailed symbols from the reverse engineered code we can compile so far, and imports them into a Ghidra for easier reverse engineering. + +Results of running the import: + +* Full parameter type, return type information, parameter names, global variable types etc are imported for the contents of cpp files listed as `Matching` in `configure.py`: + +* ![test](gimport/function_with_return.png) + +* All struct types referenced in `Matching` files are imported: + +* ![test](gimport/struct_import.png) + +* Name and parameter types but _not_ return types are imported for other name mangled functions in `symbols.txt`: + +* ![test](gimport/function_with_paramn.png) + +* All other remaining symbols from `symbols.txt` are annotated in some way in the main Ghidra listing via labels. + +## Import Instructions + +### Step 1: Install Ghidra + +Download and "install" a recent version of Ghidra from https://github.com/NationalSecurityAgency/ghidra/releases. "Install" here just means unzipping the folder, there is no global install process for Ghidra. + +Note: You may need to install the JDK if you don't have it already. You will be prompted for this when running Ghidra if you don't have it. + +### Step 2: Install the DOL Extension + +Ghidra can't understand Gamecube DOL files out of the box. Install the Ghidra Gamecube loader from https://github.com/Cuyler36/Ghidra-GameCube-Loader/releases. + +### Step 3: Import the DOL + +Open Ghidra and `File > Import File...`, selecting the DOL file you put in `bfbb/orig/GQPE78/sys/main.dol` when setting up the repo. + +Open up the imported file and ***allow analysis to run when prompted***. This importer script expects the functions to already be created by analysis. + +### Step 4: Install Ghidrathon + +We need to give Ghidra the ability to run Python 3 code, we do this with the Ghidrathon extension. Download Ghidrathon from the releases page: https://github.com/mandiant/Ghidrathon/releases + +Follow the installation instructions on that page. You probably don't need to create a venv in this case, but you do need to run `ghidrathon_configure.py`. + +### Step 5: Add Script Directory + +In Ghidra, `Window > Script Manager` to open the script manager. This is what we ill use to run the script. + +In the script manager, at the top right, click the "Manage Script Directories" button: ![image](manage_script_directories.png) + +Click `+` at the top right of the script manager, and add `bfbb/ghidra_scripts` to the list of script directories. + +### Step 6: Run the Importer + +In the Script Manager, you should now be able to filter for `bfbb_import.py`. Select it and run it through the context menu or the run button at the top of the Script Manager. + +Importing will take as long as a clean build does because we temporarily have to make a debug build of the executable to get the parameter names and other info from already reverse engineered functions (the script will restore your previous build settings after doing so) + +### Step 7: Enjoy The Results + +Most functions should now have name / parameter info rather than just being FUN_xxxxxxxx. No more having to look stuff up in symbols.txt! + + diff --git a/ghidra_scripts/bfbb_import.py b/ghidra_scripts/bfbb_import.py new file mode 100644 index 000000000..2662236b4 --- /dev/null +++ b/ghidra_scripts/bfbb_import.py @@ -0,0 +1,7 @@ +import gimport.extract_info +import gimport.import_info + +if __name__ == "__main__": + extracted_info = gimport.extract_info.extract_info() + print("Importing info into Ghidra") + gimport.import_info.import_info(currentProgram(), extracted_info) diff --git a/ghidra_scripts/gimport/__init__.py b/ghidra_scripts/gimport/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/ghidra_scripts/gimport/demangle.py b/ghidra_scripts/gimport/demangle.py new file mode 100644 index 000000000..c4fea4c46 --- /dev/null +++ b/ghidra_scripts/gimport/demangle.py @@ -0,0 +1,207 @@ +from typing import Tuple, List +import re +from .dwarf import DW_FT, DwarfSubscriptDataItem +from .gtypes import GType, GPointerType, GFundType, GArrayType + + +SPECIAL_NAME_TO_OPERATOR = { + "__as": "=", + "__ml": "*", + "__amu": "*=", + "__mi": "-", + "__ami": "-=", + "__dv": "/", + "__adv": "/=", + "__pl": "+", + "__apl": "+=", + "__nw": "new", + "__dl": "delete", + "__aor": "|=", + "__or": "|", + "__eq": "==", + "__ne": "!=", + "__vc": "<<", + "__mm": "--", + "__pp": "++", + "__rf": "*", + "__cl": "()", +} + + +SPECIAL_IGNORE = { + # Things containing a "T#", don't know what that means + "setevenodd__FUlPUlUlUlP5BLITST1": True, + "YUV_blit__FPvUlUlUlT0UlUlUlUlUlUlUlT0P5BLITS": True, + "YUV_blit_mask__FPvUlUlUlPUcUlT0UlUlUlUlUlUlUlT0P5BLITS": True, + + # Can't disambiguate with normal mangling + "__end__catch": True, +} + + +def demangle(mangled_name: str, resolve_ud) -> Tuple[str, List[GType]]: + # Cut off name + index = mangled_name.find("__", 1) # 1 instead of 0 to skip __ in names like __ct + if index == -1: + # Not a mangled function + return None + # Don't know how to demangle some things + if mangled_name in SPECIAL_IGNORE: + return None + name = mangled_name[:index] # Name part only + without_name = mangled_name[index+2:] # Cut off name + + # Cut off namespacing bits + namespaces = [] + while len(without_name) > 0 and (without_name[0] == "Q" or str.isdigit(without_name[0])): + if without_name[0] == "Q": + qualification_count = int(without_name[1]) + without_name = without_name[2:] + for i in range(qualification_count): + (namespace_len_text, rest) = re.match(r"^(\d+)(.*)", without_name).groups() + namespace_len = int(namespace_len_text) + namespaces.append(rest[:namespace_len]) + without_name = rest[namespace_len:] + else: + (len_str, rest) = re.match(r"^(\d+)(.*)", without_name).groups() + namespace_len = int(len_str) + namespaces.append(rest[:namespace_len]) + without_name = rest[namespace_len:] + this_type = resolve_ud(namespaces[-1]) if namespaces else None + + # Namespaced global variable, not a function + if len(without_name) == 0: + return None + + # Handle special names + if name.startswith("__"): + if name in SPECIAL_NAME_TO_OPERATOR: + name = f"operator{SPECIAL_NAME_TO_OPERATOR[name]}" + elif name == "__ct": + name = namespaces[-1] + elif name == "__dt": + name = f"~{namespaces[-1]}" + + # Add namespaces to name + name = "::".join(namespaces + [name]) + + # C -> Const method. + is_const = without_name[0] == "C" + if is_const: + without_name = without_name[1:] + + # F -> function, no F -> method. + is_member = without_name[0] != "F" + whole_text = without_name if is_member else without_name[1:] + + # Easier to handle this here + if whole_text == "v": + return (name, []) + + """ + Ann_ Array + P pointer + C constant + Qn qualified name, n parts + + b bool + c char + s short + i int + l long + x long long + f float + d double + e vararg + nn struct + """ + def parse_type(text: str) -> Tuple[GType, str]: + if text.startswith("A"): + (dim, rest) = re.match(r"^A([0-9]+)_(.*)", text).groups() + (type, rest) = parse_type(rest) + array_type = GArrayType() + count = DwarfSubscriptDataItem() + count.highBound.isConstant = True + count.highBound.constant = int(dim) + 1 + element_type = DwarfSubscriptDataItem() + element_type.type = type + array_type.subscripts = [count, element_type] + return (array_type, rest) + elif text.startswith("F"): + text = text[1:] + if text.startswith("v"): + text = text[1:] + else: + while text and not text.startswith("_"): + (param_type, text) = parse_type(text) + assert text.startswith("_"), f"Expect _ after function type in {mangled_name}" + # TODO: Actually handle function type + return (GPointerType(GFundType(DW_FT.void)), text[1:]) + elif text.startswith("Q"): + qualification_count = int(text[1]) + text = text[2:] + parts = [] + for i in range(qualification_count): + (namespace_len_text, rest) = re.match(r"^(\d+)(.*)", text).groups() + namespace_len = int(namespace_len_text) + parts.append(rest[:namespace_len]) + text = rest[namespace_len:] + return (resolve_ud(parts[-1]), text) + elif text.startswith("Pv"): + # Pointer to void is special + return (GPointerType(GFundType(DW_FT.void)), text[2:]) + elif text.startswith("PCv"): + return (GPointerType(GFundType(DW_FT.void)), text[3:]) + elif text.startswith("P") or text.startswith("R"): + (type, rest) = parse_type(text[1:]) + pointer_type = GPointerType(type) + return (pointer_type, rest) + elif text.startswith("C"): + # Constness ignored here + return parse_type(text[1:]) + elif text.startswith("b"): + return (GFundType(DW_FT.bool), text[1:]) + elif text.startswith("c"): + return (GFundType(DW_FT.S8), text[1:]) + elif text.startswith("s"): + return (GFundType(DW_FT.S16), text[1:]) + elif text.startswith("i"): + return (GFundType(DW_FT.S32), text[1:]) + elif text.startswith("l"): + return (GFundType(DW_FT.SLong), text[1:]) + elif text.startswith("x"): + return (GFundType(DW_FT.S64), text[1:]) + elif text.startswith("f"): + return (GFundType(DW_FT.F32), text[1:]) + elif text.startswith("d"): + return (GFundType(DW_FT.F64), text[1:]) + elif text.startswith("Uc"): + return (GFundType(DW_FT.U8), text[2:]) + elif text.startswith("Us"): + return (GFundType(DW_FT.U16), text[2:]) + elif text.startswith("Ui"): + return (GFundType(DW_FT.U32), text[2:]) + elif text.startswith("Ul"): + return (GFundType(DW_FT.ULong), text[2:]) + else: + # Handle struct + if match := re.match(r"^(\d+)(.*)", text): + (ident_len_text, rest) = match.groups() + ident_len = int(ident_len_text) + ident = rest[:ident_len] + rest = rest[ident_len:] + return (resolve_ud(ident), rest) + else: + print("Unexpected mangle:", text, mangled_name) + exit(0) + + result = [] + if this_type: + result.append(GPointerType(this_type)) + while whole_text: + # End of empty arg list, or variable args + if whole_text.startswith("v") or whole_text.startswith("e"): + return (name, result) + (type, whole_text) = parse_type(whole_text) + result.append(type) + return (name, result) diff --git a/ghidra_scripts/gimport/dwarf.py b/ghidra_scripts/gimport/dwarf.py new file mode 100644 index 000000000..2a9b4f55b --- /dev/null +++ b/ghidra_scripts/gimport/dwarf.py @@ -0,0 +1,321 @@ + +from typing import List + + +class DW_FT: + bool = 0 + char = 1 + S8 = 2 + U8 = 3 + S16 = 5 + U16 = 6 + S32 = 7 + U32 = 9 + SLong = 10 + ULong = 12 + pointer = 13 + F32 = 14 + F64 = 15 + void = 20 + S64 = 32776 + + +class DW_FORM: + ADDR = 0x1 + REF = 0x2 + BLOCK2 = 0x3 + BLOCK4 = 0x4 + DATA2 = 0x5 + DATA4 = 0x6 + DATA8 = 0x7 + STRING = 0x8 + + +class DW_AT: + bit_offset = "DW_AT_bit_offset" + bit_size = "DW_AT_bit_size" + byte_size = "DW_AT_byte_size" + common_reference = "DW_AT_common_reference" + comp_dir = "DW_AT_comp_dir" + const_value = "DW_AT_const_value" + containing_type = "DW_AT_containing_type" + default_value = "DW_AT_default_value" + discr = "DW_AT_discr" + discr_value = "DW_AT_discr_value" + element_list = "DW_AT_element_list" + friends = "DW_AT_friends" + fund_type = "DW_AT_fund_type" + high_pc = "DW_AT_high_pc" + inline = "DW_AT_inline" + is_optional = "DW_AT_is_optional" + language = "DW_AT_language" + location = "DW_AT_location" + low_pc = "DW_AT_low_pc" + lower_bound = "DW_AT_lower_bound" + member = "DW_AT_member" + mod_fund_type = "DW_AT_mod_fund_type" + mod_u_d_type = "DW_AT_mod_u_d_type" + name = "DW_AT_name" + ordering = "DW_AT_ordering" + private = "DW_AT_private" + producer = "DW_AT_producer" + program = "DW_AT_program" + protected = "DW_AT_protected" + prototyped = "DW_AT_prototyped" + public = "DW_AT_public" + pure_virtual = "DW_AT_pure_virtual" + return_addr = "DW_AT_return_addr" + sibling = "DW_AT_sibling" + specification = "DW_AT_specification" + start_scope = "DW_AT_start_scope" + stride_size = "DW_AT_stride_size" + string_length = "DW_AT_string_length" + stmt_list = "DW_AT_stmt_list" + subscr_data = "DW_AT_subscr_data" + upper_bound = "DW_AT_upper_bound" + user_def_type = "DW_AT_user_def_type" + virtual = "DW_AT_virtual" + + mangled_name = "DW_AT_user_0x200" + global_ref = "DW_AT_user_0x202" + + DW_AT_fund_type = (0x0050 | DW_FORM.DATA2) + DW_AT_mod_fund_type = (0x0060 | DW_FORM.BLOCK2) + DW_AT_user_def_type = (0x0070 | DW_FORM.REF) + DW_AT_mod_u_d_type = (0x0080 | DW_FORM.BLOCK2) + + +class DW_TAG: + array_type = "DW_TAG_array_type" + class_type = "DW_TAG_class_type" + common_block = "DW_TAG_common_block" + common_inclusion = "DW_TAG_common_inclusion" + compile_unit = "DW_TAG_compile_unit" + entry_point = "DW_TAG_entry_point" + enumeration_type = "DW_TAG_enumeration_type" + formal_parameter = "DW_TAG_formal_parameter" + global_subroutine = "DW_TAG_global_subroutine" + global_variable = "DW_TAG_global_variable" + inheritance = "DW_TAG_inheritance" + inlined_subroutine = "DW_TAG_inlined_subroutine" + label = "DW_TAG_label" + lexical_block = "DW_TAG_lexical_block" + local_variable = "DW_TAG_local_variable" + member = "DW_TAG_member" + module = "DW_TAG_module" + padding = "DW_TAG_padding" + pointer_type = "DW_TAG_pointer_type" + ptr_to_member_type = "DW_TAG_ptr_to_member_type" + reference_type = "DW_TAG_reference_type" + set_type = "DW_TAG_set_type" + source_file = "DW_TAG_source_file" + string_type = "DW_TAG_string_type" + structure_type = "DW_TAG_structure_type" + subrange_type = "DW_TAG_subrange_type" + subroutine = "DW_TAG_subroutine" + subroutine_type = "DW_TAG_subroutine_type" + typedef = "DW_TAG_typedef" + union_type = "DW_TAG_union_type" + unspecified_parameters = "DW_TAG_unspecified_parameters" + variant = "DW_TAG_variant" + with_stmt = "DW_TAG_with_stmt" + + +class DW_FMT: + ET = 0x8 + + +class DW_OP: + REG = 0x01 + BASEREG = 0x02 + ADDR = 0x03 + CONST = 0x04 + DEREF2 = 0x05 + DEREF = 0x06 + DEREF4 = 0x06 + ADD = 0x07 + lo_user = 0xe0 + hi_user = 0xff + + +class DW_MOD: + pointer_to = 0x01 + reference_to = 0x02 + const = 0x03 + volatile = 0x04 + lo_user = 0x80 + hi_user = 0xff + + +class DwarfAttribute: + def __init__(self): + self.name: int = 0 + self.value: int | List[int] = None + + def parse(self, data: List[int], index: int) -> int: + self.name = int.from_bytes(data[index:index+2], byteorder='big') + index += 2 + at_form = self.name & 0xF + if at_form == DW_FORM.DATA2: + self.value = int.from_bytes(data[index:index+2], byteorder='big') + index += 2 + elif at_form == DW_FORM.REF: + self.value = int.from_bytes(data[index:index+4], byteorder='big') + index += 4 + elif at_form == DW_FORM.BLOCK2: + block_len = int.from_bytes(data[index:index+2], byteorder='big') + index += 2 + self.value = data[index:index+block_len] + index += block_len + else: + print("Unhandled form:", at_form) + exit(0) + return index + + +class DwarfLocationAtom: + def __init__(self, op, value): + self.op = op + self.value = value + + +class DwarfLocation: + def __init__(self): + self.atoms: List[DwarfLocationAtom] = [] + + def parse(self, data: List[int], index: int) -> int: + while index < len(data): + op = data[index] + index += 1 + value = int.from_bytes(data[index:index+4], byteorder='big') + self.atoms.append(DwarfLocationAtom(op, value)) + index += 4 + + def __str__(self): + parts = [] + for atom in self.atoms: + if atom.op == DW_OP.REG: + parts.append(f"REG({atom.value})") + elif atom.op == DW_OP.BASEREG: + parts.append(f"BASEREG({atom.value})") + elif atom.op == DW_OP.ADDR: + parts.append(f"ADDR({atom.value})") + elif atom.op == DW_OP.CONST: + parts.append(f"CONST({atom.value})") + elif atom.op == DW_OP.DEREF2: + parts.append("DEREF2") + elif atom.op == DW_OP.DEREF4: + parts.append("DEREF4") + elif atom.op == DW_OP.ADD: + parts.append("ADD") + else: + parts.append(f"UNKNOWN({atom.op})") + return f"Loc({",".join(parts)})" + + +class DwarfSubscriptDataBound: + def __init__(self): + self.isConstant = False + self.constant = 0 + self.location: DwarfLocation = None + + def __str__(self): + if self.isConstant: + return f"{self.constant}" + else: + return f"{self.location}" + + +class DwarfType: + def __init__(self): + self.isFundamental = False + self.fundType = 0 + self.udOffset = 0 + self.modifiers = [] + + def parse(self, attr: DwarfAttribute): + assert attr.name in [ + DW_AT.DW_AT_fund_type, + DW_AT.DW_AT_user_def_type, + DW_AT.DW_AT_mod_fund_type, + DW_AT.DW_AT_mod_u_d_type + ], "Unexpected attribute name" + + if attr.name == DW_AT.DW_AT_fund_type: + self.isFundamental = True + self.fundType = attr.value + + elif attr.name == DW_AT.DW_AT_user_def_type: + self.isFundamental = False + self.udOffset = attr.value + + elif attr.name == DW_AT.DW_AT_mod_fund_type: + self.isFundamental = True + assert isinstance(attr.value, list) and len(attr.value) >= 2, "Invalid block length for mod_fund_type" + + data = attr.value + type_data = data[:-2] + + for modifier in type_data: + self.modifiers.append(modifier) + + self.fundType = int.from_bytes(data[-2:], byteorder='big') + + elif attr.name == DW_AT.DW_AT_mod_u_d_type: + self.isFundamental = False + assert isinstance(attr.value, list) and len(attr.value) >= 4, "Invalid block length for mod_u_d_type" + + data = attr.value + type_data = data[:-4] + + for modifier in type_data: + self.modifiers.append(modifier) + + self.udOffset = int.from_bytes(data[-4:], byteorder='big') + + +class DwarfSubscriptDataItem: + def __init__(self): + self.resolved_type = None + self.dwarf_type = DwarfType() + self.lowBound = DwarfSubscriptDataBound() + self.highBound = DwarfSubscriptDataBound() + + def __str__(self): + return f"SubscriptDataItem({self.type}[{self.lowBound}-{self.highBound}])" + + +def DW_FT_to_string(type): + if type == DW_FT.bool: + return "bool" + elif type == DW_FT.char: + # char in spec, alias to S8 in our codebase + return "S8" + elif type == DW_FT.S8: + return "S8" + elif type == DW_FT.U8: + return "U8" + elif type == DW_FT.S16: + return "S16" + elif type == DW_FT.U16: + return "U16" + elif type == DW_FT.S32: + return "S32" + elif type == DW_FT.U32: + return "U32" + elif type == DW_FT.SLong: + return "SLong" + elif type == DW_FT.ULong: + return "ULong" + elif type == DW_FT.pointer: + return "void*" + elif type == DW_FT.F32: + return "F32" + elif type == DW_FT.F64: + return "F64" + elif type == DW_FT.void: + return "void" + elif type == DW_FT.S64: + return "S64" + + return None diff --git a/ghidra_scripts/gimport/dwarfone.py b/ghidra_scripts/gimport/dwarfone.py new file mode 100644 index 000000000..309d9fb82 --- /dev/null +++ b/ghidra_scripts/gimport/dwarfone.py @@ -0,0 +1,464 @@ +# Taken from DWEX https://github.com/sevaa/dwex and modified + +# Support for DWARF v1.1 in a way that will be more or less compatible with pyelftools + +from io import BytesIO +from collections import OrderedDict, namedtuple +from bisect import bisect_left +from elftools.dwarf.dwarfinfo import DwarfConfig +from elftools.dwarf.die import AttributeValue +from elftools.dwarf.structs import DWARFStructs +from elftools.common.utils import struct_parse, bytelist2string +from elftools.dwarf.enums import ENUM_DW_TAG, ENUM_DW_AT, ENUM_DW_FORM +from elftools.dwarf.lineprogram import LineProgramEntry, LineState +from elftools.dwarf.dwarf_expr import DWARFExprOp +from typing import Iterator +from elftools.elf.elffile import ELFFile + +LineTableHeader = namedtuple('LineTableHeader', 'version file_entry') +CUv1Header = namedtuple('CUv1Header', 'version unit_length debug_abbrev_offset address_size') + +TAG_reverse = dict((v, k) for k, v in ENUM_DW_TAG.items()) +ATTR_reverse = dict((v, k) for k, v in ENUM_DW_AT.items()) +FORM_reverse = dict((v, k) for k, v in ENUM_DW_FORM.items()) + +DW_OP_name2opcode = dict( + DW_OP_reg=0x01, + DW_OP_basereg=0x02, + DW_OP_addr=0x03, + DW_OP_const=0x04, + DW_OP_deref2=0x05, + DW_OP_deref=0x06, + DW_OP_deref4=0x06, + DW_OP_add=0x07, + DW_OP_user_0x80=0x80 # Extension op, not sure what's the deal with that +) + +DW_OP_opcode2name = dict((v, k) for k, v in DW_OP_name2opcode.items()) + + +class DIEV1(object): + def __init__(self, stm, cu, di): + self.cu = cu + self.dwarfinfo = di + self.stream = stm + self.offset = stm.tell() + self.attributes = OrderedDict() + self.tag = None + self.has_children = None + self.abbrev_code = None + self.size = 0 + # Null DIE terminator. It can be used to obtain offset range occupied + # by this DIE including its whole subtree. + self._terminator = None + self._parent = None + + structs = self.dwarfinfo.structs + self.size = struct_parse(structs.Dwarf_uint32(''), stm) + # Size 8+ can be padding if the tag is 0. No attributes in those. + # DW_TAG_null and DW_TAG_padding are both code zero + if self.size < 8: + self.tag = 'DW_TAG_null' # Null terminates the sibling chain + self.has_children = False + else: + tag_code = struct_parse(structs.Dwarf_uint16(''), stm) + # Do what pyelftools does, leave tag as int if unknown + self.tag = TAG_reverse[tag_code] if tag_code in TAG_reverse else tag_code + if self.tag == 'DW_TAG_null': # TAG_padding in DWARF1 spec + self.tag == 'DW_TAG_padding' # Doesn't count for is_null + # No attributes, just advance the stream + stm.seek(self.size-6, 1) + self.has_children = False + else: + while stm.tell() < self.offset + self.size: + attr_offset = self.stream.tell() + attr = struct_parse(structs.Dwarf_uint16(''), stm) + form = FORM_reverse[attr & 0xf] + attr >>= 4 + if attr in ATTR_reverse: + name = ATTR_reverse[attr] + elif 0x200 <= attr <= 0x3ff: # DW_AT_MIPS represented as 0x204??? + name = 'DW_AT_user_0x%x' % attr + else: # Do what pyelftools does, leave tag as int if unknown + name = attr + + raw_value = struct_parse(structs.Dwarf_dw_form[form], stm) + value = raw_value + + self.attributes[name] = AttributeValue( + name=name, + form=form, + value=value, + raw_value=raw_value, + offset=attr_offset, + indirection_length=0) + self.has_children = self.attributes['DW_AT_sibling'].value >= self.offset + self.size + 8 + + def get_parent(self): + return self._parent + + def is_null(self): + return self.tag == 'DW_TAG_null' + + def iter_children(self) -> Iterator['DIEV1']: + return self.cu.iter_children(self) + + def iter_broken_children(self) -> Iterator['DIEV1']: + return self.cu.iter_broken_DIE_children(self) + + def sibling(self): + return self.attributes['DW_AT_sibling'].value + + +class CompileUnitV1(object): + def __init__(self, di, top_die): + self.dwarfinfo = di + self.structs = di.structs + end_offset = top_die.attributes['DW_AT_sibling'].value + self.header = CUv1Header(version=1, unit_length=end_offset - top_die.offset, debug_abbrev_offset=None, address_size=4) + self._dielist = [top_die] + self._diemap = [top_die.offset] + # For compatibility with v2+ CU + self.cu_offset = top_die.offset + self.cu_die_offset = top_die.offset + + def get_top_DIE(self): + return self._dielist[0] + + def __getitem__(self, name): + return self.header._asdict()[name] + + @property + def size(self): + return self.header.unit_length # No CU header here + + # Caches + def DIE_at_offset(self, offset) -> DIEV1: + i = bisect_left(self._diemap, offset) + if i < len(self._diemap) and offset == self._diemap[i]: + die = self._dielist[i] + else: + die = self.dwarfinfo.DIE_at_offset(offset, self) + self._dielist.insert(i, die) + self._diemap.insert(i, offset) + return die + + # pyelftools' iter_DIEs sets parent on discovered DIEs, we should too + def iter_DIEs(self): + offset = self.cu_offset + parent = None + parent_stack = list() + end_offset = self.get_top_DIE().attributes['DW_AT_sibling'].value + # Dump the whole section into locals to catch 1610 + if end_offset - offset <= 4096: + stm = self.dwarfinfo.stm + stm.seek(offset, 0) + import base64 + _ = base64.encodebytes(stm.read(end_offset - offset)).decode('ASCII') + while offset < end_offset: + die = self.DIE_at_offset(offset) + + if die._parent is None: + die._parent = parent + + if not die.is_null(): + yield die + offset += die.size + if offset != die.sibling(): # Start of a subtree + parent_stack.append(parent) + parent = die + else: # null - end of a sibling chain + # Catching 1610 + _ = die.size + _ = die.tag + if parent_stack: # Only pop if there are items in the stack + parent = parent_stack.pop() + else: + # We've reached the end of the DIE tree + parent = None + offset += die.size + # prev_die_tag = die.tag + + def iter_children(self, parent_die) -> Iterator[DIEV1]: + offset = parent_die.offset + parent_die.size + after = parent_die.attributes['DW_AT_sibling'].value if 'DW_AT_sibling' in parent_die.attributes else self.dwarfinfo.section_size + while offset < after: + die = self.DIE_at_offset(offset) + if die._parent is None: + die._parent = parent_die + if die.is_null(): + break + else: + yield die + offset = die.attributes['DW_AT_sibling'].value + + tag_is_child = { + "DW_TAG_global_subroutine": False, + "DW_TAG_structure_type": False, + "DW_TAG_subroutine": False, + "DW_TAG_subroutine_type": False, + "DW_TAG_union_type": False, + "DW_TAG_global_variable": False, + "DW_TAG_array_type": False, + "DW_TAG_enumeration_type": False, + "DW_TAG_typedef": False, + + "DW_TAG_local_variable": True, + "DW_TAG_formal_parameter": True, + "DW_TAG_inheritance": True, + "DW_TAG_member": True, + } + + # Iterate a broken file where all of the sibling attributes + # are zero so all we have to go on is the null children. + def iter_broken_DIE_children(self, die: DIEV1): + # Initial offset + offset = die.offset + die.size + + while offset < self.dwarfinfo.section_size: + # Yield the child + child = self.DIE_at_offset(offset) + offset += child.size + + # End of child list + if child.is_null(): + return + + # Inferred end of child list because its missing + if die.tag != "DW_TAG_compile_unit": + if not self.tag_is_child[child.tag]: + return + + yield child + + # Skip children if present + while True: + skip = self.DIE_at_offset(offset) + if skip.is_null(): + offset += skip.size + break + elif not self.tag_is_child[skip.tag]: + break + else: + offset += skip.size + + def iter_DIE_children(self, die): + if not die.has_children: + return + + # `cur_offset` tracks the stream offset of the next DIE to yield + # as we iterate over our children, + cur_offset = die.offset + die.size + + while True: + child = self.DIE_at_offset(cur_offset) + + if child._parent is None: + child._parent = die + + if child.is_null(): + die._terminator = child + return + + yield child + + if not child.has_children: + cur_offset += child.size + elif "DW_AT_sibling" in child.attributes: + sibling = child.attributes["DW_AT_sibling"] + if sibling.form == 'DW_FORM_ref': + cur_offset = sibling.value + else: + raise NotImplementedError('sibling in form %s' % sibling.form) + else: + # If no DW_AT_sibling attribute is provided by the producer + # then the whole child subtree must be parsed to find its next + # sibling. There is one zero byte representing null DIE + # terminating children list. It is used to locate child subtree + # bounds. + + # If children are not parsed yet, this instruction will manage + # to recursive call of this function which will result in + # setting of `_terminator` attribute of the `child`. + if child._terminator is None: + for _ in self.iter_DIE_children(child): + pass + + cur_offset = child._terminator.offset + child._terminator.size + + def get_DIE_from_refaddr(self, refaddr): + return self.DIE_at_offset(refaddr) + + +class LineTableV1(object): + def __init__(self, stm, structs, len, pc): + self.stm = stm + self.structs = structs + self.len = len + self.pc = pc + self._decoded_entries = None + self.header = LineTableHeader(1, (None)) + + def get_entries(self): + if self._decoded_entries is None: + stm = self.stm + offset = stm.tell() + end_offset = offset + self.len + structs = self.structs + entries = [] + pc = self.pc + while offset < end_offset: + line = struct_parse(structs.Dwarf_uint32(''), stm) + col = struct_parse(structs.Dwarf_uint16(''), stm) + pc_delta = struct_parse(structs.Dwarf_uint32(''), stm) + if line == 0: + break + state = LineState(True) + state.file = 0 + state.line = line + state.column = col if col != 0xffff else None + state.address = pc + entries.append(LineProgramEntry(0, False, [], state)) + pc += pc_delta + self._decoded_entries = entries + return self._decoded_entries + + def __getitem__(self, name): + return self.header[name] + + +class DWARFExprParserV1(object): + def __init__(self, structs): + self.structs = structs + + def parse_expr(self, expr): + stm = BytesIO(bytelist2string(expr)) + parsed = [] + + while True: + # Get the next opcode from the stream. If nothing is left in the + # stream, we're done. + byte = stm.read(1) + if len(byte) == 0: + break + + # Decode the opcode and its name. + op = ord(byte) + op_name = DW_OP_opcode2name.get(op, 'OP:0x%x' % op) + + if op <= 4 or op == 0x80: + args = [struct_parse(self.structs.Dwarf_target_addr(''), stm),] + else: + args = [] + + parsed.append(DWARFExprOp(op=op, op_name=op_name, args=args, offset=stm.tell())) + + return parsed + + +class DWARFInfoV1(object): + def __init__(self, elffile): + section = elffile.get_section_by_name(".debug") + section_data = section.data() + # TODO: relocation? Compression? + self.section_size = len(section_data) + self.stm = BytesIO(section_data) + + lsection = elffile.get_section_by_name(".line") + if lsection: + self.linestream = BytesIO(lsection.data()) + # Sections .debug_pubnames, .debug_aranges also in the spec - + # those are indices into info, we ignore them + + self.config = DwarfConfig( + little_endian=elffile.little_endian, + default_address_size=elffile.elfclass // 8, + machine_arch=elffile.get_machine_arch() + ) + + self.structs = DWARFStructs( + little_endian=self.config.little_endian, + dwarf_format=32, + address_size=self.config.default_address_size) + + def iter_CUs(self): + offset = 0 + while offset < self.section_size: + die = self.DIE_at_offset(offset, None) + if not die.is_null(): + if die.cu is None: + die.cu = cu = CompileUnitV1(self, die) + cu.cu_offset = offset + yield die.cu + offset = die.attributes['DW_AT_sibling'].value + if offset == 0: + break + else: + break + + def iter_all_dies(self): + offset = 0 + while offset < self.section_size: + die = self.DIE_at_offset(offset, None) + yield die + offset += die.size + + # Does not cache + def DIE_at_offset(self, offset, cu): + self.stm.seek(offset, 0) + return DIEV1(self.stm, cu, self) + + def location_lists(self): + return None + + def line_program_for_CU(self, cu): + top_DIE = cu.get_top_DIE() + if 'DW_AT_stmt_list' in top_DIE.attributes: + stm = self.linestream + stm.seek(top_DIE.attributes['DW_AT_stmt_list'].value, 0) + structs = self.structs + len = struct_parse(structs.Dwarf_uint32(''), stm) + pc = struct_parse(structs.Dwarf_target_addr(''), stm) + return LineTableV1(stm, structs, len, pc) + else: + return None + + def range_lists(self): + return None + + def get_aranges(self): + return None + + def has_CFI(self): + return False + + def has_EH_CFI(self): + return False + + +def parse_dwarf1(elffile): + return DWARFInfoV1(elffile) + + +def read_dwarf_info(file) -> DWARFInfoV1: + file.seek(0) + elffile = ELFFile(file, lambda s: None) + + # Retrieve the preferred loading address + load_segment = next((seg for seg in elffile.iter_segments() if seg.header.p_type == 'PT_LOAD'), None) + start_address = load_segment.header.p_vaddr if load_segment else 0 + di = None + if elffile.has_dwarf_info(): + di = elffile.get_dwarf_info() + elif section := elffile.get_section_by_name(".debug"): + if section.data_size == 0: + return None + di = parse_dwarf1(elffile) + else: + return None + + di._format = 0 + di._start_address = start_address + di._arch_code = elffile.header.e_machine + di._frames = None + return di diff --git a/ghidra_scripts/gimport/extract_info.py b/ghidra_scripts/gimport/extract_info.py new file mode 100644 index 000000000..139e5b679 --- /dev/null +++ b/ghidra_scripts/gimport/extract_info.py @@ -0,0 +1,439 @@ + +from .dwarfone import DIEV1, DWARFInfoV1, read_dwarf_info +from typing import Dict, List, assert_never +import re +from .demangle import demangle +from .dwarf import DW_AT, DW_OP, DW_TAG, DW_FMT, DW_MOD, \ + DwarfLocation, DwarfAttribute, DwarfSubscriptDataItem +import pathlib +from .gtypes import GType, GFundType, GArrayType, GPointerType, GStructType, \ + GGlobal, GGlobalSubroutine, GGlobalSubroutineParameter, GGlobalVariable, \ + GMember, GSubroutineType +import os + +GET_ORIGINAL_ADDRESS = True + + +REPO_ROOT = pathlib.Path(__file__).parent.parent.parent.resolve() + + +def get_die_name(die: DIEV1) -> str: + if DW_AT.name in die.attributes: + return die.attributes[DW_AT.name].value.decode("UTF-8") + else: + return None + + +def get_type_class(die: DIEV1): + if die.tag == DW_TAG.structure_type or die.tag == DW_TAG.class_type: + # Don't differentiate + return "struct" + elif die.tag == DW_TAG.union_type: + return "union" + elif die.tag == DW_TAG.enumeration_type: + return "enum" + else: + assert False, f"Unknown type class {die.tag}" + + +def create_array_type(die: DIEV1) -> GArrayType: + new_type = GArrayType() + data = die.attributes[DW_AT.subscr_data].value + index = 0 + + while index < len(data): + format = int.from_bytes(data[index:index+1], byteorder='big') + index += 1 + + item = DwarfSubscriptDataItem() + if format == DW_FMT.ET: + attribute = DwarfAttribute() + index = attribute.parse(data, index) + item.dwarf_type.parse(attribute) + else: + if format & 0x4: # User-defined type + item.dwarf_type.isFundamental = False + item.dwarf_type.udOffset = int.from_bytes(data[index:index+4], byteorder='big') + index += 4 + else: # Fundamental type + item.dwarf_type.isFundamental = True + item.dwarf_type.fundType = int.from_bytes(data[index:index+2], byteorder='big') + index += 2 + + if format & 0x2: # Location + item.lowBound.isConstant = False + block_len = int.from_bytes(data[index:index+2], byteorder='big') + index += 2 + item.lowBound.location.parse(data[index:index+block_len], 0) + index += block_len + else: # Constant + item.lowBound.isConstant = True + item.lowBound.constant = int.from_bytes(data[index:index+4], byteorder='big') + index += 4 + + if format & 0x1: # Location + item.highBound.isConstant = False + block_len = int.from_bytes(data[index:index+2], byteorder='big') + index += 2 + item.highBound.location.parse(data[index:index+block_len], 0) + index += block_len + else: # Constant + item.highBound.isConstant = True + item.highBound.constant = int.from_bytes(data[index:index+4], byteorder='big') # Assuming Elf32_Word is 4 bytes + index += 4 + + new_type.subscripts.append(item) + + return new_type + + +def read_symbol_addresses() -> Dict[str, int]: + symbols = {} + with open(REPO_ROOT / "config/GQPE78/symbols.txt") as f: + lines = f.readlines() + had_multiple = set() + for line in lines: + # Parse parts out of the line + parts = re.match(r"([^=]+) = ([^:]+):0x([0-9a-fA-F]+);", line) + sym_name = parts.group(1) + sym_address = int(parts.group(3), base=16) + + # strip $nnn ending used for duplicate local symbols of the same name + index = re.search(r"\$[0-9]+$", sym_name) + if index: + sym_name = sym_name[:index.start()] + + # Ignore addresses for duplicated symbols that + # we don't have any way to disambiguate. + if sym_name in symbols: + del symbols[sym_name] + had_multiple.add(sym_name) + else: + symbols[sym_name] = sym_address + return symbols + + +class ExtractedInfo: + def __init__(self): + self.structs: List[GStructType] = [] + self.globals: List[GGlobal] = [] + self.labels: Dict[int, str] = dict() + + +class DwarfInfoParser: + def __init__(self, di: DWARFInfoV1): + self.di = di + self.offset_to_type: Dict[int, GType] = dict() + self.globals: List[GGlobal] = [] + self.symbols: Dict[str, int] = None + self.type_by_name: Dict[str, GType] = dict() + self.used_mangled_names = set() + self.labels: Dict[int, str] = dict() + + def parse(self) -> ExtractedInfo: + self.symbols = read_symbol_addresses() + self.prepass_create_types() + self.process_dies() + self.postprocess_cleanup() + + result = ExtractedInfo() + for offset, type in self.offset_to_type.items(): + if isinstance(type, GStructType): + result.structs.append(type) + result.globals.extend(self.globals) + result.labels = self.labels.copy() + return result + + def parse_type_from_die(self, die: DIEV1) -> GType: + def modify(type: GType, modifiers: List[int]) -> GType: + for modifier in modifiers: + if modifier == DW_MOD.pointer_to or modifier == DW_MOD.reference_to: + type = GPointerType(type) + elif modifier == DW_MOD.const or modifier == DW_MOD.volatile: + # No impact on Ghidra use case + pass + else: + assert_never(f"Unknown modifier: {modifier}") + return type + + if DW_AT.user_def_type in die.attributes: + offset = die.attributes[DW_AT.user_def_type].value + if type := self.offset_to_type.get(offset): + return type + else: + return None + elif DW_AT.fund_type in die.attributes: + return GFundType(die.attributes[DW_AT.fund_type].value) + elif DW_AT.mod_u_d_type in die.attributes: + data = die.attributes[DW_AT.mod_u_d_type].value + ud = int.from_bytes(data[-4:], byteorder='big') + if type := self.offset_to_type.get(ud): + return modify(type, data[:-4]) + else: + return None + elif DW_AT.mod_fund_type in die.attributes: + data = die.attributes[DW_AT.mod_fund_type].value + fund_type = int.from_bytes(data[-2:], byteorder='big') + return modify(GFundType(fund_type), data[:-2]) + else: + # Void return type of function -> Hits this case + return None + + def parse_member(self, die: DIEV1) -> GMember: + new_type = GMember(get_die_name(die)) + new_type.type = self.parse_type_from_die(die) + if DW_AT.location in die.attributes: + new_type.location.parse(die.attributes[DW_AT.location].value, 0) + if DW_AT.bit_offset in die.attributes: + new_type.bit_offset = die.attributes[DW_AT.bit_offset].value + if DW_AT.bit_size in die.attributes: + new_type.bit_size = die.attributes[DW_AT.bit_size].value + + return new_type + + def prepass_create_struct_type(self, die: DIEV1) -> GStructType: + # Must be filled in later + if name := get_die_name(die): + struct = GStructType(name, get_type_class(die)) + return struct + else: + return GStructType("unnamed", get_type_class(die)) + + def prepass_create_type(self, die: DIEV1): + if die.tag == DW_TAG.structure_type or die.tag == DW_TAG.union_type or \ + die.tag == DW_TAG.enumeration_type or die.tag == DW_TAG.class_type: + struct_type = self.prepass_create_struct_type(die) + self.type_by_name[struct_type.name] = struct_type + return struct_type + elif die.tag == DW_TAG.array_type: + return create_array_type(die) + elif die.tag == DW_TAG.subroutine_type: + # We can fill in all the contents later + return GSubroutineType() + else: + return None + + def prepass_create_types(self): + for cu in self.di.iter_CUs(): + for die in cu.iter_DIE_children(cu.get_top_DIE()): + if type := self.prepass_create_type(die): + self.offset_to_type[die.offset] = type + + def fill_structure_members(self, die: DIEV1): + structure: GStructType = self.offset_to_type[die.offset] + structure.byte_size = die.attributes[DW_AT.byte_size].value + + for member_die in die.iter_children(): + member = self.parse_member(member_die) + structure.fields.append(member) + + def resolve_subroutine_types(self, die: DIEV1): + subroutine: GSubroutineType = self.offset_to_type[die.offset] + subroutine.type = self.parse_type_from_die(die) + + for parameter_die in die.iter_children(): + subroutine.parameters.append(self.parse_type_from_die(parameter_die)) + + def resolve_array_types(self, die: DIEV1): + array: GArrayType = self.offset_to_type[die.offset] + + for subscript in array.subscripts: + if subscript.dwarf_type.isFundamental: + subscript.resolved_type = GFundType(subscript.dwarf_type.fundType) + else: + if subscript.dwarf_type.udOffset in self.offset_to_type: + subscript.resolved_type = self.offset_to_type[subscript.dwarf_type.udOffset] + else: + assert_never(f"Missing type: {subscript.dwarf_type.udOffset}") + for modifier in subscript.dwarf_type.modifiers: + if modifier == DW_MOD.pointer_to or modifier == DW_MOD.reference_to: + subscript.resolved_type = GPointerType(subscript.resolved_type) + + def create_global_functions(self, die: DIEV1): + global_subroutine = GGlobalSubroutine() + global_subroutine.name = get_die_name(die) + global_subroutine.mangled_name = die.attributes[DW_AT.mangled_name].value.decode("UTF-8") + self.used_mangled_names.add(global_subroutine.mangled_name) + + if GET_ORIGINAL_ADDRESS: + if global_subroutine.mangled_name in self.symbols: + global_subroutine.address = self.symbols[global_subroutine.mangled_name] + else: + # No original address. New functions we added in our implementation. + return + else: + global_subroutine.address = die.attributes[DW_AT.low_pc].value + + # DWARF handles void return type as not present but we want it + # to be present as void type for Ghidra. + global_subroutine.type = self.parse_type_from_die(die) + if global_subroutine.type is None: + global_subroutine.type = GFundType(20) + + # Parse parameters + for child in die.iter_children(): + if child.tag == DW_TAG.formal_parameter: + name = get_die_name(child) + type = self.parse_type_from_die(child) + loc = DwarfLocation() + loc.parse(child.attributes[DW_AT.location].value, 0) + global_subroutine.params.append(GGlobalSubroutineParameter(name, type, loc)) + elif child.tag == DW_TAG.local_variable: + pass + else: + print("Unknown subroutine child tag:", child.offset, child.tag) + exit(0) + + if result := demangle(global_subroutine.mangled_name, lambda ident: self.type_by_name.get(ident)): + (name, param_types) = result + global_subroutine.name = name + same_count = len(param_types) == len(global_subroutine.params) + for i, param_type in enumerate(param_types): + if i < len(global_subroutine.params): + global_subroutine.params[i].type = param_type + # If we had a different count the names are useless + if not same_count: + global_subroutine.params[i].name = f"param{i+1}" + else: + param = GGlobalSubroutineParameter(f"param{i+1}", param_type, DwarfLocation()) + global_subroutine.params.append(param) + + self.globals.append(global_subroutine) + + def create_global_variables(self, die: DIEV1): + global_variable = GGlobalVariable() + global_variable.name = get_die_name(die) + global_variable.type = self.parse_type_from_die(die) + if DW_AT.mangled_name in die.attributes: + global_variable.mangled_name = die.attributes[DW_AT.mangled_name].value.decode("UTF-8") + else: + global_variable.mangled_name = global_variable.name + + if GET_ORIGINAL_ADDRESS: + if global_variable.mangled_name in self.symbols: + global_variable.address = self.symbols[global_variable.mangled_name] + self.globals.append(global_variable) + else: + loc = DwarfLocation() + loc.parse(die.attributes[DW_AT.location].value, 0) + assert loc.atoms[0].op == DW_OP.ADDR, "Non-address global" + assert len(loc.atoms) == 1, "Multiple atoms" + global_variable.address = loc.atoms[0].value + self.globals.append(global_variable) + + def add_enum_members(self, die: DIEV1): + enum_type: GStructType = self.offset_to_type[die.offset] + enum_type.byte_size = die.attributes[DW_AT.byte_size].value + data: List[int] = die.attributes[DW_AT.element_list].value + index = 0 + while index < len(data): + value = int.from_bytes(data[index:index+4], byteorder='big') + index += 4 + name_start = index + while data[index] != 0: + index += 1 + name = bytes(data[name_start:index]).decode("UTF-8") + index += 1 + enum_type.enum_values.append((name, value)) + + def process_die(self, die: DIEV1): + if die.tag == DW_TAG.structure_type or die.tag == DW_TAG.union_type or \ + die.tag == DW_TAG.class_type: + self.fill_structure_members(die) + elif die.tag == DW_TAG.subroutine_type: + self.resolve_subroutine_types(die) + elif die.tag == DW_TAG.array_type: + self.resolve_array_types(die) + elif die.tag == DW_TAG.global_subroutine or die.tag == DW_TAG.subroutine: + self.create_global_functions(die) + elif die.tag == DW_TAG.global_variable or die.tag == DW_TAG.local_variable: + self.create_global_variables(die) + elif die.tag == DW_TAG.enumeration_type: + self.add_enum_members(die) + else: + assert_never(f"Other: {die.tag} {get_die_name(die)}") + + def process_dies(self): + # Fill in types + for cu in self.di.iter_CUs(): + for die in cu.iter_DIE_children(cu.get_top_DIE()): + self.process_die(die) + + def postprocess_cleanup(self): + # Add functions which we don't have in the executable + # because we have not started decomping them yet. + for mangled_name in self.symbols: + if mangled_name not in self.used_mangled_names: + result = demangle(mangled_name, lambda ident: self.type_by_name.get(ident)) + if result: + (name, types) = result + func = GGlobalSubroutine() + func.address = self.symbols[mangled_name] + func.mangled_name = mangled_name + func.name = name + func.type = None + for (i, param_type) in enumerate(types): + func.params.append(GGlobalSubroutineParameter(f"param{i+1}", param_type, DwarfLocation())) + self.globals.append(func) + else: + # Just use it as a label + self.labels[self.symbols[mangled_name]] = mangled_name + + # TODO: Vtable cleanup + + # # debug output + # with open("types.txt", "w") as f: + # for offset, type in self.offset_to_type.items(): + # if isinstance(type, GStructType): + # f.write(f"{type.type_class} {type.name} [{hex(type.byte_size)}]/n") + # for field in type.fields: + # f.write(f" {field.type} {field.name} [{field.location}]\n") + # for (enum_name, enum_value) in type.enum_values: + # f.write(f" {enum_name} = {enum_value}\n") + + # for glob in self.globals: + # f.write(f"@{hex(glob.address)}: {glob}\n") + + +def do_debug_build(): + # Note: The shenanigans in this function with ninja are to avoid leaving + # someone stuck with a debug build which doen't match when that's not + # something they're used to encountering. + + # Save the old ninja file to restore after making the debug build. + ninja_file = REPO_ROOT / "build.ninja" + ninja_save = ninja_file.with_suffix(".bak") + had_ninja_file = False + if ninja_file.exists(): + had_ninja_file = True + if ninja_save.exists(): + os.remove(ninja_save) + os.rename(ninja_file, ninja_save) + + import subprocess + print("Creating a debug build to extract info from. This will take a moment.") + subprocess.run(["python", "configure.py", "--debug"], cwd=REPO_ROOT, check=True, capture_output=True) + subprocess.run(["ninja"], cwd=REPO_ROOT, check=False, capture_output=True) + + # Restore the old ninja file if they had one + os.remove(ninja_file) + if had_ninja_file: + os.rename(ninja_save, ninja_file) + + +def extract_info() -> ExtractedInfo: + executable_path = REPO_ROOT / "build/GQPE78/main.elf" + if not executable_path.exists(): + print("No executable") + do_debug_build() + + di = read_dwarf_info(open(executable_path, mode="rb")) + if di is None: + do_debug_build() + di = read_dwarf_info(open(executable_path, mode="rb")) + + assert di, "Failed to read DWARF info from built executable" + + print("Extracting info from main.elf / symbols.txt") + parser = DwarfInfoParser(di) + return parser.parse() diff --git a/ghidra_scripts/gimport/function_with_paramn.png b/ghidra_scripts/gimport/function_with_paramn.png new file mode 100644 index 0000000000000000000000000000000000000000..d954e8d1d61c4cd44cc941633f4ba4033d5599a2 GIT binary patch literal 10335 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 literal 0 HcmV?d00001 diff --git a/ghidra_scripts/gimport/function_with_return.png b/ghidra_scripts/gimport/function_with_return.png new file mode 100644 index 0000000000000000000000000000000000000000..8d0fb8939158af9601ddbb0058f41bb0f0bbab87 GIT binary patch literal 3538 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 hjr-@Sf^LH^gxmU~lVWQwsHp`zOh2=}G_q literal 0 HcmV?d00001 diff --git a/ghidra_scripts/gimport/gtypes.py b/ghidra_scripts/gimport/gtypes.py new file mode 100644 index 000000000..88a7db28d --- /dev/null +++ b/ghidra_scripts/gimport/gtypes.py @@ -0,0 +1,105 @@ +from typing import List, Tuple +from .dwarf import DwarfLocation, DwarfSubscriptDataItem, DW_FT_to_string + + +class GType: + def __init__(self): + pass + + +class GPointerType: + def __init__(self, type: GType): + self.type: GType = type + + def __str__(self): + return f"{self.type}*" + + +class GGlobal: + def __init__(self): + self.name: str = None + self.address: int = None + pass + + +class GGlobalVariable(GGlobal): + def __init__(self): + self.name: str = None + self.mangled_name: str = None + self.type: GType = None + + def __str__(self): + return f"{self.type} {self.name}" + + +class GGlobalSubroutineParameter: + def __init__(self, name, type, location): + self.name = name + self.type: GType = type + self.location: DwarfLocation = location + + def __str__(self): + return f"{self.type} {self.name}" + + +class GGlobalSubroutine(GGlobal): + def __init__(self): + self.mangled_name: str = None + self.type: GType = None + self.params: List[GGlobalSubroutineParameter] = [] + + def __str__(self): + params_str = ", ".join(str(param) for param in self.params) + return f"{self.type} {self.name}({params_str})" + + +class GStructType(GType): + def __init__(self, name, type_class: str): + self.name = name + self.type_class = type_class + self.byte_size = 0 + self.fields: List[GMember] = [] + self.enum_values: List[Tuple[str, int]] = [] + + def __str__(self): + return f"{self.name}" + + +class GArrayType(GType): + def __init__(self): + self.subscripts: List[DwarfSubscriptDataItem] = [] + + def __str__(self): + subscripts_str = ", ".join(str(subscript) for subscript in self.subscripts) + return f"ArrayType({subscripts_str})" + + +class GFundType(GType): + def __init__(self, id): + self.id = id + self.name = DW_FT_to_string(id) + assert self.name is not None, f"Unknown fund type: {id}" + + def __str__(self): + return f"{self.name}" + + +class GMember(GType): + def __init__(self, name): + self.name = name + self.type: GType = None + self.location = DwarfLocation() + self.bit_offset = None + self.bit_size = None + + def __str__(self): + return f"{self.type} {self.name}" + + +class GSubroutineType(GType): + def __init__(self): + self.type: GType = None + self.parameters: List[GType] = [] + + def __str__(self): + return f"SubroutineType({self.type}({",".join(str(param) for param in self.parameters)}))" diff --git a/ghidra_scripts/gimport/import_info.py b/ghidra_scripts/gimport/import_info.py new file mode 100644 index 000000000..58331a2a9 --- /dev/null +++ b/ghidra_scripts/gimport/import_info.py @@ -0,0 +1,231 @@ +from .extract_info import ExtractedInfo +from .gtypes import GType, GFundType, GPointerType, GArrayType, GStructType, \ + GGlobalVariable, GSubroutineType, GGlobal, GGlobalSubroutine +from ghidra.program.database import ProgramDB +from ghidra.program.model.data import BooleanDataType, SignedByteDataType, ByteDataType, \ + ShortDataType, UnsignedShortDataType, IntegerDataType, UnsignedIntegerDataType, \ + LongDataType, UnsignedLongDataType, PointerDataType, FloatDataType, DoubleDataType, \ + VoidDataType, LongLongDataType, DataType, TypedefDataType, DataTypeConflictHandler, \ + StructureDataType, UnionDataType, EnumDataType, ArrayDataType +from ghidra.program.model.symbol import SourceType +from ghidra.program.model.listing import Function +from ghidra.program.model.listing import ParameterImpl + + +fund_types_by_id = { + 0: ("bool", BooleanDataType()), + 1: ("S8", SignedByteDataType()), # char in spec, alias to S8 in our codebase + 2: ("S8", SignedByteDataType()), # Actually signed int8 + 3: ("U8", ByteDataType()), + 5: ("S16", ShortDataType()), + 6: ("U16", UnsignedShortDataType()), + 7: ("S32", IntegerDataType()), + 9: ("U32", UnsignedIntegerDataType()), + 10: ("SLong", LongDataType()), + 12: ("ULong", UnsignedLongDataType()), + 13: ("void*", PointerDataType()), + 14: ("F32", FloatDataType()), + 15: ("F64", DoubleDataType()), + 20: ("void", VoidDataType()), + 32776: ("S64", LongLongDataType()), +} +datatype_cache = {} + + +def generate_datatype_for_gtype(program: ProgramDB, gtype: GType) -> DataType: + if isinstance(gtype, GFundType): + if gtype.id == 13: + # Special case for void* because DWARF considers it a fundamental type + # but Ghidra considers it a pointer to a fundamental type. + return program.getDataTypeManager().addDataType( + PointerDataType(VoidDataType()), DataTypeConflictHandler.KEEP_HANDLER + ) + else: + fund_type_info = fund_types_by_id.get(gtype.id) + base_type = program.getDataTypeManager().addDataType(fund_type_info[1], DataTypeConflictHandler.KEEP_HANDLER) + return program.getDataTypeManager().addDataType( + TypedefDataType(fund_type_info[0], base_type), + DataTypeConflictHandler.KEEP_HANDLER + ) + elif isinstance(gtype, GPointerType): + base_type = get_datatype_for_gtype(program, gtype.type) + return program.getDataTypeManager().addDataType( + PointerDataType(base_type), DataTypeConflictHandler.KEEP_HANDLER + ) + elif isinstance(gtype, GArrayType): + index_part = gtype.subscripts[0] + type_part = gtype.subscripts[1] + assert type_part.highBound.location is None, "Type part should not have bounds" + assert type_part.lowBound.location is None, "Type part should not have bounds" + if type_part.resolved_type is None: + return PointerDataType() + else: + return program.getDataTypeManager().addDataType( + ArrayDataType( + get_datatype_for_gtype(program, type_part.resolved_type), + index_part.highBound.constant + 1 + ), + DataTypeConflictHandler.KEEP_HANDLER + ) + elif isinstance(gtype, GSubroutineType): + # TODO: + return None + elif isinstance(gtype, GStructType): + if gtype.type_class == "enum": + path = f"/{gtype.name}" + existing_type: EnumDataType = program.getDataTypeManager().getDataType(path) + if existing_type is None: + existing_type = program.getDataTypeManager().addDataType( + EnumDataType(gtype.name, gtype.byte_size), DataTypeConflictHandler.KEEP_HANDLER + ) + return existing_type + elif gtype.type_class == "union": + path = f"/{gtype.name}" + existing_type: UnionDataType = program.getDataTypeManager().getDataType(path) + if existing_type is None: + existing_type = program.getDataTypeManager().addDataType( + UnionDataType(gtype.name), DataTypeConflictHandler.KEEP_HANDLER + ) + return existing_type + elif gtype.type_class == "struct": + path = f"/{gtype.name}" + existing_type: StructureDataType = program.getDataTypeManager().getDataType(path) + if existing_type is None: + existing_type = program.getDataTypeManager().addDataType( + StructureDataType(gtype.name, gtype.byte_size), DataTypeConflictHandler.KEEP_HANDLER + ) + existing_type.setLength(gtype.byte_size) + return existing_type + + +def get_datatype_for_gtype(program: ProgramDB, gtype: GType) -> DataType: + if gtype in datatype_cache: + return datatype_cache[gtype] + else: + datatype = generate_datatype_for_gtype(program, gtype) + datatype_cache[gtype] = datatype + return datatype + + +def fill_struct_types(program: ProgramDB, parsed_info: ExtractedInfo): + for gtype in parsed_info.structs: + if gtype.type_class == "enum": + existing_type: EnumDataType = get_datatype_for_gtype(program, gtype) + for (name, value) in gtype.enum_values: + if not existing_type.contains(name): + existing_type.add(name, value) + elif gtype.type_class == "union": + existing_type: UnionDataType = get_datatype_for_gtype(program, gtype) + assert existing_type, f"Failed to get existing type for {gtype.name}" + # TODO: Union types + + elif gtype.type_class == "struct": + existing_type: StructureDataType = get_datatype_for_gtype(program, gtype) + assert existing_type, f"Failed to get existing type for {gtype.name}" + for field in gtype.fields: + if field_type := get_datatype_for_gtype(program, field.type): + if field.location.atoms: + offset = field.location.atoms[0].value + if field.bit_size is not None: + # TODO: Make bitfields work + # existing_type.setPackingEnabled(False) + # print(f"Field {field.name}") + # max_bit = field.bit_offset + field.bit_size - 1 + # bytes_needed = (max_bit // 8) + 1 + # existing_type.insertBitFieldAt(offset, bytes_needed, field.bit_offset, + # field_type, field.bit_size, + # field.name, "") + pass + else: + if existing_field := existing_type.getComponentAt(offset): + if existing_field.getDataType() == field_type: + if existing_field.getFieldName() != field.name: + existing_field.setFieldName(field.name) + else: + existing_type.replaceAtOffset(offset, field_type, -1, field.name, None) + else: + existing_type.replaceAtOffset(offset, field_type, -1, field.name, None) + + +def add_global(program: ProgramDB, glob: GGlobal): + if isinstance(glob, GGlobalVariable): + pass + elif isinstance(glob, GGlobalSubroutine): + address = program.getAddressFactory().getAddress(hex(glob.address)) + if function := program.getFunctionManager().getFunctionAt(address): + function.setName(glob.name, SourceType.USER_DEFINED) + if glob.type: + function.setReturnType( + get_datatype_for_gtype(program, glob.type), + SourceType.USER_DEFINED) + + args = [] + for ord, param in enumerate(glob.params): + args.append(ParameterImpl(param.name, + get_datatype_for_gtype(program, param.type), + program)) + function.replaceParameters(Function.FunctionUpdateType.DYNAMIC_STORAGE_ALL_PARAMS, True, + SourceType.USER_DEFINED, args) + + +def add_labels(program: ProgramDB, parsed_info: ExtractedInfo): + for (address, label) in parsed_info.labels.items(): + address = program.getAddressFactory().getAddress(hex(address)) + if function := program.getFunctionManager().getFunctionAt(address): + function.setName(label, SourceType.USER_DEFINED) + elif symbol := program.getSymbolTable().getPrimarySymbol(address): + symbol.setName(label, SourceType.USER_DEFINED) + else: + program.getSymbolTable().createLabel(address, label, SourceType.USER_DEFINED) + + +def import_info(program: ProgramDB, parsed_info: ExtractedInfo): + # Copy better info into forward declared versions of structs + + # Figure out what structs need disambiguation because there are two of + # them with the same name but different sizes. + nonzero_size_for_name = {} + needs_disambiguation = set() + for gtype in parsed_info.structs: + if gtype.byte_size > 0: + if gtype.name in nonzero_size_for_name: + if gtype.byte_size != nonzero_size_for_name[gtype.name]: + needs_disambiguation.add(gtype.name) + else: + nonzero_size_for_name[gtype.name] = gtype.byte_size + + # Rename types needing disambiguation + for gtype in parsed_info.structs: + if gtype.name in needs_disambiguation: + gtype.name = f"{gtype.name}{hex(gtype.byte_size)}" + + # Record which types are nonzero size so we can eliminate forward declares + nonzero_for_name = {} + for gtype in parsed_info.structs: + if gtype.byte_size != 0: + nonzero_for_name[gtype.name] = gtype + + # Update forward declares to have the same contents + for gtype in parsed_info.structs: + # Some library types are only forward declared + if gtype.name not in nonzero_for_name: + continue + gtype.byte_size = nonzero_for_name[gtype.name].byte_size + gtype.fields = nonzero_for_name[gtype.name].fields + + for id in fund_types_by_id: + get_datatype_for_gtype(program, GFundType(id)) + + # Make the structs exist to avoid problems with circular references + for struct in parsed_info.structs: + get_datatype_for_gtype(program, struct) + + # Now that they all exist, fill the structs + fill_struct_types(program, parsed_info) + + # Finally, handle the globals + for glob in parsed_info.globals: + add_global(program, glob) + + # Add any leftover labels + add_labels(program, parsed_info) diff --git a/ghidra_scripts/gimport/manage_script_directories.png b/ghidra_scripts/gimport/manage_script_directories.png new file mode 100644 index 0000000000000000000000000000000000000000..60b6dbe36a9018912fd017f9484c499fb22e0a38 GIT binary patch literal 16366 zcmbWe1ymf}vNjwecyPBM1A}X@Ai-hK0YY$hcb6c+br>vI7@Xh)4-h1{yF0<%{hK7` z-22|MzV*vpvuLK*bnn_-yQ-e2s&3K_m4;~;m5Nf zNkjO*r%uXJqEAW(Nw?uAFU&;bMV>sVh(WzGK!Bek+e>RWJ$dq~{rBI~F1rHbCr^X} zWyD2P-F5fV5tDHyE`9`Fd1K_rgh(`eRK|OeILIO^h2U&K^?tkjD^)1St`tdK|Gla` zJ?b-tEu5$gFs{UXpNjiySitKh7Bc!xzoGQ`byG)3x^etoaN2eA5>L-O2Y{2OCC!V| z=y`_wY8c($LwA_OzrVVl@bAwfvl+d77lF?uBl856;Mv~H8qhvao(Uvy+<~JAKg4%^ zb76ef9xRF*M6aH!K=u(C=gFeqV<#3}h1S2X!BH$l1{5k$Dq@`wM2bt?(6|QvOPKp= zPjWv*wK8X<=_2!SI47TEd!w(hcS}D*I-`T-=&uO491jq7?@*Rlv^XkGVpnNxjg5}W z>o`&k?9`z@lYk#2ZqEjIG9b8Hywp%eryzReE~c5zes40uU<1P7FkSMUNA`K|)A#w~ zxZn5D(8#MI`>hgkkWsS?6MP2w*WTLFo^BXRMw6O^D#!m~Ci-)esO6+WaH?&?9`0uW zkH;WSbkA6i`;Eb!A->(FjYJ%!@?kZgvNMiqlM_mcFx?6zh)lA1Xq{~~#(k*OvX#MH zm{h}pZBM5a1+fpXADoApNQ9=bqaUVU(Ynlqc=)bSB!b2Uk(tYV&!$!D)nZUh3>E4N zJ3=&Xol(Py!=7jgYB#AU;1dzXK5Ie1r z<}sh-#`;_>Lhg9<0RDtFxl< zi8iF)xXt5Un{T|<{l+exR`t}7kaqK^%#X~mx4-1tRjj=;z1!RWqf23fi&B3;eTLEx z81B<@UxF3g6%biI!8eyFr}6xe(GlJG)L#lx0n6$kNM?T3~i znM{gL?7OepL#q)Dju8Ds4ow=DS0o-k9hBJw92AmJIt=n}@%!YOQ^$WPT$|R)@=cYr zzulV4nhZyYy)v46Oysze^%r-!$QKW=tOR&IBN+X$vwf|8=YqaEwj20&P$0sQGZ^$9(C%YF(D zty_=`d=xfAm(u~W^j)roU)D6L9-K3@paH4ommcwl!Sgs@{cRjv%9+bnqSb>fk`YgL zOj4FxVI44BbeOYZD(z930p8CfJHX+7lytj5a#B${TKKSVytt3uA_BYB>ijNF9~(1c zrE#^x!OX1zy+{&XR)g=VS)Uda^B;8Ek>%5*KVq>^ zc#)K*Vd1s9h$YgFn0z0}wH3UAxhZbxe65ipW|_Nkhx8jM^u6L2VkcOf_Ll1%X;%5L zGQ4eN&$LFICg6w8!J-ow;inhaQr2r+=f1&T{CG}!rhepStivDm6C(^FCM&i#s7~nW zLr9}57xX!EGH8^ZUFl{UZft6RI-3U@`@TA@FS@D0m2&sfrJ~Yk1|lBpQ!^gja?%?0 zVuJ$~8X^=pp8`9<;e*NDMZoG%#g9+|QxU_M3}b$lF6Vx!TxwkjR2}u!4EoA=%-31CHn87rg?Jl&z!C0Rb)^31Z^Q_)>`SsLvq^Ms8^R;Mdcea zC$UQTuT&ZRD8>t}V&hSAQ^l?w-Ne$pcH%b%#_haXPF;+yXNWR2t}_%-Wp0SAjf6jG zEBc`E2-hMrl<}jxdE4V3s5AdM^z!Yw5lzaG4bcti>spFC#7evPj)N*o$4%S!d7J4) zx38TG@A)0t6?X;oS#yiz`!(_U_QiMU;rJOUf>Gq z;hqfkp(aoiyhLC9{LWgN(;Rc@g{8}PL|ZZl2FfcYnvObzdGNVMemO4V!zZb9pv>dq zXUfg1hb-F#gZ^apqxE=2QDtRpG!kw!a-TEs)?6*?+1AMO2G{)^Fybd3urg{OB?u_O zs8;+jS2oGB8@st{BRTr|jEk%c7Nldz0a-U!y0P_m$)elz0;hH;WS-Hoo|qUhnV@e= z?}&fj%oi=w=OU%U_K^_slR!8j26kT;qhIesCP!k{6q#~CEfl2MNVaD|0onHXRf02@ zQ0)?g;wM3G5x3w`Jf^3Mr3$t@y=siLeMHI=V0l5`0UJO4`9|OGv|3K0aH6U&>U3wJ z_NN5HiylhQ@j_-u&tXIK&+iVwH1yzbFVDiO{-ib}D0K)h%3bWO>yTn1ET}J0P&iJ4 zKIq@GSLCp<6W3)jDwRU)qBI~#WvE)&!m!`Jc{AT70H_c8j zlIizQpXPVcL$^+fu)RJqQ10SV=`BBDX1d)?e6$%^Ib5Ws!Xq+TUv+n{R3qn8CyRp= z9)3?7u%DZ*WNBP9caprQ5=3K@7yz~Er_(2S#KR@3!7O4Dq!HhSVdip*ejcn#=s}~G zNqUHdI3JfC+i!P&4H!b#(8NQWD$593(rfI3Fb(u1VO*dcARR_m4CFK6Ya!Y^>Vi}S{!?PCBdWMc|2BesP^;+Lx^hO2Iqg(RvST-zHf~<^8mgY6HZ_X;d7VS@jz_A6g^MeXwIfs8`)p;X+YbqT({vv zpa=0vlXOvRuh?1F+4H+?JhLW1kLoHDj3ek%dj1`K#GwbPIg^|7O4P6$lld)!&9&u_ zbH4b(6WF-z9&s}NKqzfBStV$1s>NVO1t_x;1q7UFy*S;cXoKb?J;TmbqaJ(hV?ul; z&UZoyD)Ag7*sW;)ZpIE-CkKZgKA=RBnu*7+hBVQ?up^3uf^&%2WRRfuQMqi2IJtGL zW$if?M#kdw2dxAWvtDc#Y?2V#wj8|G^VgKpOdt^g0BKItTFjk|U3p~L(?wo$k}m2( z4j{;q-|_eb)7{$h?yAbteih0KmIC#{${d9Subvy=9Tp%hnnL&>!Dg;ThMnJKkL1c@ zLP?$Lno<^a=6?Co-cUiqTuFY9lTw=B?fup>_FPMm_Raygu2aoypK9$0l@BbTUT54A z?o{ui06NZ1Q_q;^&(=d$``TXBm`#e+A1I2&tJ!?%g|sY3U$TmBBoIA<6BrRf5nU z(#mKwK#uv+?#~f;AHCkzxmrBGUgP40Sp%SL8+>}N53cfLE0xii zUVOx=x0YI3Jo32x#liYMziD69;+yD$l$QP z+vo7U?=rbEJzP3kXl_|~eDNrFR2%E{F~g}j1o4-3K%MxZ(T@`V{*yU5N_<8HVblj~ zYiuS=0n5O*vuf0Rl zb4eE!Qt#kEL<{S;3MJldSO!URVSF3|anEPz+Jy6LUe3w3_}d(m>3bQFOn2G@X~WPf1*@Jq(zRer z9PYB7a$AleAE#&k-i4v4~*o^i#bI)UawXpnkiM74f zPd84-X+ig1O*?+S#}oHli#yo+{v^<*DmE-VAoIl|4w47MQXY&`FZ;^W{x*43DZ6o( zck6k5pi7_(;}-0jPAGPGoCdW21r;ciMHYqe&zk!^cKOv5zI@5fqs^P*w{Jkiw0kXK zCvM#zV3M-6x7L8xIP}3q7VKqbaSnk6df#9UHL+ z(;;Vkc1SXwQcP|WMt)Y5G-vXk=ozaUx={H}s6faVEoku{NAQ!v|e5yu-3aZUE=~Gfz19`82sWEN(@X|aVQIohN^IOo(v1}du*|Q zeU0e1^a_NVziyJkYaKg=BID9NlGadQp)_RA*2@iv=`+M!1`=DkpW0TK)Z4mHjq}Cm zEBNS0Y)Hh<;lV3AvySD)nFIrm@rL|%y!<_4-Fdy@kBe2-SjG*y)*=1WBWB#4TMn*{ zp{cksuoWff5DBaa5Vxqu%-xujDAYhTiX0s&lSF3!j6@{-=F@4TSNkVHvE{u%#O3kF zi9p8uyw+%BH_@A_$xby&3=}L)a6OGTnHPq2u@(XM8_O4uVI`=ty^FMC1su2yK*5Ju z7Wcbzz2zQ+x69j~-WrVek(g+zi5K3V4K1I2`g$~w(RcLIP88yc6F?(w5g(fq^$I*& zc^OVRym0;#eV9g9TTfh&|6G$RNI>u;z@`KfC6YF(N+c!GWZ?u#sDX0ucq3F$U7UED zY3E6xo}E`Z^ub}th*Q4}@+gwpJSH$&88F}qDm~S!iBRA>kx&yyZk2|xfUYun&1jim zL0h6h9679~p+~$qQp|2L(Ga9YPSM;Lz0uR{^OW6pSq`?(6O&v+XDTC59Sb;VajZjmW0lt zsT*Pg90b&uLv-&qMMNDNVpA{-KqrjkSh+#SUxV11Z0KRoqaza79*FR~Tns|)D_?5% zP(>*J`BjM|6ZWpg^LQyahi=GasB#Q}Q0!)!PivoO>^!2=EjEBODW?-C1Kzm_=x!vG zEC)(N0x5Zw<}zF7v+w~<;xZBt1=2fWUu|weeEBz2GRc`mUHwoYmzAU9lXB0A5Draa z6MnaYfaxx|PPee3qe*|e0Gpi{QAmwQ>9%ly%|)4#`56OcLTUHQaODjN#H`UqQRwI= zV6GVp$nweMLp8Gf*>3*wVf@$S{%Cbdne^pVy_mKXiqs#4%$L$KvwVg8Og=SFHDY)g zkcc$3Ymlt#m|49kn^jwIm#wseEmVn6bf6jP<+A0^Gd!&j1x^QFNqYjUVB=gYPx<9; zRUcAq0--swkn=_a(b!n|fWu22|I?&0(Nvj7{Hd7DX`yibf!$W&n?t>%Wh2bVIx2^el z&Tn>0W%b-UHk%66df+(*#=<4y zgOvG11YbAtp@Ub&a_Q%H?0e&6KMPE5b6=(D*5-1|HgxY}c8Mm$Z3G2*lp;<=6$iVn z35`=JQHYEx0c2RfmT%W}h}c1X5;71OsAL&F%pV(&NDszFf8EN?uc_gcj%53A|sejq6y zm_?}1d!U&9bw}xoY3IMA6WslM`drOpwbPJ3~Td)zSG(+#{Te=S+CPUYBGU=a?auXO)9+}fH9vGXIcp|QO zqNnPZO0*sR0vcq`(aE{ifCd>rf1QBN!K@Fq^r3K0d7#&B_S8YbcCAMi!G?WvxHO?x zAo)boWT1zj_|oITP~4L9m1(Vu#sMtS2!y3yeBduhZ-$QMb{q^=0S0NNL~m zP0#+OU5I!5N#k{>ZVXk{kSbwZ1D>XHrd%FkfNx-5mksB9!<1@3vs|1}ClJAlt(J)B zy$B^z0I;wCst3zym@B_kkjOi8wrSMXamqN?Vu&vUYRvc*<5aLEdu@RzWhmOm*+ru{ z>IHvLvp#B}5V$rWh(WP$m}3{Lpuj?QP?M=2aB3UAh*z+nNNxQPkG1a+@TKHsky#*q zM*xThegZ*7-z01KQw4#k_YB*QrIDIDP0r;XT~R-XZ{HraBt0gAcJNS1KIHvawdnk@ z&*${($JzE6bA?|%Lig(kWa z0L*A)?60>#ytjQm_Lj0Pyp7rP+IO4?+;U%0x7Q9nkqs>%x5#~c+Slo|BpHrqIT`Q` zCD%YPE}&e`JLP~y8Oif~s6RPg?~emmZI6jpIDYUqkV%#-*;1TzN3Z_DM~Omt0_n8o zSEV{FuSg!Rv|JlY2p~b$n2@ubmZY|vg!|>}{nOW3h0nAMzS&fD)N~DYoitJZ9_h`f)faK(K z&pDzfLu2(9$-gvVVoxPCe{Gca+fQ6ditV1#tc9k2sY@C=E*QhQcW|+@*u}I^Yq%&* zI-WQ;HF;!bH57s7`MtQ1_=+ymD-I`{`*BG^0cS~x#-r!IWTE0Ie5hz(h>qP?16AQe zm~r>0rgWy@QV6g<1`KOl4B|aMV+I|s>H5blkUe_F2iE&YL03Cjp5BsFnM|dI0FMcC zW5ckq!{?qSw>n7gV9~l59)5a;fGWD*xZALS47Y;rIBMlc{?7OQ1`9iVQ+7?~_Ps3Gm3eAy8r^|9EF)nytNT*shjMv*a zkBUw_>OzUvNaJtQfv8l}MN_-M{(SSJ%_MpEn{CAIH`h{>vMpjO>z7|f1D-1xYM zx@y9bM%bxj@>dSyDSp)Jt*)%sPAtz4CfYkJohn02qrbVf(AWvQuRtR$@F7Se(96jW z!bC4rCFfTwLnbSl!YpI_QD`V(JnPT@Mqjd|e~1i9ruT(rAeBea+drCStdJjYysEj) ziD24vDa>nsL!9H|gr#QJg!K5ZWOB7SB`cc7BC-U%C^wzcR3FdW_G7Rm9|#iw^3R&2FQsWMYqrm(Ya#!?shNC(h? z5mfc_Li0-qxk_Mtk}M_I;zr+A4=eZD&TpN86YDV3cwP#*ab=y~p6xQ|{h)yBEd${v z$8`y4fnO657$D{2=nYvT2Q`aZOlu8tgm(ErU@hD0eCv<0N%af#QqsH+=fNX6Wz9FBQVIl%A0MBxSxqus9W)*;PwDx6 zsS|a00rH+p<`EaX@)CXM}h4x$zljE!3;jEaHQbT2vN9K zBTFmWAghNQolXCNb@+CJY}jbfb?d5s;pq5+XM-<0 zuUdF0@MZga7imgssucd7Hj?dr8iwW1h3Dv+T>jI)2G+kjezlDCa<(nE!8t=NEJznhp3|lqJ!=gccAC5#&e;{`SkODtaJ2Nf`l4MefD6MNed|QccZ@}A8Wzg+ z-KF-rjRMTku+-*cW@{M=3E1 ztPY|v@fD`3l}a`*o1m6E*kZ@GR&o1i)23GEF@LFK+sa;0zJ;r*jjXgXkS*!^EE1^0 zS4+g<+R=W!V6$?U8E36Rd`xB|6>l}eft2T(+(BBiOV4V2xx1%$DCQ@tk=FNp z7^m})wttX-S8bjzeiJ#q0Bo!&l*^gIf*@R*Yt+F^VVCG6Z-RFwzKU)1CS+O9R4T#5 zk%8KEZ0mhV7!TJ&exe8S^@2OY^=~p`ZMtsGZn^2fqdi4Hgy)&CAbluOUT(w7X@1%O0O4V23=wBe&B*9TdM!Or`f`Ak|JCdTPxk3`BoBxmBq6*k0oouAP^9t1#o`u8o6M~g>>A@oN9cZ z8}DE{?wQ+V8hXY3FOfb22khBevE`t3(BAOGHX5ALDhV}H-F=lM?>;wY z={{Fy>0V&wfy#yY-!K!`Zw#nH1G>{EGM!3yYuZwTcYZSPi9(b2sPn_Ig>jR9FElQF zP^E)uvOO5yxXu4Q-t0w~LPt1WoW2)HOL=pZQ``o_-T2!`WJkkc03~T`Knd>Vgc9f& zms{o;me@&bo3D$V1bGz-4b~KlAJSe%D8Kjt?;XKEY;pw!6mt{uh5-OJLXpwz_bAW` z={TAWdFbE%v(Q1of#*A$q{U2#$6c1k9J8%s4p4aymquP)IkH&slQ{3i)fz2q05r>+ ztWUHw&SWL8t52j984qw$4e!=5$&EY*NZ>Mo4#anpw7(s=0hf)41gN&vO}lCqJ$HTc zfd)y$%HO6jd|iIhQ(b@mX)~k>1& zU;#brk1**!PY#oj8_DoVd=|!D8-EQn{7fVESr8qPs?OT}f&df_REN%oO|sUTPanSh zI`vUG*NEo0WG>btK;&D7Sk`N}9$js%w7-haECerKQ8Or-il2By%pJUkvT+2h#he{* z;{wtKb?TqYDIQ)#vKa8B0oNFdj6kT_%yVGh>jfFbZ(a zJAE(;8C=INw4m6twC&zr43U;|8FKlA36^e>tS+PZ7nG-F!4>Fz2EEjiwIW!Nom76d~;!E7Q>9GbI6Z1na#F% zO?fN?HhS(uIPmt?pYr21HXyBk1i%z%a^B#K=3gPE>8L_gLQj)lFSHEpm`iVqhd^i> zx5*jOw{TzQqX7(|y}*$3A{SmoDAd+?;nz9mXKD!NbzmAYQEMj>5nutwJvmBYSp zb0%bb@q`;;m+(qf?eeYC_(NU zv1Rbo?HdpP#FOv=OqMAcU-}Ibon@-SaA&4oODvgdw1JHCkFPJZznNg=NDq7I}=&_pJURpYJUERG`8DpxY|Sr|Zr z7V}OpalHBSr?-vWrKe!5ZjVB{sz~b~8Qv1>ui-S?5aUnw-me}=t*Ql2RX#DL(5Tk< z7Zw@_+S$)Fg)0{{EIOlARFK3`k4*#2$g7UrLm`alGI9>%W-OcP1b97$cYzS9aHVj_ z>1xK#PWZ`rU$iCKcp2;a#XH7GP#-88Te`j2sCe$4pjb1nM&-@CXA(6__DRa6grD83 zrQ5ly9b=c4=Mqa4_em`tEdyPkvUUa_Xvz1s1ktYLz6J^?l6wuen}J(PCZ|Wyi6NJX z^xgf_{KT&=%LF8e6f?9kZ(~Szr8HcMp9>=z)!#bs*jeRGefep%MB<;8bkop7!JKEr zS2;jUezLlc7{gj@{k6h&>6vw|&JXW#K7-OeiaWl^($goKr7k3!8JZc_9@tEOpZuj$ zss$v?)Hw=41gfeUjtxL4#lWC-YeIMh4{2EhfDZ%#*52OQ!=(|5I?j?FW^0VB+|+`n zTpt7WKzhmV2J zq79^$MJImqsy7+n7XzqPYyhT=9XlmaiFrR1rD=d7`OUH3TY%u(jAT{$^rrGB{yv=L z!Z<-?*MmD>B9QS{Wr~4`)&_eeP(P#V^8@L+SC{YB#{JJsqmnW`VYSkGPP?~08Cx!< z2=lIR9|~H%c)}jmLvCmk$#^pHJEi?*%O_v+Jq$abvLi0{M;6>S&S2~JtK4gGdb&yn zQC}N?K6xgUd^4n=(+c|Y0$J)Jg zWv)-4o;1l#>$UpyZcui!!8oYjCtWutmnpki%U3cUlW}fm|8%5coW*?x!%631d8lHt zR?0SF%^jTCQg$j{#>u!`a|s!PVB$Ds#(d1#(O_?F3FI#D{_`c8&Q{2!@@nFX-VyPUKmftf%($4xZlO(W%y_q z4&C>YIlC5->+pmxCSNBt8ipV(U632Og^Lv4DVMXMB`saO&A7{g}wc7U*k>d{WeH{%OEk?|>sAs-g*Wa5aPj|1JiUn)BDijBux{Vf6GU*m|zq;H! zs})oMi?V@e8doHL6^E==0!=XBkh5+KhV-pVY=rV_%AHc z1&HKPS>h9aLE5&H}(%%)tugg2E#hN|2@4NVUr@|fm^Aa33Pa&EF2Z4nLPvy=T4rj?A6TzBk6VYidu%|k$iEoeC`seHu*v&Z2yw1V8c;Oj@pi14tWi$t(XuS* zMc;YXe~`U@UYe}~Rdpkn*x1I1^F^09xmc?Sk$~dM)epd1|FR--L_#A$n68AY2*bwmf-wLIO&A%Nu?dFU;;_Mt_*5(GW1g9 z6Lwp+;q|kRgJwJV<@U1Fb9$K}n~HW)sJvdgJhs0|-Ldz)R8JAsd$==B4^K|ci`@&Q zA&Sd2HeYq0E>ZD>=wP%@NPq;{J}h3$kJ#k*(!8;x+No|7)F+@QvLZO=VX(<5to(&| zNj$fnQMywHhLfBCiLl@MX`PSWepSpAnnZLGrsBwEdS9%XBF7Wh=45U4mhS_Y8X#qM zAs6o$WEf`-_jj-B-;F5Wv(C~}*YJ{Y&o0qY-a33K5 z1cBl#>ZQoUZ=znpVR42TG@$7k@bDb5J=TDl$={;_?L7fpp*ZQwZfY}*c%md={u7<& zd|eeFq83H_-jVHv>|D|;!t{A|Oa<%1=ZHKBc&Cer;yv>V!G{AQlx_~Ai>O&(FvVK{ z=%kub?W7u;gU^2MphmaW@xMg^g%&)|0w(w0LLN=+#nxNDs|OcF{C;KyrU>QNkf1L| zq_f)Z2f^VA_Z-@W-6K!vC8poF-EE5VKBy2(w=_f3%xFQuX>|>C+N0S$Q3PNPW66!N zhC?|M?~iKx5Tne~qPk9Pm}>TdIN{pbEKe^{A6rvd zFJmt=H}VIzavG4vQ#<0YOECuZa?JH1y&u>JD7ZmX5)q=?$sES%E8^&@d807AKN$_7 z-W9d*!#X;5R%xaIjBJfK8$l|GYLf#O#@D`Z(z7A7(}O}u06;)dkxSvg?;$cYbZ)A5 zJ7i}9$=TbZNDJTLnTxx#ci1wyVKjR6?S3Ucy>X7z%ZM(hG*`bJmxH!gr@I)cFU@Rh z&f-&#+ZEegq?rsJJZnmQa>_e}6Kwkw?k15|y{SNtylOJWQsGri7_Lk~ zDc`vrFM}f2AKrkyhMN5pcPa%FQUk-{TK{>qu?3~*D(mh)9kux7(y%kSgCF%(wzFJB z&B7Kys{*|A0K`~9$zu=SaV z=jVopg0pu9V8grMKjEj7Amf=KSEQPFkDKlX;%1&TS^Z~NfF>cM1xexv>~{6`x;*Wv za9^_e&FrMztD87npaB+ON6+TUt}cxOKkOLeUyDK*mCeo4Ezb+&J5ESaXumY=zg|c( z;(IeSWwVICFZd7+XncWgkUf=sGF%4VTNh)Zi_s{J8RwVO3WqBLVk-%+Zn^}b8$UbfDAdf1g!Ftv-LcGe$a1K#|S zMl6%J#H_v6%$toZ)-^N0%8lrn^7vxhgC_OaQ3$SS<67V`wH(}gZP9X;&4qQ_WYopC z?^c`x#bkAy{P}uw)(h1g;HYN&dxo~))sR(5o=#S&DOULo?@K+n)Au7q0Mi#PQPO4; z_9BTjS}8O*mIO)I4-)qmrHe?Uin#0)uan#^e`!5;{{$xldwII?6`Z8r(QeIoLt-HNk!*i4cR+^a3dfe{@|%0e2P@*aPTbDpYMOR38Q(*1+ze*vM53Pe|P2|aZ}Tns)%B#`0bzx+_N2YFlNmZ(o_e8-N!;Y)!L z(o*;;-z_HL`gCJRak7x@;LCn2#n_spfH*QjciaJ}y?(c~y7XqgkK+IiV7)IB{t&>$xcz^S8~h>rt(TCFl$HX#+hc}EpSo^A*}LHLVs z)fC&$6%{j{ab_r1y!iE#LsbV29)+E;@xlCch2PRLrx*MDWh0{>$2EqLqiyzI&)k~O z^L5?9Il}8*@iTU*L6hkG2Nf`-8y`n*Sc6g}FizQpljX0Si5`1cA7RS6ZFUO4H|r|7 zu)~KQk?EE|KKvFny2k2p^6OKZL^#BSYFgg(p^F&Sumqfmvk)yMPEg>I!9~Gec$44D z@vpJkfZ(}T>C)`V-wvIH(TF*4XWhOl{$sw5;I>rB)%#c!e5M!Irv&BV{?8Dyq(@*% z{CQ{^bn#8Uvkn~bFbH)z5ocz|Mqd&f)7zsinLq~Aid|g>!9ILwyHjxregYF)3VK3h zmM%vbJXN0k>)SkJ_}6in2Fb%&rB22pXTF0Q)%mP$EVXkgSt?SOyLf#wDild&Z*v=h zSGHVUM8rlwo_(UvlOu|-r=g>RM^_02f?NY?Nsx@^Zb@y{+I{*42Y&9+imZJk*V}&g zdFeg@)(DJtapKNuE9uy4e#vX_R7;8g*=;M^h^vwA-3n*F=-W8FHd;`B)bH{mp9YkC zZ;}k2iay)k-X6}^^V@X%JzmV+ReX{`a`(F!6iPw~WvncP!o@NRBD~37z9g$>CJKEh zOrXf6-&#Un8rL0J+48kkB;b}A=B-f@anib}!XB+%Xpp|w^ zr2QlqEy=g?t6tgs6)#TDg)`5s%jU`KQD~Y;Gi$~!&!$l_37Othm=}51n}D(b9VcCw zQtS6YI6>Ap#m~aaJq;N{f5q%Ffba_#56L(D$u|m(|6LJ*R||z+BHOWq#pS%ZTsWka zLTO$xb_b*5zjD0bexmutRP99B0@rh4aZ;=at|Q4nTeNr8w;K7^)kX0Jw$4h<{ZsJ{ z%<3IA6Z8ePcfmhxOJ-3qI`*{9FojW8P+T`+XHn|JR zGT|-?`hqhhc*NS3VA0s=`+m>nd7mGBeD;Vd-HJG88Xs8>9QloMVh-6PV%FL% zpt#~Y!Ay8E0E_?s3ny21+}^)ZKdX3%qDNrMyTZA{3mo_Yg(>{6AcWl1N;(uv0~Aa< zh&X5Xd3VYf1nU-Kiub4}cCz+Aw%5*IrfC>u6Glm4DC>yqwNj9^92YOuDpAP;1N{!eJEDA`}vmWYCqrq6K7_hN-(MH{qrk7boHw)dn5mV`lQr5d<;xS*J0bpXc{ z`qe-WCCA7O*Zl5fN1mr`4f8pmKE|JYl=hnUzs}dD8FbR95dOFDg9Qdt2`)@=-~mL- zUt##4951E?1CdYq^LyUEfS0e}!+M8dzi0iEVsgQi7sCA^!3hdeX+cXObmV#m!g3uT zd*s7{V+X=T87p8jB@9AIb`U@f=%S=z$lPHVqvpBc5QEP5y*9l*K40V<*jnX`z|Nzu z=aki@`4614WnkWf;QIGKa<|A;$gIXH!Z zb$EZdF#P&Bg>DHM|2ABeyhsVE4^$lOTb>d9BJ(K!SRpt$-!C{;aaG~brV;&J)BB6{ zgDiUjO4qI7wVR zb|D99aJ0werD6Qzso-Ah$GfmRtURTyAI(AU!Jw8#Ezd{vk9op_m+KOvz2%2WPD|rY z{l-0YYt^?^i)n7{iEgvecP%TMs{g~TmH)eBu}0QZ$d45x6>dKCfpgO&0QG+guC@wF z)8oC#XpKvVW~t-ibz7F_ZS*5gTj2S?MiTA)fnsnko${0bs2*F4bBqaCO|g_S|3_uu zf8oVp?xD(t;in>1o}W)$DtG#Z#7>ji|?FUA!0f;Y$JGY4p=@K$0ddrpGaM72J73yQwb~UgRS@yGV!D(a|0?+ZN=Rrh?w&T*aWJy zjU6n0BW6?ePdkEZ{Qtd{03R*_Xz{#GFjuP2%$Jp>8=%$POXvvDlXBof{)cVCnEAFtj>YpIQ76tsIWmr)BiRb;HOZs?l(IVDl_zn2IQ0!o1@&Er?KhFu zb!=TJrX!>;PfxyrH0dQmocc3c-aV~U=n(t(EEKqH6!ks9trTNyO!@UsN)<~Fu7B8Y zkTVGh0%8$xHN3)Id5YS0;WM+!;yM4-VtU{biC#3`CkY`}{y+HIqRe<#s@So1n8 zh$pJ!mJ*Li9qD9~RX^+*0*YuHOd`u!j;L~Qk7^dtf$<;K_TL>sXz&aA}@u**$CJFzKiU?N|ArEl>az|CoO+jhPDD}M2GS3t_c(9-w{BRzQJ?* z3u4`O^@>#{SuCV~{D=>3aQ99}1SR&{=g@Ed!-L^2J4m6x!~1Rg4MB3Y94I3kZ&O-D m;cu?{r_{zSUWjlfjPS&CrB8VI2L6uz6B!8w@lsL!fd2zC$c;k) literal 0 HcmV?d00001 diff --git a/ghidra_scripts/gimport/struct_import.png b/ghidra_scripts/gimport/struct_import.png new file mode 100644 index 0000000000000000000000000000000000000000..5e6c073ff04a855da461cff2a2cff36dce62f6b9 GIT binary patch literal 9358 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%0A|SG^Lcbu!rS7ymCaoNA}68& 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 Q^A@0{q7Bbcy8q;V0GqPYBme*a literal 0 HcmV?d00001 From cf2f9cf6c78917cd5c0d21b3bcba1136fbd135f7 Mon Sep 17 00:00:00 2001 From: Mark Langen Date: Thu, 22 May 2025 00:00:04 -0700 Subject: [PATCH 2/3] Update instructions to include installing python package --- ghidra_scripts/README.md | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/ghidra_scripts/README.md b/ghidra_scripts/README.md index 69c0bfe81..a5929ca6e 100644 --- a/ghidra_scripts/README.md +++ b/ghidra_scripts/README.md @@ -44,7 +44,15 @@ We need to give Ghidra the ability to run Python 3 code, we do this with the Ghi Follow the installation instructions on that page. You probably don't need to create a venv in this case, but you do need to run `ghidrathon_configure.py`. -### Step 5: Add Script Directory +### Step 5: Install Importer Script Dependencies + +The importer script has a single additional Python package dependency on `elftools` to parse the elf file. Install it with the following command: + +```bash +pip install pyelftools +``` + +### Step 6: Add Script Directory In Ghidra, `Window > Script Manager` to open the script manager. This is what we ill use to run the script. @@ -52,13 +60,21 @@ In the script manager, at the top right, click the "Manage Script Directories" b Click `+` at the top right of the script manager, and add `bfbb/ghidra_scripts` to the list of script directories. -### Step 6: Run the Importer +### Step 7: Run the Importer In the Script Manager, you should now be able to filter for `bfbb_import.py`. Select it and run it through the context menu or the run button at the top of the Script Manager. Importing will take as long as a clean build does because we temporarily have to make a debug build of the executable to get the parameter names and other info from already reverse engineered functions (the script will restore your previous build settings after doing so) -### Step 7: Enjoy The Results +### Step 8: (Optionally) Change Additional Files to Matching + +The importer script only imports functions and types referenced in files linked into the final DOL file the bulid generates. To generate matching DOLs, the build normally only links compilation units which are 100% matching. + +If you're working on a cpp file with structures you want to import into Ghidra, you're not bound by this limitation! As long as enough contents are defined in the file you're working on for it to link you can import things from it. + +Temporarily change the file in question to "Matching" in `configure.py`, and re-run the importer. Note that if you build with the file changed to Matching when it is not a 100% match yet, this will give you a "not matching" error at the end of the build. That's expected: The import will still be able to import the symbols correctly regardless because it uses the memory mapping in symbols.txt. + +### Step 9: Enjoy The Results Most functions should now have name / parameter info rather than just being FUN_xxxxxxxx. No more having to look stuff up in symbols.txt! From d0e4b4392d5edbdaffb390c73aeeb8df88c32b88 Mon Sep 17 00:00:00 2001 From: Mark Langen Date: Mon, 26 May 2025 01:51:10 -0700 Subject: [PATCH 3/3] Update README.md --- ghidra_scripts/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghidra_scripts/README.md b/ghidra_scripts/README.md index a5929ca6e..bf4552976 100644 --- a/ghidra_scripts/README.md +++ b/ghidra_scripts/README.md @@ -68,7 +68,7 @@ Importing will take as long as a clean build does because we temporarily have to ### Step 8: (Optionally) Change Additional Files to Matching -The importer script only imports functions and types referenced in files linked into the final DOL file the bulid generates. To generate matching DOLs, the build normally only links compilation units which are 100% matching. +The importer script only imports types referenced in files linked into the final DOL file the bulid generates. To generate matching DOLs, the build normally only links compilation units which are 100% matching. If you're working on a cpp file with structures you want to import into Ghidra, you're not bound by this limitation! As long as enough contents are defined in the file you're working on for it to link you can import things from it.