Этапы трансляции программы в машинный код. Стандарты

Практический курс по C/C++: https://stepik.org/course/193691

Здравствуйте, дорогие друзья! Мы начинаем курс по языку программирования Си. На предыдущем занятии мы с вами увидели принцип обработки команд процессором, когда в памяти содержалась последовательность чисел:

B8 22 11 00 FF 01 CA 31 F6 53 8B 5C 24 04 8D 34 48 39 C3 72 EB C3

и подавалась в виде исполняемых инструкций на центральный процессор. Такая последовательность закодированных команд в виде целых чисел называется машинным кодом. Только такой код «понимает» процессор компьютера. Но человеку создавать программы на таком уровне было бы очень непросто. Поэтому для облегчения работы программиста было предложено все эти числовые команды представлять в символьном виде, а именно, в виде мнемоник языка Ассемблер. Например, представленная числовая последовательность на уровне мнемоник ассемблера, предложенных компанией AT&T, выглядит следующим образом:

Метка

Мнемоника

Машинный код

foo:

movl $0xFF001122, %eax

B8 22 11 00 FF

addl %ecx, %edx

01 CA

xorl %esi, %esi

31 F6

pushl %ebx

53

movl 4(%esp), %ebx

8B 5C 24 04

leal (%eax, %ecx, 2), %esi

8D 34 48

cmpl %eax, %ebx

39 C3

jnae foo

72 EB

Retl

C3

Как видите, язык Ассемблер дает заметное удобство при создании программ. Теперь программисту достаточно запомнить общие команды, вроде movl (переместить из памяти или регистра информацию в память или регистр); addl (сложить содержимое регистров или памяти); xorl (побитовая операция XOR) и т.д. Запоминать числовое (кодовое) представление команд процессора уже не нужно. Достаточно написать программу на уровне мнемоник, а затем, с помощью компилятора языка Ассемблер, перевести ее в числовой вид (машинные коды) понятные центральному процессору компьютера.

Обратите внимание, я здесь употребил слово «компилятор». В программировании под ним понимается специальная исполняемая программа, которая переводит текст программы, написанной программистом на каком-либо языке программирования, как правило, в машинные коды. Именно в этом смысле я буду использовать слово «компилятор».

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

Для преодоления этих и некоторых других недостатков стали создавать языки, на которых можно описывать логику программы на более высоком (абстрактном) уровне. В частности, на прошлом занятии мы увидели, что столкнувшись с проблемой переноса ОС с компьютера PDP-7 на компьютер PDP-11, Кен Томпсон решил воспользоваться языком высокого уровня. Сначала это был язык B, но из-за его ограниченности в 1972 году Деннис Ритчи создал другой новый, на тот момент, язык Си. Время показало, что язык Си хорошо сочетает более высокий уровень программирования, по сравнению с языком Ассемблер, а программы, написанные на Си, качественно и эффективно можно перевести на уровень машинных кодов с помощью компилятора языка Си. То есть, конечный результат получается таким, словно программу изначально написал профессиональный программист на Ассемблере, а не на языке высокого уровня Си. Это одно из ключевых преимуществ языка Си перед другими языками высокого уровня. В результате, грамотно написанная программа, будет выполняться на компьютере с максимально возможной скоростью и разумно, без излишеств использовать его ресурсы. Это свойство языка Си незаменимо в ряде направлений, например: игровой индустрии (разработка движков); дополненной реальности; обучение нейронных сетей; создание надежных программ управления оборудованием в реальном режиме времени и многое другое. Именно по этим причинам язык Си остается востребованным и будет таковым до тех пор, пока программы пишет человек привычным для нас сейчас способом.

Этапы перевода (трансляции) текстов программы в исполняемый код

Итак, все, что требуется от программиста – это создать один или несколько файлов с текстами программы, которая решает поставленные задачи. Каждый такой независимый файл в программировании часто называют модулем. Так как мы говорим о языке Си, то все эти текстовые варианты программ далее переводятся (транслируются) на уровень машинных кодов. Как это происходит? Сначала каждый модуль независимо пропускается через тестовый препроцессор. Его задача в программе найти все, так называемые, директивы (указания) для этого препроцессора и выполнить их. Что это за директивы, мы с вами еще будем говорить. В результате, исходный текст программы несколько меняется. После этого, преобразованные тексты подаются на компилятор (также независимо друг от друга), которые сначала проходят через лексический анализ программы. На этом этапе выделяются возможные синтаксические ошибки. Если ошибок не обнаружено, то программа далее переводится непосредственно в машинные коды. На выходе получаются объектные файлы модулей. Но в этих объектных файлах отсутствуют связи с другими модулями (если они были прописаны в тексте программы), реализации библиотечных функций (если они были использованы), код запуска всей программы. Все это делает на последнем этапе линкер (по-простому, редактор связей). Он связывает все объектные файлы модулей в единый исполняемый файл, добавляет в него необходимые реализации библиотечных функций и код запуска для текущей операционной системы. На выходе получается окончательный результат в виде исполняемого файла.

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

Второе. Линкер добавляет в итоговые исполняемые файлы только те реализации библиотечных функций, которые используются в программе. Ничего дополнительного, лишнего на выходе не образуется.

Третье. Использование компиляторов для разных архитектур процессоров и разных ОС позволяет относительно просто и быстро переносить ранее написанную программу с одной ОС на другую, или с одной архитектуры процессора на другую. И, так как компиляторы языка Си реализованы практически везде, на всех платформах, то программа на языке Си оказывается самой переносимой среди многих других языков высокого уровня.

Стандарты языка Си

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

Первым таким неформальным стандартом стала книга Брайана Кернигана и Денниса Ритчи «Язык программирования Си». На первых этапах становления языка Си этого было достаточно.

Стандарт ANSI C (ISO C)

Но по мере того, как все больше и больше программистов по всему миру стали использовать этот язык программирования, остро встал вопрос создания вполне официального стандарта, который бы, к тому же, включал все полезные новшества этого нового языка. С этой целью в 1983 году национальный институт стандартизации США – ANSI (American National Standards Institute) образовал специальную рабочую группу (комитет) под названием X3J11. И в 1989 году миру был предъявлен новый стандарт, который сейчас часто называют ANSI C или C89. Правда, официально он был принят только в 1990 году, поэтому вместо C89 иногда используют запись C90. Но это означает, по сути, одно и то же.

По итогам своей работы комитет X3J11 сформулировал несколько довольно важных принципов:

  • доверять программисту;
  • не мешать программисту делать то, что он считает необходимым;
  • без необходимости не усложнять язык, сохранять его простоту;
  • каждая операция языка должна иметь только один способ выполнения;
  • операция должна выполняться максимально быстро, даже в ущерб переносимости языка.

Конструкции языка Си, так или иначе, отражают эти принципы, заложенные в самом начале становления этого языка.

Стандарт C99

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

  • в интернационализации (поддержке различных международных языков) на программном уровне;
  • в устранении некоторых неточностей предыдущей версии языка стандарта ANSI C;
  • в повышении стабильности математических вычислений для возможности безопасного использования языка в научных проектах.

В 1999 году этот новый стандарт был принят и стал известен под кодовым названием C99.

Другие стандарты

Язык Си стал хорошей базой для развития и создания нового подобного и во многом похожего на него языка программирования C++. Его автором считается Бьёрн Страуструп – сотрудник Bell Laboratories. В этом языке появилась объектно-ориентированная составляющая, шаблоны классов и функций и некоторые другие полезные инструменты для современной разработки программ любого уровня сложности.

Новые стандарты языка Си появляются с завидной регулярностью. И, как вы догадываетесь, этот безудержный процесс, скорее всего, будет продолжаться, пока существует язык Си. Но я остановлюсь на стандарте C99, принятом в 1999-м году. На мой взгляд, он удачно сочетает возможности и красоту языка Си, а также сохраняет его дух, заложенный создателем Деннисом Ритчи в далеком 1972 году.

Практический курс по C/C++: https://stepik.org/course/193691

Видео по теме