How Are Strings Manipulated in Assembly Language?

How are strings manipulated in Assembly language

Strings are one of those things that feel trivial in a high-level language and then turn into a genuine education the first time you handle them in assembly. There’s no built-in “string type” — just bytes in memory, a convention for where they end, and a set of instructions (some surprisingly purpose-built) for moving, comparing, and searching through them. Once you understand those conventions and instructions, string handling in assembly stops feeling tedious and starts feeling like one of the more satisfying parts of low-level programming — you can see exactly where every byte goes.

What a String Actually Is at the Assembly Level

There’s no such thing as a “string object” at this level — just a contiguous run of bytes in memory, plus a convention for knowing where it ends. The two dominant conventions:

  • Null-terminated strings (C-style): the string ends wherever a 0x00 byte appears. This is the convention x86/ARM system programming and most OS APIs use by default.
  • Length-prefixed strings (Pascal-style): the string’s length is stored explicitly (often as a byte or word before the character data), so no terminator scanning is required.
section .data
    c_string        db "Hello, world!", 0        ; null-terminated
    pascal_string   db 13, "Hello, world!"        ; length-prefixed (length=13)

x86 String Instructions: Purpose-Built Hardware Support

x86 is somewhat unusual among modern architectures in that it has a dedicated family of string instructions designed explicitly for operating on blocks of memory, byte by byte, word by word, or dword by dword — with automatic pointer advancement built in.

InstructionPurpose
MOVSMove data from [RSI] to [RDI], advancing both pointers
CMPSCompare [RSI] and [RDI], advancing both pointers
SCASCompare AL/AX/EAX against [RDI], advancing RDI
STOSStore AL/AX/EAX into [RDI], advancing RDI
LODSLoad [RSI] into AL/AX/EAX, advancing RSI

Each of these respects the Direction Flag (DF) in EFLAGS — CLD clears it (pointers increment), STD sets it (pointers decrement) — and each can be prefixed with REP, REPE/REPZ, or REPNE/REPNZ to repeat automatically based on the RCX counter and/or the zero flag.

Example: String Length (strlen) Using SCASB

; RDI = pointer to null-terminated string
; Returns length in RAX
strlen:
    push    rdi
    xor     al, al          ; search for the null byte
    mov     rcx, -1         ; max possible count (will underflow-count down)
    cld                     ; ensure forward direction
    repne   scasb           ; scan until AL (0) found or RCX hits 0
    not     rcx              ; RCX = -(count) - 1  -> invert to get length+1
    dec     rcx              ; subtract 1 for the null terminator itself
    mov     rax, rcx
    pop     rdi
    ret

Example: String Copy Using MOVSB

; RSI = source, RDI = destination, both null-terminated, dest has enough space
strcpy:
    cld
copy_loop:
    lodsb                   ; load [RSI] into AL, RSI++
    stosb                   ; store AL into [RDI], RDI++
    test    al, al
    jnz     copy_loop       ; continue until we just copied a null byte
    ret

Example: String Comparison Using CMPSB

; RSI, RDI = two null-terminated strings; returns ZF=1 if equal
strcmp_equal:
    cld
compare_loop:
    mov     al, [rsi]
    cmpsb                    ; compare [RSI] vs [RDI], advance both
    jne     not_equal
    test    al, al
    jz      strings_equal    ; hit null terminator on both — match
    jmp     compare_loop
not_equal:
    ; ZF is already 0 here
    ret
strings_equal:
    ; ZF is 1
    ret

ARM: No Dedicated String Instructions, But Efficient Loops

ARM doesn’t provide the same “block string operation” instruction family x86 does. Instead, string manipulation is expressed with ordinary load/store instructions inside loops — but ARM’s addressing modes (particularly post-increment addressing, covered in depth in addressing-mode discussions) make these loops compact and efficient.

ARM strlen Example

; R0 = pointer to null-terminated string
; Returns length in R0
strlen:
    MOV     R1, R0           ; R1 = working pointer
count_loop:
    LDRB    R2, [R1], #1     ; load byte, post-increment R1
    CMP     R2, #0
    BNE     count_loop
    SUB     R0, R1, R0        ; length = final pointer - start pointer
    SUB     R0, R0, #1        ; subtract 1 for the null terminator
    BX      LR

ARM strcpy Example

; R0 = dest, R1 = source
strcpy:
copy_loop:
    LDRB    R2, [R1], #1     ; load source byte, advance source pointer
    STRB    R2, [R0], #1     ; store to dest byte, advance dest pointer
    CMP     R2, #0
    BNE     copy_loop
    BX      LR

Internal Working Process: Scanning a String

flowchart TD
    A[Start: pointer at string base] --> B[Load byte at current pointer]
    B --> C{Byte == 0?}
    C -- No --> D[Process byte<br/>e.g. copy, compare, count]
    D --> E[Advance pointer by 1]
    E --> B
    C -- Yes --> F[Terminator found:<br/>stop, return length/result]

This loop — load, test, act, advance, repeat — is the fundamental shape behind almost every string operation you’ll write in assembly, whether you’re using x86’s dedicated string instructions or ARM’s general-purpose load/store-in-a-loop approach.

Comparison Table: x86 vs. ARM String Handling

Aspectx86/x86-64ARM
Dedicated string instructionsYes — MOVS, CMPS, SCAS, STOS, LODSNo — implemented via loops with LDRB/STRB
Automatic repetitionREP/REPE/REPNE prefixesManual loop with branch instruction
Pointer auto-advanceBuilt into string instructions via DF flagExplicit via post/pre-indexed addressing ([R1], #1)
Code density for simple copy/scanVery high (few instructions)Slightly larger but very predictable
Performance on modern CPUsREP MOVSB/STOSB are often microcode-optimized on modern Intel/AMD (fast-string optimization)Loop performance depends on compiler/hand-tuning and pipeline behavior

x86 advantage: extremely compact code for simple linear string operations, and on modern CPUs, REP MOVSB in particular benefits from hardware fast-string optimizations that can rival hand-tuned loops. ARM advantage: more predictable, uniform instruction timing (useful in real-time contexts), and no dependency on quirky flag-prefix combinations — everything is explicit in the loop.

Practical Use Cases

  • Implementing your own C-runtime-style functions (strlen, strcpy, strcmp, memcpy, memset) when no libc is available (bootloaders, kernels, embedded firmware).
  • Parsing text-based protocols or file formats directly in performance-critical assembly routines.
  • Implementing simple command-line argument parsing in freestanding (no-OS-runtime) programs.
  • Writing highly optimized memcpy/memmove replacements for specific, known alignment and size patterns.

Operating System Interaction

Most OS APIs that accept or return strings — file paths, command-line arguments, environment variables — use null-terminated byte sequences (on Unix-like systems) or, in Windows’ case, frequently null-terminated UTF-16 sequences. That means low-level assembly interacting with OS syscalls or Win32 API calls needs to respect whichever convention the target OS expects, including the correct character width (1 byte vs. 2 bytes per character) when working with wide-character APIs.

Debugging and Performance Considerations

  • Common mistake: forgetting CLD before using REP MOVSB/STOSB/etc. If the Direction Flag was left set from earlier code, pointers will decrement instead of increment, silently corrupting memory in the “wrong” direction.
  • Common mistake: off-by-one errors around the null terminator — many bugs come from forgetting whether a computed length should or shouldn’t include the terminating byte.
  • Common mistake: buffer overruns from copying a string into a fixed-size destination without checking the source length first — assembly gives you zero automatic bounds checking.
  • Debugging tip: use a debugger’s memory view to watch the destination buffer live while stepping through a copy loop; off-by-one and direction-flag bugs become immediately visible.
  • Optimization tip: on modern x86-64, REP MOVSB with properly aligned, sufficiently large buffers can outperform naive hand-written byte loops due to microarchitectural “fast string” support — but for very small, fixed-size copies, a few explicit MOV instructions can beat the fixed overhead of setting up a REP operation.
  • Optimization tip: SIMD instructions (SSE2/AVX on x86, NEON on ARM) can process 16+ bytes per instruction for string scanning/copying, dramatically outperforming a byte-at-a-time loop for long strings — worth reaching for once basic byte-loop logic works correctly.

Best Practices

  1. Always know your string convention (null-terminated vs. length-prefixed) before writing any manipulation code — mixing conventions is a common source of bugs when combining code that assumes one over the other.
  2. Explicitly set the direction flag (CLD) before every x86 string-instruction sequence rather than assuming its prior state.
  3. Validate destination buffer sizes before copying — assembly won’t stop you from writing past the end of a buffer.
  4. Consider SIMD-based string routines for performance-critical, long-string operations once your scalar implementation is correct.
  5. Keep string helper routines (length, copy, compare, concatenate) as small, well-tested, reusable functions rather than inlining ad hoc string logic everywhere.

FAQs

Why does x86 have dedicated string instructions but ARM doesn’t? It reflects differing design philosophies — x86’s CISC heritage favored complex, purpose-built instructions for common patterns, while ARM’s RISC philosophy favors a small set of simple, general-purpose instructions (like post-indexed load/store) composed into loops, which tends to be more predictable for pipelining.

Is REP MOVSB always faster than a manual copy loop? Not always — for very short copies, the fixed setup overhead of the REP mechanism can outweigh its benefits. For long, well-aligned copies on modern CPUs, it often benefits from dedicated fast-string microcode paths and can be very competitive or faster.

How do I handle strings with embedded null bytes in assembly? You can’t rely on null-termination in that case — you need a length-prefixed or explicitly bounded representation instead, and all your routines (copy, compare, scan) need to use the known length rather than searching for a terminator.

What’s the safest way to avoid buffer overflows when writing string routines by hand? Always pass and check an explicit maximum length alongside any destination buffer pointer, and stop processing (or truncate deliberately) once that limit is reached — never trust that a source string will fit without checking.

Summary and Key Takeaways

String manipulation in assembly comes down to walking a sequence of bytes in memory according to a known convention — usually null-termination — and performing simple operations (copy, compare, scan, store) at each step. x86 offers a family of dedicated string instructions (MOVS, CMPS, SCAS, STOS, LODS) with automatic pointer advancement and optional repetition via REP prefixes, while ARM achieves the same results through general-purpose load/store instructions combined with post/pre-indexed addressing inside simple loops.

Key takeaways:

  • Know your string convention (null-terminated vs. length-prefixed) before writing manipulation code.
  • x86’s string instruction family plus REP prefixes offers very compact, and on modern CPUs often fast, string operations — but requires careful direction-flag management.
  • ARM achieves equivalent results through explicit, predictable load/store loops leveraging post/pre-indexed addressing.
  • There’s no automatic bounds checking anywhere — buffer size validation is entirely the programmer’s responsibility.
  • SIMD instructions are the natural next step for performance once correct scalar string routines are in place.

References

  • Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 1 — String Operations chapter (MOVS, CMPS, SCAS, STOS, LODS, REP prefixes).
  • AMD64 Architecture Programmer’s Manual, Volume 3 — General-Purpose and System Instructions.
  • ARM Architecture Reference Manual — Load/Store instruction addressing modes.
  • GNU C Library (glibc) manual — string function implementations and conventions.
  • GNU Assembler (GAS) documentation — x86 string instruction syntax and ARM load/store syntax.
Total
0
Shares

Leave a Reply

Previous Post
What is the purpose of the base register in Assembly language

What Is the Purpose of the Base Register in Assembly Language?

Next Post
What is the role of the status register in Assembly language

What Is the Role of the Status Register in Assembly Language?

Related Posts