Structs in disassembly
Compiled code has no field names. A struct turns into a base pointer plus fixed offsets, and recovering the struct layout is one of the most useful things you can do while reversing. Once a decompiler knows the struct, everything that uses it becomes readable.
Layout and padding
struct packet {
unsigned char type;
unsigned short length;
unsigned int seq;
char *data;
};Each field is aligned to its own size, so the compiler inserts padding:
offset 0x0 type 1 byte
offset 0x1 (padding, 1 byte)
offset 0x2 length 2 bytes
offset 0x4 seq 4 bytes
offset 0x8 data 8 bytes
size 0x10 (16 bytes)The rule on x86-64: a field of size n starts at a multiple of n (up to 8), and the whole struct's size is rounded up to a multiple of its largest member. Reordering fields can change the size: putting type last would still give 16 here, but char, long, char is 24 bytes while long, char, char is 16.
Field access
unsigned int next_seq(const struct packet *p)
{
return p->seq + p->length;
}movzx eax, WORD PTR [rdi+0x2] ; p->length (unsigned short)
add eax, DWORD PTR [rdi+0x4] ; + p->seq
retRead it backwards: rdi is a pointer (first argument). Something 2 bytes wide lives at offset 2 and is zero-extended (movzx), so it's unsigned. Something 4 bytes wide lives at offset 4. That's already a partial struct definition.
Arrays of structs
void set_type(struct packet *p, int i, unsigned char t)
{
p[i].type = t;
}movsxd rsi, esi ; i, sign-extended
shl rsi, 0x4 ; i * 16
mov BYTE PTR [rdi+rsi*1], dl ; p[i].type = t
retThe index is multiplied by 16, the size of the struct, and then the field offset (0 here) is added. So i * size + offset is the signature of an array of structs: the multiplier gives you the struct's size directly. Compilers often use a shift (shl 4 = ×16) or lea/imul for other sizes.
Rebuilding a struct
A practical method:
- Pick a pointer that's used with several different offsets, like
rdiabove. - List every access: offset, width (
BYTE/WORD/DWORD/QWORD), and signed or unsigned (movsxvsmovzx). - If a field is passed to another function or dereferenced again (
mov rax, [rdi+0x8]then[rax]), it's a pointer: note what it points to. - Find the size from
malloc/calloccalls or from array indexing like theshl 4above. - Fill the gaps with padding and give the fields provisional names.
Then create the struct in your tool. In Ghidra: Data Type Manager → New → Structure, or right-click a pointer variable in the decompiler and choose Auto Create Structure, which does steps 1 to 3 for you. In IDA: the Local Types window (Shift+F1) or Create new struct type. Retype the pointer and the decompiler output switches from *(int *)(param_1 + 4) to p->seq.
Common surprises
- Bitfields show up as an
and/shron one field rather than separate offsets. - C++ classes with virtual functions have a hidden pointer at offset 0: the vtable.
- Packed structs (
#pragma pack(1), common in file formats and network protocols) have no padding, so fields can sit at odd offsets.