2025-10-26 03:50:40
諏訪子
lowlevel
assembly

【Assembly Language】Functions and Includes

So far, I have explained in detail how to output text in assembly language, but I have not yet touched on input.
However, writing an entire article on how to reproduce scanf() in assembly would be boring.
Instead, I will create an article about how to make functions and how to include other assembly files.
Input processing will be treated as part of that.

Prerequisites

To understand this article, please read the following articles first:

Functions

If you have read the previous assembly articles, you will have noticed that functions have already been mentioned.
To have functions, they require slightly different handling.
Unlike writing code in _start, functions need a prologue and epilogue.
The prologue and epilogue are parts unique to assembly language, which are all abstracted in high-level languages.

The prologue saves the previous base pointer (rbp) to the stack and moves it to the current stack pointer (rsp).
The epilogue restores the stack pointer and base pointer.

To achieve this, do the following:

foo:
  ; Function prologue
  push rbp                            ; Save the previous base pointer to the stack
  mov rbp, rsp                        ; Set the base pointer to the current stack pointer

  ; Do something

  ; Function epilogue
  mov rsp, rbp                        ; Restore stack pointer
  pop rbp                             ; Restore base pointer
  ret                                 ; Return from function

This allows you to use call foo within _start, keeping the main function clean.

Includes

The method of including files differs depending on the assembler used.
In GCC, it is #include "io.s", but in NASM, it is %include "io.s".
Since we are using NASM here, we will use that syntax.

Example

Before creating main.s, let's create io.s.

; io.s
section .text

printf:                               ; void printf(char *buf, size_t len)
  ; Function prologue
  push rbp                            ; Save the previous base pointer to the stack
  mov rbp, rsp                        ; Set the base pointer to the current stack pointer

  push rsi                            ; Save len
  push rdi                            ; Save the buffer (buf)

  ; Write output
  mov rax, 4                          ; System call number for write on Free FreeBSD
  mov rdi, 1                          ; File descriptor 1 (standard output)
  mov rsi, [rsp]                      ; Buffer (saved buf)
  mov rdx, [rsp + 8]                  ; Length (saved len)
  syscall                             ; Execute system call

  ; Function epilogue
  add rsp, 16                         ; Clean up the stack (pop rdi and rsi)
  ;mov rsp, rbp                        ; Restore stack pointer
  ;pop rbp                             ; Restore base pointer
  leave                               ; Equivalent to: mov rsp, rbp ; pop rbp
  ret                                 ; Return from function


scanf:                                ; size_t scanf(char *buf, size_t max_len)
  ; Function prologue
  push rbp                            ; Save the previous base pointer to the stack
  mov rbp, rsp                        ; Set the base pointer to the current stack pointer

  push rsi                            ; Save max_len
  push rdi                            ; Save the buffer (buf)

  ; Read input
  mov rax, 3                          ; System call number for read on FreeBSD
  mov rdi, 0                          ; File descriptor 0 (standard input)
  mov rsi, [rsp]                      ; Buffer (saved buf)
  mov rdx, [rsp + 8]                  ; Maximum length (saved max_len)
  syscall                             ; Execute system call

  ; Function epilogue
  add rsp, 16                         ; Clean up the stack (pop rdi and rsi)
  ;mov rsp, rbp                        ; Restore stack pointer
  ;pop rbp                             ; Restore base pointer
  leave                               ; Equivalent to: mov rsp, rbp ; pop rbp
  ret                                 ; Return from function

Next, in main.s, do the following:

; main.s
section .rodata
  prompt db "What is your name? ", 0  ; Prompt (null-terminated string)
  promptLen equ $-prompt-1            ; Length of prompt (excluding null char)

  greet db "Hello, "                  ; First part of greeting
  greetLen equ $-greet                ; Length of first part

  grEnd db "!", 10                    ; Second part of greeting (! + \n)
  grEndLen equ $-grEnd                ; Length of second part

section .bss
  buffer resb 65                      ; Input buffer (64 bytes reserved) + 1 byte for NULL

section .text
  %include "io.s"                     ; Include io.s
  global _start

_start:
  ; Display prompt
  mov rdi, prompt                     ; Buffer (prompt)
  mov rsi, promptLen                  ; Length
  call printf                         ; Output

  ; Read name
  mov rdi, buffer                     ; Buffer (input destination)
  mov rsi, 64                         ; Maximum length
  call scanf                          ; Read input
  ; rax = number of bytes read
  mov rbx, rax                        ; Save number of bytes read to rbx
  ; We saved the value of rax to rbx to prevent clobbering.

  ; If 0 bytes were read, exit
  test rbx, rbx                       ; Check if rax == 0
  jz .done                            ; If zero, jump to .done

  ; Null-terminate the buffer
  mov byte [buffer + rbx], 0          ; Write 0 at [read position]
  cmp byte [buffer + rbx - 1], 10     ; Is the last character a newline (\n)?
  jne .print                          ; If not, jump to .print
  dec rbx                             ; If there was a newline, reduce output length by 1

.print:
  mov r12, rbx                        ; Save the length of the name to output in r12

  ; Output "Hello, "
  mov rdi, greet
  mov rsi, greetLen
  call printf

  ; Output the name
  mov rdi, buffer
  mov rsi, r12
  call printf

  ; Output "!\n"
  mov rdi, grEnd
  mov rsi, grEndLen
  call printf

.done:
  ; Exit program
  mov rax, 1                          ; System call number for exit on FreeBSD
  mov rdi, 0                          ; Exit status 0 (success) = return 0;
  syscall                             ; Invoke kernel

Note: All system call numbers in the code are specific to FreeBSD.
On Linux, the system call numbers need to be changed.
On Linux, exit is 60, but on FreeBSD, it is 1.
On Linux, write is 1, but on FreeBSD, it is 4.
On Linux, read is 0, but on FreeBSD, it is 3.
The rest should work without issues.

Note: You do not need to pass io.s to the assembler.
Since it is already included in main.s, the assembler will automatically include it.

$ nasm -f elf64 main.s -o main.o
$ ld main.o -o io
$ file io
io: ELF 64-bit LSB executable, x86-64, version 1 (FreeBSD), statically linked, stripped
$ ls -thal io
-rwxr-xr-x  1 suwako suwako  1.1K 10月 26 01:58 io
$ ./io
What is your name?
What is your name? Suwako
Hello, Suwako!

How to Pass Parameters

The way parameters are passed differs depending on the ABI.
For System V (Linux, BSD, Illumos, etc.), it is as follows:

Parameter Register
1st rdi
2nd rsi
3rd rdx
4th rcx
5th r8
6th r9
7th+ Stack

The return value is in rax.

Verifying the Binary

Let's check the binary using objdump.
Since it was created in assembly, nothing from libc is included, so there is no need to pipe the result to less this time.
The entire output fits perfectly on a 4K screen!

$ objdump -d -Mintel ./io
./io:   file format elf64-x86-64

Disassembly of section .text:

0000000000201180 <.text>:
  201180: 55                              push    rbp
  201181: 48 89 e5                        mov     rbp, rsp
  201184: 56                              push    rsi
  201185: 57                              push    rdi
  201186: b8 04 00 00 00                  mov     eax, 0x4
  20118b: bf 01 00 00 00                  mov     edi, 0x1
  201190: 48 8b 34 24                     mov     rsi, qword ptr [rsp]
  201194: 48 8b 54 24 08                  mov     rdx, qword ptr [rsp + 0x8]
  201199: 0f 05                           syscall
  20119b: 48 83 c4 10                     add     rsp, 0x10
  20119f: 48 89 ec                        mov     rsp, rbp
  2011a2: 5d                              pop     rbp
  2011a3: c3                              ret
  2011a4: 55                              push    rbp
  2011a5: 48 89 e5                        mov     rbp, rsp
  2011a8: 56                              push    rsi
  2011a9: 57                              push    rdi
  2011aa: b8 03 00 00 00                  mov     eax, 0x3
  2011af: bf 00 00 00 00                  mov     edi, 0x0
  2011b4: 48 8b 34 24                     mov     rsi, qword ptr [rsp]
  2011b8: 48 8b 54 24 08                  mov     rdx, qword ptr [rsp + 0x8]
  2011bd: 0f 05                           syscall
  2011bf: 48 83 c4 10                     add     rsp, 0x10
  2011c3: 48 89 ec                        mov     rsp, rbp
  2011c6: 5d                              pop     rbp
  2011c7: c3                              ret
  2011c8: 48 bf 58 01 20 00 00 00 00 00   movabs  rdi, 0x200158
  2011d2: be 10 00 00 00                  mov     esi, 0x10
  2011d7: e8 a4 ff ff ff                  call    0x201180 <.text>
  2011dc: 48 bf 54 22 20 00 00 00 00 00   movabs  rdi, 0x202254
  2011e6: be 40 00 00 00                  mov     esi, 0x40
  2011eb: e8 b4 ff ff ff                  call    0x2011a4 <.text+0x24>
  2011f0: 48 89 c3                        mov     rbx, rax
  2011f3: 48 85 db                        test    rbx, rbx
  2011f6: 74 50                           je      0x201248 <.text+0xc8>
  2011f8: c6 83 54 22 20 00 00            mov     byte ptr [rbx + 0x202254], 0x0
  2011ff: 80 bb 53 22 20 00 0a            cmp     byte ptr [rbx + 0x202253], 0xa
  201206: 75 03                           jne     0x20120b <.text+0x8b>
  201208: 48 ff cb                        dec     rbx
  20120b: 49 89 dc                        mov     r12, rbx
  20120e: 48 bf 69 01 20 00 00 00 00 00   movabs  rdi, 0x200169
  201218: be 12 00 00 00                  mov     esi, 0x12
  20121d: e8 5e ff ff ff                  call    0x201180 <.text>
  201222: 48 bf 54 22 20 00 00 00 00 00   movabs  rdi, 0x202254
  20122c: 4c 89 e6                        mov     rsi, r12
  20122f: e8 4c ff ff ff                  call    0x201180 <.text>
  201234: 48 bf 7b 01 20 00 00 00 00 00   movabs  rdi, 0x20017b
  20123e: be 04 00 00 00                  mov     esi, 0x4
  201243: e8 38 ff ff ff                  call    0x201180 <.text>
  201248: b8 01 00 00 00                  mov     eax, 0x1
  20124d: bf 00 00 00 00                  mov     edi, 0x0
  201252: 0f 05                           syscall

You can see how closely it corresponds to the assembly code.
You will also notice that some instructions use 32-bit (esi, eax) and some use 64-bit (rsi, rax).
Why is that?
The simple answer is that the assembler is smart enough to shorten the bit length for small values.
This avoids wasting resources!

That's all