← SEARCHABLE ARCHIVE
Assembly · 2026-08-15

Reading x86-64 Array, String, and Structure Loops

Trace pointer-length contracts, strides, sentinels, and bounds checks in real loops

INTERACTIVE READING MODE →

Reading x86-64 Array, String, and Structure Loops

These blog posts are written as lessons. Examples use Intel syntax. Pointer validity, object lifetime, aliasing, and overflow remain contracts at the C/C++ boundary.

Many real loops walk from a starting address at a regular stride while testing or accumulating values. Arrays, strings, and arrays of structures all lower to this pattern, but differ in termination and stride.

Find four pieces of loop state

  1. A base or current pointer
  2. An index, end pointer, or remaining count
  3. An accumulator or search result
  4. A terminating condition and its signedness
; rcx = int32_t* values, rdx = count
xor eax, eax
xor r8d, r8d
loop_test:
    cmp r8, rdx
    jae loop_done
    add eax, dword ptr [rcx + r8*4]
    inc r8
    jmp loop_test
loop_done:
    ret

rcx is the base, r8 the index, eax the accumulator, and unsigned r8 >= rdx ends the loop.

Index and end-pointer loops can express the same range

lea rdx, [rdi + rsi*4]      ; end = begin + count
xor eax, eax
cmp rdi, rdx
je done
loop_begin:
    add eax, dword ptr [rdi]
    add rdi, 4
    cmp rdi, rdx
    jne loop_begin
done:
    ret

There is no explicit index. Recover the range from the initial pointer, four-byte increment, and computed end. Machine addition can wrap, but valid C/C++ pointer arithmetic has a narrower object-range contract.

Strings terminate by length or sentinel

A byte span with an explicit length behaves like an array. A C string uses a zero byte as a sentinel.

strlen_like:
    mov rax, rcx
scan:
    cmp byte ptr [rax], 0
    je found
    inc rax
    jmp scan
found:
    sub rax, rcx
    ret

This code receives no maximum length. It assumes that a NUL terminator exists inside readable memory. Without one, it continues beyond the object.

Do not mix byte count, UTF-8 code units, Unicode code points, and user-perceived grapheme clusters. A strlen-style scan counts bytes, not characters.

Search-result representation needs a contract

mov rax, -1
xor r9d, r9d
loop_test:
    cmp r9, rdx
    jae not_found
    cmp byte ptr [rcx + r9], r8b
    je found
    inc r9
    jmp loop_test
found:
    mov rax, r9
not_found:
    ret

If the return type is size_t, bitwise -1 is SIZE_MAX, much like string_view::npos. Caller and callee must not disagree about signed -1 versus an unsigned maximum.

Structures combine stride and field offset

struct Particle {
    float x, y, z;
    uint32_t live;
};
; rcx = Particle*, rdx = count
xorps xmm0, xmm0
test rdx, rdx
jz done
loop_begin:
    cmp dword ptr [rcx + 12], 0
    je skip
    addss xmm0, dword ptr [rcx]
skip:
    add rcx, 16
    dec rdx
    jne loop_begin
done:
    ret

Offset 12 suggests live, offset 0 suggests x, and the 16-byte step suggests sizeof(Particle). These are observations about this binary layout, not permanent source names. Verify them with compiler layout information and static_assert.

AoS and SoA produce different address patterns

Array of Structures repeats a stride and several fixed offsets. Structure of Arrays tends to use separate base pointers for each field. SoA can make one-field bulk processing contiguous and SIMD-friendly; AoS can be better when all fields of an object are consumed together. Data layout can matter more than a clever instruction.

Bounds-check elimination is a proof

for (size_t i = 0; i < n; ++i)
    out[i] = in[i] * 2;

The loop condition dominates each access, so there may be no additional per-element check. However, potential overlap between out and in can block reordering or vectorization. Do not silently assume non-overlap in handwritten assembly.

Recognize unrolling and vector tails

loop4:
    add eax, [rcx]
    add eax, [rcx + 4]
    add eax, [rcx + 8]
    add eax, [rcx + 12]
    add rcx, 16
    sub rdx, 4
    jae loop4

One machine iteration handles four source elements. A remainder path usually handles zero to three. Do not infer that length must be a multiple of four until that tail is examined.

A vectorized loop often has a prologue, SIMD main loop, and scalar tail:

movdqu xmm0, [rcx]
paddd  xmm1, xmm0       ; four 32-bit lanes

The XMM value is not necessarily one 128-bit integer; the opcode defines packed lane width.

Copy contracts differ

  • memcpy requires non-overlapping source and destination.
  • memmove must work with overlap.
  • A typed loop adds object lifetime, alignment, aliasing, and type semantics.

Compilers may inline a fixed-size copy, call a runtime routine, or use rep movsb. No one form is universally fastest for every CPU and size distribution.

Size arithmetic is a security boundary

bytes = count * sizeof(Item);

If multiplication wraps to a small allocation but a later loop still uses count, a heap overflow can result. Audit allocation-size computation and loop bounds together. Validate count <= SIZE_MAX / sizeof(Item) or use checked multiplication.

Loop checklist

  1. What minimum valid range does each pointer require?
  2. Is the count measured in bytes or elements?
  3. What are the stride and field offsets?
  4. Does termination use index, end pointer, or sentinel?
  5. Does zero length skip the body?
  6. Are last-element and one-past-end handled correctly?
  7. Is there a remainder after unrolled or SIMD work?
  8. May source and destination overlap?
  9. Can size multiplication or pointer addition overflow?

Summary

Data loops decompose into pointer, stride, accumulator, and termination. Index and end-pointer forms can represent the same range; sentinel loops replace an explicit maximum with a strong terminator assumption. Recover the complete memory contract, including empty input, tails, overlap, units, and size overflow.

Official references