Skip to content

Ghidra import: testing various inheritance patterns - #528

Open
disinvite wants to merge 10 commits into
isledecomp:masterfrom
disinvite:ghidra-inheritance-tests
Open

disinvite wants to merge 10 commits into
isledecomp:masterfrom
disinvite:ghidra-inheritance-tests

Conversation

@disinvite

Copy link
Copy Markdown
Collaborator

Part of #106. The Ghidra importer uses the structures from the types db directly. I want to push these behind an API so we can swap out the mechanism that loads debug artifacts. But we need more tests first.

Claude created these sample .cpp files with various inheritance patterns. I compiled each one and generated the cvdump TYPES output with both MSVC 4.2 and 6. (They were identical.) The docstrings and comments are all mine. One of my goals for doing this was to understand how this works and why we can't assume the location of the indirect virtual base class without reading the vbtable. (Certainly doable in a follow-up.)

This corrects an "issue" where we created slim copies of base classes where this was not necessary. (I say "issue" because there was not an observable difference.) For example: if class D has base classes B and C, but only B uses virtual inheritance, we do not need a slim copy of C.

@disinvite
disinvite requested a review from jonschz August 30, 2026 19:39

@jonschz jonschz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice overall! The logic change looks sound to me. This really would have benefitted from unit tests back when I wrote the code.

I really like the cvdump + source code approach. Not sure if there's a good way of keeping them in sync, but the current setting (i.e. try to remember to update both when changing any one of them) will work for me. Building the cvdump files dynamically feels overkill to me.

Comment thread tests/ghidra_integration_test_setup.py Outdated

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a short README.md to the cvdump_sample directory, including

  • how to compile these (toolchain etc.)
  • how to make the .txt files out of them (doesn't need to duplicate the documentation in __init__.py, but should link to it and maybe enhance it)

Comment thread tests/test_ghidra_integration_inheritance.py Outdated
Comment thread tests/test_ghidra_integration_inheritance.py Outdated
Comment thread tests/test_ghidra_integration_inheritance.py Outdated
Comment thread tests/test_ghidra_integration_inheritance.py Outdated
Comment thread tests/test_ghidra_integration_inheritance.py
Comment thread reccmp/ghidra/importer/type_importer.py Outdated
Comment thread reccmp/ghidra/importer/type_importer.py Outdated
Comment thread reccmp/ghidra/importer/type_importer.py Outdated
@disinvite

Copy link
Copy Markdown
Collaborator Author

I rewrote a lot of the docstrings to try and express that:

  1. Direct and indirect virtual inheritance requires that all virtually inherited members appear last in the struct list
  2. We choose to embed the parent structs instead of copying members, but virtual inheritance requires the slim copy of a base class without the gap left for its virtually inherited members

Let me know what you think. There's one reference to "sibling" still in there. By that I only meant that it is another base class at the same level, as opposed to a grandparent class.

@jonschz jonschz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! Feel free to merge after addressing the final comments, no second re-review needed.


Parent classes are embedded in the struct for the derived class instead of
copying their members instead.
By convention, the `vftable` pointer appears at offset 0 of a class with virtual functions.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whose convention? MSVC's?

Comment on lines +5 to +7
To represent inheritance, we create the base class struct (or use one created earlier in the run)
and set its position to the offset given in the PDB fieldlist. The other option is to copy members
from the base classes with new offsets, but we don't do it this way.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's good to explain that, maybe also mention why? From the top of my head, polymorphism is one of the arguments (e.g. calling void f(A a) when you have an instance of class B: A), and also that it's easier to track where each property comes from

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In terms of Ghidra, though, does it make a difference to the decompiler output? (copying the members versus embedding the structs)

Comment on lines +510 to +511
structs get the member. The effect is that C's virtual function is "merged" into A's vtable.
(This may or may not be correct and we should revisit when we create a vtable struct during import.)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Off-topic - I believe the way it works is that C has two vftables:

  • The one inherited from A: This is where A's functions are inherited / overwritten and where C's newly introduced virtual functions go
  • The one inherited from B (at offset 12): This is where B's functions are inherited / overwritten

Let's say I have a function void f(B b). If I call f(c), then a vtordisp is inserted into that function call which introduces an offset of 12. But if f calls any of B's virtual functions, it has to call the one that's overridden by C, so the overrides must take place in B's vftable within C.

In any case, let's not dwell on it, the comment is perfectly fine

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants