Начнем по традиции с простейшей программы, делающей хоть что-то… Кто сказал «что-то полезное»? До чего-то
полезного нам еще пилить и пилить. Достаточно типовой «Hello, World!» можно увидеть во врезке. Теперь можно
сохранить этот текст в файл (например, hello.pp
1), скомпилировать командой fpc hello
и запустить.
См. консольный фрагмент — если вы работаете в Linux или FreeBSD, результат должен быть похож. Да и в других
системах отличия не особо существенны. Еще тут может выпасть предупреждение от компоновщика — на него внимания
обращать не надо. Получилось? Теперь будем разбираться, что именно.
В действительности, это не самый минимальный пример — я таки оставил одну необязательную строку, а именно — первую…
Но давайте по порядку. Исходный код программы начинается с необязательного заголовка, состоящего из ключевого слова
program
и идентификатора. Заканчиваться идентификатор заголовок2 должен точкой с запятой,
как и любое предложение языка. Впрочем, не совсем так.
В отличие от многих других языков программирования, в Pascal точка с запятой не заканчивает предложения языка, а отделяет предложения друг от друга. Но в процессе развития диалектов языка этот принцип соблюдался не всегда последовательно… С учетом еще и того, что существует понятие «пустого предложения», которое позволяет ставить точку с запятой во многих местах, где она не требуется, ситуация в целом довольно запутана.
Там, где есть разница между «окончанием» и «разделением», я буду отдельно пояснять необходимость (или допустимость) точки с запятой. В данном же случае несущественно, заканчивает ли она предложение заголовка, или отделяет его от следующего.
Вернемся к нашим баранам. Заголовок в исходном файле программы необязателен, но лично я настоятельно рекомендую его указывать — все-таки код должен быть хорошо читаем не только компилятором, но и людьми, которым, возможно, придется его развивать и поддерживать. К тому же так получится единообразно с другими вариантами исходников — кодом динамических библиотек и модулей.
После заголовка идет область описаний, где мы должны расписать используемые модули, типы, переменные, константы, метки и т.д. Естественно, только в том случае, если мы их реально используем. В нашем примере ничего такого нет, поэтому область описаний осталась пустой.
Затем, как мы можем видеть, идет исполняемый блок, начинающийся с ключевого слова begin
и заканчивающийся ключевым
словом end
с точкой. Между ними расположены собственно исполняемые операторы. В нашем случае — один оператор вызова
процедуры. Системная процедура WriteLn()
записывает строку в стандартный вывод (или в произвольный текстовый файл,
если он указан).
Глядя на вывод компилятора можно подумать, что формирование исполняемого файла происходит в два этапа — «Compiling» и «Linking». Так оно и есть. И мы сейчас немного о них поговорим.
Собственно, если кто не в курсе, компиляция — это формирование машинного (в т.ч. — для виртуальной машины) кода из исходного. Тогда как компоновка (линковка, linking) — формирование готового исполняемого файла для конкретной операционной системы, со служебной информацией, нужной для запуска и т.д. Это разные по своей сути процессы.
Пара слов о компиляции
Если мы посмотрим в каталог, где только что скомпилировали нашу программу, то помимо исходного hello.pp
и конечного исполняемого
hello
(hello.exe
на некоторых ОС), увидим файл hello.o
. Это так называемый объектный файл, содержащий результат компиляции —
машинный код, не содержащий служебной информации, необходимой для запуска программы, зато содержащий служебную информацию, нужную
компоновщику, а именно — символьные имена функций, переменных и т.д., которые компоновщик уже потом преобразует в адреса.
Мы можем дизассемблировать этот файл посредством утилиты objdump
, однако это не лучший способ. Гораздо удобнее
воспользоваться тем, что FPC позволяет получить и сохранить ассемблерный результат компиляции3. Для этого нам
нужно выполнить команду fpc -al hello
. Рядом с прочими появился еще один файл — hello.s
, который содержит
ассемблерный код. Тем, кто раньше пользовался ассемблером вроде MASM или TASM этот код покажется странным и непривычным,
поскольку составлен в так называемом AT&T-синтаксисе, а не в синтаксисе Intel4. Файл довольно большой, поэтому
я не буду приводить его полностью, а возьму только то, что имеет отношение к непосредственно коду. Всякая служебная
и отладочная информация нам сейчас ни к чему.
На врезке видно, что́ у меня получилось. Если у вас не 64-битная система, то соответствующий фрагмент может отличаться,
скорее всего — именами регистров и суффиксами команд. Тем не менее, общий смысл должен сохраниться — исполняемый блок
pascal-программы объявлен в виде функции с двумя именами PASCALMAIN
и main
, а строка, которую мы указали в явном виде
при вызове WriteLn()
помещена в секцию неизменяемых данных под автоматически сгенерированным именем _$HELLO$_Ld1
.
Из последовательности команд можно сделать вывод, что данный код не обращается напрямую к каким-либо системным вызовам,
а использует подпрограммы из стандартной библиотеки Free Pascal. Об этом нам явственно сообщают префиксы fpc_
и FPC_
.
Этих подпрограмм мы не писали, и в файле hello.o
их нет. Но где-то же они есть… Это «где-то» — стандартный модуль
System
и, соответственно, объектный файл system.o
, который расположен в каталоге модулей компилятора. Этот модуль
используется всегда, а объектный файл так же всегда передается компоновщику, который сопоставляет символьные имена там
и там и подставляет вместо них нужные адреса — компонует программу из множества отдельных объектных файлов.
Однако мы нигде не видим собственно вызова WriteLn()
. Вместо него вызываются целых три разных подпрограммы:
fpc_get_output
, fpc_write_text_shortstr
и fpc_writeln_end
… Вот так, неожиданно мы узнали «страшную тайну» —
процедура WriteLn()
на самом деле настоящей процедурой не является, а обрабатывается компилятором совершенно особым
образом. Как должны компилироваться и вызываться настоящие процедуры и функции мы обязательно рассмотрим когда-нибудь
потом. Пока можно отметить, что синтаксис языка вообще не позволяет объявить процедуру такого типа. Тем не менее,
WriteLn()
и еще несколько процедур ввода/вывода предусмотрены в определении языка и должны быть так или иначе реализованы.
Просто отметим это как факт и не будем заморачиваться.
Может возникнуть закономерный вопрос: а зачем нам знать, что WriteLn()
— ненормальная? Отвечаю: хотя бы для того,
чтобы не терять в дальнейшем времени на попытки выяснить, как объявить свою процедуру с аналогичным синтаксисом, или
почему не получается использовать с ней процедурные переменные…
Что еще интересного мы можем узнать из нашего ассемблерного листинга? Обратим внимание на секцию данных — совершенно
четко видно, что строка, заданная нами в программе непосредственно между одинарными кавычками — строковый литерал —
а) трактуется как «короткая» (тип ShortString
), но б) при этом завершается нулевым байтом, чтобы при необходимости
приведения ее в дальнейшем к типу «длинной» строки или PChar
не требовалось выполнять какие-либо преобразования.
Как показали эксперименты, если строковый литерал содержит символы не из первой половины кодовой таблицы ASCII (например,
русские буквы), и при этом явно определена кодировка исходника, посредством ключа -FcXXX
или директивы {$CODEPAGE XXX}
,
такой литерал будет представлен уже в виде UnicodeString
. Должен заметить, что внутреннее представление литералов
зависит от версии компилятора и может поменяться в дальнейшем. Его стоит иметь в виду, но полагаться на него в серьезных
проектах нельзя.
Теперь становится очевидным известное из документации ограничение для непосредственно указываемых строк в 255 байт.
Пара слов о компоновке
Компоновка может производиться на некоторых платформах самим FPC, а на других (и в частности, на моем Linux-amd64)
посредством GNU-компоновщика ld
[3]. Принципиальных отличий между этими двумя способами нет — при нормальной
работе компоновщик вызывается компилятором автоматически, без участия пользователя. Мы лучше рассмотрим другой момент.
Скомпилировав исполняемый файл, как было описано в начале заметки, мы можем изрядно удивиться, посмотрев на его размер —
более 100 KB (конкретно сейчас на моей системе получается 155 KB). Казалось бы, зачем для такой простой задачи так много
кода? Впрочем, в предыдущем подразделе я написал, что помимо объектного кода собственно программы, компоновщику передается
еще и объектный файл модуля System
, а ведь там не только те несколько подпрограмм, вызов которых присутствует в листинге,
но и множество других для разнообразных задач. Вот только эти задачи актуальны не для нашей программы, а для каких-то других.
Таким образом получается, что мы тянем за собой кучу неиспользуемого кода, что не есть хорошо.
Чтобы этого не происходило, следует использовать так называемое «умное связывание» (smartlinking). В этом случае,
компоновщик использует не большой объектный файл, такой как system.o
, а маленькие объектные фрагменты, собранные
в объектную библиотеку (или объектный архив), т.е. в файл libpsystem.a
, откуда он берет только нужные данной программе
фрагменты (подпрограммы, переменные и т.п.). Включив «умное связывание» через параметр командной строки -XX
, т.е. выполнив
fpc -XX hello
, мы увидим, что размер исполняемого файла резко сократился (у меня сейчас 27 KB).
Однако, надо бы разобраться, откуда берутся .a
-файлы. Хотя о модулях мы еще не говорили, замечу, что при умолчательной
компиляции из модуля генерируется обычный объектный файл, а не библиотека. Чтобы получить библиотеку, модуль нужно компилировать
с ключом -CX
, или указать в самом модуле директиву компилятора {$SMARTLINK ON}
. Для большинства стандартных модулей RTL
(и в частности, System
) это уже сделано, так что нам остается только не забыть -XX
. Для модулей, компилируемых на месте,
самописных или полученных откуда-то в исходном коде, генерацию .a
надо контролировать.
Напоследок замечу, что использование «умного связывания» несколько замедляет компиляцию. Впрочем, обычно линковка проходит в целом достаточно быстро, чтобы такое замедление не стало критичным. Но на больших проектах может быть весьма заметно.
Еще один нюанс заключается в том, что «умное связывание» не может избавить нас от неиспользуемого кода в виртуальных методах объектов.
На этом я, пожалуй, закончу приветствовать мир…
Ссылки
[1] Википедия: AT&T-синтаксис http://ru.wikipedia.org/wiki/AT&T-синтаксис [ru]
[2] Documentation for binutils 2.21: Using as http://sourceware.org/binutils/docs-2.21/as/ [en]
[3] Documentation for binutils 2.21: LD http://sourceware.org/binutils/docs-2.21/ld/ [en]
-
FPC ищет исходные тексты по имени, подставляя расширения
.pas
,.pp
и.p
. Второй вариант указывает, что исходник предназначен именно для FPC, а не для произвольного компилятора Pascal. Именно его мы и будем использовать. ↩ -
Исправил грубую ошибку при переносе поста на shikhalev.org. Была бы не столь грубая, оставил бы. 2021.04.14. ↩
-
В некоторых источниках можно прочитать, что FPC не генерирует сам машинный код, а работает через промежуточный ассемблер. Для современных версий Free Pascal это не совсем верно. На самом деле на архитектурах x86 и x86-64 (наиболее распространенных) компилятор может использовать внешний ассемблер, но без особого указания генерирует машинный код самостоятельно. Тогда как, например, для процессоров ARM внешний ассемблер действительно необходим. ↩
-
Различия ассемблеров совершенно определенно не являются предметом этих заметок, так что могу лишь порекомендовать ознакомиться с краткой характеристикой на Википедии [1] или обратиться к руководству по GNU-ассемблеру [2]. ↩