Sparse, offset-based C/C++ structs

Often when reversing software it is useful to be able to create binary-compatible interfaces to the code being reversed. This can be challenging or just annoying since you may not have reversed complete data types for whatever reason.

Here I'll show a helpful macro (based on an idea from comex) which you may use to easily define sparse structures with properly-typed fields at specific offsets.

First, a minimal example of usage:

OSTRUCT(example, 0x120)  
OFIELD(0x10, u64 field10);  
OFIELD(0x100, struct some_type field100);  
OSTRUCT_END  

This defines a struct example type, which will be 0x120 bytes and have 2 members accessible by name: a u64 at offset 0x10, and a struct some_type at offset 0x100.

The (admittedly simple) magic behind the scenes:

#define CAT_(x, y) x ## y
#define CAT(x, y) CAT_(x, y)

#define OPAD(size) u8 CAT(_pad_, __COUNTER__)[size]
#define OSTRUCT(name, size) struct name { union { OPAD(size);
#define OSTRUCT_END };};
#define OFIELD(off, field) struct { OPAD(off); field; }

As you can see, the method works by inserting each named field preceeded by a uniquely named padding array. All fields are encapsulated in unions, which the compiler will "squish" together to create the final view of the struct.

Helper Macros

In the example above, struct some_type may be larger than 0x20 bytes. In this case, the final struct size will be larger than what you intend. I have some handy macros for such scenarios, as well:

#define ASSERT_STRSIZE(struc, size) \
    _Static_assert(sizeof( struc ) == (size), "size of " #struc " != " #size )
#define ASSERT_STROFF(struc, member, offset) \
    _Static_assert(offsetof( struc , member ) == (offset), "offset of " #struc "." #member " != " #offset )

You may merge ASSERT_STRSIZE in with the OSTRUCTx macros in order to enforce the check on all OSTRUCTs, however I personally only sprinkle them where necessary.

Other Notes

I've found this method to be especially helpful if you are targeting multiple versions of the binary interface you're reversing, and structures are changed slightly between versions:

#if COMPAT_VER == 1
#define example_sizeof 0x50
#define example_field_o 0x10
#elif COMPAT_VER == 2
#define example_sizeof 0x70
#define example_field_o 0x18
#endif
OSTRUCT(example, example_sizeof)  
OFIELD(example_field_o, u64 field);  
OSTRUCT_END  

...and etc, I'm sure there are many ways to abuse it such that it fits your needs :)