AsAssembly · Lesson 9 of 10

Mini Project: String Operations

Let's write a small library of string utilities in pure assembly — strlen, strcpy, strcmp, and a number-to-string converter. No libc. Just registers, memory, and loops.

x86-64 ASM
; strings.asm — string utility library + demo
; nasm -f elf64 strings.asm -o strings.o && ld strings.o -o strings && ./strings

section .data
    str1  db "Hello, Assembly!", 0
    str2  db "Hello, Assembly!", 0
    str3  db "Different string", 0
    newln db 10, 0

section .bss
    outbuf resb 32      ; output buffer for number conversion

section .text
global _start

; strlen(rdi: *str) -> rax: length
strlen:
    xor  rax, rax           ; rax = 0 (counter)
.loop:
    cmp  byte [rdi + rax], 0
    je   .done
    inc  rax
    jmp  .loop
.done:
    ret

; puts(rdi: *str) — print string + newline
puts_asm:
    push rbx
    mov  rbx, rdi
    call strlen             ; rax = length
    mov  rdx, rax           ; count
    mov  rax, 1             ; sys_write
    mov  rdi, 1             ; stdout
    mov  rsi, rbx           ; string pointer
    syscall
    ; print newline
    mov  rax, 1
    mov  rdi, 1
    lea  rsi, [rel newln]
    mov  rdx, 1
    syscall
    pop  rbx
    ret

; strcmp(rdi: *s1, rsi: *s2) -> rax: 0 if equal, nonzero if not
strcmp_asm:
    xor  rax, rax
.loop:
    mov  al,  byte [rdi]    ; al = *s1
    mov  ah,  byte [rsi]    ; ah = *s2
    cmp  al,  ah
    jne  .differ
    test al,  al            ; end of string?
    jz   .equal
    inc  rdi
    inc  rsi
    jmp  .loop
.equal:
    xor  rax, rax
    ret
.differ:
    movzx rax, al
    movzx rcx, ah
    sub   rax, rcx          ; return difference (like C strcmp)
    ret

; itoa(rdi: number, rsi: *buf) -> rax: *buf (null-terminated decimal string)
itoa:
    push rbp
    mov  rbp, rsp
    push rbx
    push r12
    push r13

    mov  r12, rsi           ; save buf start
    mov  rbx, rdi           ; save number

    ; Handle 0 specially
    test rbx, rbx
    jnz  .convert
    mov  byte [r12], '0'
    mov  byte [r12+1], 0
    lea  rax, [r12]
    jmp  .done

.convert:
    ; Write digits in reverse, then flip
    mov  r13, rsi           ; current position in buffer
    mov  rax, rbx
.digit_loop:
    xor  rdx, rdx
    mov  rcx, 10
    div  rcx                ; rax = rax/10, rdx = rax%10
    add  dl,  '0'
    mov  byte [r13], dl
    inc  r13
    test rax, rax
    jnz  .digit_loop
    mov  byte [r13], 0      ; null terminate

    ; Reverse the string (r12..r13-1)
    mov  rdi, r12
    lea  rsi, [r13-1]
.reverse:
    cmp  rdi, rsi
    jge  .reversed
    mov  al,  byte [rdi]
    mov  bl,  byte [rsi]
    mov  byte [rdi], bl
    mov  byte [rsi], al
    inc  rdi
    dec  rsi
    jmp  .reverse
.reversed:
    lea  rax, [r12]

.done:
    pop  r13
    pop  r12
    pop  rbx
    pop  rbp
    ret

_start:
    ; Test strlen
    lea  rdi, [rel str1]
    call strlen
    ; rax = 16

    ; Print str1
    lea  rdi, [rel str1]
    call puts_asm

    ; Test strcmp — equal strings
    lea  rdi, [rel str1]
    lea  rsi, [rel str2]
    call strcmp_asm
    ; rax = 0 (equal)

    ; Test strcmp — different strings
    lea  rdi, [rel str1]
    lea  rsi, [rel str3]
    call strcmp_asm
    ; rax != 0 (different)

    ; Convert 2024 to string and print
    mov  rdi, 2024
    lea  rsi, [rel outbuf]
    call itoa
    mov  rdi, rax
    call puts_asm

    mov  rax, 60
    xor  rdi, rdi
    syscall
Bash
nasm -f elf64 strings.asm -o strings.o
ld strings.o -o strings
./strings
# Hello, Assembly!
# 2024

# Inspect generated machine code:
objdump -d strings | head -60

# Check binary size (should be tiny — no libc):
ls -lh strings
✦ Tip
Use `gdb` to step through assembly line by line: `gdb ./strings`, then `layout asm` for the disassembly view, `layout regs` for register display, `si` to step one instruction. Or use `gdb -tui`. Seeing registers change as each instruction executes makes assembly click faster than anything else.