← Back to list

Assembly Breakdown: x86–64 structure alignment examples

x64 Structure Alignment in MSVC: What the Compiler Does to Your Structs

Yen Wang in RE: Exploit · 2026-05-07 22:01 · 0 claps · 4.4 min read
#assembly-language #x86-64 #msvc #low-level-programming #struct-alignment
Open on Medium ↗
Wiki topics: SAF · Safety & Alignment 💻 · Programming

Assembly Breakdown: x86–64 structure alignment examples

x64 Structure Alignment in MSVC: What the Compiler Does to Your Structs

When you declare a struct in C, the compiler does not simply pack the members back-to-back in memory. It inserts padding bytes to ensure each member sits at an address that is a multiple of its own size. The x64 ABI formalises this as natural alignment: a short must be 2-byte aligned, an int 4-byte aligned, a double or pointer 8-byte aligned. The struct's total size is then rounded up to a multiple of its largest member's alignment, so that arrays of structs remain correctly aligned at every element.

The Microsoft documentation illustrates this with four concrete examples. A struct containing only a short needs no padding at all -- two bytes, done. Add an int followed by a double followed by a short, and the picture changes: four bytes of padding appear between the int and the double to push the double to an 8-byte boundary, and six bytes of trailing padding follow the short to bring the total to 24. A struct with interleaved char, short, char, and int members accumulates internal padding in two places, wasting 4 bytes out of 12. The union example shows the other side of the coin: all members start at offset 0, the total size is simply the largest member, and smaller members leave the high bytes unused.

Two practical points follow from this.

  • First, member ordering is not neutral. Declaring fields largest-to-smallest eliminates most internal padding; the interleaved-type example shrinks from 12 bytes to 8 if reordered.
  • Second, Rust’s default #[repr(Rust)] layout allows the compiler to reorder fields freely to minimise padding, so a Rust struct in a binary will not necessarily match the source declaration order. Only #[repr(C)] guarantees the C-compatible layout described above, which matters when reconstructing types during reverse engineering.

The four examples demonstrate the two core alignment rules: each member sits at an offset that is a multiple of its own size, and the total struct size is padded to a multiple of the struct’s alignment (which equals the largest member’s alignment).

Let’s review the examples from Microdoft Doc.

Part one is based on Microsoft Doc. Check it out: https://learn.microsoft.com/en-us/cpp/build/x64-software-conventions?view=msvc-170

Example 1: trivial, no padding

_declspec(align(2)) struct {
    short a;      // +0; size = 2 bytes
}
// Total: 2 bytes, alignment: 2 bytes

short requires 2-byte alignment. Placed at offset 0, which satisfies that. Total size 2 is already a multiple of 2. No padding needed anywhere.

Example 2 : internal and trailing padding

_declspec(align(8)) struct {
    int    a;     // +0;  size = 4 bytes
    double b;     // +8;  size = 8 bytes
    short  c;     // +16; size = 2 bytes
}
// Total: 24 bytes, alignment: 8 bytes

a is an int (4 bytes, 4-byte aligned) — placed at offset 0, no issue. b is a double (8 bytes, 8-byte aligned) — the next available offset after a is 4, which is not a multiple of 8, so 4 bytes of padding are inserted at offsets 4–7 to push b to offset 8. c is a short (2 bytes, 2-byte aligned) — placed at offset 16, aligned correctly. That leaves offsets 18–23 as trailing padding: the struct's alignment is 8 (driven by double b), so total size must be a multiple of 8. 18 rounded up to the next multiple of 8 = 24. Six bytes of trailing padding are added.

The trailing padding matters when the struct is used in an array: arr[1] must also start at an 8-byte boundary, which requires each element to be 24 bytes.

Example 3: interleaved padding from mixed types

_declspec(align(4)) struct {
    char  a;      // +0;  size = 1 byte
    short b;      // +2;  size = 2 bytes
    char  c;      // +4;  size = 1 byte
    int   d;      // +8;  size = 4 bytes
}
// Total: 12 bytes, alignment: 4 bytes
Offset │ 0    1    2    3
───────┼────────────────────
       │  a   [p]   b    b

Offset │ 4    5    6    7
───────┼────────────────────
       │  c   [p]  [p]  [p]
Offset │ 8    9   10   11
───────┼────────────────────
       │  d    d    d    d
  • a is a char at offset 0 : 1-byte aligned, no padding needed.
  • b is a short : requires 2-byte alignment; offset 1 is odd, so 1 byte of padding inserted before b, placing it at offset 2.
  • c is a char at offset 4 .
  • d is an int : requires 4-byte alignment; the next offset after c is 5, which is not a multiple of 4, so 3 bytes of padding inserted before d, placing it at offset 8.
  • Total size is 12, which is a multiple of 4 (the struct's alignment). No trailing padding needed.

This example illustrates why member ordering matters: reordering to {int d; short b; char a; char c;} would produce a 8-byte struct with no internal padding at all.

Example 4: union, all members at offset 0

_declspec(align(8)) union {
    char *p;      // +0; size = 8 bytes
    short  s;     // +0; size = 2 bytes
    long   l;     // +0; size = 4 bytes
}
// Total: 8 bytes, alignment: 8 bytes
Offset │ 0    1    2    3    4    5    6    7
───────┼────────────────────────────────────
  p    │  p    p    p    p    p    p    p    p   (all 8 bytes)
  s    │  s    s   [p]  [p]  [p]  [p]  [p]  [p] (2 bytes + 6 padding)
  l    │  l    l    l    l   [p]  [p]  [p]  [p] (4 bytes + 4 padding)

All union members share offset 0 — that is the defining property of a union. The total size is determined by the largest member (char* = 8 bytes on x64), and the alignment is determined by the largest alignment requirement, also 8 (char* is a pointer = quadword). s and l occupy only the low bytes of the 8-byte slot; the remainder is unused padding within the union's footprint.

RE Relevance

Struct layout rules are directly relevant to binary analysis of Rust code. Rust’s compiler (via LLVM) applies the same natural alignment rules by default for #[repr(C)] types. For #[repr(Rust)] types, the compiler is free to reorder fields to minimise padding — which means a Rust struct in a malware sample will not necessarily have the same field ordering as the declared source, complicating type reconstruction in Binary Ninja or IDA. Recognising padding bytes versus data bytes is essential when recovering struct layouts from MLIL/HLIL.

Microsoft Corporation (2023) x64 ABI conventions. Microsoft Learn. Available at: https://learn.microsoft.com/en-us/cpp/build/x64-software-conventions


메타데이터
post_id
a40a0ef325e5
slug
assembly-breakdown-x86-64-structure-alignment-examples-a40a0ef325e5
url
https://medium.com/re-exploit/assembly-breakdown-x86-64-structure-alignment-examples-a40a0ef325e5
canonical_url
https://medium.com/re-exploit/assembly-breakdown-x86-64-structure-alignment-examples-a40a0ef325e5
author_url
https://medium.com/@MonlesYen
status
ok
fetched_at
2026-07-10 19:15:58