Arrays, strings and the string instructions

Arrays, strings and the string instructions

An array is a consecutive run of equal-size elements. If a qword array begins at numbers, then its elements begin at numbers + 0, numbers + 8, numbers + 16, and so on. There are two common ways to walk it:

  • keep an index and calculate base + index * element_size;
  • keep a pointer to the current element and advance it by element_size.

Both forms need a boundary so that they never read beyond the array.

Walking an array by index

This loop sums six qwords. The index starts at zero, so [rbx + rcx*8] implements base + index * size.

The dq directive emits 48 bytes. Dividing that byte count by the eight-byte element size makes COUNT equal to 6. COUNT equ ... is an assembly-time calculation: it allocates no memory and there is no stored variable named COUNT at run time.

The condition is tested before the load. If COUNT were zero, the first jae would skip the body, so the program would not try to read an element that does not exist.

Walking the same array with a pointer

A label immediately after the array gives its one-past-end address. That address is a boundary; the program compares against it but never dereferences it.

The pointer takes the values numbers, numbers + 8, through numbers + 40. After the final element it becomes numbers_end. The pre-test happens before every load, including the first one, so this shape is safe for an empty array whose start and end labels have the same address.

The index form keeps the element number available. The pointer form already holds the next address to use. x86 string instructions use the pointer form through implicit registers.

Fixed-length bytes and zero-terminated strings

A string is byte data plus a rule that says where the data ends. Two common rules are an explicit length and a terminating zero byte:

fixed:      db "hello"
FIXED_LEN   equ $ - fixed       ; five bytes

c_text:     db "hello", 0      ; six bytes including the terminator

fixed contains exactly five bytes, so code must receive or calculate its length. Those five bytes may include zero; the length, rather than a byte value, determines the end.

c_text uses the C-style convention: the first zero byte ends the string. NASM does not append that byte to a quoted db value. The explicit , 0 creates it.

For the smaller declaration message: db "Hi!", 0, memory contains:

addressbytemeaning
message + 048H
message + 169i
message + 221!
message + 300terminator

A byte-at-a-time walk examines offsets 0, 1, 2, and 3. The load at offset 3 reads the terminator; test al, al then sets ZF, and the loop stops without treating that byte as a character. A valid zero-terminated string therefore needs accessible storage for the terminator as well as the visible characters.

Implicit registers and the direction flag

This lesson uses four byte-form string instructions. They use fixed registers rather than written operands:

instructionoperationpointer change when DF=0pointer change when DF=1
movsbcopy byte [rsi] to [rdi]rsi += 1, rdi += 1rsi -= 1, rdi -= 1
stosbstore al at [rdi]rdi += 1rdi -= 1
scasbset flags for al - [rdi]rdi += 1rdi -= 1
cmpsbset flags for [rsi] - [rdi]rsi += 1, rdi += 1rsi -= 1, rdi -= 1

The direction flag, DF, is persistent processor state. cld clears it for forward movement; std sets it for backward movement. A string instruction does not restore the old direction when it finishes. Execute cld whenever code requires forward movement instead of relying on whatever some earlier code left in DF.

The suffix selects the element width. The w, d, and q forms move each affected pointer by 2, 4, or 8 bytes respectively; the byte forms above move by 1. For example, movsq copies a qword and then adds or subtracts 8 from both pointers.

movsb is also a useful exception to the ordinary operand rule. An explicit mov [destination], [source] is invalid because ordinary mov cannot have two memory operands. movsb performs a memory-to-memory copy through its implicit [rsi] source and [rdi] destination.

Repeating a string instruction

A repeat prefix uses rcx as a count. Before each possible iteration, a zero rcx stops the instruction. Each completed iteration decrements rcx.

  • As used here, rep repeats movs or stos while rcx is nonzero.
  • repe repeats a comparison while rcx is nonzero and, after each comparison, ZF=1.
  • repne repeats a comparison while rcx is nonzero and, after each comparison, ZF=0.

Here a bounded rep movsb copies six bytes, including the terminator. A bounded rep stosb then fills four other bytes.

The resulting destination bytes are:

68 65 6C 6C 6F 00 00 00   41 41 41 41 00 00 00 00

After the copy, rsi is source + 6, rdi is dest + 6, and rcx is zero. The following setup changes rdi; after the fill, it is dest + 12 and rcx is zero again.

Scanning within a capacity

repne scasb can look for a byte, but it must have a real bound. This example searches an eight-byte accessible range for a zero terminator:

scasb sets flags for al - [rdi], then advances rdi. The scan examines h, e, l, l, o, and zero. On the match, ZF=1, rcx=2, and rdi=text+6, one byte past the terminator. Subtracting the start gives six examined bytes; subtracting one more gives the five-character length.

If all eight bytes were nonzero, the scan would finish with rcx=0 and ZF=0. The immediate jnz distinguishes that exhausted-input case from a match. A zero capacity is handled before the scan because no comparison would run and there would be no new ZF result from scasb.

Comparing two bounded byte sequences

repe cmpsb compares corresponding bytes while they are equal and a count remains. This example has an explicit five-byte bound:

cmpsb sets flags for [rsi] - [rdi], then advances both pointers. The first three pairs match. The fourth comparison is n - v, so ZF=0; repetition stops with rcx=1 and both pointers one byte past the differing pair. r8 becomes zero. Two zero-length sequences count as equal, so that path should set the result to 1 without executing cmpsb.

Your turn

Each array below is nonempty and contains unsigned qwords. Walk each array with a pointer and find its unsigned maximum. Leave the maxima in r12 and r13. Preserve the final one-past-end pointers in r14 and r15 before the exit setup.

Show solution

Now copy the eight source bytes to dest with one rep movsb. The skeleton executes std immediately before your code, so execute cld before the forward copy. Preserve the final rsi, rdi, and rcx values in r12, r13, and r14.

Show solution