現在のブログ
ゲーム開発ブログ (2025年~) Gamedev Blog (2025~)
レガシーブログ
テクノロジーブログ (2018~2024年) リリースノート (2023~2025年) MeatBSD (2024年)
【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