Ghidra Import: Support virtual inheritance (#1071)

* Implement/fix Ghidra imports for multiple and virtual inheritance

Unfortunately, the handling in Ghidra is still far from perfect. This is a good place to start, though.

* Support offsets in vbase pointers

* Support `this adjust`

* minor stylistic improvement

* Improvements to documentation

---------

Co-authored-by: jonschz <jonschz@users.noreply.github.com>
This commit is contained in:
jonschz
2024-07-30 19:02:15 +02:00
committed by GitHub
parent 1f251ff817
commit 412200ecbc
6 changed files with 549 additions and 72 deletions

View File

@@ -1,3 +1,4 @@
from dataclasses import dataclass
import re
import logging
from typing import Any, Dict, List, NamedTuple, Optional
@@ -26,6 +27,19 @@ class FieldListItem(NamedTuple):
type: str
@dataclass
class VirtualBaseClass:
type: str
index: int
direct: bool
@dataclass
class VirtualBasePointer:
vboffset: int
bases: list[VirtualBaseClass]
class ScalarType(NamedTuple):
offset: int
name: Optional[str]
@@ -157,6 +171,16 @@ class CvdumpTypesParser:
r"^\s+list\[\d+\] = LF_BCLASS, (?P<scope>\w+), type = (?P<type>.*), offset = (?P<offset>\d+)"
)
# LF_FIELDLIST virtual direct/indirect base pointer, line 1/2
VBCLASS_RE = re.compile(
r"^\s+list\[\d+\] = LF_(?P<indirect>I?)VBCLASS, .* base type = (?P<type>.*)$"
)
# LF_FIELDLIST virtual direct/indirect base pointer, line 2/2
VBCLASS_LINE_2_RE = re.compile(
r"^\s+virtual base ptr = .+, vbpoff = (?P<vboffset>\d+), vbind = (?P<vbindex>\d+)$"
)
# LF_FIELDLIST member name (2/2)
MEMBER_RE = re.compile(r"^\s+member name = '(?P<name>.*)'$")
@@ -206,7 +230,7 @@ class CvdumpTypesParser:
re.compile(r"\s*Arg list type = (?P<arg_list_type>[\w()]+)$"),
re.compile(
r"\s*This adjust = (?P<this_adjust>[\w()]+)$"
), # TODO: figure out the meaning
), # By how much the incoming pointers are shifted in virtual inheritance; hex value without `0x` prefix
re.compile(
r"\s*Func attr = (?P<func_attr>[\w()]+)$"
), # Only for completeness, is always `none`
@@ -282,12 +306,12 @@ class CvdumpTypesParser:
members: List[FieldListItem] = []
super_id = field_obj.get("super")
if super_id is not None:
super_ids = field_obj.get("super", [])
for super_id in super_ids:
# May need to resolve forward ref.
superclass = self.get(super_id)
if superclass.members is not None:
members = superclass.members
members += superclass.members
raw_members = field_obj.get("members", [])
members += [
@@ -526,7 +550,57 @@ class CvdumpTypesParser:
# Superclass is set here in the fieldlist rather than in LF_CLASS
elif (match := self.SUPERCLASS_RE.match(line)) is not None:
self._set("super", normalize_type_id(match.group("type")))
superclass_list: dict[str, int] = self.keys[self.last_key].setdefault(
"super", {}
)
superclass_list[normalize_type_id(match.group("type"))] = int(
match.group("offset")
)
# virtual base class (direct or indirect)
elif (match := self.VBCLASS_RE.match(line)) is not None:
virtual_base_pointer = self.keys[self.last_key].setdefault(
"vbase",
VirtualBasePointer(
vboffset=-1, # default to -1 until we parse the correct value
bases=[],
),
)
assert isinstance(
virtual_base_pointer, VirtualBasePointer
) # type checker only
virtual_base_pointer.bases.append(
VirtualBaseClass(
type=match.group("type"),
index=-1, # default to -1 until we parse the correct value
direct=match.group("indirect") != "I",
)
)
elif (match := self.VBCLASS_LINE_2_RE.match(line)) is not None:
virtual_base_pointer = self.keys[self.last_key].get("vbase", None)
assert isinstance(
virtual_base_pointer, VirtualBasePointer
), "Parsed the second line of an (I)VBCLASS without the first one"
vboffset = int(match.group("vboffset"))
if virtual_base_pointer.vboffset == -1:
# default value
virtual_base_pointer.vboffset = vboffset
elif virtual_base_pointer.vboffset != vboffset:
# vboffset is always equal to 4 in our examples. We are not sure if there can be multiple
# virtual base pointers, and if so, how the layout is supposed to look.
# We therefore assume that there is always only one virtual base pointer.
logger.error(
"Unhandled: Found multiple virtual base pointers at offsets %d and %d",
virtual_base_pointer.vboffset,
vboffset,
)
virtual_base_pointer.bases[-1].index = int(match.group("vbindex"))
# these come out of order, and the lists are so short that it's fine to sort them every time
virtual_base_pointer.bases.sort(key=lambda x: x.index)
# Member offset and type given on the first of two lines.
elif (match := self.LIST_RE.match(line)) is not None:
@@ -579,7 +653,7 @@ class CvdumpTypesParser:
else:
logger.error("Unmatched line in arglist: %s", line[:-1])
def read_pointer_line(self, line):
def read_pointer_line(self, line: str):
if (match := self.LF_POINTER_ELEMENT.match(line)) is not None:
self._set("element_type", match.group("element_type"))
else:

View File

@@ -6,6 +6,9 @@ from isledecomp.cvdump.types import (
CvdumpTypesParser,
CvdumpKeyError,
CvdumpIntegrityError,
FieldListItem,
VirtualBaseClass,
VirtualBasePointer,
)
TEST_LINES = """
@@ -245,10 +248,111 @@ NESTED, enum name = JukeBox::JukeBoxScript, UDT(0x00003cc2)
list[12] = LF_MEMBER, private, type = T_USHORT(0021), offset = 12
member name = 'm_length'
0x4dee : Length = 406, Leaf = 0x1203 LF_FIELDLIST
list[0] = LF_VBCLASS, public, direct base type = 0x15EA
virtual base ptr = 0x43E9, vbpoff = 4, vbind = 3
list[1] = LF_IVBCLASS, public, indirect base type = 0x1183
virtual base ptr = 0x43E9, vbpoff = 4, vbind = 1
list[2] = LF_IVBCLASS, public, indirect base type = 0x1468
virtual base ptr = 0x43E9, vbpoff = 4, vbind = 2
list[3] = LF_VFUNCTAB, type = 0x2B95
list[4] = LF_ONEMETHOD, public, VANILLA, index = 0x15C2, name = 'LegoRaceMap'
list[5] = LF_ONEMETHOD, public, VIRTUAL, index = 0x15C3, name = '~LegoRaceMap'
list[6] = LF_ONEMETHOD, public, VIRTUAL, index = 0x15C5, name = 'Notify'
list[7] = LF_ONEMETHOD, public, VIRTUAL, index = 0x15C4, name = 'ParseAction'
list[8] = LF_ONEMETHOD, public, VIRTUAL, index = 0x4DED, name = 'VTable0x70'
list[9] = LF_ONEMETHOD, public, INTRODUCING VIRTUAL, index = 0x15C2,
vfptr offset = 0, name = 'FUN_1005d4b0'
list[10] = LF_MEMBER, private, type = T_UCHAR(0020), offset = 8
member name = 'm_parentClass2Field1'
list[11] = LF_MEMBER, private, type = T_32PVOID(0403), offset = 12
member name = 'm_parentClass2Field2'
0x4def : Length = 34, Leaf = 0x1504 LF_CLASS
# members = 21, field list type 0x4dee, CONSTRUCTOR,
Derivation list type 0x0000, VT shape type 0x12a0
Size = 436, class name = LegoRaceMap, UDT(0x00004def)
0x4db6 : Length = 30, Leaf = 0x1504 LF_CLASS
# members = 16, field list type 0x4db5, CONSTRUCTOR, OVERLOAD,
Derivation list type 0x0000, VT shape type 0x1266
Size = 16, class name = MxString, UDT(0x00004db6)
0x5591 : Length = 570, Leaf = 0x1203 LF_FIELDLIST
list[0] = LF_VBCLASS, public, direct base type = 0x15EA
virtual base ptr = 0x43E9, vbpoff = 4, vbind = 3
list[1] = LF_IVBCLASS, public, indirect base type = 0x1183
virtual base ptr = 0x43E9, vbpoff = 4, vbind = 1
list[2] = LF_IVBCLASS, public, indirect base type = 0x1468
virtual base ptr = 0x43E9, vbpoff = 4, vbind = 2
list[3] = LF_VFUNCTAB, type = 0x4E11
list[4] = LF_ONEMETHOD, public, VANILLA, index = 0x1ABD, name = 'LegoCarRaceActor'
list[5] = LF_ONEMETHOD, public, VIRTUAL, index = 0x1AE0, name = 'ClassName'
list[6] = LF_ONEMETHOD, public, VIRTUAL, index = 0x1AE1, name = 'IsA'
list[7] = LF_ONEMETHOD, public, VIRTUAL, index = 0x1ADD, name = 'VTable0x6c'
list[8] = LF_ONEMETHOD, public, VIRTUAL, index = 0x1ADB, name = 'VTable0x70'
list[9] = LF_ONEMETHOD, public, VIRTUAL, index = 0x1ADA, name = 'SwitchBoundary'
list[10] = LF_ONEMETHOD, public, VIRTUAL, index = 0x1ADC, name = 'VTable0x9c'
list[11] = LF_ONEMETHOD, public, INTRODUCING VIRTUAL, index = 0x558E,
vfptr offset = 0, name = 'FUN_10080590'
list[12] = LF_ONEMETHOD, public, INTRODUCING VIRTUAL, index = 0x1AD8,
vfptr offset = 4, name = 'FUN_10012bb0'
list[13] = LF_ONEMETHOD, public, INTRODUCING VIRTUAL, index = 0x1AD9,
vfptr offset = 8, name = 'FUN_10012bc0'
list[14] = LF_ONEMETHOD, public, INTRODUCING VIRTUAL, index = 0x1AD8,
vfptr offset = 12, name = 'FUN_10012bd0'
list[15] = LF_ONEMETHOD, public, INTRODUCING VIRTUAL, index = 0x1AD9,
vfptr offset = 16, name = 'FUN_10012be0'
list[16] = LF_ONEMETHOD, public, INTRODUCING VIRTUAL, index = 0x1AD8,
vfptr offset = 20, name = 'FUN_10012bf0'
list[17] = LF_ONEMETHOD, public, INTRODUCING VIRTUAL, index = 0x1AD9,
vfptr offset = 24, name = 'FUN_10012c00'
list[18] = LF_ONEMETHOD, public, INTRODUCING VIRTUAL, index = 0x1ABD,
vfptr offset = 28, name = 'VTable0x1c'
list[19] = LF_MEMBER, protected, type = T_REAL32(0040), offset = 8
member name = 'm_parentClass1Field1'
list[25] = LF_ONEMETHOD, public, VIRTUAL, (compgenx), index = 0x15D1, name = '~LegoCarRaceActor'
0x5592 : Length = 38, Leaf = 0x1504 LF_CLASS
# members = 26, field list type 0x5591, CONSTRUCTOR,
Derivation list type 0x0000, VT shape type 0x34c7
Size = 416, class name = LegoCarRaceActor, UDT(0x00005592)
0x5593 : Length = 638, Leaf = 0x1203 LF_FIELDLIST
list[0] = LF_BCLASS, public, type = 0x5592, offset = 0
list[1] = LF_BCLASS, public, type = 0x4DEF, offset = 32
list[2] = LF_IVBCLASS, public, indirect base type = 0x1183
virtual base ptr = 0x43E9, vbpoff = 4, vbind = 1
list[3] = LF_IVBCLASS, public, indirect base type = 0x1468
virtual base ptr = 0x43E9, vbpoff = 4, vbind = 2
list[4] = LF_IVBCLASS, public, indirect base type = 0x15EA
virtual base ptr = 0x43E9, vbpoff = 4, vbind = 3
list[5] = LF_ONEMETHOD, public, VANILLA, index = 0x15CD, name = 'LegoRaceCar'
list[6] = LF_ONEMETHOD, public, VIRTUAL, index = 0x15CE, name = '~LegoRaceCar'
list[7] = LF_ONEMETHOD, public, VIRTUAL, index = 0x15D2, name = 'Notify'
list[8] = LF_ONEMETHOD, public, VIRTUAL, index = 0x15E8, name = 'ClassName'
list[9] = LF_ONEMETHOD, public, VIRTUAL, index = 0x15E9, name = 'IsA'
list[10] = LF_ONEMETHOD, public, VIRTUAL, index = 0x15D5, name = 'ParseAction'
list[11] = LF_ONEMETHOD, public, VIRTUAL, index = 0x15D3, name = 'SetWorldSpeed'
list[12] = LF_ONEMETHOD, public, VIRTUAL, index = 0x15DF, name = 'VTable0x6c'
list[13] = LF_ONEMETHOD, public, VIRTUAL, index = 0x15D3, name = 'VTable0x70'
list[14] = LF_ONEMETHOD, public, VIRTUAL, index = 0x15DC, name = 'VTable0x94'
list[15] = LF_ONEMETHOD, public, VIRTUAL, index = 0x15E5, name = 'SwitchBoundary'
list[16] = LF_ONEMETHOD, public, VIRTUAL, index = 0x15DD, name = 'VTable0x9c'
list[17] = LF_ONEMETHOD, public, INTRODUCING VIRTUAL, index = 0x15D4,
vfptr offset = 32, name = 'SetMaxLinearVelocity'
list[18] = LF_ONEMETHOD, public, INTRODUCING VIRTUAL, index = 0x15D4,
vfptr offset = 36, name = 'FUN_10012ff0'
list[19] = LF_ONEMETHOD, public, INTRODUCING VIRTUAL, index = 0x5588,
vfptr offset = 40, name = 'HandleSkeletonKicks'
list[20] = LF_MEMBER, private, type = T_UCHAR(0020), offset = 84
member name = 'm_childClassField'
0x5594 : Length = 34, Leaf = 0x1504 LF_CLASS
# members = 30, field list type 0x5593, CONSTRUCTOR,
Derivation list type 0x0000, VT shape type 0x2d1e
Size = 512, class name = LegoRaceCar, UDT(0x000055bb)
"""
@@ -309,6 +413,31 @@ def test_members(parser: CvdumpTypesParser):
(12, "m_length", "T_USHORT"),
]
# LegoRaceCar with multiple superclasses
assert parser.get("0x5594").members == [
FieldListItem(offset=0, name="vftable", type="T_32PVOID"),
FieldListItem(offset=0, name="vftable", type="T_32PVOID"),
FieldListItem(offset=8, name="m_parentClass1Field1", type="T_REAL32"),
FieldListItem(offset=8, name="m_parentClass2Field1", type="T_UCHAR"),
FieldListItem(offset=12, name="m_parentClass2Field2", type="T_32PVOID"),
FieldListItem(offset=84, name="m_childClassField", type="T_UCHAR"),
]
def test_virtual_base_classes(parser: CvdumpTypesParser):
"""Make sure that virtual base classes are parsed correctly."""
lego_car_race_actor = parser.keys.get("0x5591")
assert lego_car_race_actor is not None
assert lego_car_race_actor["vbase"] == VirtualBasePointer(
vboffset=4,
bases=[
VirtualBaseClass(type="0x1183", index=1, direct=False),
VirtualBaseClass(type="0x1468", index=2, direct=False),
VirtualBaseClass(type="0x15EA", index=3, direct=True),
],
)
def test_members_recursive(parser: CvdumpTypesParser):
"""Make sure that we unwrap the dependency tree correctly."""