View Categories

Программирование процессора

Машинный код #

Процессор, в зависимости от своего устройства, умеет выполнять конкретные команды - инструкции: например взять число из памяти по адресу, записать число в память по адресу, сложить два числа, сравнить числа и так далее. При проектировании процессора предусматривается этот фиксированный набор команд, каждой команде присваивается некий числовой код (opcode), а процессор имеет возможность прочитать код и выполнить действие, исходя из его значения. Таким образом, программа для процессора - это набор известных ему инструкций, поданных вместе с аргументами, с которыми каждая инструкция должна выполняться. Такой код называется машинным кодом, то есть это непосредственно код, который выполняется процессором. Пример машинного кода программы, которая создаёт две переменные со значениями 10 и 20 и записывает в третью переменную их сумму:

:020000040000FA
:100000000C9440000C9480000C9480000C9400000C9400000C9400000C9400000001
:00000001FF

Программа записывается в постоянную память, на каждом шаге процессор просто берет из неё следующую инструкцию и выполняет - программа выполняется последовательно сверху вниз, шаг за шагом. Такой шаг называется тактом (cycle) работы процессора. Тактируюший сигнал (clock) на процессор подаёт внешнее устройство, некий тактовый генератор. Чем выше частота тактов - тем быстрее работает процессор. Но есть предел, обусловленный быстродействием транзисторов, устройством процессора и его отдельных блоков, поэтому у любого процессора в характеристиках указана максимальная частота, на которой он будет стабильно работать.

Тут появляется такое понятие, как разгон процессора - повышение частоты выше номинальной. Чтобы процессор работал стабильно на повышенной частоте, обычно также повышают и напряжение питания, чтобы транзисторы быстрее переключались. Что закономерно приводит к повышению энергопотребления и нагреву процессора - с этим знаком каждый, кто хоть раз занимался разгоном процессора своего компьютера.

Ассемблер #

Писать программу на языке машинных кодов, вручную указывая процессору команды и адреса очень сложно. Первые компьютеры программирвались вручную - механическими переключателями, перфокартами и просто машинным кодом в памяти. Затем умные люди придумали язык программирования ассемблер (assembly) и компилятор (compiler) - программу, которая переводит человеко-читаемый код программы в машинный код для процессора, файл с машинным кодом принято называть бинарным (binary file) или в народе "бинарником". В компиляторе указывается модель процессора, благодаря чему он может перевести код на ассемблере в команды для конкретного процессора. Пример предыдущей программы на ассемблере:

.org 0x00
    rjmp main
main:
    ldi r16, 10
    ldi r17, 20
    add r16, r17
    rjmp .

Всё ещё ничего не понятно, но логика уже прослеживается. Вон числа 10 и 20, вон команда add...

Программа почти на любом языке программирования выполняется сверху вниз и слева направо

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

Именно поэтому разные модели процессоров будут выполнять одни и те же операции с разной скоростью, даже если у них одинаковая рабочая частота

Си #

Сложность и неуниверсальность ассемблера закономерно привела к появлению языков более высокого уровня, например - C (Си) в 1972 году. Код на Си выглядит лаконично, читаемо и самое главное - позволяет писать серьезные программы без оглядки на набор команд процессора и ручные манипуляции с данными. Всё тот же пример, но на Си:

int main(void) {
    uint8_t var1 = 10;
    uint8_t var2 = 20;
    uint8_t result = var1 + var2;
}

По сути написанная на Си программа может без изменений работать на любом процессоре, потому что основную работу делает компилятор. Он знает, какие команды и особенности есть у процессора и на основе этого максимально оптимально переводит программу в машинный код. Причём не просто переводит, а также:

  • Проверяет программу на ошибки
  • Оптимизирует: вырезает неиспользуемый код, сокращает то что может сократить, меняет операции на более оптимальные и так далее
  • Содержит в себе готовые оптимальные наборы инструкций для выполнения некоторых операций. Например, если используется операция деления, а процессор не поддерживает деление - компилятор подставит набор инструкций, который это деление выполнит

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

Компилятор Си также поддерживает вставки на ассемблере, то есть посреди кода на Си можно вставить часть кода на ассемблере и он будет работать, такая практика довольно часто встречается. Для чего это нужно: некоторые ресурсоёмкие задачи получается решить более оптимально "вручную" на ассемблере, например какие то сложные вычисления или реализацию интерфейсов связи. В большинстве случаев компилятор сам генерирует оптимальный код, но иногда его можно ускорить или сделать более гибким с использованием ассемблерных вставок. Например в синтаксисе Си нет возможности пропустить один такт процессора, а на ассемблере - есть.

C++ #

Язык Си считается самым лёгким и быстрым языком программирования, так как он очень близок к "железу". Тем не менее, писать на нём сложные программы неудобно и в начале 80-х годов появился язык "Си с классами", в дальнейшем получивший название C++. Классы позволяют писать более структурный, лаконичный и гибкий код, а также комфортно разрабатывать и поддерживать крупные проекты. Изначально код на C++ компилировался в C-код, то есть язык по сути был просто "синтаксическим сахаром" для Си, делая его удобнее и добавляя новый уровень абстракции в виде классов. Потом появились и отдельные компиляторы для C++.

Язык C++ практически полностью совместим с C за очень редкими исключениями, по сути C++ просто расширяет C и делает его мощнее и удобнее. Внутри программы на C++ могут использоваться стандартные инструменты и библиотеки из Си, а сам проект - состоять вперемешку из c и cpp файлов. Сам по себе Си - очень старый, корявый и неудобный (оценочное суждение), его практически отовсюду вытеснили более современные языки. Си остался в основном во всяких низкоуровневых штуках, драйверах, библиотеках и в программировании микроконтроллеров, где на нём может быть например написан весь SDK и встроенные библиотеки.

C++ очень универсальный язык, сочетает в себе как низкоуровневые (близкие к процессору и машинному коду), так и высокоуровневые (более абстрактное программирование) свойства. На C/C++ очень просто "выстрелить себе в ногу" - написать правильный с точки зрения компилятора код, который буквально сломает программу и приведёт к непредсказуемому поведению. C/C++ - это язык с возможностью ручного управления памятью, то есть чтения и записи в любое место. Это позволяет писать очень эффективный и быстрый код, но если делать это неаккуратно - можно получить сбой, причину которого будет очень сложно найти. В языках более высокого уровня таких возможностей просто нет, но и работают они чаще всего медленнее.

Так как Си - старый язык, он тянет за собой всё, что казалось нормальным в прошлом веке. Стандартные библиотеки часто критикуются разработчиками за странный, неудобный и местами плохо читаемый API - как вам функции strncasecmp() или vsnprintf()? Но это не проблема - всегда можно взять и написать свой набор инструментов и программировать с удовольствием!

Интерпретируемые языки #

Что касается других языков программирования, то можно разделить их на две группы: компилируемые и некомпилируемые, или интерпретируемые. Как вы уже знаете - при компиляции программа превращается в машинный код и в таком виде выполняется процессором. Это например такие языки как C, C++, Java. Вторая группа работает иначе: код не компилируется и существует в том виде, в котором мы его написали - в виде текста. Для его выполнения нужен интерпретатор - дополнительная программа (написанная на компилируемом языке), которая будет читать код, анализировать его и отдавать команды процессору. Это например такие языки как JavaScript, Python. Программы на этих языках выполняются медленнее, чем на Си, потому что код читается как текст, анализируется и только потом кусочками выполняется. У некоторых интерпретируемых языков есть возможность полноценно компилироваться в исполняемую программу или код на C/C++.

На микроконтроллерах кстати такое тоже возможно: в память загружается основная программа - интерпретатор, которая будет читать код программы из условно другой области памяти, например существуют интерпретаторы языков JavaScript и Python. Для микроконтроллера такой подход крайне не оптимален - интерпретатор занимает большое количество памяти, а программа работает очень медленно по сравнению с аналогичной на C/C++. Тем не менее, это довольно популярная практика при обучении: учиться программировать на том же JavaScript гораздо комфортнее, чем на C/C++, а также не нужно ждать компиляции программы - она сразу загружается в микроконтроллер и начинает выполняться.

По сути можно придумать свой язык и написать интерпретатор к нему, загрузить его в память и писать программу уже на своём языке

Основные тезисы урока #

  • Процессор имеет фиксированный набор инструкций
  • Процессор выполняет машинный код, состоящий из инструкций и данных
  • Процессор выполняет код по тактам с некоторой частотой, заданной внешним тактирующим устройством
  • Ассемблер - низкоуровневый язык программирования, в котором расписывается каждая команда процессору
  • Компилятор - программа, которая проверяет, оптимизирует и переводит программу на языке программирования в машинный код для процессора
  • Бинарный файл, бинарник - результат работы компилятора, файл с машинным кодом
  • Машинный код нельзя автоматически перевести обратно в программу, из которой он был скомпилирован
  • C/C++ - один из самых лёгких для процессора и быстрый по скорости выполнения язык программирования
  • C/C++ поддерживает вставки кода на ассемблере
  • C++ расширяет возможности C и почти полностью с ним совместим
  • Бывают интерпретируемые языки - код программы в виде текста читается интерпретатором и выполняется
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Прокрутить вверх