2025-10-07 21:21:21
Suwako
lowlevel
assembly

【Assembly Language】Explaining the For Loop

Previously, I taught how to learn assembly by decompiling C programs into assembly and converting them back to C.
Inspired by this, I decided to write a shorter, more focused series of articles to teach the basics of assembly.
As a reminder, most people reading this article likely have an x86_64 (Intel or AMD) computer (unless you're a poor smartphone slave).
Therefore, the assembly code will be written for x86_64.
This time, we'll cover how to write a for loop.

While Loop

Before that, let's review the while loop introduced in the previous article.

  2016ca: 8b 45 f8   mov     eax, dword ptr [rbp - 0x8]
  2016cd: 3b 45 ec   cmp     eax, dword ptr [rbp - 0x14]
  2016d0: 7f 36      jg      0x201708 <main+0x68>
  ...
  201706: eb c2      jmp     0x2016ca <main+0x2a>

As you may know, this is equivalent to the following in C:

while (fahr <= upper) {
  ...
}

However, this assembly code is written this way because it was decompiled from C code using objdump.
When writing assembly manually, a while loop in NASM would look like this:

section .data

section .rodata
  fahr: dq 0        ; dq = 64-bit
                    ; dd = 32-bit
                    ; dw = 16-bit
                    ; db = 8-bit
  upper: dq 300

section .text
  global _start

_start:
while_loop:
  mov rax, [fahr]
  cmp rax, [upper]
  jg end_loop

continue_loop:
  ...

  inc qword [fahr]
  jmp while_loop

end_loop:
  mov rax, 1
  mov rbx, 0
  syscall

Note that I chose NASM as the assembler.
Unlike high-level languages like C, assembly varies not only by architecture or OS but also by the assembler used.
Thus, the syntax for assembling with gas (GNU Assembler) differs from fasm (Flat Assembler), masm (Microsoft Macro Assembler), tasm (Borland's Turbo Assembler), or nasm (Netwide Assembler).
From here on, I'll use NASM.
This is because NASM is available on all OSes, supports multiple architectures, is widely used, and has been actively maintained for about 30 years (version 3.00 was released just 4 days ago!).
Also, if you're using Linux or BSD, NASM is probably already installed.
Even if it's not, it's almost certainly available in your distribution or OS's repository.
Furthermore, NASM is BSD-licensed, making it freely usable for both open-source and proprietary code, which is ideal for game developers.

It's also worth noting that .rodata is for initialized constant variables, .data is for mutable variables, and .bss is for reserving memory for uninitialized variables.To reserve space for an uninitialized variable in 64-bit, you'd define it as fahr: resq 1.
In assembly, there's no distinction between int, float, string, bool, etc.—everything is handled as bits.
However, anyone who has programmed in C (which I assume readers of my assembly articles have) knows you need to manage memory manually.
That's why.

For Loop

Now that the while loop explanation is done, from here on, I'll write assembly code manually instead of writing in C and decompiling.
Also, in assembly, it's standard to comment each line by default, so I encourage you to adopt this habit.

section .rodata
  hello:     db 'こんにちは、世界',10 ; With newline code
                                      ; UTF-8 string,
                                      ; 10 = \n
  helloLen:  equ $-hello      ; Length of the string,
                              ; $ = current address,
                              ; -hello = minus the starting
                              ; address of the string

section .text
  global _start

_start:
  mov rbx, 0        ; Initialize loop counter (int i = 1;)

begin_loop:
  cmp rbx, 5        ; Compare i with 5 (i < 5;)
  jge end_loop      ; Exit loop if i >= 5

continue_loop:
  mov rax, 4        ; System call number for write in FreeBSD
  mov rdi, 1        ; File descriptor 1 (standard output)
  mov rsi, hello    ; Address of the string
  mov rdx, helloLen ; Length of the string
  syscall           ; Call the kernel

  inc rbx           ; Increment loop counter (i++)
  jmp begin_loop    ; Return to the start of the loop

end_loop:
  mov rax, 1        ; System call number for exit in FreeBSD
  mov rbx, 0        ; Exit status 0 (success)
  syscall           ; Call the kernel

Note that system call numbers are specific to FreeBSD.
For Linux, use the following:

section .rodata
  hello:     db 'こんにちは、世界',10 ; With newline code
                                      ; UTF-8 string,
                                      ; 10 = \n
  helloLen:  equ $-hello      ; Length of the string,
                              ; $ = current address,
                              ; -hello = minus the starting
                              ; address of the string

section .text
  global _start

_start:
  mov rbx, 0        ; Initialize loop counter (int i = 1;)

begin_loop:
  cmp rbx, 5        ; Compare i with 5 (i < 5;)
  jge end_loop      ; Exit loop if i >= 5

continue_loop:
  mov rax, 1        ; System call number for write in Linux
  mov rdi, 1        ; File descriptor 1 (standard output)
  mov rsi, hello    ; Address of the string
  mov rdx, helloLen ; Length of the string
  syscall           ; Call the kernel

  inc rbx           ; Increment loop counter (i++)
  jmp begin_loop    ; Return to the start of the loop

end_loop:
  mov rax, 60       ; System call number for exit in Linux
  mov rbx, 0        ; Exit status 0 (success)
  syscall           ; Call the kernel

This assembly code is equivalent to the following C code:

#include <stdio.h>

int main() {
  for (int i = 0; i < 5; i++) {
    printf("こんにちは、世界\n");
  }

  return 0;
}

In assembly, you don't compile the code; you first assemble it into an object file and then link it into a binary file.
This shows just how low-level it is!

To assemble and link, you need the following two commands:

$ nasm -f elf64 for.asm -o for.o
$ ld for.o -o for

To assemble and link, you need the following two commands:

$ ./for
こんにちは、世界
こんにちは、世界
こんにちは、世界
こんにちは、世界
こんにちは、世界

Pretty simple, right?

That's all