shikhalev.*
program Hello;

begin
 WriteLn('Hello, World!')
end.
$ fpc hello
Target OS: Linux for x86-64
Compiling hello.pp
Linking hello
5 lines compiled, 0.9 sec
$ ./hello
Hello, World!
$

Начнем по традиции с простейшей программы, делающей хоть что-то… Кто сказал «что-то полезное»? До чего-то полезного нам еще пилить и пилить. Достаточно типовой «Hello, World!» можно увидеть во врезке. Теперь можно сохранить этот текст в файл (например, hello.pp1), скомпилировать командой fpc hello и запустить. См. консольный фрагмент — если вы работаете в Linux или FreeBSD, результат должен быть похож. Да и в других системах отличия не особо существенны. Еще тут может выпасть предупреждение от компоновщика — на него внимания обращать не надо. Получилось? Теперь будем разбираться, что именно.

В действительности, это не самый минимальный пример — я таки оставил одну необязательную строку, а именно — первую… Но давайте по порядку. Исходный код программы начинается с необязательного заголовка, состоящего из ключевого слова program и идентификатора. Заканчиваться идентификатор заголовок2 должен точкой с запятой, как и любое предложение языка. Впрочем, не совсем так.

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

Там, где есть разница между «окончанием» и «разделением», я буду отдельно пояснять необходимость (или допустимость) точки с запятой. В данном же случае несущественно, заканчивает ли она предложение заголовка, или отделяет его от следующего.

Вернемся к нашим баранам. Заголовок в исходном файле программы необязателен, но лично я настоятельно рекомендую его указывать — все-таки код должен быть хорошо читаем не только компилятором, но и людьми, которым, возможно, придется его развивать и поддерживать. К тому же так получится единообразно с другими вариантами исходников — кодом динамических библиотек и модулей.

После заголовка идет область описаний, где мы должны расписать используемые модули, типы, переменные, константы, метки и т.д. Естественно, только в том случае, если мы их реально используем. В нашем примере ничего такого нет, поэтому область описаний осталась пустой.

Затем, как мы можем видеть, идет исполняемый блок, начинающийся с ключевого слова begin и заканчивающийся ключевым словом end с точкой. Между ними расположены собственно исполняемые операторы. В нашем случае — один оператор вызова процедуры. Системная процедура WriteLn() записывает строку в стандартный вывод (или в произвольный текстовый файл, если он указан).

Глядя на вывод компилятора можно подумать, что формирование исполняемого файла происходит в два этапа — «Compiling» и «Linking». Так оно и есть. И мы сейчас немного о них поговорим.

Собственно, если кто не в курсе, компиляция — это формирование машинного (в т.ч. — для виртуальной машины) кода из исходного. Тогда как компоновка (линковка, linking) — формирование готового исполняемого файла для конкретной операционной системы, со служебной информацией, нужной для запуска и т.д. Это разные по своей сути процессы.

Пара слов о компиляции

Если мы посмотрим в каталог, где только что скомпилировали нашу программу, то помимо исходного hello.pp и конечного исполняемого hello (hello.exe на некоторых ОС), увидим файл hello.o. Это так называемый объектный файл, содержащий результат компиляции — машинный код, не содержащий служебной информации, необходимой для запуска программы, зато содержащий служебную информацию, нужную компоновщику, а именно — символьные имена функций, переменных и т.д., которые компоновщик уже потом преобразует в адреса.

.section .text
    .balign 8,0x90
.globl  PASCALMAIN
    .type   PASCALMAIN,@function
PASCALMAIN:
.globl main
    .type   main,@function
main:
# [hello.pp]
# [3] begin
    subq    $8,%rsp
    movq    %rbx,(%rsp)
    call    FPC_INITIALIZEUNITS
# [4] WriteLn('Hello, World!')
    call    fpc_get_output
    movq    %rax,%rbx
    movq    %rbx,%rsi
    movq    $_$HELLO$_Ld1,%rdx
    movl    $0,%edi
    call    fpc_write_text_shortstr
    call    FPC_IOCHECK
    movq    %rbx,%rdi
    call    fpc_writeln_end
    call    FPC_IOCHECK
# [5] end.
    call    FPC_DO_EXIT
    movq    (%rsp),%rbx
    addq    $8,%rsp
    ret

.section .rodata
    .balign 8
.globl      _$HELLO$_Ld1
_$HELLO$_Ld1:
    .ascii "\015Hello, World!\000"

Мы можем дизассемблировать этот файл посредством утилиты 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. FPC ищет исходные тексты по имени, подставляя расширения .pas, .pp и .p. Второй вариант указывает, что исходник предназначен именно для FPC, а не для произвольного компилятора Pascal. Именно его мы и будем использовать. ↩

  2. Исправил грубую ошибку при переносе поста на shikhalev.org. Была бы не столь грубая, оставил бы. 2021.04.14. ↩

  3. В некоторых источниках можно прочитать, что FPC не генерирует сам машинный код, а работает через промежуточный ассемблер. Для современных версий Free Pascal это не совсем верно. На самом деле на архитектурах x86 и x86-64 (наиболее распространенных) компилятор может использовать внешний ассемблер, но без особого указания генерирует машинный код самостоятельно. Тогда как, например, для процессоров ARM внешний ассемблер действительно необходим. ↩

  4. Различия ассемблеров совершенно определенно не являются предметом этих заметок, так что могу лишь порекомендовать ознакомиться с краткой характеристикой на Википедии [1] или обратиться к руководству по GNU-ассемблеру [2]. ↩