Основы программирования на языке Assembler под архитектуру RISC-V

Примеры

Показывать лекцию целиком

В этой Лекции рассматриваются более продвинутые темы программирования под RISC-V на примере использования структур данных (реализуется связный список) и работы с числами с плавающей запятой. Цель этой лекции - показать, как реализуются базовые алгоритмы, применяемые в вычислительной технике.

После прочтения этой лекции вы должны быть способны:

  • реализовывать структуры данных и использовать функции malloc и free стандартной библиотеки языка C;
  • писать программы, работающие с числами с плавающей запятой.
  • Примеры

    Связный список

    Связанный список - простейшая структура данных. Связный список состоит из элементов, содержащих данные, и указателей на другие аналогичные элементы. Указатель - специальная переменная, которая хранит адрес в памяти, в данном случае - адрес элемента списка. Элементы списка упорядочены в том смысле, что у каждого элемента есть потомок, связанный с текущим элементом по указателю. Есть первый элемент, называемый головой списка - указатель на самый первый элемент списка. Есть последний элемент, называемый хвостом списка - задаётся элементом без потомка, указатель на потомка у такого элемента равен нулю (нулевой указатель).

    Рассмотрим список, состоящий из двух элементов, имеющих значения 0x32 и 0x12. Первый элемент расположен в памяти по адресу 0x32c0, а второй - по адресу 0x32a0. Каждый элемент содержит значение и указатель на следующий элемент (он может быть нулевым). Голова списка указывает на самый первый элемент: head = 0x32c0:

    Адрес           Значение    Указатель
    0x32c0:         0x32        0x32a0 
    0x32a0:         0x12    0x0000
    

    Самый простой способ вставить элемент в список - затолкнуть его в голову, при этом для каждого нового элемента требуется выделить место в памяти, что обычно делается динамически через менеджер памяти и область динамической памяти, называемой "куча". В процессе удаления элемента из списка (выталкивание) должна освободиться память. Для решения задач выделения и освобождения памяти используются C-функции malloc и free.

    Элемент списка на языке Assembler может быть задан с помощью полей, которые предоставляют доступ к значению элемента списка и указателю на следующий элемент. Это делается в примере:

    # linkedlist_struct.s
    # offset to value
    .equ node_val, 0
    # offset to pointer(address) of next element
    .equ node_next, 8
    # size of one element
    .equ node_size, 16
    
    # calls:
    # linkedlist_push 
    # input> a0: head, a1: value
    # output< a0: head or -1 if error
    
    # linkedlist_pop
    # input> a0: head
    # output< a0: head, a1: value
    
    # linkedlist_print
    # input> a0: head
    

    В этом примере для работы со связным список реализованы три функции: для добавления элемента в список (операция заталкивания, push), для удаления элемента из списка (операция выталкивания, pop), и для печати значений всех элементов списка от головы до хвоста. Функция push создаёт новый элемент, при этом бывшая голова списка становится потомком для только что созданного элемента, а голова (указатель на начало списка) теперь указывает на созданный элемент. Функция pop инвертирует это действие, голова списка начинает указывать на потомок текущего элемента, а сам элемент, ранее бывший головой списка, удаляется. Функция print печатает все элементы.

    # linkedlist.s
    .include "src/linkedlist_struct.s"
    .section .text
    .globl linkedlist_push
    linkedlist_push:
        addi sp, sp, -24
        sd ra, 16(sp)
        sd a0, 8(sp)
        sd a1, 0(sp)
    
        # alloc memory
        li a0, node_size
        call malloc
        beqz a0, .L0err
    
        # value
        ld t1, 0(sp)
        sd t1, node_val(a0)
        # insert as new head head
        ld t0, 8(sp)
        sd t0, node_next(a0)
    
        # || val | next ->|| -> || ... | ... ||
        j .L0exit
    .L0err:
        li a0, -1
    .L0exit:
        ld ra, 16(sp)
        addi sp, sp, 24
        ret
    
    .globl linkedlist_pop
    linkedlist_pop:
        addi sp, sp, -16
        sd ra, 8(sp)
        sd s0, 0(sp)
        # if head is zero
        beqz a0, .L1err
    
        # return value
        ld a1, node_val(a0)
        # pointer to next element will be new head
        ld s0, node_next(a0)
    
        # free memory
        call free
    
        # return new head
        mv a0, s0
        j .L1exit
    .L1err:
        li a0, -1
    .L1exit:
        ld s0, 0(sp)
        ld ra, 8(sp)
        addi sp, sp, 16 
        ret
    
    .globl linkedlist_print
    linkedlist_print:
        addi sp, sp, -24
        sd ra, 16(sp)
        sd a0, 8(sp)
        sd a0, 0(sp)
    .L2loop:
        beqz a0, .L2exit
    
        sd a0, 0(sp)
        ld a1, node_val(a0)
        la a0, .L2prompt
        call printf
    
        ld a0, 0(sp)
        ld a0, node_next(a0)
        j .L2loop
    
    .L2exit:
        ld a0, 8(sp)
        ld ra, 16(sp)
        addi sp, sp, 24
        ret
    .section .rodata
    .L2prompt: 
        .asciz "%u \n"
    

    Использование связного списка показано в основном коде. Элементы добавляются, удаляются, печатаются.

    # main.s
    .include "src/linkedlist_struct.s"
    .section .text
    .globl main
    main:
        addi sp, sp, -8
        sd ra, 0(sp)
        # new head of list
        li s0, 0
    
        mv a0, s0
        li a1, 0x12
        call linkedlist_push
    
        li a1, 0x23
        call linkedlist_push
    
        li a1, 0x45
        call linkedlist_push
    
        call linkedlist_pop
    
        li a1, 0x56
        call linkedlist_push
    
        call linkedlist_print
    
        call linkedlist_pop
        call linkedlist_pop
        call linkedlist_pop
    
        ld ra, 0(sp)
        addi sp, sp, 8
        ret
    

    Подсчёт среднего значения

    Следующий пример показывает использование регистров для работы с числами с плавающей запятой. Она начинается с чтения дробных чисел с помощью функции scanf (строки 09-19) в память (по метке buffer). Числа с плавающей запятой занимают 4 байта. Программа в ходе своей работы подсчитывает сумму всех значений, для этой цели используются специальные регистры для работы с числами с плавающей запятой ft0, ft1 и ft3. Целочисленные значения могут быть сконвертированы в числа с плавающей запятой инструкцией fcvt.s.w (конвертирует знаковое целое длинной в слово в дробное значение), это происходит на строках 24 и 25. Второй цикл (строки 26-36) вычисляет сумму путём загрузки чисел с плавающей запятой из памяти (строка 34) и суммируя числа с сохранением результата в регистре ft1 (строка 35). Среднее значение вычисляется благодаря операции деления (строка 38). В итоге, результат печатается с использованием функции printf. Функция printf требует аргумент в виде числа двойной точности, загруженного в регистр a1, эта операция реализуется в строчках 41 и 42. Программе нужны расширения F и D, описанные в спецификации. Программе на стандартный ввод можно перенаправить данные из файла используя возможности перенаправления потоков операционной системы: average < numbers.txt.

    00 # average.s
    01 .equ maxNb, 100         # maximal numbers to be read
    02 .section .text
    03 .globl main             # run in C 'environment' 
    04 main:
    05     addi sp, sp, -16    # store ra (return address) and saved regs on stack
    06     sd ra, 0(sp)
    07     sd s0, 8(sp)
    08 
    09     li s0, 0            # counter numbers: i
    10 .L0input:               # read in numbers
    11     la a0, scanfmt
    12     la a1, buffer
    13     slli t0, s0, 2      # next input in buffer+4*i
    14     add a1, a1, t0
    15     call scanf          # read float number
    16     blez a0, .L0avg    # if no number could be read anymore, continue..
    17     addi s0, s0, 1      # count numbers read in s0: i = i + 1
    18     li t0, maxNb        # if i < maxNb, continue reading
    19     blt s0, t0, .L0input
    20 
    21 .L0avg:
    22     beqz s0, .L0err # check if at least one number is read, else exit
    23 
    24     fcvt.s.w ft0, s0    # divider: convert from int in s0 to float
    25     fcvt.s.w ft1, x0    # sum: init with zero, convert from x0
    26 .L0avgloop:
    27     beqz s0, .L0out     # count down s0 to zero: i = i - 1
    28     addi s0, s0, -1
    29 
    30     slli t1, s0, 2      # compute address to float number in memory
    31     la t0, buffer
    32     add  t0, t0, t1
    33 
    34     flw ft2, 0(t0)      # load from memory to float register ft2
    35     fadd.s ft1, ft1, ft2    # sum in ft1
    36     j .L0avgloop
    37 .L0out:
    38     fdiv.s ft1, ft1, ft0    # average in ft1
    39 
    40     la a0, resultfmt    # print result using printf
    41     fcvt.d.s ft1, ft1   # need to prepare for printf, expand to double size
    42     fmv.x.d a1, ft1     # move from ft1 to a1
    43     call printf         # and print
    44 
    45 .L0err:
    46     li a0, 0
    47 
    48     ld s0, 8(sp)
    49     ld ra, 0(sp)        # restore ra
    50     addi sp, sp,16
    51     ret                 # return to caller
    52 
    53 .section .rodata
    54 scanfmt:
    55 .asciz "%f"
    56 resultfmt:
    57 .asciz "Average: %f\n"
    58 
    59 .section .bss
    60 buffer:
    61 .zero 4*maxNb
    
    Вернуться к учебному плану