Выдавать
Hello World на ассемблере научились, а давайте теперь сбацаем что-нибудь простое арифметическое. Факториал посчитаем, к примеру. Дело нехитрое.
.text
factorial:
mv t0, a0 # Устанавливаем начальное значение счётчика
1: addi t0, t0, -1 # Уменьшаем счётчик в регистре t0
beqz t0, 2f # Если получился ноль, идём на выход
mul a0, a0, t0 # Умножаем регистр результата на счётчик
j 1b # На следующую итерацию цикла
2: ret # Возвращаем результат в регистре a0
Попробуем вызвать эту функцию и показать результат... Однако натыкаемся на проблему! Как напечатать целое число из ассемблера? Из Си это сделать нетрудно, есть развитая библиотека libc, а в ней
printf(). Ничего подобного не существует для ассемблера. Попытка вызвать Си-шный
printf() из ассемблера плохо заканчивается. Потому что библиотека libc не проинициализирована толком. Для этого нужно линковаться с crt0.o и прочей ерундой. Выходит слишком громоздко.
Единственный выход - написать все нужные процедуры печати на ассемблере. Что мы и сделаем. Начнём с печати целого десятичного беззнакового числа.
#include <sys/syscall.h>
.text
.globl print_uns
print_uns:
mv a2, sp # end
addi sp, sp, -32 # allocate buf
mv a1, a2 # ptr
li a3, 10 # base
1:
remu a5, a0, a3 # value % base
addi a1, a1, -1 # --ptr
addi a5, a5, 48 # + '0'
sb a5, 0(a1) # *ptr = character
mv a5, a0 # old value
divu a0, a0, a3 # value /= base
bgeu a5, a3, 1b # if (old value >= base) continue
sub a2, a2, a1 # end - ptr
li a0, 1 # stdout
li a7, SYS_write # write() system call
ecall
addi sp, sp, 32
ret
Понадобится также процедура печати произвольной текстовой строки.
#include <sys/syscall.h>
.text
.globl print_string
print_string:
addi sp, sp, -16 # allocate space in stack
sd ra, 0(sp) # save return address
sd a0, 8(sp) # save string pointer
call strlen
mv a2, a0 # byte count
ld a1, 8(sp) # restore string pointer
li a0, 1 # stdout
li a7, SYS_write # write() system call
ecall
ld ra, 0(sp) # restore return address
addi sp, sp, 16 # free space in stack
ret
Заметьте, чтобы напечатать строку, требуется сначала посчитать её длину. Делаем функцию
strlen().
.text
.globl strlen
strlen: # a0 = const char *str
addi t1, a0, 1 # ptr + 1
1:
lb t0, 0(a0) # get byte from string
addi a0, a0, 1 # increment pointer
bnez t0, 1b # continue if not end
sub a0, a0, t1 # compute length - 1 for '\0' char
ret
Ну и отдельная процедура для выдачи конца строки.
#include <sys/syscall.h>
.text
.globl print_newline
print_newline:
li a7, SYS_write # write() system call
li a0, 1 # stdout
la a1, newline # string
li a2, 1 # one character
ecall
ret
newline:
.string "\n"
Теперь можем соорудить вызов факториала и показать результат.
#include <sys/syscall.h>
.globl _start
_start:
ld a0, input
call print_uns
la a0, text
call print_string
ld a0, input
call factorial
call print_uns
call print_newline
li a7, SYS_exit # exit the program
li a0, 0 # status code
ecall
.align 3
input: .dword 20
text: .string "! = "
Компилируем, запускаем.
$ cpp factorial.S | as -o factorial.o -
$ cpp print_uns.S | as -o print_uns.o -
$ cpp print_newline.S | as -o print_newline.o -
$ cpp print_string.S | as -o print_string.o -
$ as -o strlen.o strlen.s
$ ld -o factorial factorial.o print_uns.o print_newline.o print_string.o strlen.o
$ file factorial
factorial: ELF 64-bit LSB executable, UCB RISC-V, double-float ABI, version 1 (SYSV), statically linked, not stripped
$ size factorial
text data bss dec hex filename
268 0 0 268 10c factorial
$ ./factorial
20! = 2432902008176640000
Всё работает как положено. Размер программы 268 байт. Но становится понятно, почему народ перестал программировать на ассемблере. Всякую мелось приходится делать самому: никаких полезных библиотек. Напомню, что всё это происходит под Ubuntu на процессоре
PIC64.
Файлы можно взять здесь:
github.com/sergev/vak-opensource/tree/master/languages/assembler/riscv