Языки программирования низкого уровня или низкоуровневые традиционно появились первыми и в последующем стали базисом для развития всей ИТ индустрии. Они так называются потому, что в своих командах обращаются фактически напрямую к железу компьютера, его микропроцессору. Каждый процессор способен воспринимать определенный набор понятных только ему команд, поэтому к каждой модели немногочисленных устройств в то далекое время создавались свои языки. Изначально такие языки имели минимальный набор команд, и в отличие от высокоуровневых не имели такого большого количества абстрактных классов, разнообразного синтаксиса.
Все существующие языки программирования обычно делят на две большие группы: низкоуровневые и высокоуровневые. Сейчас большинство используемых языков относятся ко второй категории, но так было не всегда. Если смотреть историю развития программирования как можно ближе к ее истокам, то там картина была прямо противоположной.
Высокоуровневые языки программирования
Языки программирование низкого уровня, либо имеющие их возможности, активно применяются также и сейчас. Ведь только благодаря им возможно писать драйвера на компьютерное железо, подключаемые периферийные устройства, создавать операционные системы и ядра прошивок, а также многие другие важнейшие задачи. В военной сфере, инженерии, медицине программы должны управлять непосредственно определенными устройствами и их физическими параметрами, именно поэтому здесь также языки очень востребовательны. Список используемых сейчас низкоуровневых языков не такой большой, но, тем не менее, они все еще актуальны и способны решать важные задачи.
Некоторые представители низкоуровневых языков
Изначально первым языком программирования низкого уровня считается так называемый машинный код. Он имел вид набора последовательных команд, которые передавались на процессор в виде нулей и единиц. За нуль отвечало отсутствие электрического сигнала на устройстве, а за единицу – подача на него определенного импульса. Таким образом, череда сигналов заставляла процессор решать поставленные перед ним задачи.
Первые такие коды были способны заставить ЭВМ выполнять элементарные простые операции, куда относятся арифметические операции, передача между регистрами простейшей информации, сравнение двух или больше разных кодов и тому подобные действия. Позже машинные языки научились решать сложные задачи, то есть такие, которые состоят из набора элементарных команд. В зависимости от архитектуры процессора он может выполнять разное количество команд и с разной скоростью. Чтобы машинные коды работали нормально на нескольких представителях одного семейства процессоров, их начали разбивать на микропрограммы. Список языков машинного кода вряд ли возможно составить, ведь на каждый процессор и ЭВМ своего времени создавался свой язык.
Ассемблер
Следующими после машинных кодов появились так называемые языки ассемблера. Их главная особенность состоит в том, что набор возможных команд здесь уже существенно более широкий и он не обязан строго соответствовать командам данной ЭВМ. Благодаря этому открылись многие новые возможности. Главными преимуществами ассемблера по сравнению с машинным кодом называют:
06. Низкоуровневое программирование. Ассемблер. Пример программы. [Универсальный программист]
- Возможность создания наиболее компактного кода, что увеличивает быстродействие машины;
- Возможность хранить в оперативной памяти часть выполнения задачи и использовать ее по желанию;
- Программы получили больший функционал, при этом их ресурсоемкость стала существенно ниже.
И это только некоторые сильные стороны, которые предлагает ассемблер. Примеры кода этого семейства языков все еще часто используются в образовательных целях и дают возможность лучше понять суть процесса взаимодействия с микропроцессорами.
Forth
Низкоуровневый язык, появившийся приблизительно в 70-х. Имел свои существенные преимущества, которые сделали его довольно популярным в определенных кругах специалистов. Машинные языки программирования к этому времени уже начали уходить в прошлое, поэтому функционал Форт многим пришелся по вкусу.
С его помощью программисты, знающие архитектуру процессора, могли написать устройству ядро за считанные дни. Сложно сказать, какая парадигма программирования здесь поддерживается. При использовании языка опытным программистом здесь можно реализовать самые оригинальные задумки.
С
Один из самых известных и используемых языков программирования, который начал свое существование в 70-х и не сходит с арены до сих пор. Он по своей структуре оказался довольно близок к машинным языкам и ассемблеру, благодаря чему начал активно использоваться для создания операционных систем, драйверов, системного ПО. Часто Си причисляют к высокоуровневым языкам, но здесь все не так однозначно, ведь он также полностью совпадает с определением низкоуровневых языков, поэтому вполне может быть занесен и в эту категорию. Вопрос о том, какой язык программирования относится к низкоуровневым, не насколько простой и здесь нужно смотреть скорее функционал и назначение языка, нежели его официальные определения.
Если принимать, что языком программирования низкого уровня является тот язык, который может обращаться непосредственно к набору поддерживаемых процессором или другими устройствами команд, то Си вполне подходит под это определение.
В целом, история языков программирования низкого уровня довольно объемна, но известность получили далеко не многие представители этого класса. Все дело в том, что каждый язык такого типа создавался под определенные «железки», поэтому проследить все такие попытки очень и очень сложно.
Источник: programmera.ru
[Введение в низкоуровневое программирование
Низкоуровневое программирование — это тип программирования, приближенный к аппаратному, то есть использующий языки программирования, очень близкие к машинному языку. Программирование на низкоуровневых языках позволяет полностью контролировать ресурсы машины; вы можете напрямую обращаться к регистрам, памяти и кэшу, а также выделять пространство в памяти.
Как выполняется программа?
Сначала код компилируется из языка высокого уровня в язык низкого уровня. После этого этот низкоуровневый код должен быть собран и скомпонован.
Учитывая эти шаги, код может быть выполнен, но выполнение также имеет некоторые шаги, а именно:
- Получить
- Декодировать
- Выполнить
- Переместите указатель на следующую инструкцию
- Вернитесь к первому шагу
Что такое языки высокого уровня?
Высокоуровневые языки программирования — это языки, которые, грубо говоря, ближе к человеческому языку, чем к машинному. Как правило, большинство терминов в них написано на английском языке и не требует прямого манипулирования памятью и регистрами.
Примеры языков программирования высокого уровня
- JavaScript
- Java
- Python
- Эликсир
- PHP
Примеры языков программирования низкого уровня
- Ассемблер ARM — 64 бит
- Сборка AMD/Intel — 64 бит
- Сборка 8086
Являются ли C и C++ языками высокого уровня?
Многие люди приходят в замешательство, когда слышат, что C и C++ — языки высокого уровня, это происходит потому, что эти языки считаются «сложными» и «трудными». Если рассматривать формальное определение, то да, они считаются языками высокого уровня, потому что они гораздо ближе к английскому языку, чем к ассемблеру, и не требуют прямого манипулирования памятью и регистрами; КАК БЫ то ни было, это возможно.
Однако если вы сравните, например, C или C++ с Python или JavaScript, то разница будет огромной настолько, что их можно будет считать языками низкого уровня. Однако если сравнить эти же языки с Ассемблером или двоичным кодом, то они считаются языками высокого уровня.
Официально эти языки относятся к языкам высокого уровня, но из-за этого вопроса перспективы я предпочитаю считать их языками промежуточного уровня.
Что такое Ассамблея и как она работает?
Ассемблер — это язык ассемблера, он классифицируется как язык низкого уровня. Ассемблер не компилируется и не интерпретируется, он ассемблируется. Когда мы говорим, что язык является «ассемблером» или «собранным», это означает, что для каждого слова в этом языке существует точный бинар, идентичный, поэтому нет перевода, только ассоциация между пневмоникой (словами) и соответствующим бинаром.
Ассемблер — это язык, однако он имеет множество разновидностей, поскольку, будучи языком, чрезвычайно близким к машине, он зависит от архитектуры компьютера. Существует множество типов Ассемблера для различных архитектур, 16 бит, 32 бит, 64 бит, Intel, ARM и т.д. Вот почему мы называем языки низкого уровня «непортативными».
Программа, отвечающая за сборку ассемблерного кода в двоичный, называется Ассемблером, и, как уже говорилось, каждый Ассемблер отличается в зависимости от архитектуры машины.
Генерация кода низкого уровня
Рассмотрим сравнительный пример низкоуровневого кода (Ассемблер) и кода на языке C. Очень простой алгоритм — пузырьковая сортировка, широко используемая для сортировки массивов. Код на языке Си для этого алгоритма выглядит следующим образом:
#include void print_array(int arr[], int arr_length) for (int k = 0; k arr_length; k++) printf(«%d, «, arr[k]); > > void bubblesort(int arr[], int arr_length) int swap; for (int i = 0; i arr_length; i++) printf(«n[%d] «, i+1); for (int j = 0; j arr_length; j++) if (arr[j] > arr[j+1]) swap = arr[j]; arr[j] = arr[j+1]; arr[j+1] = swap; > > print_array(arr, arr_length); > printf(«n»); > int main() int arr[25] = -223, -12, -1000, -90, -3, 40, 55, 11, 32, 67, 5, 74, 89, 38, 66, 27, 36, 79, 99, 2, 0, 1, 100, 282, 370>; int arr_length = sizeof arr / sizeof *arr; bubblesort(arr, arr_length); return 0; >
Войдите в полноэкранный режим Выход из полноэкранного режима
Этот код C для многих уже не считается простым, но он по-прежнему очень нагляден и ближе к английскому, чем к машинному языку. Теперь выполним следующую команду:
gcc bubble_sort.c -o bubble_sort.o objdump -S —disassemble bubble_sort.o > bubble_sort.asm
Войдите в полноэкранный режим Выход из полноэкранного режима
Приведенная выше команда компилирует C-код и генерирует исполняемый файл в машинном коде, вторая часть команды генерирует ассемблерный код из исполняемого файла. Откройте файл bubble_sort.asm и посмотрите, что произошло. Простой код алгоритма пузырьковой сортировки на Ассемблере содержит более 300 строк, а код на Си — всего 30 строк.
Заключение
Теперь вы знаете немного больше о низкоуровневом программировании, вы можете продолжить обучение в этой области или вернуться к языкам высокого уровня. Программирование на низком уровне гораздо сложнее и скучнее, чем программирование на высоком уровне, однако важно знать хотя бы основы, это позволит вам лучше понять, как работают компьютеры, компиляторы, интерпретаторы и различные архитектуры.
Источник: procodings.ru
И всё же C — низкоуровневый язык
За прошедшие с момента появления языка C десятилетия было создано множество интереснейших языков программирования. Какие-то из них используются до сих пор, другие — повлияли на следующие поколения языков, популярность третьих тихо сошла на нет. Между тем архаичный, противоречивый, примитивный, сделанный в худших традициях своего поколения языков C (и его наследники) живее всех живых.
Критика C — классический для нашей индустрии эпистолярный жанр. Она звучит то громче, то тише, но в последнее время буквально оглушает. Пример — перевод статьи Дэвида Чизнэлла «C — не низкоуровневый язык», опубликованный в нашем блоге некоторое время назад. Про C можно говорить разное, в дизайне языка действительно много неприятных ошибок, но отказывать C в «низкоуровневости» — это уже слишком!
Чтобы не терпеть такую несправедливость, я собрался с духом и постарался определиться с тем, что есть язык программирования низкого уровня и чего хотят от него практики, после чего перебрал аргументы критиков C. Так получилась эта статья.
Аргументы критиков C
Вот некоторые аргументы критиков С, перечисленные в том числе и в статье Дэвида Чизнэлла:
- Абстрактная машина языка C слишком похожа на устаревшую архитектуру PDP-11, которая давно уже не соответствует устройству популярных современных процессоров.
- Несоответствие абстрактной машины C устройству реальных машин усложняет разработку оптимизирующих компиляторов языка.
- Неполнота и сложность стандарта языка ведут к разночтениям в реализациях стандарта.
- Доминирование C-подобных языков не позволяет исследовать альтернативные архитектуры процессоров.
Давайте для начала определимся с требованиями к низкоуровневому языку, после чего вернёмся к приведённым аргументам.
Язык программирования низкого уровня
Общепризнанного определения языка низкого уровня не существует. Но перед обсуждением спорных моментов желательно иметь хоть какие-то исходные требования к предмету спора.
Никто не станет спорить с тем, что язык ассемблера находится на самом низком уровне. Но на каждой платформе он уникален, поэтому код на таком языке не может быть переносимым. Даже на обратно совместимой платформе может потребоваться задействовать какие-то новые инструкции.
Отсюда следует первое требование к языку низкого уровня: он должен сохранять общие для популярных платформ черты. Проще говоря, компилятор должен быть портируемым. Портируемость компилятора упрощает разработку компиляторов языка для новых платформ, а многообразие поддерживаемых компиляторами платформ избавляет разработчиков от необходимости переписывать прикладные программы для каждой новой машины.
С первым требованием конфликтуют пожелания разработчиков специальных программ: языков программирования, драйверов, операционных систем и высокопроизводительных баз данных. Программисты, пишущие эти программы, хотят иметь возможности ручной оптимизации, прямой работы с памятью и так далее. Словом, низкоуровневый язык должен позволять работать с деталями реализации платформы.
Поиск баланса между этими двумя требованиями — выделение общих для платформ аспектов и доступ к по возможности большему числу деталей — фундаментальная причина сложности разработки языка низкого уровня.
Заметьте, что для такого языка не так важны высокоуровневые абстракции, — для него важнее служить контрактом между платформой, компилятором и разработчиком. А если есть контракт, то возникает необходимость в независимом от конкретной реализации стандарте языке.
Наше первое требование — общие для целевых платформ черты — выражается в абстрактной машине языка, поэтому с неё мы и начнём обсуждение C.
Дело не только в PDP-11
Платформа, на которой появился язык C, — PDP-11. В её основе лежит традиционная архитектура фон Неймана, в которой программы выполняются последовательно центральным процессором, а память представляет собой плоскую ленту, где хранятся и данные, и сами программы. Такая архитектура легко реализуется в железе, и со временем все компьютеры общего назначения стали использовать именно её.
Современные усовершенствования архитектуры фон Неймана направлены на устранение её главного узкого места — задержек при обмене данными между процессором и памятью (англ. von Neuman bottleneck). Разница в производительности памяти и центрального процессора привела к появлению кеширующих подсистем процессоров (одноуровневых и позже — многоуровневых).
Но даже кешей в наши дни уже недостаточно. Современные процессоры стали суперскалярными (англ. superscalar). Задержки при получении инструкциями данных из памяти частично компенсируются внеочередным выполнением (англ. instruction-level parallelism) инструкций вкупе с предсказателем ветвлений (англ. branch predictor).
Последовательная абстрактная машина C (и многих других языков) имитирует работу не столько конкретно PDP-11, сколько любых компьютеров, устроенных по принципу архитектуры фон Неймана. К нему относятся архитектуры, построенные вокруг процессоров с единственным ядром: настольные и серверные x86, мобильные ARM, сходящие со сцены Sun/Oracle SPARC и IBM POWER.
Со временем в один процессор стали интегрировать несколько вычислительных ядер, в результате чего появилась необходимость поддерживать когерентность кешей каждого ядра и потребовались протоколы межъядерного взаимодействия. Архитектура фон Неймана, таким образом, была масштабирована на несколько ядер.
Изначальный вариант абстрактной машины С был последовательным, никак не отражая наличие взаимодействующих через память потоков исполнения программ. Появление в стандарте модели памяти расширило возможности абстрактной машины до параллельной.
Таким образом, утверждение о том, что абстрактная машина C давно не соответствует устройству современных процессоров, касается не столько конкретного языка, сколько компьютеров, использующих архитектуру фон Неймана, в том числе и в параллельном исполнении.
Но как практик хочу отметить следующее: можно считать, что фоннеймановский подход устарел, можно считать, что он актуален, но это никак не отменяет того факта, что популярные сегодня архитектуры общего назначения используют производные от традиционного подходы.
Стандартизированное и переносимое воплощение архитектуры фон Неймана — абстрактная машина C — удобно реализуется на всех основных платформах и поэтому пользуется своей популярностью как портативного ассемблера вполне заслуженно.
Оптимизирующие компиляторы и язык низкого уровня
Наше второе требование к языку низкого уровня — доступ к низкоуровневым деталям реализации каждой из популярной платформ. В случае C это непосредственная работа с памятью и объектами в ней как с массивом байтов, возможность напрямую работать с адресами байтов и развитая арифметика указателей.
Критики C указывают на то, что в стандарте языка даётся слишком много гарантий касательно, например, расположения отдельных полей в структурах и объединениях. Вместе с указателями и примитивными механизмами циклов это усложняет работу оптимизатора.
Действительно, более декларативный подход позволил бы компилятору самостоятельно решать проблемы выравнивания данных в памяти или оптимального порядка полей в структурах; а высокоуровневые циклы дают свободу, необходимую при векторизации.
Позиция разработчиков C в данном случае такова: низкоуровневый язык должен позволять работать на уровне, достаточно низком для самостоятельного решения программистом задач оптимизации. В рамках C возможно поработать компилятором, выбрав, к примеру, инструкции SIMD и правильно разместив данные в памяти.
Другими словами, наше требование доступа к деталям реализации каждой из платформ вступает в конфликт с пожеланиями разработчиков оптимизирующих компиляторов именно в силу наличия низкоуровневых инструментов.
Интересно, что Чизнэлл в статье под названием «C — не низкоуровневый язык» парадоксально утверждает, что C — слишком низкоуровневый, указывая на отсутствие в нём высокоуровневых инструментов. Но практикам бывают нужны именно низкоуровневые инструменты, иначе язык не получится использовать для разработки операционных систем и других низкоуровневых программ, то есть он не будет удовлетворять второму из наших требований.
Отвлекаясь от описания проблем оптимизации именно C, хочу заметить, что в настоящий момент в оптимизирующие компиляторы высокоуровневых языков (тех же C# и Java) вложено не меньше усилий, чем в GCC или Clang. У функциональных языков тоже хватает эффективных компиляторов: MLTon, OCaml и другие. Но разработчики того же OCaml пока могут похвастаться производительностью в лучшем случае в половину скорости кода на C…
Стандарт как безусловное благо
Чизнэлл приводит в своей статье ссылку на результаты опроса, проведённого в 2015 году: многие программисты допускали ошибки в решении задач на понимание стандартов C.
Полагаю, что кто-то из читателей имел дело со стандартом C. У меня версия C99 есть в бумажном виде, страниц эдак на 900. Это не лаконичная спецификация Scheme объёмом меньше 100 страниц и не вылизанный Standard ML, состоящий из 300. Удовольствие от работы со стандартом C не получает никто: ни разработчики компиляторов, ни разработчики документа, ни программисты.
Но надо понимать, что стандарт C разрабатывался постфактум, уже после появления множества «почти-еле-только местами» совместимых диалектов. Авторы ANSI C проделали огромную работу, обобщив существующие реализации и прикрыв бесчисленными «костылями» неортогональности в дизайне языка.
Может показаться странным, что такой документ вообще кто-то взялся реализовывать. Но C был реализован множеством компиляторов. Я не буду пересказывать чужие байки о зоопарке мира UNIX конца 80-х, тем более что сам в то время считал не слишком уверенно и только до пяти. Но, очевидно, стандарт был всем в индустрии действительно нужен.
Прекрасно то, что он есть и реализован по меньшей мере тремя крупными компиляторами и множеством компиляторов поменьше, в совокупности поддерживающими сотни платформ. Ни один из языков — конкурентов C, претендующих на корону короля языков низкого уровня, не может похвастаться таким многообразием и универсальностью.
На самом деле современный стандарт C не так уж и плох. Более-менее опытный программист способен разработать неоптимизирующий компилятор C в разумные сроки, что подтверждается существованием множества полулюбительских реализаций (тех же TCC, LCC и 8cc).
Наличие общепринятого стандарта означает, что C удовлетворяет последнему из наших требований к языку низкого уровня: этот язык строится от спецификации, а не конкретной реализации.
Альтернативные архитектуры — удел специальных вычислений
Но Чизнэлл приводит ещё один аргумент, возвращаясь к устройству современных процессоров общего назначения, реализующих варианты архитектуры фон Неймана. Он утверждает, что имеет смысл изменить принципы работы центрального процессора. Повторюсь, что эта критика касается не конкретно C, а самой базовой модели императивного программирования.
Действительно, существует множество альтернатив традиционному подходу с последовательным исполнением программ: модели SIMD в стиле GPU, модели в стиле абстрактной машины Erlang и другие. Но каждый из этих подходов имеет ограниченную применимость при использовании в центральном процессоре.
GPU, например, замечательно перемножают матрицы в играх и машинном обучении, но их сложно использовать для трассировки лучей. Другими словами, эта модель подходит для специализированных ускорителей, но не работает для процессоров общего назначения.
Erlang прекрасно работает в кластере, но эффективную quick sort или быструю хеш-таблицу на нём сделать трудно. Модель независимых акторов лучше использовать на более высоком уровне, в большом кластере, где каждый узел — всё та же высокопроизводительная машина с традиционным процессором.
Между тем, современные x86-совместимые процессоры давно уже включают в себя наборы векторных инструкций, схожие с GPU по назначению и принципам работы, но сохраняющие общую схему процессора в стиле фон Неймана в целом. Не сомневаюсь, что любые достаточно общие подходы к вычислениям будут включены в популярные процессоры.
Есть такое авторитетное мнение: будущее за специализированными программируемыми ускорителями. Под такие неординарные железки действительно имеет смысл разрабатывать языки с особой семантикой. Но компьютер общего назначения был и остаётся похожим на ту самую PDP-11, для которой так хорошо подходят C-подобные императивные языки.
С будет жить
В статье Чизнэлла есть фундаментальное противоречие. Он пишет, что для обеспечения скорости программ на C процессоры имитируют абстрактную машину C (и давно забытую PDP-11), после чего указывает на ограниченность такой машины. Но я не понимаю, почему это означает, что «C — не низкоуровневый язык».
Вообще же речь идёт не о недостатках C как языка, а о критике распространённых архитектур в стиле фон Неймана и вытекающей из них модели программирования. Но пока не похоже, что индустрия готова отказаться от привычной архитектуры (по крайней мере, не в процессорах общего назначения).
Несмотря на доступность множества специализированных процессоров вроде GPU и TPU, в настоящий момент архитектура фон Неймана правит бал и индустрии нужен язык, позволяющий в рамках популярнейшей архитектуры работать на как можно более низком уровне. Достаточно простой, портированный на десятки платформ и стандартизированный язык программирования — это C (и его ближайшие родственники).
При всём при этом у C хватает недостатков: архаичная библиотека функций, запутанный и противоречивый стандарт, грубые ошибки в дизайне. Но, судя по всему, кое-что создатели языка всё же сделали правильно.
Так или иначе, нам по-прежнему нужен язык низкого уровня, причём построенный именно для популярных фоннеймановских компьютеров. И пускай C устарел, но, видимо, любому его преемнику всё равно придётся отталкиваться от тех же самых принципов.
Источник: habr.com