Таблица символов
Таблица символов — это структура данных, используемая трансляторами языков программирования, такими как компиляторы и интерпретаторы, для хранения информации о каждом идентификаторе, символе, константе, процедуре и функции, встречающихся в исходном коде программы. Для каждого элемента исходного кода таблица символов связывает сведения, относящиеся к его объявлению или использованию[1].
Предпосылки
Таблица символов может существовать только в оперативной памяти во время процесса трансляции, либо быть встроенной в выходные данные трансляции, например, в объектный файл ABI — для последующего использования. Например, таблица символов может использоваться во время отладки или как источник сведений при формировании диагностического отчёта во время или после выполнения программы[2].
Описание
Минимальная информация, содержащаяся в таблице символов, используемой транслятором и промежуточным представлением, включает в себя имя символа и его местоположение или адрес. Для компиляторов, целевыми платформами которых являются системы с поддержкой перемещаемости кода, таблица также содержит соответствующие атрибуты (абсолютный, релокейтабельный(перемещаемый) и т. д.) и необходимые сведения для обработки таких символов. В таблицах символов для языков высокого уровня могут храниться сведения о типе (строка, целое число, число с плавающей запятой и др.), размере, измерениях и границах символа. Не вся эта информация включается в выходной файл, но может предоставляться для целей отладки. Во многих случаях к каждому символу хранится перекрёстная ссылка. Большинство компиляторов формируют итоговые отчёты, где перечисляют информацию из таблицы символов и перекрестных ссылок[1].
Реализация
Для реализации таблицы символов доступны различные структуры данных. Можно использовать деревья, линейные списки или самоупорядочивающиеся списки. Таблица символов используется на большинстве этапов работы компилятора, начиная с лексического анализа и заканчивая оптимизацией.
Компилятор может использовать единую большую таблицу символов для всех символов или раздельные (иерархические) таблицы для каждого блока видимости. Например, в языках с блочной структурой, таких как ALGOL или PL/I, символ «p» может быть объявлен неоднократно — в разных процедурах и с разными атрибутами. Область видимости каждого объявления — это участок программы, где использование «p» разрешается к этой конкретной декларации. Таблица символов должна обеспечивать различие между такими случаями.
Часто таблицы символов реализуются в виде хеш-таблиц. Время поиска в хеш-таблице не зависит от количества хранимых элементов, что делает этот подход эффективным при большом их числе. Кроме того, это упрощает классификацию литералов — необходимая информация включается в вычисление хеш-ключа[3].
Так как лексический анализатор значительную часть времени занят поиском в таблице символов, эффективность её организации существенно сказывается на скорости компиляции. В таблице символов элемент должен быть найден максимально быстро. Для этого она, как правило, реализуется как хеш-таблица, где по ключевому слову или идентификатору вычисляется индекс массива. Коллизии неизбежны, и часто для их разрешения используется помещение синонимов в ближайшую свободную ячейку таблицы.
Применение
Объектный файл содержит таблицу символов используемых идентификаторов, видимых извне. При компоновке нескольких объектных файлов компоновщик идентифицирует и разрешает такие символы. Обычно все неопределённые внешние символы ищутся в одной или нескольких библиотеках объектных файлов. Если найден модуль, определяющий требуемый символ, он связывается с первым объектным файлом, а все новые неопределённые идентификаторы добавляются к списку для поиска. Этот процесс продолжается до тех пор, пока все внешние ссылки не будут разрешены. Если после завершения процесса остались неразрешённые ссылки, это считается ошибкой.
Во время реверс-инжиниринга выполняемых файлов многие инструменты используют таблицу символов для поиска адресов глобальных переменных и известных функций. Если перед созданием исполняемого файла таблица символов была удалена или очищена, становится существенно сложнее определить адреса или понять структуру программы.
Пример
Рассмотрим следующий фрагмент программы на языке C:
// Объявление внешней функции
extern double bar(double x);
// Определение публичной функции
double foo(int count)
{
double sum = 0.0;
// Суммирование значений bar(1) до bar(count)
for (int i = 1; i <= count; i++)
sum += bar((double) i);
return sum;
}
Компилятор C, который анализирует этот код, как правило, сформирует как минимум следующие записи таблицы символов:
| Имя символа | Тип | Область видимости |
|---|---|---|
bar |
функция, double | extern |
x |
double | параметр функции |
foo |
функция, double | глобальная |
count |
int | параметр функции |
sum |
double | локальная (блочная) |
i |
int | оператор цикла for |
Кроме перечисленных, компилятор может добавить в таблицу служебные записи, генерируемые для промежуточных выражений (например, преобразование i в double), возвратных значений функций, меток перехода и т. п.
SysV ABI
| Адрес | Тип | Имя |
|---|---|---|
| 00000020 | a | T_BIT |
| 00000040 | a | F_BIT |
| 00000080 | a | I_BIT |
| 20000004 | t | irqvec |
| 20000008 | t | fiqvec |
| 2000000c | t | InitReset |
| 20000018 | T | _main |
| 20000024 | t | End |
| 20000030 | T | AT91F_US3_CfgPIO_useB |
| 2000005c | t | AT91F_PIO_CfgPeriph |
| 200000b0 | T | main |
| 20000120 | T | AT91F_DBGU_Printk |
| 20000190 | t | AT91F_US_TxReady |
| 200001c0 | t | AT91F_US_PutChar |
| 200001f8 | T | AT91F_SpuriousHandler |
| 20000214 | T | AT91F_DataAbort |
| 20000230 | T | AT91F_FetchAbort |
| 2000024c | T | AT91F_Undef |
| 20000268 | T | AT91F_UndefHandler |
| 20000284 | T | AT91F_LowLevelInit |
| 200002e0 | t | AT91F_DBGU_CfgPIO |
| 2000030c | t | AT91F_PIO_CfgPeriph |
| 20000360 | t | AT91F_US_Configure |
| 200003dc | t | AT91F_US_SetBaudrate |
| 2000041c | t | AT91F_US_Baudrate |
| 200004ec | t | AT91F_US_SetTimeguard |
| 2000051c | t | AT91F_PDC_Open |
| 2000059c | t | AT91F_PDC_DisableRx |
| 200005c8 | t | AT91F_PDC_DisableTx |
| 200005f4 | t | AT91F_PDC_SetNextTx |
| 20000638 | t | AT91F_PDC_SetNextRx |
| 2000067c | t | AT91F_PDC_SetTx |
| 200006c0 | t | AT91F_PDC_SetRx |
| 20000704 | t | AT91F_PDC_EnableRx |
| 20000730 | t | AT91F_PDC_EnableTx |
| 2000075c | t | AT91F_US_EnableTx |
| 20000788 | T | __aeabi_uidiv |
| 20000788 | T | __udivsi3 |
| 20000884 | T | __aeabi_uidivmod |
| 2000089c | T | __aeabi_idiv0 |
| 2000089c | T | __aeabi_ldiv0 |
| 2000089c | T | __div0 |
| 200009a0 | D | _data |
| 200009a0 | A | _etext |
| 200009a4 | A | __bss_end__ |
| 200009a4 | A | __bss_start |
| 200009a4 | A | __bss_start__ |
| 200009a4 | A | _edata |
| 200009a4 | A | _end |
Пример таблицы символов можно найти в спецификации SysV ABI (англ. System V Application Binary Interface), где стандартизируется способ размещения символов в бинарных файлах. Это обеспечивает совместную работу различных компиляторов, компоновщиков и загрузчиков с символами в скомпилированных объектах.
SysV ABI реализован, например, в утилите англ. nm пакета GNU binutils. Формат таблицы включает отсортированное поле с адресом памяти, поле «тип символа» и идентификатор символа («имя»)[4].
Типы символов в спецификации SysV ABI (и выводе утилиты англ. nm) характеризуют назначение каждой записи: например, «d» для инициализированных данных, а «t» для функций (исполняемый код размещается в секции «text» объектного файла). Заглавные буквы обозначают внешнюю (глобальную) связь, а строчные — локальную.
Таблица символов Python
Язык программирования Python содержит широкие возможности для создания и анализа таблиц символов[5]. Можно, например, проверять, является ли символ свободной или связанной переменной, к какому пространству имён относится, импортирован ли он и принадлежит ли к блочной или глобальной области видимости.
Динамические таблицы символов
В некоторых языках таблица символов может изменяться во время выполнения программы — позволяется создавать новые символы на лету. Примером такого языка является англ. Racket[6].
Языки LISP и Scheme позволяют ассоциировать с каждым символом произвольные свойства[7].
Язык Prolog во многом представляет собой язык обработки таблицы символов: символы называются атомами, а взаимосвязи между ними могут анализироваться логически. Похожая система реализована в OpenCog, где динамическая таблица символов («atomspace») служит для представления знаний.
Примечания
Литература
- Copper, Keith D. Engineering a Compiler : [англ.] / Keith D. Copper, Linda Torczon. — 2. — Хьюстон, Техас : Elsevier, Rice University, 18 января 2011. — ISBN 978-0-12-088478-0. — doi:10.1016/C2009-0-27982-7.