.text:0041E800; =============== S U B R O U T I N E ===============
.text:0041E800
.text:0041E800

Structs in disassembly

.text:0041E800; author: Can Ozdemir date: 2025-02-08 size: 0xCF0 tags: x86-64, data-structures, tutorial
.text:0041E800
.text:0041E800sub_41E800procnear
.text:0041E801

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.

.text:0041E905

Layout and padding

.text:0041E91A
struct packet {
    unsigned char  type;
    unsigned short length;
    unsigned int   seq;
    char          *data;
};
.text:0041E991

Each field is aligned to its own size, so the compiler inserts padding:

.text:0041E9D8
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)
.text:0041EA7D

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.

.text:0041EBAE

Field access

.text:0041EBBD
unsigned int next_seq(const struct packet *p)
{
    return p->seq + p->length;
}
.text:0041EC0D
movzx  eax, WORD PTR [rdi+0x2]    ; p->length (unsigned short)
add    eax, DWORD PTR [rdi+0x4]   ; + p->seq
ret
.text:0041EC7C

Read 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.

.text:0041ED68

Arrays of structs

.text:0041ED7C
void set_type(struct packet *p, int i, unsigned char t)
{
    p[i].type = t;
}
.text:0041EDCA
movsxd  rsi, esi                    ; i, sign-extended
shl     rsi, 0x4                    ; i * 16
mov     BYTE PTR [rdi+rsi*1], dl    ; p[i].type = t
ret
.text:0041EE65

The 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.

.text:0041EF8F

Rebuilding a struct

.text:0041EFA5

A practical method:

.text:0041EFB8
  1. Pick a pointer that's used with several different offsets, like rdi above.
  2. List every access: offset, width (BYTE/WORD/DWORD/QWORD), and signed or unsigned (movsx vs movzx).
  3. 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.
  4. Find the size from malloc/calloc calls or from array indexing like the shl 4 above.
  5. Fill the gaps with padding and give the fields provisional names.
.text:0041F1AB

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.

.text:0041F327

Common surprises

.text:0041F33A
  • Bitfields show up as an and/shr on 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.
.text:0041F471
.text:0041F471retn
.text:0041F471sub_41E800endp
.text:0041F471; © 2026 Can Ozdemir. All rights reserved. terms
.text:0041F471
.text:0041F472; -------- cross references --------
Output
Loading ozdc.us… 34 functions, 3 data segments.
Type help in the command line below and press Enter.