Arrays, strings and the string instructions
The overview of this topic is in Assembly basics. The same topic in M68K, MIPS, RISC-V, Z80.
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:
| address | byte | meaning |
|---|---|---|
message + 0 | 48 | H |
message + 1 | 69 | i |
message + 2 | 21 | ! |
message + 3 | 00 | terminator |
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:
| instruction | operation | pointer change when DF=0 | pointer change when DF=1 |
|---|---|---|---|
movsb | copy byte [rsi] to [rdi] | rsi += 1, rdi += 1 | rsi -= 1, rdi -= 1 |
stosb | store al at [rdi] | rdi += 1 | rdi -= 1 |
scasb | set flags for al - [rdi] | rdi += 1 | rdi -= 1 |
cmpsb | set flags for [rsi] - [rdi] | rsi += 1, rdi += 1 | rsi -= 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,
reprepeatsmovsorstoswhilercxis nonzero. reperepeats a comparison whilercxis nonzero and, after each comparison,ZF=1.repnerepeats a comparison whilercxis 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.