English version draft

Assisted-by: Claude:claude-4.6-sonnet
This commit is contained in:
Andrei Solodovnikov
2026-04-12 13:53:25 +03:00
parent 63260f434e
commit f3fcd27387
74 changed files with 5133 additions and 5875 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -9,21 +9,21 @@ See https://github.com/MPSU/APS/blob/master/LICENSE file for licensing details.
* ------------------------------------------------------------------------------
*/
OUTPUT_FORMAT("elf32-littleriscv") /* Указываем порядок следования байт */
OUTPUT_FORMAT("elf32-littleriscv") /* Specify the byte order */
ENTRY(_start) /* мы сообщаем компоновщику, что первая
исполняемая процессором инструкция
находится у метки "_start"
ENTRY(_start) /* We tell the linker that the first
instruction executed by the processor
is at the label "_start"
*/
/*
В данном разделе указывается структура памяти:
Сперва идет регион "instr_mem", являющийся памятью с исполняемым кодом
(об этом говорит аттрибут 'x'). Этот регион начинается
с адреса 0x00000000 и занимает 1024 байта.
Далее идет регион "data_mem", начинающийся с адреса 0x00000000 и занимающий
2048 байт. Этот регион является памятью, противоположной региону "instr_mem"
(в том смысле, что это не память с исполняемым кодом).
This section specifies the memory structure:
First comes the "instr_mem" region, which is executable memory
(indicated by the 'x' attribute). This region starts
at address 0x00000000 and occupies 1024 bytes.
Next comes the "data_mem" region, starting at address 0x00000000 and
occupying 2048 bytes. This region is non-executable memory
(in the sense that it does not contain executable code).
*/
MEMORY
{
@@ -31,51 +31,50 @@ MEMORY
data_mem (!x) : ORIGIN = 0x00000000, LENGTH = 2K
}
_trap_stack_size = 640; /* Размер стека обработчика перехватов.
Данный размер позволяет выполнить
до 8 вложенных вызовов при обработке
перехватов.
_trap_stack_size = 640; /* Size of the trap handler stack.
This size allows up to 8 nested
calls during trap handling.
*/
_stack_size = 640; /* Размер программного стека.
Данный размер позволяет выполнить
от 8 вложенных вызовов.
_stack_size = 640; /* Size of the program stack.
This size allows up to 8 nested
calls.
*/
/*
В данном разделе описывается размещение программы в памяти.
Программа разделяется на различные секции:
- секции исполняемого кода программа;
- секции статических переменных и массивов, значение которых должно быть
"вшито" в программу;
и т.п.
This section describes the placement of the program in memory.
The program is divided into various sections:
- sections of the executable code;
- sections of static variables and arrays whose values must be
embedded in the program;
etc.
*/
SECTIONS
{
/*
В скриптах компоновщика есть внутренняя переменная, записываемая как '.'
Эта переменная называется "счетчиком адресов". Она хранит текущий адрес в
памяти.
В начале файла она инициализируется нулем. Добавляя новые секции, эта
переменная будет увеличиваться на размер каждой новой секции.
Если при размещении секций не указывается никакой адрес, они будут размещены
по текущему значению счетчика адресов.
Этой переменной можно присваивать значения, после этого, она будет
увеличиваться с этого значения.
Подробнее:
In linker scripts there is an internal variable written as '.'
This variable is called the "location counter". It stores the current
address in memory.
At the beginning of the file it is initialized to zero. As new sections
are added, this variable is incremented by the size of each new section.
If no address is specified when placing sections, they will be placed
at the current value of the location counter.
This variable can be assigned values; after that, it will increment
from that value.
More details:
https://home.cs.colorado.edu/~main/cs1300/doc/gnu/ld_3.html#IDX338
*/
/*
Следующая команда сообщает, что начиная с адреса, которому в данных момент
равен счетчик адресов (в данный момент, начиная с нуля) будет находиться
секция .text итогового файла, которая состоит из секций .boot, а также всех
секций, начинающихся на .text во всех переданных компоновщику двоичных
файлах.
Дополнительно мы указываем, что данная секция должна быть размещена в
регионе "instr_mem".
The following command specifies that starting from the address currently
held by the location counter (at this point, starting from zero), the
.text section of the output file will be located, consisting of the .boot
sections as well as all sections starting with .text from all binary files
passed to the linker.
We additionally specify that this section must be placed in the
"instr_mem" region.
*/
.text : {
PROVIDE(_start = .);
@@ -85,29 +84,29 @@ SECTIONS
/*
Секция данных размещается аналогично секции инструкций за исключением
адреса загрузки в памяти (Load Memory Address, LMA). Поскольку память
инструкций и данных физически разделены, у них есть пересекающееся адресное
пространство, которое мы бы хотели использовать (поэтому в разделе MEMORY мы
указали что стартовые адреса обоих памятей равны нулю). Однако компоновщику
это не нравится, ведь как он будет размещать две разные секции в одно и то же
место. Поэтому мы ему сообщаем, с помощью оператора "AT", что загружать секцию
данных нужно на самом деле не по нулевому адресу, а по какому-то другому,
заведомо большему чем размер памяти инструкций, но процессор будет
использовать адреса, начинающиеся с нуля. Такой вариант компоновщика
устраивает и он собирает исполняемый файл без ошибок. Наша же задача,
загрузить итоговую секцию данных по нулевым адресам памяти данных.
The data section is placed similarly to the instruction section, except
for the Load Memory Address (LMA). Since instruction and data memories are
physically separate, they have overlapping address spaces that we would
like to use (which is why in the MEMORY section we set the start addresses
of both memories to zero). However, the linker does not like this, since
how can it place two different sections in the same location? So we tell it,
using the "AT" operator, that the data section should actually be loaded
at a different address — one that is guaranteed to be larger than the size
of the instruction memory — while the processor will use addresses starting
from zero. The linker accepts this arrangement and builds the executable
without errors. Our task is then to load the final data section at address
zero in data memory.
*/
.data : AT (0x00800000) {
/*
Общепринято присваивать GP значение равное началу секции данных, смещенное
на 2048 байт вперед.
Благодаря относительной адресации со смещением в 12 бит, можно адресоваться
на начало секции данных, а также по всему адресному пространству вплоть до
4096 байт от начала секции данных, что сокращает объем требуемых для
адресации инструкций (практически не используются операции LUI, поскольку GP
уже хранит базовый адрес и нужно только смещение).
Подробнее:
It is conventional to assign GP a value equal to the start of the data
section offset forward by 2048 bytes.
With 12-bit relative addressing with offset, it is possible to address
the beginning of the data section as well as the entire address space up
to 4096 bytes from the start of the data section, which reduces the number
of addressing instructions needed (LUI operations are rarely used since GP
already holds the base address and only an offset is needed).
More details:
https://groups.google.com/a/groups.riscv.org/g/sw-dev/c/60IdaZj27dY/m/s1eJMlrUAQAJ
*/
_gbl_ptr = . + 2048;
@@ -117,37 +116,38 @@ SECTIONS
/*
Поскольку мы не знаем суммарный размер всех используемых секций данных,
перед размещением других секций, необходимо выровнять счетчик адресов по
4х-байтной границе.
Since we do not know the total size of all data sections used,
before placing other sections, we must align the location counter
to a 4-byte boundary.
*/
. = ALIGN(4);
/*
BSS (block started by symbol, неофициально его расшифровывают как
better save space) — это сегмент, в котором размещаются неинициализированные
статические переменные. В стандарте Си сказано, что такие переменные
инициализируются нулем (или NULL для указателей). Когда вы создаете
статический массив — он должен быть размещен в исполняемом файле.
Без bss-секции, этот массив должен был бы занимать такой же объем
исполняемого файла, какого объема он сам. Массив на 1000 байт занял бы
1000 байт в секции .data.
Благодаря секции bss, начальные значения массива не задаются, вместо этого
здесь только записываются названия переменных и их адреса.
Однако на этапе загрузки исполняемого файла теперь необходимо принудительно
занулить участок памяти, занимаемый bss-секцией, поскольку статические
переменные должны быть проинициализированы нулем.
Таким образом, bss-секция значительным образом сокращает объем исполняемого
файла (в случае использования неинициализированных статических массивов)
ценой увеличения времени загрузки этого файла.
Для того, чтобы занулить bss-секцию, в скрипте заводятся две переменные,
указывающие на начало и конец bss-секции посредством счетчика адресов.
Подробнее:
BSS (block started by symbol, unofficially expanded as
"better save space") is a segment where uninitialized static
variables are placed. The C standard states that such variables
are initialized to zero (or NULL for pointers). When you create
a static array, it must be placed in the executable file.
Without a bss section, the array would occupy as much space in the
executable as its own size. A 1000-byte array would take 1000 bytes
in the .data section.
Thanks to the bss section, the initial values of the array are not stored;
instead, only the variable names and their addresses are recorded.
However, during executable loading, the memory region occupied by the
bss section must be explicitly zeroed out, since static variables must
be initialized to zero.
Thus, the bss section significantly reduces the size of the executable
(when using uninitialized static arrays) at the cost of increased
loading time.
To zero out the bss section, two variables are defined in the script
that point to the beginning and end of the bss section via the
location counter.
More details:
https://en.wikipedia.org/wiki/.bss
Дополнительно мы указываем, что данная секция должна быть размещена в
регионе "data_mem".
We additionally specify that this section must be placed in the
"data_mem" region.
*/
_bss_start = .;
.bss : {
@@ -158,30 +158,32 @@ SECTIONS
/*=================================
Секция аллоцированных данных завершена, остаток свободной памяти отводится
под программный стек, стек прерываний и (возможно) кучу. В соглашении о
вызовах архитектуры RISC-V сказано, что стек растет снизу вверх, поэтому
наша цель разместить его в самых последних адресах памяти.
Поскольку стеков у нас два, в самом низу мы разместим стек прерываний, а
над ним программный стек. При этом надо обеспечить защиту программного
стека от наложения на него стека прерываний.
Однако перед этим, мы должны убедиться, что под оба стека хватит места.
The allocated data section is complete; the remaining free memory is
reserved for the program stack, the trap stack, and (possibly) the heap.
The RISC-V calling convention states that the stack grows downward
(from higher to lower addresses), so our goal is to place it at the
highest addresses in memory.
Since we have two stacks, the trap stack is placed at the very bottom
and the program stack above it. We must also ensure that the program
stack is protected from being overwritten by the trap stack.
Before doing so, however, we must verify that there is enough room
for both stacks.
=================================
*/
/* Мы хотим гарантировать, что под стек останется место */
/* We want to guarantee that there is room left for the stack */
ASSERT(. < (LENGTH(data_mem) - _trap_stack_size - _stack_size),
"Program size is too big")
/* Перемещаем счетчик адресов над стеком прерываний (чтобы после мы могли
использовать его в вызове ALIGN) */
/* Move the location counter above the trap stack (so that we can
use it in the ALIGN call later) */
. = LENGTH(data_mem) - _trap_stack_size;
/*
Размещаем указатель программного стека так близко к границе стека
прерываний, насколько можно с учетом требования о выравнивании адреса
стека до 16 байт.
Подробнее:
Place the program stack pointer as close to the trap stack boundary
as possible, subject to the requirement that the stack address be
aligned to 16 bytes.
More details:
https://riscv.org/wp-content/uploads/2015/01/riscv-calling.pdf
*/
_stack_ptr = ALIGN(16) <= LENGTH(data_mem) - _trap_stack_size?
@@ -189,15 +191,15 @@ SECTIONS
ASSERT(_stack_ptr <= LENGTH(data_mem) - _trap_stack_size,
"SP exceed memory size")
/* Перемещаем счетчик адресов в конец памяти (чтобы после мы могли
использовать его в вызове ALIGN) */
/* Move the location counter to the end of memory (so that we can
use it in the ALIGN call later) */
. = LENGTH(data_mem);
/*
Обычно память имеет размер, кратный 16, но на случай, если это не так, мы
делаем проверку, после которой мы либо остаемся в самом конце памяти (если
конец кратен 16), либо поднимаемся на 16 байт вверх от края памяти,
округленного до 16 в сторону большего значения
Memory size is usually a multiple of 16, but in case it is not, we
perform a check: we either stay at the very end of memory (if the
end is a multiple of 16), or move 16 bytes up from the memory edge
rounded up to the nearest multiple of 16.
*/
_trap_stack_ptr = ALIGN(16) <= LENGTH(data_mem) ? ALIGN(16) : ALIGN(16) - 16;
ASSERT(_trap_stack_ptr <= LENGTH(data_mem), "ISP exceed memory size")

View File

@@ -12,10 +12,10 @@ See https://github.com/MPSU/APS/blob/master/LICENSE file for licensing details.
.global _start
_start:
la gp, _gbl_ptr # Инициализация глобального указателя
la sp, _stack_ptr # Инициализация указателя на стек
la gp, _gbl_ptr # Initialize the global pointer
la sp, _stack_ptr # Initialize the stack pointer
# Инициализация (зануление) сегмента bss
# Initialize (zero out) the bss segment
la t0, _bss_start
la t1, _bss_end
_bss_init_loop:
@@ -24,67 +24,65 @@ _bss_init_loop:
addi t0, t0, 4
j _bss_init_loop
# Настройка вектора (mtvec) и маски (mie) прерываний, а также указателя на стек
# прерываний (mscratch).
# Configure the interrupt vector (mtvec), interrupt mask (mie),
# and trap stack pointer (mscratch).
_irq_config:
la t0, _int_handler
li t1, -1 # -1 (все биты равны 1) означает, что разрешены все прерывания
li t1, -1 # -1 (all bits set to 1) means all interrupts are enabled
la t2, _trap_stack_ptr
csrw mtvec, t0
csrw mscratch, t2
csrw mie, t1
# Вызов функции main
# Call the main function
_main_call:
li a0, 0 # Передача аргументов argc и argv в main. Формально, argc должен
li a1, 0 # быть больше нуля, а argv должен указывать на массив строк,
# нулевой элемент которого является именем исполняемого файла,
# Но для простоты реализации оба аргумента всего лишь обнулены.
# Это сделано для детерминированного поведения программы в случае,
# если программист будет пытаться использовать эти аргументы.
li a0, 0 # Pass argc and argv arguments to main. Formally, argc should
li a1, 0 # be greater than zero, and argv should point to an array of
# strings whose zeroth element is the executable name.
# For simplicity of implementation, both arguments are simply
# set to zero. This is done for deterministic program behavior
# in case the programmer tries to use these arguments.
# Вызов main.
# Для того чтобы программа скомпоновалась, где-то должна быть описана
# функция именно с таким именем.
# Call main.
# For the program to link successfully, a function with exactly
# this name must be defined somewhere.
call main
# Зацикливание после выхода из функции main
# Infinite loop after main returns
_endless_loop:
j _endless_loop
# Низкоуровневый обработчик прерывания отвечает за:
# * Сохранение и восстановление контекста;
# * Вызов высокоуровневого обработчика с передачей id источника прерывания в
# качестве аргумента.
# В основе кода лежит обработчик из репозитория urv-core:
# The low-level interrupt handler is responsible for:
# * Saving and restoring context;
# * Calling the high-level handler with the interrupt source id
# as an argument.
# The code is based on the handler from the urv-core repository:
# https://github.com/twlostow/urv-core/blob/master/sw/common/irq.S
# Из реализации убраны сохранения нереализованных CS-регистров. Кроме того,
# в реализации сохраняются только необерегаемые регистры регистрового файла.
# Это сделано по причине того, что при вызове высокоуровневого обработчика
# прерываний, тот будет обязан сохранить оберегаемые регистры в соответствии
# с соглашением о вызовах.
# Saves of unimplemented CS registers have been removed. Additionally,
# only caller-saved registers are saved here, because the high-level
# interrupt handler is required to preserve callee-saved registers
# in accordance with the calling convention.
_int_handler:
# Данная операция меняет местами регистры sp и mscratch.
# В итоге указатель на стек прерываний оказывается в регистре sp, а вершина
# программного стека оказывается в регистре mscratch.
# This operation swaps the sp and mscratch registers.
# As a result, the trap stack pointer ends up in sp, and the top
# of the program stack ends up in mscratch.
csrrw sp, mscratch,sp
# Далее мы поднимаемся по стеку прерываний и сохраняем все регистры.
addi sp, sp, -80 # Указатель на стек должен быть выровнен до 16 байт, поэтому
# поднимаемся вверх не на 76, а на 80.
# Move up the trap stack and save all registers.
addi sp, sp, -80 # The stack pointer must be aligned to 16 bytes,
# so we move up by 80, not 76.
sw ra, 4(sp)
# Мы хотим убедиться, что очередное прерывание не наложит стек прерываний на
# программный стек, поэтому записываем в освободившийся регистр низ
# программного стека, и проверяем что приподнятый указатель на верхушку
# стека прерываний не залез в программный стек.
# В случае, если это произошло (произошло переполнение стека прерываний),
# мы хотим остановить работу процессора, чтобы не потерять данные, которые
# могут помочь нам в отладке этой ситуации.
# We want to ensure that a subsequent interrupt does not cause the trap
# stack to overwrite the program stack, so we load the bottom of the
# program stack into the freed register and verify that the raised trap
# stack pointer has not encroached on the program stack.
# If this has happened (trap stack overflow), we want to halt the
# processor to avoid losing data that could help us debug the situation.
la ra, _stack_ptr
blt sp, ra, _endless_loop
sw t0, 12(sp) # Мы перепрыгнули через смещение 8, поскольку там должен
# лежать регистр sp, который ранее сохранили в mscratch.
# Мы запишем его на стек чуть позже.
sw t0, 12(sp) # We skipped offset 8 because that is where the sp
# register saved into mscratch earlier should go.
# We will write it to the stack a little later.
sw t1, 16(sp)
sw t2, 20(sp)
sw a0, 24(sp)
@@ -100,8 +98,8 @@ _int_handler:
sw t5, 64(sp)
sw t6, 68(sp)
# Кроме того, мы сохраняем состояние регистров прерываний на случай, если
# произойдет ещё одно прерывание.
# We also save the interrupt register state in case another
# interrupt occurs.
csrr t0, mscratch
csrr t1, mepc
csrr a0, mcause
@@ -109,16 +107,16 @@ _int_handler:
sw t1, 72(sp)
sw a0, 76(sp)
# Вызов высокоуровневого обработчика прерываний.
# Для того чтобы программа скомпоновалась, где-то должна быть описана
# функция именно с таким именем.
# Call the high-level interrupt handler.
# For the program to link successfully, a function with exactly
# this name must be defined somewhere.
call int_handler
# Восстановление контекста. В первую очередь мы хотим восстановить CS-регистры,
# на случай, если происходило вложенное прерывание. Для этого, мы должны
# вернуть исходное значение указателя стека прерываний. Однако его нынешнее
# значение нам ещё необходимо для восстановления контекста, поэтому мы
# сохраним его в регистр a0, и будем восстанавливаться из него.
# Restore context. First, we want to restore the CS registers in case
# a nested interrupt occurred. To do this, we must restore the original
# value of the trap stack pointer. However, its current value is still
# needed for context restoration, so we save it to register a0 and
# restore from there.
mv a0,sp
lw t1, 72(a0)
@@ -132,9 +130,9 @@ _int_handler:
lw t0, 12(a0)
lw t1, 16(a0)
lw t2, 20(a0)
lw a1, 28(a0) # Мы пропустили a0, потому что сейчас он используется в
# качестве указателя на верхушку стека и не может быть
# восстановлен.
lw a1, 28(a0) # We skipped a0 because it is currently used as a
# pointer to the top of the stack and cannot be
# restored.
lw a2, 32(a0)
lw a3, 36(a0)
lw a4, 40(a0)
@@ -147,5 +145,6 @@ _int_handler:
lw t6, 68(a0)
lw a0, 24(a0)
# Выход из обработчика прерывания
# Return from the interrupt handler
mret