#!/usr/bin/env python3 # ABOUT ME # Disassembles methods of named classes in Cosmoteer.dll to readable CIL, with # call/field/string operands resolved to names. Exists because the wiki # documents none of the component internals: this is how the mod learned that # PartThresholdToggle compares plain floats (no percent logic) and that a # NetworkResourceInput's Value is the free fraction of the connected network. # # Usage: # python3 tools/ildump.py [method-substring[,...]] # # python3 tools/ildump.py Bin/Cosmoteer.dll PartThresholdToggle OnAmountChanged # python3 tools/ildump.py Bin/Cosmoteer.dll PartNetworkResourceInput get_Value # python3 tools/ildump.py Bin/Cosmoteer.dll BaseResourceStorageRules .ctor # field defaults # # Needs the pure-Python packages dnfile (0.18.0) and dncil (1.0.2), plus pefile. # Set ILDUMP_PKGS to a directory holding unpacked wheels if they are not # installed system-wide. tools/dotnet_enum.py answers the cheaper question # "which fields does this Rules class have" without any dependency. import os import sys if os.environ.get("ILDUMP_PKGS"): sys.path.insert(0, os.environ["ILDUMP_PKGS"]) import dnfile # noqa: E402 from dncil.cil.body import CilMethodBody # noqa: E402 from dncil.cil.body.reader import CilMethodBodyReaderBase # noqa: E402 TABLES = { 0x01: "TypeRef", 0x02: "TypeDef", 0x04: "Field", 0x06: "MethodDef", 0x0A: "MemberRef", 0x1B: "TypeSpec", 0x2B: "MethodSpec", } class _Reader(CilMethodBodyReaderBase): def __init__(self, pe, row): self.pe = pe self.off = pe.get_offset_from_rva(row.Rva) def read(self, n): data = self.pe.get_data(self.pe.get_rva_from_offset(self.off), n) self.off += n return data def tell(self): return self.off def seek(self, offset): self.off = offset return offset class Dumper: def __init__(self, dll_path): self.pe = dnfile.dnPE(dll_path) self.method_owner = {} for tdef in self.pe.net.mdtables.TypeDef.rows: for method in tdef.MethodList: self.method_owner[id(method.row)] = str(tdef.TypeName) def resolve(self, token): value = token.value if hasattr(token, "value") else int(token) table, rid = value >> 24, value & 0xFFFFFF if table == 0x70: try: return repr(self.pe.net.user_strings.get(rid).value) except Exception: return f"str#{rid}" name = TABLES.get(table) if name is None: return f"tok{value:08x}" try: row = getattr(self.pe.net.mdtables, name).rows[rid - 1] except Exception: return f"{name}#{rid}" if name == "MethodDef": return f"{self.method_owner.get(id(row), '?')}::{row.Name}" if name == "MemberRef": return f"{getattr(row.Class.row, 'TypeName', '?')}::{row.Name}" if name == "Field": return f"fld {row.Name}" if name in ("TypeDef", "TypeRef"): return f"type {row.TypeName}" return name def find_type(self, type_name): for tdef in self.pe.net.mdtables.TypeDef.rows: if str(tdef.TypeName) == type_name: return tdef return None def dump(self, type_name, only=None, out=sys.stdout): tdef = self.find_type(type_name) if tdef is None: print(f"no type {type_name}", file=out) return parent = getattr(tdef.Extends.row, "TypeName", "?") print(f"=== {type_name} extends {parent}", file=out) print("fields:", [str(f.row.Name) for f in tdef.FieldList], file=out) for method in (m.row for m in tdef.MethodList): name = str(method.Name) if only and not any(o in name for o in only): continue print(f"--- {name}", file=out) if not method.Rva: continue try: body = CilMethodBody(_Reader(self.pe, method)) except Exception as error: print(f" (no body: {error})", file=out) continue for ins in body.instructions: operand = ins.operand if type(operand).__name__.endswith("Token"): operand = self.resolve(operand) shown = "" if operand is None else operand print(f" {ins.offset:04x} {ins.opcode.name} {shown}", file=out) def main(argv): if len(argv) < 3: print(__doc__ or "usage: ildump.py [method[,method]]") return 2 dumper = Dumper(argv[1]) only = argv[3].split(",") if len(argv) > 3 else None for type_name in argv[2].split(","): dumper.dump(type_name, only) return 0 if __name__ == "__main__": sys.exit(main(sys.argv))