Loops in assembly

Author

Clayton Cafiero

Published

2025-09-10

Here’s just about the simplest loop we could implement in C.

/**
 * A minimal loop in C
 * Can't get much simpler than this. This loop has no body!
 *
 * CS / CMPE 2210
 * Computer Organization
 * Clayton Cafiero <cbcafier@uvm.edu>
 * 2025-09-09
 */
int main(void) {

    for (unsigned int i = 65535; i > 0; --i);
    // Why did I choose 65,535?

    return 0;
}

When we compile (on ARM, without compiler optimization) this is what we get (with my annotations):

    .section    __TEXT,__text,regular,pure_instructions
    .build_version macos, 15, 0 sdk_version 15, 5
    .globl  _main                           ; -- Begin function main
    .p2align    2                           ; 2 ** 2 = 4 bytes
_main:                                  ; @main
    .cfi_startproc                      ; for debugging (can ignore)
                                        ; (just fyi, "cfi" is call frame info)
; %bb.0:                                ; annotation provided by compiler
; OVERHEAD and INITIALIZATION
    sub sp, sp, #16                     ; initialize 16-byte stack frame
    .cfi_def_cfa_offset 16              ; for debugging (can ignore)
    str wzr, [sp, #12]                  ; store 32-bit zero in [sp, #12] (padding)
                                        ; (that's offset +12 in stack frame)
    mov w8, #65535                      ; =0xffff move 65,535 to register w8
    str w8, [sp, #8]                    ; store value in [sp, #8] (loop var, i)
    b   LBB0_1                          ; branch (goto) LBB0_1
; LOOP START
;
; LBB0_1, LBB0_2, LBB0_3, and LBB0_4 are just labels. They're not executed.
; On ARM architecture: L = label, BB = basic block (single entrance, single exit)
; 0 indicates the function (in this case main), and the first basic block within
; main, hence LBB0_1. Similarly for the other blocks.
;
LBB0_1:                                 ; =>This Inner Loop Header: Depth=1
    ldr w8, [sp, #8]                    ; load i into register w8
    subs    w8, w8, #0                  ; subtract: i - 0, set NZCV flags
                                        ; (this is just a fancy way of writing
                                        ;  w8 to itself, and setting flags)
    b.ls    LBB0_4                      ; exit to LBB0_4 if i <= 0 (unsigned compare)
                                        ; b.ls means lower or same in unsigned
                                        ; comparison terms. It branches if C=0
                                        ; (borrow) or Z=1 (equal).
    b   LBB0_2                          ; otherwise branch to LBB0_2 (continue loop)
LBB0_2:                                 ;   in Loop: Header=BB0_1 Depth=1
    b   LBB0_3                          ; no loop body, just skip to loop control
                                        ; branch (go to) LBB0_3
LBB0_3:                                 ;   in Loop: Header=BB0_1 Depth=1
    ldr w8, [sp, #8]                    ; load counter (i) at [sp] into w8
    subs    w8, w8, #1                  ; decrement counter, set NZCV flags
    str w8, [sp, #8]                    ; write counter to [sp, #8]
    b   LBB0_1                          ; branch (go to) LBB0_1
; LOOP END
LBB0_4:
    mov w0, #0                          ; =0x0 write 0 to register w0
                                        ; (set return = 0, per AArch64 convention)
    add sp, sp, #16                     ; pop stack frame (restore stack pointer)
    ret                                 ; return (with contents of w0)
    .cfi_endproc                        ; main() ends here
                                        ; -- End function
.subsections_via_symbols

© 2025 Clayton Cafiero.

No generative AI was used in writing this material. This was written the old-fashioned way.