В этой Лекции приводятся некоторые примеры того, как писать программы под ОС Linux и использованием взаимодействия с кодом на языке Assembler. Рассмотренные действия выполнятся на компьютере с Intel-архитектурой с установленной ОС Linux и установленным набором инструментов для кросс-компиляции. Исполняемые файлы запускаются под эмулятором Qemu в режиме пользователя. Несмотря на это, программы могут быть собраны и запущены на процессоре с архитектурой RISC-V как это было показано в Лекции 2.
После изучения этой лекции вы должны быть способны:
Операционная система предоставляет интерфейс для использования её ресурсов и сервисов путём выполнения системных вызовов. О том, чтобы получаемый в результате компиляции и линковки бинарный код соответствовал целевому окружению, заботится скрипт компоновщика.
ОС Linux предоставляет интерфейс для системных вызовов. Стоит отметить, что несмотря на то, что приведённые примеры были выполнены и проверены в ОС Debian, они должны работать и в других версия ОС Linux.
Введите следующую команду:
man syscall.2
И посмотрите на строки и столбцы с текстом "riscv":
Arch/ABI Instruction System Ret Ret Error Notes
call # val val2
-------------------------------------------------------------------
riscv ecall a7 a0 a1 -
...
Arch/ABI arg1 arg2 arg3 arg4 arg5 arg6 arg7 Notes
--------------------------------------------------------------
riscv a0 a1 a2 a3 a4 a5 -
Видно, что регистр a7 используется для задания номера системного вызова, регистры a0 и a1 используются для хранения возвращаемых значений, а аргументы передаются через регистры с a0 по a5.
Есть документация на различные системные вызовы:
man syscalls.2
NAME
syscalls - Linux system calls
SYNOPSIS
Linux system calls.
DESCRIPTION
The system call is the fundamental interface between an application and the Linux kernel.
...
System call Kernel Notes
------------------------------------------------------------------------
write(2) 1.0
...
В качестве примера рассмотрим системный вызов для вывода значений write. Чтобы получить информацию о параметрах системного вызова write, необходимо ввести в консоли:
man write.2
NAME
write - write to a file descriptor
SYNOPSIS
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
DESCRIPTION
write() writes up to count bytes from the buffer starting at buf to the file referred to by the file descriptor fd.
...
Системный вызов write позволяет выводить информацию в поток стандартного вывода. Файловый дескриптор со значением 1 является стандартным выводом, что, во-первых, является общим соглашением, а во-вторых, описано в заголовочном файле /usr/include/unistd.h.
Но эта информация - для программистов на языке C. Для программистов на языке Assembler необходимо знать численное значение номера системного вызова. В файле /usr/include/asm-generic/unistd.h содержится следующая строчка, задающая идентификатор для системного вызова:
grep write /usr/include/asm-generic/unistd.h /* fs/read_write.c */ #define __NR_write 64 __SYSCALL(__NR_write, sys_write) …
Кроме того, необходимо оформить правильный выход из программы. Это можно сделать с использованием системного вызова exit. Системный вызов exit имеет номер 93, что можно выяснить способом, аналогичным показанному выше.
Обладая этой информацией, теперь есть возможность написать программу, которая выводит традиционное сообщение "Hello World!". Для этого нам нужно загрузить регистры необходимыми параметрами и выполнить корректные системные вызовы. Параметрами системного вызова write являются файловый дескриптор, который необходимо поместить в регистр a0, адрес начала выводимого сообщения, который необходимо поместить в регистр a1 и длина сообщения в байтах, которую нужно поместить в регистр a2. Номер системного вызова необходимо помещать в регистр a7, а сам системный вызов осуществляется с помощью инструкции ecall:
# file hello.s
.equ write, 64
.equ exit, 93
.section .text
.globl _start
_start:
li a0, 1
la a1, msgbegin
lbu a2, msgsize
li a7, write
ecall
li a0, 0
li a7, exit
ecall
.section .rodata
msgbegin:
.ascii "Hello World!\n"
msgsize:
.byte .-msgbegin
Соберём и запустим программу:
riscv64-linux-gnu-as hello.s -o hello.o riscv64-linux-gnu-ld hello.o -o hello ./hello Hello World!
Помимо прямых системных вызовов, операционная система предоставляет целый набор библиотек, которые могут быть выполнены в двух вариантах: для динамической или статической компоновки. Используя связывание (линковку) с библиотеками, можно в своих программах использовать предоставляемый ими набор функций. Интерфейс обычно представляет собой описание функции для языка C, доступ к нему можно получить, путём линковки в окружении компилятора C.
Построение интерфейса с библиотеками на языке С требует соблюдения соглашений о вызовах RISC-V (RISC-V Calling Conventions, документ доступен по ссылке https://github.com/riscv-non-isa/riscv-elf-psabi-doc/blob/master/riscv-cc.adoc). Имеется возможность указать компилятору/компоновщику путём задания соответствующих аргументов, какой вариант соглашения ABI нужно использовать. Например, вариантом ABI по умолчанию для архитектуры RV64G является LP64D. ABI регламентирует размер типов данных, использованных компилятором, порядок передачи аргументов в функции, и т.д.
Компоновщик в процессе работы добавляет специальный служебный настроечный код для работы с библиотеками С, который запускается до разработанной программы на языке Assembler и затем передаёт управление на точку входа программы. В предыдущих примерах точкой входа был участок кода с меткой _start, поскольку до сих пор сборка в окружении языка C не использовалась, однако при сборке в окружении языка C необходимо, чтобы имя метки было _main.
В следующем примере программы на языке Assembler будут использованы функции printf и scanf стандартной библиотеки glibc, получить информацию по использованию которых можно в стандартное документации man printf.3 и man scanf.3. Демонстрируемая далее программа считает хэш по алгоритму djb2 (подробнее про алгоритм можно почитать по ссылке: http://www.cse.yorku.ca/~oz/hash.html) за авторством Д. Бернштейна. Хэш-функция вычисляет значение фиксированной длины для задаваемого в строковом виде набора данных. Алгоритм djb2 начинается с значения hash(0) = 5381 и вычисляет каждое следующее значение по формуле:
hash(i+1) = 33 * hash(i) ^ character(i),
где character(i) соответствует символу на i-той позиции во входной строке. Вычисления начинаются с самого первого символа и заканчиваются на последнем символе.
Следует обратить внимание, что программа написана для набора инструкций RV64I, поэтому для компиляции для набора инструкций RV32 вызовы addiw и slliw должны быть заменены на вызовы addi и slli. Оригинальная хэш-функция оперирует 32-битными числами (как и в приведённом ниже примере), однако алгоритм применим и для 64-битных чисел.
# djb2.s
.section .text
.globl main # run in C 'environment'
main:
addi sp, sp, -8 # store ra (return address) on stack
sd ra, 0(sp)
la a0, prompt # printf the prompt string
call printf
la a0, scanfmt # scanf from stdin (console)
la a1, input # into buffer input
call scanf # with format scanfmt
la a0, input # process input with djb2
call djb2
mv a1, a0
la a0, result # print result
call printf
li a0, 0
ld ra, 0(sp) # restore ra
addi sp, sp,8
ret # return to caller
djb2: # compute djb2
li t1, 5381 # init hash = 5381
djb2_loop:
lb t0, 0(a0) # process every char of input
beqz t0, dbj2_end # until zero appears
mv t2, t1
slliw t2, t2, 5 # t2 = hash << 5 = 32 * hash
addw t1, t1, t2 # t1 = 32 * hash + hash = 33 * hash
addw t1, t1, t0 # t1 = 33 * hash + char
addi a0, a0, 1 # next iteration
j djb2_loop
dbj2_end:
mv a0, t1 # return hash value
ret
.section .rodata
prompt:
.asciz "Enter text: "
scanfmt:
.asciz "%127[^\n]" # scanf max 127 chars and end with return
result:
.asciz "Hash is %lu\n" # write out the parameter as long unsigned
.section .bss
input: # storage for input
.zero 128
Файл djb2.s необходимо слинковать компилятором C, а собранный в результате бинарный исполняемый файл должна обладать информацией о том, где располагаются динамические библиотеки. Этого можно достичь, задействовав переменную окружения QEMU_LD_PREFIX как показано ниже:
riscv64-linux-gnu-as djb2.s -o djb2.o riscv64-linux-gnu-gcc djb2.o -o djb2 export QEMU_LD_PREFIX=/usr/riscv64-linux-gnu/ qemu-riscv64-static djb2 Enter text: hallo Hash is 261095189
Можно просмотреть исполняемый файл с помощью утилиты objdump и увидеть, что исполняемый файл отличается от предыдущих примеров за счёт настройки среды окружения C.
Следующая программа подсчитывает количество символов, слов и строк, задаваемых в стандартном вводе. В ней используется функция getchar библиотеки C, которая передаёт ASCII-коды вводимых символов. При подсчете слов предполагается, что слово представляет собой по крайней мере букву алфавита или десятичную цифру. Код перевода строки в Linux имеет ASCII-код 0xa, ASCII-коды десятичных цифр лежат в диапазоне от 0x30 до 0x39 для цифр от 0 до 9, буквы латинского алфавита в верхнем регистре имеют коды от 0x41 до 0x5a для букв от A до Z, и коды от 0x61 до 0x7a для букв нижнего регистра от a до z. Для детального анализа некоторых инструкций в приведённом исходном коде заданы номера строк.
00 # wordcount.s 01 .section .text 02 .globl main # run in C 'environment' 03 main: 04 addi sp, sp, -40 # store ra (return address) and saved regs on stack 05 sd ra, 0(sp) 06 sd s0, 8(sp) 07 sd s1, 16(sp) 08 sd s2, 24(sp) 09 sd s3, 32(sp) 10 11 li s0, 0 # counter chars 12 li s1, 0 # counter line feeds 13 li s2, 0 # counter words 14 li s3, 0 # indicator if current input is in word 15 16 loop: 17 call getchar # get input from stdin in a0 18 bltz a0, end # if end of file (eof is -1) goto end 19 20 addi s0, s0, 1 # count characters 21 22 li t0, 0xa # is linefeed (ascii 0xa)? 23 bne a0, t0, nolf # no -> continue 24 addi s1, s1, 1 # yes -> count 25 nolf: 26 27 # is this a word: char digit or alphabet? 28 addi t0, a0, -0x30 # digits go from 0x30 to 0x39 29 li t1, 0x9 # if (char-0x30) >= 0 and <= 0x9 then digit 30 bleu t0, t1, aldi # trick: treat negative value as unsigned 31 # value (or neg. as unsign.) > 0x9, continue 32 andi t0, a0, ~0x20 # 0x60 to range > 0x40, lower to upper cast 33 addi t0, t0, -0x41 # letter go from 0x41 to 0x5a 34 li t1, 0x19 # (char-0x41) >= 0 and <= 0x19, then alphabet 35 bleu t0, t1, aldi # trick again 36 # reached here, then not in word (anymore) 37 add s2, s2, s3 # count word, indicator is one if word else 0 38 li s3, 0 # clear indicator 39 j loop 40 41 aldi: 42 li s3, 1 # char is part of word, indicate for word counter 43 j loop 44 end: 45 la a0, result # print result 46 mv a1, s1 47 mv a2, s2 48 mv a3, s0 49 call printf 50 51 li a0, 0 52 53 ld s3, 32(sp) # restore saved regs. 54 ld s2, 24(sp) 55 ld s1, 16(sp) 56 ld s0, 8(sp) 57 ld ra, 0(sp) # restore ra 58 addi sp, sp,40 59 ret # return to caller 60 61 .section .rodata 62 result: 63 .asciz "Lines: %u Words: %u Chars: %u\n" # write out result
Инструкции на строках 04-09 и 53-58 сохраняют значения регистров ra и s0-s3 согласно ABI, регистры s0-s2 используются в роли счётчиков для подсчёта символов, слов и строк соответственно. Регистр s3 принимает значение 1, если входной символ представляет собой букву слова. Все эти регистры изначально инициализируются нулями в строках 11-14.
В цикле со строки 16 вызывается функция getchar. Если в результате вызова getchar было получено значение -1 (признак конца файла или конца водимых данных), происходит выход из цикла (строка 18). Если getchar вернула ASCII-код символа, увеличивается значение счётчика символов (строка 20). Если getchar вернула символ перевода строки, увеличивается счётчик строк (строки 22-24). Считанный символ проверяется на то, что он является числом (строки 27-30) или буквой (строки 32-36) и в случае, если он попадает в один из этих диапазонов, значение регистра s3 выставляется равным 1 (наличие 1 в регистре s3 говорит о том, что обнаружено слово) и осуществляется переход на новую итерацию цикла считывания (строки 42-43). Если же прочитанный символ не входит в требуемые диапазоны, значение регистра r3 добавляется к счётчику слов, и его значение становится равным 0.
Строки 28-30 и 32-35 определяют, находится ли символ в заданном диапазоне. Первая проверка - на попадание в диапазон от 0x30 до 0x39 (цифры), это достигается путём вычитания из кода символа значения 0x30 и проверки на то, что результат лежит в диапазоне от 0 до 9. При этом выполняется операция если-меньше-или-равно (bleu). Строки 33-35 ведут себя похожим образом для диапазона от 0x41 до 0x5a за исключением того, что в строке 32 очищается бит 5 рассматриваемого символа инструкцией andi с инверсией значения 0x20. Очищая этот бит, все значения, большие или равные 0x60, отображаются в диапазон от значения 0x40, а значения ниже 0x60 не затрагиваются. Это позволяет выполнить проверку в строках 33-35 также и для значений в диапазоне от 0x61 до 0x7a.
riscv64-linux-gnu-as -o wordcount.o wordcount.s riscv64-linux-gnu-gcc -o wordcount wordcount.o export QEMU_LD_PREFIX=/usr/riscv64-linux-gnu/ qemu-riscv64-static wordcount word Word .... hello next line should be two lines Lines: 2 Words: 9 Chars: 60
Следующий код запрашивает число и проверяет его на простоту, результат этой проверки выводится на экран. Программа реализует алгоритм "решето Эратосфена". C-функция scanf принимает аргумент - строку "%u", которая читает беззнаковое целое. Метки, начинающиеся с .L, являются локальными и не экспортируются в таблицу символов.
00 # prime.s 01 .equ maxnb, 0x100000 02 .section .text 03 .globl main # run in C 'environment' 04 main: 05 addi sp, sp, -8 # store ra (return address) on stack 06 sd ra, 0(sp) 07 08 la a0, prompt # printf the prompt string 09 call printf 10 11 la a0, scanfmt # scanf from stdin (console) 12 la a1, input # into buffer input 13 call scanf # with format scanfmt 14 15 blez a0, .Lerr # input error 16 17 la a1, input # check if input number n fits 18 lw a1, 0(a1) 19 li t0, maxnb 20 bge a1, t0, .Lerr 21 22 la a0, input # process input with sieve 23 call sieve 24 25 bnez a0, .Lp1 26 .Lp0: 27 la a0, outno 28 j .Lpp 29 .Lp1: 30 la a0, outyes # print result 31 j .Lpp 32 .Lerr: 33 la a0, error 34 .Lpp: 35 call printf 36 37 li a0, 0 38 39 ld ra, 0(sp) # restore ra 40 addi sp, sp,8 41 ret # return to caller 42 43 sieve: 44 # input: register a0 points to number n 45 # that is checked if it is a prime. 46 # output: if n is prime a0 is one else zero 47 # sieve of Erastosthenes 48 # init array with numbers 49 lw t1, 0(a0) # nb to check 50 li t2, 2 # counter start with 2 51 la t3, array # pointer to array 52 .Ls0: 53 sw t2, 8(t3) # set item to index, 8() is begin with index 2 54 addi t3, t3, 4 # increment by four for word size 55 addi t2, t2, 1 # counter 56 ble t2, t1, .Ls0 # until counter == nb to check 57 58 # array has now the values: 0 0 2 3 4 5 6 7 8 9 10... 59 60 # non-primes are cancelled out 61 # by setting their array items to zero 62 li t2, 2 # start with 2, t2 is index i 63 la t3, array # t3 is pointer to array 64 .Ls1: 65 lw t4, 8(t3) # t4 is current array item (offset by 2) 66 beqz t4, .Ls3 # no prime, continue at .Ls3 67 68 mul t4, t2, t2 # t4 = t2 * t2; t4 is index j 69 .Ls2: 70 slli t5, t4, 2 # t5 = t4 * 4 for offset (words) in array 71 add t5, t3, t5 # t5 = t3 + t5; t5 is address in array for j 72 sw x0, 0(t5) # set entry to 0, no prime nb, array[j] = 0 73 add t4, t4, t2 # t4 = t4 + t2; j += i 74 ble t4, t1, .Ls2 # cancel out all multiples of i for i < n 75 76 .Ls3: 77 addi t2, t2, 1 # continue with next number 78 mul t0, t2, t2 # as long as n*n > index 79 ble t0, t1, .Ls1 80 81 slli t0, t1, 2 # use n as index 82 add t0, t3, t0 # compute address in array 83 lw t0, 0(t0) # load its item 84 snez a0, t0 # set a0 to 1 if array[n] != 0 85 86 ret 87 88 89 .section .rodata 90 prompt: 91 .asciz "Enter number (<1048576): " 92 scanfmt: 93 .asciz "%u" # scanf an unsigned int number 94 outyes: 95 .asciz "is a prime number.\n" 96 outno: 97 .asciz "is not a prime number.\n" 98 error: 99 .asciz "wrong input.\n" 100 .section .bss 101 input: # storage for numbers 102 .word 0 103 array: 104 .zero 4*maxnb # max number storage
Строки 01-41 реализуют с вводом числа и выводом результата. Решето Эратосфена реализовано в строках 43-86. Строки 89-104 описывают данные, с которыми происходит работа.
Первая часть проверки чисел на простоту инициализирует массив (строки 49-56). Основная часть состоит в исключении всех кратных чисел, начиная с числа два (строки 62-79). Для этого обрабатывается каждый элемент массива и если он уже исключён (равен 0), берется следующий элемент (строки 64-66). В противном случае аннулируются все кратные числа, начиная с квадрата рассматриваемого числа (строки 68-74, внутренний цикл) и заканчивая самим введенным числом. Затем обрабатывается следующий элемент массива (строки 64-79, внешний цикл). Это продолжается до тех пор, пока рассматриваемое число меньше или равно введенному числу (строки 77-79). Элемент массива, соответствующий введённому числу, проверяется в конце на предмет того, равен он нулю (число не простое) или нет (число- простое) (строки 81-84).
riscv64-linux-gnu-as -o prime.o prime.s riscv64-linux-gnu-gcc -o prime prime.o export QEMU_LD_PREFIX=/usr/riscv64-linux-gnu/ qemu-riscv64-static prime Enter number (<1048576): 1034123 is a prime number.
В этой Лекции приводятся некоторые примеры того, как писать программы под ОС Linux и использованием взаимодействия с кодом на языке Assembler. Рассмотренные действия выполнятся на компьютере с Intel-архитектурой с установленной ОС Linux и установленным набором инструментов для кросс-компиляции. Исполняемые файлы запускаются под эмулятором Qemu в режиме пользователя. Несмотря на это, программы могут быть собраны и запущены на процессоре с архитектурой RISC-V как это было показано в Лекции 2.
После изучения этой лекции вы должны быть способны:
Операционная система предоставляет интерфейс для использования её ресурсов и сервисов путём выполнения системных вызовов. О том, чтобы получаемый в результате компиляции и линковки бинарный код соответствовал целевому окружению, заботится скрипт компоновщика.
ОС Linux предоставляет интерфейс для системных вызовов. Стоит отметить, что несмотря на то, что приведённые примеры были выполнены и проверены в ОС Debian, они должны работать и в других версия ОС Linux.
Введите следующую команду:
man syscall.2
И посмотрите на строки и столбцы с текстом "riscv":
Arch/ABI Instruction System Ret Ret Error Notes
call # val val2
-------------------------------------------------------------------
riscv ecall a7 a0 a1 -
...
Arch/ABI arg1 arg2 arg3 arg4 arg5 arg6 arg7 Notes
--------------------------------------------------------------
riscv a0 a1 a2 a3 a4 a5 -
Видно, что регистр a7 используется для задания номера системного вызова, регистры a0 и a1 используются для хранения возвращаемых значений, а аргументы передаются через регистры с a0 по a5.
Есть документация на различные системные вызовы:
man syscalls.2
NAME
syscalls - Linux system calls
SYNOPSIS
Linux system calls.
DESCRIPTION
The system call is the fundamental interface between an application and the Linux kernel.
...
System call Kernel Notes
------------------------------------------------------------------------
write(2) 1.0
...
В качестве примера рассмотрим системный вызов для вывода значений write. Чтобы получить информацию о параметрах системного вызова write, необходимо ввести в консоли:
man write.2
NAME
write - write to a file descriptor
SYNOPSIS
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
DESCRIPTION
write() writes up to count bytes from the buffer starting at buf to the file referred to by the file descriptor fd.
...
Системный вызов write позволяет выводить информацию в поток стандартного вывода. Файловый дескриптор со значением 1 является стандартным выводом, что, во-первых, является общим соглашением, а во-вторых, описано в заголовочном файле /usr/include/unistd.h.
Но эта информация - для программистов на языке C. Для программистов на языке Assembler необходимо знать численное значение номера системного вызова. В файле /usr/include/asm-generic/unistd.h содержится следующая строчка, задающая идентификатор для системного вызова:
grep write /usr/include/asm-generic/unistd.h /* fs/read_write.c */ #define __NR_write 64 __SYSCALL(__NR_write, sys_write) …
Кроме того, необходимо оформить правильный выход из программы. Это можно сделать с использованием системного вызова exit. Системный вызов exit имеет номер 93, что можно выяснить способом, аналогичным показанному выше.
Обладая этой информацией, теперь есть возможность написать программу, которая выводит традиционное сообщение "Hello World!". Для этого нам нужно загрузить регистры необходимыми параметрами и выполнить корректные системные вызовы. Параметрами системного вызова write являются файловый дескриптор, который необходимо поместить в регистр a0, адрес начала выводимого сообщения, который необходимо поместить в регистр a1 и длина сообщения в байтах, которую нужно поместить в регистр a2. Номер системного вызова необходимо помещать в регистр a7, а сам системный вызов осуществляется с помощью инструкции ecall:
# file hello.s
.equ write, 64
.equ exit, 93
.section .text
.globl _start
_start:
li a0, 1
la a1, msgbegin
lbu a2, msgsize
li a7, write
ecall
li a0, 0
li a7, exit
ecall
.section .rodata
msgbegin:
.ascii "Hello World!\n"
msgsize:
.byte .-msgbegin
Соберём и запустим программу:
riscv64-linux-gnu-as hello.s -o hello.o riscv64-linux-gnu-ld hello.o -o hello ./hello Hello World!
Помимо прямых системных вызовов, операционная система предоставляет целый набор библиотек, которые могут быть выполнены в двух вариантах: для динамической или статической компоновки. Используя связывание (линковку) с библиотеками, можно в своих программах использовать предоставляемый ими набор функций. Интерфейс обычно представляет собой описание функции для языка C, доступ к нему можно получить, путём линковки в окружении компилятора C.
Построение интерфейса с библиотеками на языке С требует соблюдения соглашений о вызовах RISC-V (RISC-V Calling Conventions, документ доступен по ссылке https://github.com/riscv-non-isa/riscv-elf-psabi-doc/blob/master/riscv-cc.adoc). Имеется возможность указать компилятору/компоновщику путём задания соответствующих аргументов, какой вариант соглашения ABI нужно использовать. Например, вариантом ABI по умолчанию для архитектуры RV64G является LP64D. ABI регламентирует размер типов данных, использованных компилятором, порядок передачи аргументов в функции, и т.д.
Компоновщик в процессе работы добавляет специальный служебный настроечный код для работы с библиотеками С, который запускается до разработанной программы на языке Assembler и затем передаёт управление на точку входа программы. В предыдущих примерах точкой входа был участок кода с меткой _start, поскольку до сих пор сборка в окружении языка C не использовалась, однако при сборке в окружении языка C необходимо, чтобы имя метки было _main.
В следующем примере программы на языке Assembler будут использованы функции printf и scanf стандартной библиотеки glibc, получить информацию по использованию которых можно в стандартное документации man printf.3 и man scanf.3. Демонстрируемая далее программа считает хэш по алгоритму djb2 (подробнее про алгоритм можно почитать по ссылке: http://www.cse.yorku.ca/~oz/hash.html) за авторством Д. Бернштейна. Хэш-функция вычисляет значение фиксированной длины для задаваемого в строковом виде набора данных. Алгоритм djb2 начинается с значения hash(0) = 5381 и вычисляет каждое следующее значение по формуле:
hash(i+1) = 33 * hash(i) ^ character(i),
где character(i) соответствует символу на i-той позиции во входной строке. Вычисления начинаются с самого первого символа и заканчиваются на последнем символе.
Следует обратить внимание, что программа написана для набора инструкций RV64I, поэтому для компиляции для набора инструкций RV32 вызовы addiw и slliw должны быть заменены на вызовы addi и slli. Оригинальная хэш-функция оперирует 32-битными числами (как и в приведённом ниже примере), однако алгоритм применим и для 64-битных чисел.
# djb2.s
.section .text
.globl main # run in C 'environment'
main:
addi sp, sp, -8 # store ra (return address) on stack
sd ra, 0(sp)
la a0, prompt # printf the prompt string
call printf
la a0, scanfmt # scanf from stdin (console)
la a1, input # into buffer input
call scanf # with format scanfmt
la a0, input # process input with djb2
call djb2
mv a1, a0
la a0, result # print result
call printf
li a0, 0
ld ra, 0(sp) # restore ra
addi sp, sp,8
ret # return to caller
djb2: # compute djb2
li t1, 5381 # init hash = 5381
djb2_loop:
lb t0, 0(a0) # process every char of input
beqz t0, dbj2_end # until zero appears
mv t2, t1
slliw t2, t2, 5 # t2 = hash << 5 = 32 * hash
addw t1, t1, t2 # t1 = 32 * hash + hash = 33 * hash
addw t1, t1, t0 # t1 = 33 * hash + char
addi a0, a0, 1 # next iteration
j djb2_loop
dbj2_end:
mv a0, t1 # return hash value
ret
.section .rodata
prompt:
.asciz "Enter text: "
scanfmt:
.asciz "%127[^\n]" # scanf max 127 chars and end with return
result:
.asciz "Hash is %lu\n" # write out the parameter as long unsigned
.section .bss
input: # storage for input
.zero 128
Файл djb2.s необходимо слинковать компилятором C, а собранный в результате бинарный исполняемый файл должна обладать информацией о том, где располагаются динамические библиотеки. Этого можно достичь, задействовав переменную окружения QEMU_LD_PREFIX как показано ниже:
riscv64-linux-gnu-as djb2.s -o djb2.o riscv64-linux-gnu-gcc djb2.o -o djb2 export QEMU_LD_PREFIX=/usr/riscv64-linux-gnu/ qemu-riscv64-static djb2 Enter text: hallo Hash is 261095189
Можно просмотреть исполняемый файл с помощью утилиты objdump и увидеть, что исполняемый файл отличается от предыдущих примеров за счёт настройки среды окружения C.
Следующая программа подсчитывает количество символов, слов и строк, задаваемых в стандартном вводе. В ней используется функция getchar библиотеки C, которая передаёт ASCII-коды вводимых символов. При подсчете слов предполагается, что слово представляет собой по крайней мере букву алфавита или десятичную цифру. Код перевода строки в Linux имеет ASCII-код 0xa, ASCII-коды десятичных цифр лежат в диапазоне от 0x30 до 0x39 для цифр от 0 до 9, буквы латинского алфавита в верхнем регистре имеют коды от 0x41 до 0x5a для букв от A до Z, и коды от 0x61 до 0x7a для букв нижнего регистра от a до z. Для детального анализа некоторых инструкций в приведённом исходном коде заданы номера строк.
00 # wordcount.s 01 .section .text 02 .globl main # run in C 'environment' 03 main: 04 addi sp, sp, -40 # store ra (return address) and saved regs on stack 05 sd ra, 0(sp) 06 sd s0, 8(sp) 07 sd s1, 16(sp) 08 sd s2, 24(sp) 09 sd s3, 32(sp) 10 11 li s0, 0 # counter chars 12 li s1, 0 # counter line feeds 13 li s2, 0 # counter words 14 li s3, 0 # indicator if current input is in word 15 16 loop: 17 call getchar # get input from stdin in a0 18 bltz a0, end # if end of file (eof is -1) goto end 19 20 addi s0, s0, 1 # count characters 21 22 li t0, 0xa # is linefeed (ascii 0xa)? 23 bne a0, t0, nolf # no -> continue 24 addi s1, s1, 1 # yes -> count 25 nolf: 26 27 # is this a word: char digit or alphabet? 28 addi t0, a0, -0x30 # digits go from 0x30 to 0x39 29 li t1, 0x9 # if (char-0x30) >= 0 and <= 0x9 then digit 30 bleu t0, t1, aldi # trick: treat negative value as unsigned 31 # value (or neg. as unsign.) > 0x9, continue 32 andi t0, a0, ~0x20 # 0x60 to range > 0x40, lower to upper cast 33 addi t0, t0, -0x41 # letter go from 0x41 to 0x5a 34 li t1, 0x19 # (char-0x41) >= 0 and <= 0x19, then alphabet 35 bleu t0, t1, aldi # trick again 36 # reached here, then not in word (anymore) 37 add s2, s2, s3 # count word, indicator is one if word else 0 38 li s3, 0 # clear indicator 39 j loop 40 41 aldi: 42 li s3, 1 # char is part of word, indicate for word counter 43 j loop 44 end: 45 la a0, result # print result 46 mv a1, s1 47 mv a2, s2 48 mv a3, s0 49 call printf 50 51 li a0, 0 52 53 ld s3, 32(sp) # restore saved regs. 54 ld s2, 24(sp) 55 ld s1, 16(sp) 56 ld s0, 8(sp) 57 ld ra, 0(sp) # restore ra 58 addi sp, sp,40 59 ret # return to caller 60 61 .section .rodata 62 result: 63 .asciz "Lines: %u Words: %u Chars: %u\n" # write out result
Инструкции на строках 04-09 и 53-58 сохраняют значения регистров ra и s0-s3 согласно ABI, регистры s0-s2 используются в роли счётчиков для подсчёта символов, слов и строк соответственно. Регистр s3 принимает значение 1, если входной символ представляет собой букву слова. Все эти регистры изначально инициализируются нулями в строках 11-14.
В цикле со строки 16 вызывается функция getchar. Если в результате вызова getchar было получено значение -1 (признак конца файла или конца водимых данных), происходит выход из цикла (строка 18). Если getchar вернула ASCII-код символа, увеличивается значение счётчика символов (строка 20). Если getchar вернула символ перевода строки, увеличивается счётчик строк (строки 22-24). Считанный символ проверяется на то, что он является числом (строки 27-30) или буквой (строки 32-36) и в случае, если он попадает в один из этих диапазонов, значение регистра s3 выставляется равным 1 (наличие 1 в регистре s3 говорит о том, что обнаружено слово) и осуществляется переход на новую итерацию цикла считывания (строки 42-43). Если же прочитанный символ не входит в требуемые диапазоны, значение регистра r3 добавляется к счётчику слов, и его значение становится равным 0.
Строки 28-30 и 32-35 определяют, находится ли символ в заданном диапазоне. Первая проверка - на попадание в диапазон от 0x30 до 0x39 (цифры), это достигается путём вычитания из кода символа значения 0x30 и проверки на то, что результат лежит в диапазоне от 0 до 9. При этом выполняется операция если-меньше-или-равно (bleu). Строки 33-35 ведут себя похожим образом для диапазона от 0x41 до 0x5a за исключением того, что в строке 32 очищается бит 5 рассматриваемого символа инструкцией andi с инверсией значения 0x20. Очищая этот бит, все значения, большие или равные 0x60, отображаются в диапазон от значения 0x40, а значения ниже 0x60 не затрагиваются. Это позволяет выполнить проверку в строках 33-35 также и для значений в диапазоне от 0x61 до 0x7a.
riscv64-linux-gnu-as -o wordcount.o wordcount.s riscv64-linux-gnu-gcc -o wordcount wordcount.o export QEMU_LD_PREFIX=/usr/riscv64-linux-gnu/ qemu-riscv64-static wordcount word Word .... hello next line should be two lines Lines: 2 Words: 9 Chars: 60
Следующий код запрашивает число и проверяет его на простоту, результат этой проверки выводится на экран. Программа реализует алгоритм "решето Эратосфена". C-функция scanf принимает аргумент - строку "%u", которая читает беззнаковое целое. Метки, начинающиеся с .L, являются локальными и не экспортируются в таблицу символов.
00 # prime.s 01 .equ maxnb, 0x100000 02 .section .text 03 .globl main # run in C 'environment' 04 main: 05 addi sp, sp, -8 # store ra (return address) on stack 06 sd ra, 0(sp) 07 08 la a0, prompt # printf the prompt string 09 call printf 10 11 la a0, scanfmt # scanf from stdin (console) 12 la a1, input # into buffer input 13 call scanf # with format scanfmt 14 15 blez a0, .Lerr # input error 16 17 la a1, input # check if input number n fits 18 lw a1, 0(a1) 19 li t0, maxnb 20 bge a1, t0, .Lerr 21 22 la a0, input # process input with sieve 23 call sieve 24 25 bnez a0, .Lp1 26 .Lp0: 27 la a0, outno 28 j .Lpp 29 .Lp1: 30 la a0, outyes # print result 31 j .Lpp 32 .Lerr: 33 la a0, error 34 .Lpp: 35 call printf 36 37 li a0, 0 38 39 ld ra, 0(sp) # restore ra 40 addi sp, sp,8 41 ret # return to caller 42 43 sieve: 44 # input: register a0 points to number n 45 # that is checked if it is a prime. 46 # output: if n is prime a0 is one else zero 47 # sieve of Erastosthenes 48 # init array with numbers 49 lw t1, 0(a0) # nb to check 50 li t2, 2 # counter start with 2 51 la t3, array # pointer to array 52 .Ls0: 53 sw t2, 8(t3) # set item to index, 8() is begin with index 2 54 addi t3, t3, 4 # increment by four for word size 55 addi t2, t2, 1 # counter 56 ble t2, t1, .Ls0 # until counter == nb to check 57 58 # array has now the values: 0 0 2 3 4 5 6 7 8 9 10... 59 60 # non-primes are cancelled out 61 # by setting their array items to zero 62 li t2, 2 # start with 2, t2 is index i 63 la t3, array # t3 is pointer to array 64 .Ls1: 65 lw t4, 8(t3) # t4 is current array item (offset by 2) 66 beqz t4, .Ls3 # no prime, continue at .Ls3 67 68 mul t4, t2, t2 # t4 = t2 * t2; t4 is index j 69 .Ls2: 70 slli t5, t4, 2 # t5 = t4 * 4 for offset (words) in array 71 add t5, t3, t5 # t5 = t3 + t5; t5 is address in array for j 72 sw x0, 0(t5) # set entry to 0, no prime nb, array[j] = 0 73 add t4, t4, t2 # t4 = t4 + t2; j += i 74 ble t4, t1, .Ls2 # cancel out all multiples of i for i < n 75 76 .Ls3: 77 addi t2, t2, 1 # continue with next number 78 mul t0, t2, t2 # as long as n*n > index 79 ble t0, t1, .Ls1 80 81 slli t0, t1, 2 # use n as index 82 add t0, t3, t0 # compute address in array 83 lw t0, 0(t0) # load its item 84 snez a0, t0 # set a0 to 1 if array[n] != 0 85 86 ret 87 88 89 .section .rodata 90 prompt: 91 .asciz "Enter number (<1048576): " 92 scanfmt: 93 .asciz "%u" # scanf an unsigned int number 94 outyes: 95 .asciz "is a prime number.\n" 96 outno: 97 .asciz "is not a prime number.\n" 98 error: 99 .asciz "wrong input.\n" 100 .section .bss 101 input: # storage for numbers 102 .word 0 103 array: 104 .zero 4*maxnb # max number storage
Строки 01-41 реализуют с вводом числа и выводом результата. Решето Эратосфена реализовано в строках 43-86. Строки 89-104 описывают данные, с которыми происходит работа.
Первая часть проверки чисел на простоту инициализирует массив (строки 49-56). Основная часть состоит в исключении всех кратных чисел, начиная с числа два (строки 62-79). Для этого обрабатывается каждый элемент массива и если он уже исключён (равен 0), берется следующий элемент (строки 64-66). В противном случае аннулируются все кратные числа, начиная с квадрата рассматриваемого числа (строки 68-74, внутренний цикл) и заканчивая самим введенным числом. Затем обрабатывается следующий элемент массива (строки 64-79, внешний цикл). Это продолжается до тех пор, пока рассматриваемое число меньше или равно введенному числу (строки 77-79). Элемент массива, соответствующий введённому числу, проверяется в конце на предмет того, равен он нулю (число не простое) или нет (число- простое) (строки 81-84).
riscv64-linux-gnu-as -o prime.o prime.s riscv64-linux-gnu-gcc -o prime prime.o export QEMU_LD_PREFIX=/usr/riscv64-linux-gnu/ qemu-riscv64-static prime Enter number (<1048576): 1034123 is a prime number.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.