This is something I always found voodoo, figuring out how to recognize structures in decompiled binary code.
Table of contents
- Introduction
- Inspecting the inflatePrime() function
- Recognizable struct patterns
- Creating the structures
- Comparing with original code
- Note on alignment and padding
Introduction
A struct is a data structure in C/C++ that groups variables of different types.
I will be using BinaryNinja in this demonstration.

I chose libz.1.3.1.dylib and also had access to the source code via https://github.com/madler/zlib/blob/develop/inflate.c.
We are going to be working with a 64-bit library, so a pointer is 8 bytes, uint64_t is 8 bytes, and a uint32_t is 4 bytes.
file libz.dylibΒ
libz.dylib: Mach-O 64-bit dynamically linked shared library arm64
Inspecting the inflatePrime() function
I chose (randomly) the inflatePrime function from the inflate.c file. The structure definitions are going to be in the inflate.h header file.
Decompiled code

This decompiled code looks complicated, with lots of memory offset access, and many variables that are just making comprehension of what is going on more difficult. So to clean it up we will identify structures and create them using the correct struct members and offsets.
Recognizable struct patterns
Let’s start by pointing out some recognizable struct patterns.
void* x8_1 = *(uint64_t*)((char*)arg1 + 0x38);
βGo to the memory address (arg1 + 0x38 bytes), read the 8 bytes stored there, and return that 8-byte value as a number.β β Later on, we will uncover that the 8 bytes value is actually an address to another structure π.
Arg1 is a pointer to something (could be a structure or just a buffer). The fact that we have a fixed offset memory access like + 0x38 is already a hint that we are looking at a structure member.
Additionally, compilers refer to fields inside structs using fixed offsets after layout is decided. If we keep reading the code, we also notice how there are multiple consistent offsets (0x38, 0x50, 0x58), which could also suggest a defined memory layout, just like struct members.
A structure in C
struct Example {
int a; // offset 0x00
char b[4]; // offset 0x04
void *ptr; // offset 0x08
};
Decompiled
*(int*)((char*)arg1 + 0x00) // member 'a'
*(char*)((char*)arg1 + 0x04) // member 'b'
*(uint64_t*)((char*)arg1 + 0x08) // member 'ptr'
If we return to the block of code (I renamed x8_1 to ptr_Struct), we can see the same pattern we noticed earlier. So it seems as though ptr_Struct points to another structure!

Creating the structures
Let’s then create two structs. One for arg1, and one for ptr_Struct.
Create struct_1 of size 0x40, because at offset 0x38 is our pointer to struct_2, and a struct pointer is 8 bytes, so 0x40 (0x38 = 56 bytes ; 0x40 = 64 bytes).

In struct_2, we know that we have a uint32_t at offset 0x58, so adding 4 bytes (sizeof(uint32_t)) we have a struct size of 0x5c. We don’t specifically know the actual size of struct_2, but for now we can guess that it will be 0x5c, and we can always adjust it later on if needed.


We add padding until offset 0x38, because we don’t know yet what will reside in this struct, but we do know that at offset 0x38 we have a pointer to another struct.
In our struct_2, we know we have two members, one uint64_t at offset 0x50, and one uint32_t at offset 0x58.

Now that we’ve created our two structs, our code looks a lot cleaner !

Over time, you will be able to understand what each struct member does, and you will be able to rename them appropriately. I didn’t do it here because the purpose of this exercise was first to understand what a struct looks like in a decompiled code.
Here is a visual layout showing how arg1 at offset 0x38 contains another pointer to struct_2.

Comparing with original code

By comparing our decompiled code, we notice z_streamp strm as our arg1, which in the zlib manual is a pointer to struct z_stream_s.
typedef struct z_stream_s {
[...]
struct internal_state FAR *state; /* not visible by applications */
[...]
} z_stream;
typedef z_stream FAR *z_streamp;
So, in the function defintion of int ZEXPORT inflatePrime(z_streamp strm, int bits, int value), the z_streamp argument can be written as int ZEXPORT inflateInit(struct z_stream_s *strm, int bits, int value);
Note on alignment and padding
Compilers sometimes add extra βpaddingβ bytes between struct members to make data align better in memory. This means that the offsets you see in a binary might not exactly match the sizes of the fields you expect. Knowing this can help you accurately map memory when reverse engineering.
struct Example {
char a; // 1 byte
int b; // 4 bytes
};
Address: 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07
Field: a pad pad pad b b b b

Leave a comment