vak: (Default)
[personal profile] vak
Выдавать 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

Date: 2024-11-09 01:40 (UTC)
lxe: (Default)
From: [personal profile] lxe
Лайк.
Возникает вопрос, почему не появилось (хотя бы в междусобоях) стандартных библиотек (или оберток таковых), которые было бы легко звать на ассемблере (как EMT/INT).

Date: 2024-11-09 02:14 (UTC)
ircicq: (Default)
From: [personal profile] ircicq
(как EMT/INT)

Прерывания - это API к ядру. Он есть в любой ОС. Но Си-функции так вызывать было бы накладно.

Date: 2024-11-09 02:47 (UTC)
lxe: (Default)
From: [personal profile] lxe
Я не имел в виду context switch, я имел в виду calling convention.

Date: 2024-11-09 02:00 (UTC)
ircicq: (Default)
From: [personal profile] ircicq
библиотека libc не проинициализирована толком.

Как правило Си-библиотеки инициализируются вызовом одной функции.
Что-то вроде __crt_init()

и есть негромоздкие библиотеки: https://github.com/picolibc/picolibc
Edited Date: 2024-11-09 02:11 (UTC)

Date: 2024-11-10 08:07 (UTC)
ircicq: (Default)
From: [personal profile] ircicq
Проверил c вызовом Си-шной putchar() на Linux под x64:
инициализация не потребовалась.
$ ./factorial
20! = 2432902008176640000
$ size factorial
   text    data     bss     dec     hex filename
   1330     586       6    1922     782 factorial
$ stat -c%s factorial
16336


16KB всего лишь (в реальности в основном нули, наверное можно опциями линкера покомпактнее уложить).
Но это динамическая libc.so.6. Статически было бы больше

Date: 2024-11-09 08:02 (UTC)
tiresome_cat: (CuriousCat)
From: [personal profile] tiresome_cat
Ассемблер хорош для кода, который должен быть максимально компактным. Для космических аппаратов, например. Там с трудозатратами можно не считаться.

Date: 2024-11-09 17:59 (UTC)
perdakot: (Default)
From: [personal profile] perdakot
Если б не те странные 200k, которые тащит crt от вызова инициализации, C++ не намного хуже. memmove или atoi не нужно инициализировать.