В подавляющем большинстве случаев, современные программы пишутся на языках высокого уровня. Процессоры не понимают языков высокого уровня, поэтому компиляторы переводят текст (программу), написанный на языке высокого уровня в последовательность простых инструкций языка ассемблера. После этого, программу на языке ассемблера переводят в последовательность машинных команд — то, что понятно процессору.
На лекции было разобрано несколько примеров основных синтаксических конструкций языка высокого уровня C. Условный оператор if реализуется за счёт использования инструкций условного перехода.
if (/*условие*/) {
//тело условия если True
} else {
// тело условия если False
}
# условие вычисляется в xN регистр
beqz xN, else
#тело условия если True для if
j endif
else:
#тело условия если False для if
endif:
Оператор цикла while также реализуется за счёт применения инструкций условного перехода.
while (/*условие*/) {
// тело цикла
}
while:
# условие вычисляется в xN
beqz xN, endwhile
# тело цикла
j while
endwhile:
Процедуры (они же функции или подпрограммы) – это повторно используемые фрагменты кода, реализующие вычисления определённой задачи. Использование процедур позволяет абстрагироваться и повторно использовать один и тот же код, но с разными входными параметрами. Большие программы состоят из подпрограмм, включающих в себя другие подпрограммы и так далее.
Программа, которая вызывает подпрограмму называется вызывающей. Подпрограмма которую вызывают называется вызываемой подпрограммой. Вызывающая программа использует тот же набор регистров, что и вызываемая, поэтому: либо вызывающая, либо вызываемая должна сохранять регистры вызывающей в памяти и восстанавливает их, когда процедура завершает своё выполнение.
Вызов подпрограммы означает передачу управления этой подпрограмме, то есть загрузки в PC (program counter, указатель на инструкцию) адреса первой инструкции вызываемой подпрограммы. Чтобы вернуться к месту вызова процедуры, когда выполнение подпрограммы закончится, при её вызове необходимо использовать специальную инструкцию jal (jump and link).
Соглашение о вызовах устанавливает правила использования регистров между процедурами. В соглашении о вызовах RISC-V даются символические имена регистров x0 — x31 для обозначения их роли. Вызываемая подпрограмма получает аргументы через регистры a0 — a7. В таблице ниже приводится указывается какие из регистров должны быть сохранены неизменными при возврате из подпрограммы, и, какие регистры следует сохранить перед вызовом подпрограммы, если их содержимое планируется использовать после.
Каждый вызов процедуры имеет свой собственный экземпляр данных, включающий: аргументы функции, содержимое регистрового файла и адрес возврата, и называемый активационной записью. Для хранения активационных записей функций используется стек, занимающий часть основной памяти. Стек — это способ организации памяти, при котором первая запись будет считана в последнюю очередь (LIFO — last-in-first-out). Для поддержания работы стека используется регистр x2, также именуемый sp (Stack Pointer — указатель стека), который указывает на последнюю ячейку памяти помещённую в стек. Далее приводится пример вызова подпрограммы с сохранением сохраняемых регистров на стек.
addi sp, sp, -8 # выделить место на стеке для двух элементов
sw ra, 0(sp) # сохранить регистр ra на стек
sw a1, 4(sp) # сохранить регистр a1 на стек
jal ra, func # сохранить в ra адрес возврата PC+4 и перейти к func
lw ra, 0(sp) # восстановить из стека значение ra
lw a1, 4(sp) # восстановить из стека значение a1
addi sp, sp, 8 # освободить место на стеке
func: # вызываемая функция
addi sp, sp, -4 # выделить место на стеке
sw s0, 0(sp) # сохранить регистр s0 на стеке
# ...... некий код, выполнение функции, использующей регистр s0
lw s0, 0(sp) # восстановить из стека значение s0
addi sp, sp, 4 # освободить место на стеке
jr ra # вернуться в основную программу
Большинство языков программирования (в том числе C) используют три отдельных области памяти для данных:
-
Stack: Содержит данные используемые процедурными вызовами. Регистр sp указывает на вершину стека
-
Static: Содержит глобальные переменные, которые существуют в течении всего времени жизни программы. Регистр gp (Global Pointer) указывает на начало этой области
-
Heap: Содержит динамически-распределяемые данные и растёт в сторону старших адресов. В C программист управляет кучей в ручную, размещая новые данные с помощью malloc() и освобождая с помощью free(). В Python, Java, и большинстве современных языков, куча управляется автоматически
-
Text: область памяти содержащая программный код
Также на лекции затронули вопрос компиляции программ с языков высокого уровня. Этот процесс происходит в несколько этапов. Сначала высокоуровневый код компилируется в код на языке ассемблера, который затем ассемблируется в машинный код и сохраняется в виде объектного файла. Компоновщик, также называемый редактором связей или линкером (linker), объединяет полученный объектный код с объектным кодом библиотек и других файлов, в результате чего получается готовая к исполнению программа. На практике, большинство компиляторных пакетов выполняют все три шага: компиляцию, ассемблирование и компоновку. Наконец, загрузчик загружает программу в память и запускает её.
- Ссылка на видеозапись лекции
- Все материалы лекции можно найти в этом источнике, к сожалению аналога на русском пока не нашел [Patterson Hennessy. Computer organization and design. RISC-V edition — 2 глава]
- Про процесс компиляции можно почитать, например, в этом источнике [Харрис и Харрис. Цифровая схемотехника и архитектура компьютера — весь параграф 6.6 с подпунктами]
- Полезная информация по программированию на языке ассемблера RISC-V, на английском языке
- Здесь можно познакомиться, разобраться и пописать ассемблерные программки под архитектуру 6502 — классический процессор, на котором работают Бендер, Терминатор и Денди. Давай-давай, не стесняйся, заходи. Прямо на странице есть встроенный симулятор и объясняют как написать простенькую игру — змейку. Если не в курсе, люди соревнуется, у кого она получится меньше по объёму кода. Да и вообще в интернете полно информации по этому процессору. Процитирую автора ресурса: 6502 is fun. Nobody ever called x86 fun. А тут на русском про программирование 6502.
- В этом онлайн-компиляторе можно смотреть в какую последовательность ассемблерных инструкций скомпилируется твой код на C++ для самых разных архитектур. Можно, например, сравнить x86, ARM и RISC-V, при том разных версий компиляторов — чем отличается генерируемые инструкции процессору в каждом случае, где код длиннее, где требуется много подготовительных операций и тому подобное. В конце концов можно наглядно посмотреть разницу между программами для CISC и RISC архитектур