Введение
С недавнего времени я увлекся микроконтроллерами. Сначала AVR, затем ARM. Для программирования микроконтроллеров существует два основных варианта: ассемблер и С. Однако, я фанат языка программирования Форт и занялся портированием его на эти микроконтроллеры. Конечно, существуют и готовые решения, но ни в одном из них не было того, что я хотел: отладки с помощью gdb. И я задался целью заполнить этот пробел (пока только для ARM). В моем распоряжении была плата stm32vldiscovery с 32-битным процессором ARM Cortex-M3, 128кБ flash и 8 кБ RAM, поэтому я и начал с нее.
Писал я кросс-транслятор Форта конечно на Форте, и кода в статье не будет, так как этот язык считается экзотическим. Ограничусь достаточно подробными рекомендациями. Документации и примеров в сети по предмету почти нет, некоторые параметры подбирались мной путем проб и ошибок, некоторые — путем анализа выходных файлов компилятора gcc. Кроме того, я использовал только необходимый минимум отладочной информации, не касаясь, например, relocation-ов и множества других вещей. Тема очень обширна и, признаюсь, разобрался я с ней только процентов на 30, что оказалось для меня достаточным.
Кого заинтересует этот проект, может скачать код здесь.
Обзор ELF
Стандартные средства разработки компилируют вашу программу в файл ELF (Executable and Linkable Format) с возможностью включения отладочной информации. Спецификацию формата можно прочитать здесь. Кроме того, для каждой архитектуры имеются свои особенности, например особенности ARM. Рассмотрим кратко этот формат.
Исполняемый файл формата ELF состоит из таких частей:
1. Заголовок (ELF Header)
Содержит общую информацию о файле и его основные характеристики.
2. Заголовок программы (Program Header Table)
Это таблица соответствия секций файла сегментам памяти, указывает загрузчику, в какую область памяти писать каждую секцию.
3. Секции
Секции содержат всю информацию в файле (программа, данные, отладочная информация и т.д)
У каждой секции есть тип, имя и другие параметры. В секции ".text" обычно хранится код, в ".symtab" — таблица символов программы (имена файлов, процедур и переменных), в ".strtab" — таблица строк, в секциях с префиксом ".debug_" — отладочная информация и т.д. Кроме того, в файле должна обязательно быть пустая секция с индексом 0.
4. Заголовок секций (Section Header Table)
Это таблица, содержащая массив заголовков секций.
Более подробно формат рассматривается в разделе Создание ELF.
Обзор DWARF
DWARF — это стандартизованный формат отладочной информации. Стандарт можно скачать на официальном сайте. Там же лежит замечательное краткое обозрение формата: Introduction to the DWARF Debugging Format (Michael J. Eager).
Зачем нужна отладочная информация? Она позволяет:
- устанавливать точки останова (breakpoints) не на физический адрес, а на номер строки в файле исходного кода или на имя функции
- отображать и изменять значения глобальных и локальных переменных, а также параметров функции
- отображать стек вызовов (backtrace)
- исполнять программу пошагово не по одной инструкции ассемблера, а по строкам исходного кода
Эта информация хранится в виде древовидной структуры. Каждый узел дерева имеет родителя, может иметь потомков и называется DIE (Debugging Information Entry). Каждый узел имеет свой тэг (тип) и список атрибутов (свойств), описывающих узел. Атрибуты могут содержать все, что угодно, например, данные или ссылки на другие узлы. Кроме того, существует информация, хранящаяся вне дерева.
Узлы делятся на два основных типа: узлы, описывающие данные, и узлы, описывающие код.
Узлы, описывающие данные:
- Типы данных:
- Базовые типы данных (узел с типом DW_TAG_base_type), например такие как тип int в C.
- Составные типы данных (указатели и т.д.)
- Массивы
- Структуры, классы, объединения, интерфейсы
- Объекты данных:
- константы
- параметры функций
- переменные
- и т.д.
Каждый объект данных имеет атрибут DW_AT_location, который указывает, как вычисляется адрес, по которому находится данные. Например переменная может иметь фиксированный адрес, находиться в регистре или на стеке, быть членом класса или объекта. Этот адрес может вычисляться довольно сложным образом, поэтому стандарт предусматривает так называемые Location Expressions, которые могут содержать последовательность операторов специальной внутренней стековой машины.
Узлы, описывающие код:
- Процедуры (функции) — узлы с тэгом DW_TAG_subprogram. Узлы-потомки могут содержать описания переменных — параметров функции и локальных переменных функции.
- Единица компиляции (Compilation Unit). Содержит информацию программе и является родителем всех остальных узлов.
Информация, описанная выше, находится в секциях ".debug_info" и ".debug_abbrev".
Другая информация:
- Информация о номерах строк (секция ".debug_line")
- Информация о макросах (секция ".debug_macinfo")
- Информация о формате фрейма (Call Frame Information) (секция ".debug_frame")
Создание ELF
Создавать файлы в формате EFL мы будем при помощи библиотеки libelf из пакета elfutils. В сети есть хорошая статья по использованию libelf — LibELF by Example (к сожалению, созданию файлов в ней описано очень кратко) а также документация.
Создание файла состоит из нескольких этапов:
- Инициализация libelf
- Создание заголовка файла (ELF Header)
- Создание заголовка программы (Program Header Table)
- Создание секций
- Запись файла
Рассмотрим этапы подробнее
Инициализация libelf
Сначала вам нужно будет вызвать функцию elf_version(EV_CURRENT) и проверить результат. Если он равен EV_NONE — возникла ошибка и дальнейшие действия производить нельзя. Затем нужно создать нужный нам файл на диске, получить его дескриптор и передать его в функцию elf_begin:
Elf * elf_begin(
int fd,
Elf_Cmd cmd,
Elf *elf)
- fd — дескриптор только что открытого файла
- cmd — режим (ELF_C_READ для чтения информации, ELF_C_WRITE для записи или ELF_C_RDWR для чтения/записи), он должен соответствовать режиму открытого файла (ELF_C_WRITE в нашем случае)
- elf — нужен только для работы с файлами архивов (.a), в нашем случае нужно передать 0
Функция возвращает указатель на созданный дескриптор, который будет использоваться во всех функциях libelf, 0 возвращается в случае ошибки.
Создание заголовка
Новый заголовок файла создается функцией elf32_newehdr:
Elf32_Ehdr * elf32_newehdr(
Elf *elf);
- elf — дескриптор, возвращенный функцией elf_begin
Возвращает 0 при ошибке или указатель на структуру — заголовок ELF-файла:
#define EI_NIDENT 16
typedef struct {
unsigned char e_ident[EI_NIDENT];
Elf32_Half e_type;
Elf32_Half e_machine;
Elf32_Word e_version;
Elf32_Addr e_entry;
Elf32_Off e_phoff;
Elf32_Off e_shoff;
Elf32_Word e_flags;
Elf32_Half e_ehsize;
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize;
Elf32_Half e_shnum;
Elf32_Half e_shstrndx;
} Elf32_Ehdr;
Некоторые поля ее заполнены стандартным образом, некоторые нужно заполнить нам:
- e_ident — байтовый массив идентификации, имеет такие индексы:
- EI_MAG0, EI_MAG1, EI_MAG2, EI_MAG3 — эти 4 байта должны содержать симовлы 0x7f,'ELF', что за нас уже сделала функция elf32_newehdr
- EI_DATA — указывает на тип кодирования данных в файле: ELFDATA2LSB или ELFDATA2MSB. Нужно установить ELFDATA2LSB так: e_ident[EI_DATA] = ELFDATA2LSB
- EI_VERSION — версия заголовка файла, уже установлена за нас
- EI_PAD — не трогаем
- e_type — тип файла, может быть ET_NONE — без типа, ET_REL — перемещаемый файл, ET_EXEC — исполняемый файл, ET_DYN — разделяемый объектный файл и т.д. Нам нужно установить тип файла в ET_EXEC
- e_machine — архитектура, требуемая для данного файла, например EM_386 — для архитектуры Intel, для ARM нам нужно записать сюда EM_ARM (40) — см. ELF for the ARM Architecture
- e_version — версия файла, нужно обязательно установить в EV_CURRENT
- e_entry — адрес точки входа, для нас не обязательно
- e_phoff — смещение в файле заголовка программы, e_shoff — смещение заголовка секций, не заполняем
- e_flags — спецефичные для процессора флаги, для нашей архитектуры (Cortex-M3) нужно установить равным 0x05000000 (ABI version 5)
- e_ehsize, e_phentsize, e_phnum, e_shentsize, e_shnum — не трогаем
- e_shstrndx — содержит номер секции, в которой находится таблица строк с заголовками секций. Так как никаких секций у нас еще нет, этот номер мы установим позднее
Создание заголовка программы
Как уже говорилось, заголовок программы (Program Header Table) — это таблица соответствия секций файла сегментам памяти, которая указывает загрузчику, куда писать каждую секцию. Загоовок создается создаются с помощью функции elf32_newphdr:
Elf32_Phdr * elf32_newphdr(
Elf *elf,
size_t count);
- elf — наш дескриптор
- count — количество создаваемых элементов таблицы. Так как у нас будет только одна секция (с программным кодом), то count будет равен 1.
Возвращает 0 при ошибке или указатель на заголовок программы.
Каждая элемент в таблице заголовка описывается такой структурой:
typedef struct {
Elf32_Word p_type;
Elf32_Off p_offset;
Elf32_Addr p_vaddr;
Elf32_Addr p_paddr;
Elf32_Word p_filesz;
Elf32_Word p_memsz;
Elf32_Word p_flags;
Elf32_Word p_align;
} Elf32_Phdr;
- p_type — тип сегмента (секции), тут мы должны указать PT_LOAD — загружаемый сегмент
- p_offset — смещений в файле, откуда начинается данные секции, которая будет загружаться в память. У нас это секция .text, которая будет находиться сразу после заголовка файла и заголовка программы, смещение мы можем вычислить как сумму длин этих заголовков. Длину любого типа можно получить с помощью функции elf32_fsize:
size_t elf32_fsize(Elf_Type type, size_t count, unsigned int version);
type — здесь константа ELF_T_ххх, нам нужны будут размеры ELF_T_EHDR и ELF_T_PHDR; count — количество элементов нужного типа, version — нужно установить в EV_CURRENT
- p_vaddr, p_paddr — виртуальный и физический адрес, по которому будет загружено содержимое секции. Так как у нас виртуальных адресов нет, устанавливаем его равным физическому, в простейшем случае — 0, потому что именно сюда будет загружаться наша программа.
- p_filesz, p_memsz — размер секции в файле и памяти. У нас они одинаковы, но так как секции с программным кодом еще нет, установим их позднее
- p_flags — разрешения для загруженного сегмента памяти. Могут быть PF_R — чтение, PF_W — запись, PF_X — выполнение или их комбинацией. Установим p_flags равным PF_R + PF_X
- p_align — выравнивание сегмента, у нас 4
Создание секций
После создания заголовков можно приступать к созданию секций. Пустая секция создается при помощи функции elf_newscn:
Elf_Scn * elf_newscn(
Elf *elf);
- elf — дескриптор, возвращенный ранее функцией elf_begin
Функция возвращает указатель на секцию или 0 при ошибке.
После создания секции нужно заполнить заголовок секции и создать описатель данных секции.
Указатель на заголовок секции мы можем получить при помощи функции elf32_getshdr:
Elf32_Shdr * elf32_getshdr(
Elf_Scn *scn);
- scn — указатель на секцию, который мы получили из функции elf_newscn.
Заголовок секции выглядит так:
typedef struct {
Elf32_Word sh_name;
Elf32_Word sh_type;
Elf32_Word sh_flags;
Elf32_Addr sh_addr;
Elf32_Off sh_offset;
Elf32_Word sh_size;
Elf32_Word sh_link;
Elf32_Word sh_info;
Elf32_Word sh_addralign;
Elf32_Word sh_entsize;
} Elf32_Shdr;
- sh_name — имя секции — смещение в строковой таблице заголовков секций (секция .shstrtab) — см. «Таблицы строк» далее
- sh_type — тип содержимого секции, для секции с кодом программы нужно установить SHT_PROGBITS, для секций с таблицей строк — SHT_STRTAB, для таблицы символов — SHT_SYMTAB
- sh_flags — флаги секции, которые можно комбинировать, и из которых нам нужны только три:
- SHF_ALLOC — означает, что секция будет загружаться в память
- SHF_EXECINSTR — секция содержит исполняемый код
- SHF_STRINGS — секция содержит таблицу строк
Соответственно, для секции .text с программой нужно установить флаги SHF_ALLOC + SHF_EXECINSTR
- sh_addr — адрес, по которому секция будет загружена в память
- sh_offset — смещение секции в файле — не трогаем, библиотека установит за нас
- sh_size — размер секции — не трогаем
- sh_link — содержит номер связанной секции, нужна для связи секции с соответствующей ей таблицей строк (см. далее)
- sh_info — дополнительная информация, зависящая от типа секции, устанавливаем в 0
- sh_addralign — выравнивание адреса, не трогаем
- sh_entsize — если секция состоит из нескольких элементов одинаковой длины, указывает на длину такого элемента, не трогаем
После заполнения заголовка нужно создать описатель данных секции функцией elf_newdata:
Elf_Data * elf_newdata(
Elf_Scn *scn);
- scn — только что полученный указатель на новую секцию.
Функция возвращает 0 при ошибке, или указатель на структуру Elf_Data, которую нужно будет заполнить:
typedef struct {
void* d_buf;
Elf_Type d_type;
size_t d_size;
off_t d_off;
size_t d_align;
unsigned d_version;
} Elf_Data;
- d_buf — указатель на данные, которые нужно записать в секцию
- d_type — тип данных, для нас везде подойдет ELF_T_BYTE
- d_size — размер данных
- d_off — смещение в секции, установить в 0
- d_align — выравнивание, можно установить в 1 — без выравнивания
- d_version — версия, обязательно установить в EV_CURRENT
Специальные секции
Для наших целей нам нужно будет создать минимально необходимый набор секций:
- .text — секция с кодом программы
- .symtab — таблица символов файла
- .strtab — таблица строк, содержащая имена символов из секции .symtab, так как в последней хранятся не сами имена, а их индексы
- .shstrtab — таблица строк, содержащая имена секций
Все секции создаются так, как описано в предыдущем разделе, но у каждой специальной секции есть свои особенности.
Секция .text
Эта секция содержит исполняемый код, поэтому нужно sh_type установить в SHT_PROGBITS, sh_flags — в SHF_EXECINSTR + SHF_ALLOC, sh_addr — установить равным адресу, по которому будет загружен этот код
Секция .symtab
Секция содержит описание всех символов (функций) программы и файлов, в которых они были описаны. Она состоит из таких элементов длиной по 16 байт:
typedef struct {
Elf32_Word st_name;
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info;
unsigned char st_other;
Elf32_Half st_shndx;
} Elf32_Sym;
- st_name — имя символа (индекс в таблице строк .strtab)
- st_value — значение (адрес входа для функции или 0 для файла). Так как Cortex-M3 имеет систему команд Thumb-2, этот адрес обязательно должен быть нечетным (реальный адрес + 1)
- st_size — длина кода функции (0 для файла)
- st_info — тип символа и его область видимости. Для определения значения этого поля существует макрос
#define ELF32_ST_INFO(b,t) (((b)<<4)+((t)&0xf))
где b — область видимости, а t — тип символа
Область видимости может быть STB_LOCAL (символ не виден из других объектных файлов) или STB_GLOBAL (виден). Для упрощения используем STB_GLOBAL.
Тип символа — STT_FUNC для функции, STT_FILE для файла - st_other — установить в 0
- st_shndx — индекс секции, для которой определен символ (индекс секции .text), или SHN_ABS для файла.
Индекс секции по ее дескриптору scn можно определить с помощью elf_ndxscn:size_t elf_ndxscn( Elf_Scn *scn);
Данные для секции можно собирать при проходе по исходному тексту в массив, указатель на который затем записать в описатель данных секции (d_buf).
Эта секция создается обычным образом, только sh_type нужно установить в SHT_SYMTAB, а индекс секции .strtab записать в поле sh_link, таким образом эти секции станут связанными.
Секция .strtab
В этой секции находятся имена всех символов из секции .symtab. Создается как обычная секция, но sh_type нужно установить в SHT_STRTAB, sh_flags — в SHF_STRINGS, таким образом эта секция становится таблицей строк.
Данные для секции можно собирать при проходе по исходному тексту в массив, указатель на который затем записать в описатель данных секции (d_buf).
Секция .shstrtab
Секция — таблица строк, содержит заголовки всех секций файла, в том числе и свой заголовок. Создается так же, как и секция .strtab. После создания ее индекс нужно записать в поле e_shstrndx заголовка файла.
Таблицы строк
Таблицы строк содержат идущие подряд строки, оканчивающиеся нулевым байтом, первый байт в этой таблице должен быть также 0. Индекс строки в таблице — это просто смещение в байтах от начала таблицы, таким образом, первая строка 'name' имеет индекс 1, следующая строка 'var' имеет индекс 6.
Индекс 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 n a m e v a r
Запись файла
Итак, заголовки и секции уже сформированы, теперь их нужно записать в файл и завершить работу с libelf. Запись производит функция elf_update:
off_t elf_update(
Elf *elf,
Elf_Cmd cmd);
- elf — дескриптор
- cmd — команда, должна быть равна ELF_C_WRITE для записи.
Функция возвращает -1 при ошибке. Текст ошибки можно получить вызвав функцию elf_errmsg(-1), которая возвратит указатель на строку с ошибкой.
Заканчиваем работу с библиотекой функцией elf_end, которой передаем наш дескриптор. Осталось только закрыть ранее открытый файл.
Однако наш созданный файл не содержит отладочной информации, которую мы добавим в следующем разделе.
Создание DWARF
Создавать отладочную информацию будем с помощью библиотеки libdwarf, в комплекте с которой идет pdf-файл с документацией (libdwarf2p.1.pdf — A Producer Library Interface to DWARF).
Создание отладочной информации состоит из таких этапов:
- Инициализация libdwarf producer
- Создание узлов (DIE — Debugging Information Entry)
- Создание атрибутов узла
- Создание Единицы Компиляции (Compilation Unit)
- Создание Common Information Entry
- Создание типов данных
- Создание процедур (функций)
- Создание переменных и констант
- Создание секций с отладочной информацией
- Окончание работы с библиотекой
Рассмотрим этапы подробнее
Инициализация libdwarf producer
Мы будем создавать отладочную информацию во время компиляции одновременно с созданием символов в секции .symtab, поэтому инициализацию библиотеки нужно осуществлять после инициализации libelf, создания ELF-заголовка и заголовка программы, до создания секций.
Для инициализации будем использовать функцию dwarf_producer_init_c. В библиотеке есть еще несколько функций инициализации (dwarf_producer_init, dwarf_producer_init_b), которые различаются некоторыми нюансами, описанными в документации. В принципе, можно использовать любую из них.
Dwarf_P_Debug dwarf_producer_init_c(
Dwarf_Unsigned flags,
Dwarf_Callback_Func_c func,
Dwarf_Handler errhand,
Dwarf_Ptr errarg,
void * user_data,
Dwarf_Error *error)
- flags — комбинация по «или» нескольких констант которые определяют некоторые параметры, например разрядность информации, следование байтов (little-endian, big-endian), формат релокаций, из которых нам обязательно нужны DW_DLC_WRITE и DW_DLC_SYMBOLIC_RELOCATIONS
- func — callback-функция, которая будет вызываться при создании ELF-секций с отладочной информацией. Более подробно см. ниже в разделе «Создание секций с отладочной информацией»
- errhand — указатель на функцию, которая будет вызываться при возникновении ошибок. Можно передать 0
- errarg — данные, которые будут передаваться в функцию errhand, можно ставить 0
- user_data — данные, которые будут переданы в функцию func, можно ставить 0
- error — возвращаемый код ошибки
Функция возвращает Dwarf_P_Debug — дескриптор, используемый во всех последующих функциях, или -1 в случае ошибки, при этом в error будет код ошибки (получить текст сообщения об ошибке по его коду можно при помощи функции dwarf_errmsg, передав ей этот код)
Создание Узлов (DIE — Debugging Information Entry)
Как было описано выше, отладочная информация образует древовидную структуру. Для того, чтобы создать узел этого дерева, нужно:
- создать его функцией dwarf_new_die
- добавить к нему атрибуты (каждый тип атрибутов добавляется своей функцией, которые будут описаны далее)
Узел создается при помощи функции dwarf_new_die:
Dwarf_P_Die dwarf_new_die(
Dwarf_P_Debug dbg,
Dwarf_Tag new_tag,
Dwarf_P_Die parent,
Dwarf_P_Die child,
Dwarf_P_Die left_sibling,
Dwarf_P_Die right_sibling,
Dwarf_Error *error)
- dbg — дескриптор Dwarf_P_Debug, полученный при инициализации библиотеки
- new_tag — тэг (тип) узла — константа DW_TAG_xxxx, которые можно найти в файле libdwarf.h
- parent, child, left_sibling, right_sibling — соответственно родитель, потомок, левый и правый соседи узла. Необязательно указывать все эти параметры, достаточно указать один, вместо остальных поставить 0. Если все параметры равны 0, узел будет или корневым, или изолированным
- error — будет содержать код ошибки при ее возникновении
Функция возвращает DW_DLV_BADADDR при ошибке или дескриптор узла Dwarf_P_Die в случае успеха
Создание атрибутов узла
Для создания атрибутов узла есть целое семейство функций dwarf_add_AT_хххх. Иногда проблематично определить, какой функцией нужно создавать необходимый атрибут, так что я даже несколько раз копался в исходном коде библиотеки. Некоторые из функций будут описаны здесь, некоторые ниже — в соответствующих разделах. Все они принимают параметр ownerdie — дескриптор узла, к которому будет добавлен атрибут, и возвращают код ошибки в параметре error.
Функция dwarf_add_AT_name добавляет к узлу атрибут «имя» (DW_AT_name). У большинства узлов должно быть имя (например у процедур, переменных, констант), у некоторых имени может и не быть (например у Compilation Unit)
Dwarf_P_Attribute dwarf_add_AT_name(
Dwarf_P_Die ownerdie,
char *name,
Dwarf_Error *error)
- name — собственно значение атрибута (имя узла)
Возвращает DW_DLV_BADADDR в случае ошибки или дескриптор атрибута при успешном завершении.
Функции dwarf_add_AT_signed_const, dwarf_add_AT_unsigned_const добавляют к узлу указанный атрибут и его знаковое (беззнаковое) значение. Знаковые и беззнаковые атрибуты используются для задания значений констант, размеров, номеров строк и т.д. Формат функций:
Dwarf_P_Attribute dwarf_add_AT_(un)signed_const(
Dwarf_P_Debug dbg,
Dwarf_P_Die ownerdie,
Dwarf_Half attr,
Dwarf_Signed value,
Dwarf_Error *error)
- dbg — дескриптор Dwarf_P_Debug, полученный при инициализации библиотеки
- attr — атрибут, значение которого задается, — константа DW_AT_xxxx, которые можно найти в файле libdwarf.h
- value — значение атрибута
Возвращают DW_DLV_BADADDR в случае ошибки или дескриптор атрибута при успешном завершении.
Создание Единицы Компиляции (Compilation Unit)
В любом дереве должен быть корень — у нас это единица компиляции, которая содержит информацию о программе (например, имя главного файла, используемый язык программирования, название компилятора, чувствительность символов (переменных, функций) к регистру, главную функцию программы, начальный адрес и.т.д). В принципе, никакие атрибуты не являются обязательными. Для примера создадим информацию о главном файле и компиляторе.
Информация о главном файле
Для хранения информации о главном файле используется атрибут «имя» (DW_AT_name), применяйте функцию dwarf_add_AT_name, как показано в разделе «Создание атрибутов узла».
Информация о компиляторе
Используем функцию dwarf_add_AT_producer:
Dwarf_P_Attribute dwarf_add_AT_name(
Dwarf_P_Die ownerdie,
char *producer_string,
Dwarf_Error *error)
- producer_string — строка с текстом информации
Возвращает DW_DLV_BADADDR в случае ошибки или дескриптор атрибута при успешном завершении.
Создание Common Information Entry
Обычно при вызове функции (подпрограммы) ее параметры и адрес возврата помещается в стек (хотя каждый компилятор может делать это по-своему), все это называется Call Frame. Отладчику нужна информация о формате фрейма чтобы правильно определить адрес возврата из функции и построить backtrace — цепочку вызовов функций, которая привела нас в текущую функцию, и параметры этих функций. Также обычно указываются регистры процессора, которые сохраняются на стеке. Код, который резервирует место на стеке и сохраняет регистры процессора, называется прологом функции, код, восстанавливающий регистры и стек — эпилогом.
Эта информация сильно зависит от компилятора. Например, пролог и эпилог необязательно должны быть в самом начале и конце функции; иногда фрейм используется, иногда нет; регистры процессора могут сохраняться в других регистрах и т.д.
Итак, отладчику нужно знать, как меняют свое значение регистры процессора и где они будут сохранены при входе в процедуру. Эта информация называется Call Frame Information — информация о формате фрейма. Для каждого адреса в программе (содержащего код) указывается адрес фрейма в памяти (Canonical Frame Address — CFA) и информация о регистрах процессора, к примеру можно указать, что:
- регистр не сохраняется в процедуре
- регистр не изменяет своего значения в процедуре
- регистр сохраняется на стеке по адресу CFA+n
- регистр сохраняется в другом регистре
- регистр сохраняется в памяти по некоторому адресу, который может вычисляться довольно неочевидным способом
- и т.д.
Поскольку информация должна указываться для каждого адреса в коде, она очень объемна и сохраняется в сжатом виде в секции .debug_frame. Так как от адреса к адресу она изменяется мало, то кодируются только ее изменения в виде инструкций DW_CFA_хххх. Каждая инструкция указывает на одно изменение, например:
- DW_CFA_set_loc — указывает на текущий адрес в программе
- DW_CFA_advance_loc — продвигает указатель на некоторое количество байт
- DW_CFA_def_cfa — указывает адрес стекового фрейма (числовая константа)
- DW_CFA_def_cfa_register — указывает адрес стекового фрейма (берется из регистра процессора)
- DW_CFA_def_cfa_expression — указывает, как нужно вычислить адрес стекового фрейма
- DW_CFA_same_value — указывает, что регистр не изменяется
- DW_CFA_register — указывате, что регистр сохраняется в другом регистре
- и т.д.
Элементы секции .debug_frame — это записи, которые могут быть двух типов: Common Information Entry (CIE) и Frame Description Entry (FDE). CIE содержит информацию, которая является общей для многих записей FDE, грубо говоря она описывает определенный тип процедур. FDE же описывают каждую конкретную процедуру. При входе в процедуру отладчик сначала выполняет инструкции из CIE, а затем из FDE.
Мой компилятор создает процедуры, в которых CFA находится в регистре sp (r13). Создадим CIE для всех процедур. Для этого есть функция dwarf_add_frame_cie:
Dwarf_Unsigned dwarf_add_frame_cie(
Dwarf_P_Debug dbg,
char *augmenter,
Dwarf_Small code_align,
Dwarf_Small data_align,
Dwarf_Small ret_addr_reg,
Dwarf_Ptr init_bytes,
Dwarf_Unsigned init_bytes_len,
Dwarf_Error *error);
- augmenter — строка в кодировке UTF-8, наличие которой показывает, что к CIE или FDE есть дополнительная платформозависимая информация. Ставим пустую строку
- code_align — выравнивание кода в байтах (у нас 2)
- data_align — выравнивание данных во фрейме (ставим -4, что значит все параметры занимают по 4 байта на стеке и он растет в памяти вниз)
- ret_addr_reg — регистр, содержащий адрес возврата из процедуры (у нас 14)
- init_bytes — массив, содержащий инструкции DW_CFA_хххх. К сожалению, нет удобного способа сгенерировать этот массив. Можно сформировать его вручную или подсмотреть его в elf-файле, который был сгенерирован компилятором С, что я и сделал. Для моего случая он содержит 3 байта: 0x0C, 0x0D, 0, что расшифровывается как DW_CFA_def_cfa: r13 ofs 0 (CFA находится в регистре r13, смещение равно 0)
- init_bytes_len — длина массива init_bytes
Функция возвращает DW_DLV_NOCOUNT при ошибке или дескриптор CIE, который должен быть использован при создании FDE для каждой процедуры, что мы рассмотрим далее в разделе «Создание FDE процедуры»
Создание типов данных
Перед тем, как создавать процедуры и переменные, нужно сначала создать узлы, соответствующие типам данных. Типов данных существует множество, но все они основываются на базовых типах (элементарные типы вроде int, double и т.д), остальные типы строятся из базовых.
Базовый тип — это узел с тэгом DW_TAG_base_type. У него должны быть атрибуты:
- «имя» (DW_AT_name)
- «кодировка» (DW_AT_encoding) — означает, какие именно данные описыват данный базовый тип (например, DW_ATE_boolean — логический, DW_ATE_float — с плавающей точкой, DW_ATE_signed — целый знаковый, DW_ATE_unsigned — целый беззнаковый и т.д)
- «размер» (DW_AT_byte_size — размер в байтах или DW_AT_bit_size — размер в битах)
Также узел может содержать другие необязательные атрибуты.
Например, чтобы создать 32-битный целый знаковый базовый тип «int», нам нужно будет создать узел с тэгом DW_TAG_base_type и установить ему атрибуты DW_AT_name — «int», DW_AT_encoding — DW_ATE_signed, DW_AT_byte_size — 4.
После создания базовых типов можно создавать производные от них. Такие узлы должны содержать атрибут DW_AT_type — ссылку на их базовый тип. Например указатель на int — узел с тэгом DW_TAG_pointer_type должен содержать в атрибуте DW_AT_type ссылку на ранее созданный тип «int».
Атрибут со ссылкой на другой узел создается функцией dwarf_add_AT_reference:
Dwarf_P_Attribute dwarf_add_AT_reference(
Dwarf_P_Debug dbg,
Dwarf_P_Die ownerdie,
Dwarf_Half attr,
Dwarf_P_Die otherdie,
Dwarf_Error *error)
- attr — атрибут, в данном случае DW_AT_type
- otherdie — дескриптор узла типа, на который ссылаемся
Создание процедур
Для создания процедур мне необходимо пояснить еще один тип отладочной информации — информация о номерах строк (Line Number Information). Она служит для сопоставления каждой машинной инструкции определенной строке исходного кода а также для возможности построковой отладки программы. Эта информация хранится в секции .debug_line. Если бы у нас было достаточно места, то она хранилась бы в виде матрицы, по одной строке для каждой инструкции с такими колонками:
- имя файла с исходным кодом
- номер строки в этом файле
- номер колонки в файле
- является ли инструкция началом оператора или блока операторов
- и т.д.
Такая матрица была бы очень большая, поэтому ее приходится сжимать. Во-первых, дублирующиеся строки удаляются, и во-вторых, сохраняются не сами строки, а только изменения в них. Эти изменения выглядят как команды для конечного автомата, а сама информация уже считается программой, которая будет «исполняться» этим автоматом. Команды этой программы выглядят, например так: DW_LNS_advance_pc — продвинуть счетчик команд в некоторый адрес, DW_LNS_set_file — установить файл, в котором определена процедура, DW_LNS_const_add_pc — продвинуть счетчик команд на несколько байт и т.д.
На таком низком уровне создавать эту информацию сложно, поэтому в библиотеке libdwarf предусмотрено несколько функций, облегчающие эту задачу.
Хранить имя файла для каждой инструкции накладно, поэтому вместо имени хранится его индекс в специальной таблице. Для создания индекса файла нужно использовать функцию dwarf_add_file_decl:
Dwarf_Unsigned dwarf_add_file_decl(
Dwarf_P_Debug dbg,
char *name,
Dwarf_Unsigned dir_idx,
Dwarf_Unsigned time_mod,
Dwarf_Unsigned length,
Dwarf_Error *error)
- name — имя файла
- dir_idx — индекс папки, в которой находится файл. Индекс можно получить при помощи функции dwarf_add_directory_decl. Если используются полные пути, можно ставить 0 в качестве индекса папки и не использовать dwarf_add_directory_decl совсем
- time_mod — время модификации файла, можно не указывать (0)
- length — размер файла, также не обязательно (0)
Функия вернет индекс файла или DW_DLV_NOCOUNT при ошибке.
Для создания информации о номерах строк есть три функции dwarf_add_line_entry_b, dwarf_lne_set_address, dwarf_lne_end_sequence, которые мы рассмотрим ниже.
Создание отладочной информации для процедуры проходит в несколько этапов:
- создание символа процедуры в секции .symtab
- создание узла процедуры с атрибутами
- создание FDE процедуры
- создание параметров процедуры
- создание информации о номерах строк
Создание символа процедуры
Символ процедуры создается как описано выше в разделе «Секция .symtab». В ней симолы процедур перемежаются с символами файлов в которых находится исходный код этих процедур. Сначала создаем символ файла, затем процедуры. При этом файл становится текущим, и если следующая процедура находится в текущем файле, символ файла опять создавать не нужно.
Создание узла процедуры с атрибутами
Сначала создаем узел с помощью функции dwarf_new_die (см. раздел «Создание Узлов»), указав в качестве тега DW_TAG_subprogram, а в качестве родителя — Compilation Unit (если это глобальная процедура) или соответствующий DIE (если локальная). Далее создаем атрибуты:
- имя процедуры (функция dwarf_add_AT_name, см. «Создание атрибутов узла»)
- номер строки в файле, где начинается код процедуры (атрибут DW_AT_decl_line), функция dwarf_add_AT_unsigned_const (см. «Создание атрибутов узла»)
- индекс имени файла (атрибут DW_AT_decl_file), функция dwarf_add_AT_unsigned_const (см. «Создание атрибутов узла»)
- начальный адрес процедуры (атрибут DW_AT_low_pc), функция dwarf_add_AT_targ_address, см. ниже
- конечный адрес процедуры (атрибут DW_AT_high_pc), функция dwarf_add_AT_targ_address, см. ниже
- тип возвращаемого процедурой результата (атрибут DW_AT_type — ссылка на ранее созданный тип, см. «Создание типов данных»). Если процедура ничего не возвращает — этот атрибут создавать не нужно
Атрибуты DW_AT_low_pc и DW_AT_high_pc нужно создавать специально предназначенной для этого функцией dwarf_add_AT_targ_address_b:
Dwarf_P_Attribute dwarf_add_AT_targ_address_b(
Dwarf_P_Debug dbg,
Dwarf_P_Die ownerdie,
Dwarf_Half attr,
Dwarf_Unsigned pc_value,
Dwarf_Unsigned sym_index,
Dwarf_Error *error)
- attr — атрибут (DW_AT_low_pc или DW_AT_high_pc)
- pc_value — значение адреса
- sym_index — индекс символа процедуры в таблице .symtab. Необязателен, можно передать 0
Функция вернет DW_DLV_BADADDR при ошибке.
Создание FDE процедуры
Как говорилось выше в разделе «Создание Common Information Entry», для каждой процедуры нужно создать описатель фрейма, что происходит в несколько этапов:
- создание нового FDE (см. Создание Common Information Entry)
- присоединение созданного FDE к общему списку
- добавление инструкций к созданному FDE
Cоздать новый FDE можно функцией dwarf_new_fde:
Dwarf_P_Fde dwarf_new_fde(
Dwarf_P_Debug dbg,
Dwarf_Error *error)
Функция вернет дескриптор нового FDE или DW_DLV_BADADDR при ошибке.
Присоединить новый FDE к списку можно при помощи dwarf_add_frame_fde:
Dwarf_Unsigned dwarf_add_frame_fde(
Dwarf_P_Debug dbg,
Dwarf_P_Fde fde,
Dwarf_P_Die die,
Dwarf_Unsigned cie,
Dwarf_Addr virt_addr,
Dwarf_Unsigned code_len,
Dwarf_Unsigned sym_idx,
Dwarf_Error* error)
- fde — только что полученный дескриптор
- die — DIE процедуры (см. Создание узла процедуры с атрибутами)
- cie — дескриптор CIE (см. Создание Common Information Entry)
- virt_addr — начальный адрес нашей процедуры
- code_len — длина процедуры в байтах
- sym_idx — индекс символа (не обязателен, можно указать 0)
Функция вернет DW_DLV_NOCOUNT при ошибке.
После всего этого можно добавлять инструкции DW_CFA_хххх к нашему FDE. Делается это функциями dwarf_add_fde_inst и dwarf_fde_cfa_offset. Первая добавляет к списку заданную инструкцию:
Dwarf_P_Fde dwarf_add_fde_inst(
Dwarf_P_Fde fde,
Dwarf_Small op,
Dwarf_Unsigned val1,
Dwarf_Unsigned val2,
Dwarf_Error *error)
- fde — дескриптор созданного FDE
- op — код инструкции (DW_CFA_хххх)
- val1, val2 — параметры инструкции (различные для каждой инструкции, см. Стандарт, раздел 6.4.2 Call Frame Instructions)
Функция dwarf_fde_cfa_offset добавляет инструкцию DW_CFA_offset:
Dwarf_P_Fde dwarf_fde_cfa_offset(
Dwarf_P_Fde fde,
Dwarf_Unsigned reg,
Dwarf_Signed offset,
Dwarf_Error *error)
- fde — дескриптор созданного FDE
- reg — регистр, который записывается в фрейм
- offset — его смещение в фрейме (не в байтах, а в элементах фрейма, см. Создание Common Information Entry, data_align)
Например, компилятор создает процедуру, в прологе которой в стековый фрейм сохраняется регистр lr (r14). Первым делом нужно добавить инструкцию DW_CFA_advance_loc с первым параметром, равным 1, что значит продвижение регистра pc на 2 байта (см. Создание Common Information Entry, code_align), затем добавить DW_CFA_def_cfa_offset с параметром 4 (задание смещения данных во фрейме на 4 байта) и вызвать функцию dwarf_fde_cfa_offset с параметром reg=14 offset=1, что означает запись регистра r14 в фрейм со смещением -4 байта от CFA.
Создание параметров процедуры
Создание параметров процедуры аналогично созданию обычных переменных, см. «Создание переменных и констант»
Cоздание информации о номерах строк
Создание этой информации происходит так:
- в начале процедуры начинаем блок инструкций функцией dwarf_lne_set_address
- для каждой строки кода (или машинной инструкции) создаем информацию об исходном коде (dwarf_add_line_entry)
- в конце процедуры завершаем блок инструкций функцией dwarf_lne_end_sequence
Функция dwarf_lne_set_address задает адрес, по которому начинается блок инструкций:
Dwarf_Unsigned dwarf_lne_set_address(
Dwarf_P_Debug dbg,
Dwarf_Addr offs,
Dwarf_Unsigned symidx,
Dwarf_Error *error)
- offs — адрес процедуры (адрес первой машинной инструкции)
- sym_idx — индекс символа (не обязателен, можно указать 0)
Возвращает 0 (успех) или DW_DLV_NOCOUNT (ошибка).
Функция dwarf_add_line_entry_b добавляет в секцию .debug_line информацию о строках исходного кода. Эту функцию я вызываю для каждой машинной инструкции:
Dwarf_Unsigned dwarf_add_line_entry_b(
Dwarf_P_Debug dbg,
Dwarf_Unsigned file_index,
Dwarf_Addr code_offset,
Dwarf_Unsigned lineno,
Dwarf_Signed column_number,
Dwarf_Bool is_source_stmt_begin,
Dwarf_Bool is_basic_block_begin,
Dwarf_Bool is_epilogue_begin,
Dwarf_Bool is_prologue_end,
Dwarf_Unsigned isa,
Dwarf_Unsigned discriminator,
Dwarf_Error *error)
- file_index — индекс файла исходного кода, полученный ранее функцией dwarf_add_file_decl (см. «Создание процедур»)
- code_offset — адрес текущей машинной инструкции
- lineno — номер строки в файле исходного кода
- column_number — номер колонки в файле исходного кода
- is_source_stmt_begin — 1 если текущая инструкция первая в коде в строке lineno (я всегда использую 1)
- is_basic_block_begin — 1 если текущая инструкция первая в блоке операторов (я всегда использую 0)
- is_epilogue_begin — 1 если текущая инструкция первая в эпилоге процедуры (не обязательно, у меня всегда 0)
- is_prologue_end — 1 если текущая инструкция последняя в прологе процедуры (обязательно!)
- isa — instruction set architecture (архитектура набора команд). Обязательно надо указать DW_ISA_ARM_thumb для ARM Cortex M3!
- discriminator. Одна позиция (файл, строка, колонка) исходного кода может отвечать разным машинным инструкциям. В таком случае для наборов таких инструкций нужно устанавливать разные дискриминаторы. Если таких случаев нет, должен быть 0
Функция возвращает 0 (успех) или DW_DLV_NOCOUNT (ошибка).
И наконец, функция dwarf_lne_end_sequence завершает процедуру:
Dwarf_Unsigned dwarf_lne_end_sequence(
Dwarf_P_Debug dbg,
Dwarf_Addr address;
Dwarf_Error *error)
- address — адрес текущей машинной инструкции
Возвращает 0 (успех) или DW_DLV_NOCOUNT (ошибка).
На этом завершаем создание процедуры.
Создание переменных и констант
В общем, переменные довольно просты. У них есть имя, участок памяти (или регистр процессора), где находится их данные а также тип этих данных. Если переменная глобальная — ее родителем должна быть Единица Компиляции, если локальная — соответсвующий узел (особенно это касается параметров процедур, у них родителем должна быть сама процедура). Также можно указать, в каком файле, строке и колонке находится объявление переменной.
В простейшем случае значение переменной находится по некоторому фиксированному адресу, но многие переменные динамически создаются при входе в процедуру на стеке или регистре, иногда вычисление адреса значения может быть весьма нетривиальным. В стандарте предусмотрен механизм описания того, где находится значение переменной — адресные выражения (location expressions). Адресное выражение — это набор инструкций (константы DW_OP_хххх) для форт-подобной стековой машины, фактически это отдельный язык с ветвлениями, процедурами и арифметическими операциями. Не будем обозревать полностью этот язык, нас фактически будут интересовать только несколько инструкций:
- DW_OP_addr — указывает адрес переменной
- DW_OP_fbreg — указывает смещение переменной от базового регистра (обычно указателя стека)
- DW_OP_reg0… DW_OP_reg31 — указывает на то, что переменная хранится в соответствующем регистре
Для того чтобы создать адресное выражение, нужно сначала создать пустое выражение (dwarf_new_expr), добавить в него инструкции (dwarf_add_expr_addr, dwarf_add_expr_gen и др.) и добавить его к узлу в качестве значения атрибута DW_AT_location (dwarf_add_AT_location_expression).
Функция создания пустого адресного выражения возвращает его дескриптор или 0 при ошибке:
Dwarf_Expr dwarf_new_expr(
Dwarf_P_Debug dbg,
Dwarf_Error *error)
Для добавления инструкций в выражение нужно использовать функцию dwarf_add_expr_gen:
Dwarf_Unsigned dwarf_add_expr_gen(
Dwarf_P_Expr expr,
Dwarf_Small opcode,
Dwarf_Unsigned val1,
Dwarf_Unsigned val2,
Dwarf_Error *error)
- expr — дескриптор адресного выражения, в которое добавляется инструкция
- opcode — код операции, константа DW_OP_хххх
- val1, val2 — параметры инструкции (см. Стандарт)
Функция возвращает DW_DLV_NOCOUNT при ошибке.
Для явного задания адреса переменной вместо предыдущей должна использоваться функция dwarf_add_expr_addr:
Dwarf_Unsigned dwarf_add_expr_addr(
Dwarf_P_Expr expr,
Dwarf_Unsigned address,
Dwarf_Signed sym_index,
Dwarf_Error *error)
- expr — дескриптор адресного выражения, в которое добавляется инструкция
- address — адрес переменной
- sym_index — индекс символа в таблице .symtab. Необязателен, можно передать 0
Функция также возвращает DW_DLV_NOCOUNT при ошибке.
И наконец, добавить созданное адресное выражение к узлу можно функцией dwarf_add_AT_location_expr:
Dwarf_P_Attribute dwarf_add_AT_location_expr(
Dwarf_P_Debug dbg,
Dwarf_P_Die ownerdie,
Dwarf_Half attr,
Dwarf_P_Expr loc_expr,
Dwarf_Error *error)
- ownerdie — узел, к которому добавляется выражение
- attr — атрибут (в нашем случае DW_AT_location)
- loc_expr — дескриптор ранее созданного адресного выражения
Функция возвращает дескриптор атрибута или DW_DLV_NOCOUNT при ошибке.
Переменные (а также параметры процедур) и константы — это обычные узлы с тэгом DW_TAG_variable, DW_TAG_formal_parameter и DW_TAG_const_type соответственно. Для них нужны такие атрибуты:
- имя переменной/константы (функция dwarf_add_AT_name, см. «Создание атрибутов узла»)
- номер строки в файле, где объявлена переменная (атрибут DW_AT_decl_line), функция dwarf_add_AT_unsigned_const (см. «Создание атрибутов узла»)
- индекс имени файла (атрибут DW_AT_decl_file), функция dwarf_add_AT_unsigned_const (см. «Создание атрибутов узла»)
- тип данных переменной/константы (атрибут DW_AT_type — ссылка на ранее созданный тип, см. «Создание типов данных»)
- адресное выражение (см. выше) — нужно для переменной или параметра процедуры
- или значение — для константы (атрибут DW_AT_const_value, см. «Создание атрибутов узла»)
Создание секций с отладочной информацией
После создания всех узлов дерева отладочной информации можно приступать к формированию elf-секций с ней. Это происходит в два этапа:
- сначала нужно вызвать функцию dwarf_transform_to_disk_form, которая будеть вызывать написанную нами функцию для создания нужных elf-секций один раз для каждой секции
- для каждой секции функция dwarf_get_section_bytes возвратит нам данные, которые нужно будет записать в соответствующую секцию
Функция
dwarf_transform_to_disk_form (
Dwarf_P_Debug dbg,
Dwarf_Error* error)
переводит созданную нами отладочную информацию в бинарный формат, но ничего не записывает на диск. Она возвратит нам количество созданных elf-секций или DW_DLV_NOCOUNT при ошибке. При этом для каждой секции будет вызвана callback-функция, которую мы передали при инициализации библиотеки в функцию dwarf_producer_init_c. Эту функцию должны написать мы сами. Ее спецификация такая:
typedef int (*Dwarf_Callback_Func_c)(
char* name,
int size,
Dwarf_Unsigned type,
Dwarf_Unsigned flags,
Dwarf_Unsigned link,
Dwarf_Unsigned info,
Dwarf_Unsigned* sect_name_index,
void * user_data,
int* error)
- name — имя elf-секции, которую нужно создать
- size — размер секции
- type — тип секции
- flags — флаги секции
- link — поле связи секции
- info — поле информации секции
- sect_name_index — нужно вернуть индекс секции с релокейшенами (не обязательно)
- user_data — передается нам таким же, каким мы его задали в функции инициализации библиотеки
- error — сюда можно передать код ошибки
В этой функции мы должны:
- создать новую секцию (функция elf_newscn, см. Создание секций)
- создать заголовок секции (функция elf32_getshdr, там же)
- правильно его заполнить (см. там же). Это просто, так как поля заголовка секции соответствуют параметрам нашей функции. Недостающие поля sh_addr, sh_offset, sh_entsize установим в 0, а sh_addralign в 1
- вернуть индекс созданной секции (функция elf_ndxscn, см. «Секция .symtab») или -1 при ошибке (установив в error код ошибки)
- также мы должны пропустить секцию ".rel" (в нашем случае), вернув 0 при возврате из функции
После завершения функция dwarf_transform_to_disk_form вернет нам количество созданных секций. Нам нужно будет пройтись в цикле от 0 по каждой секции, выполнив такие шаги:
- создать данные для записи в секцию функцией dwarf_get_section_bytes:
Dwarf_Ptr dwarf_get_section_bytes( Dwarf_P_Debug dbg, Dwarf_Signed dwarf_section, Dwarf_Signed *elf_section_index, Dwarf_Unsigned *length, Dwarf_Error* error)
- dwarf_section — номер секции. Должен быть в интервале 0..n, где n — число, возвращенное нам функцией dwarf_transform_to_disk_form
- elf_section_index — возвращает индекс секции, в которую нужно записывать данные
- length — длина этих данных
- error — не используется
Функция возвращает указатель на полученный данные или 0 (в том случае,
когда секций для создания больше не осталось) - создать дескриптор данных текущей секции (функция elf_newdata, см. Создание секций) и заполнить его (см. там же), установив:
- d_buf — указатель на данные, полученный нами из предыдущей функции
- d_size — размер этих данных (там же)
Окончание работы с библиотекой
После формирования секций можно завершать работу с libdwarf функцией dwarf_producer_finish:
Dwarf_Unsigned dwarf_producer_finish(
Dwarf_P_Debug dbg,
Dwarf_Error* error)
Функция возвращает DW_DLV_NOCOUNT при ошибке.
Замечу, что запись на диск на этом этапе не производится. Запись нужно делать посредством функций из раздела «Создание ELF — Запись файла».
Заключение
На этом все.
Повторюсь, создание отладочной информации — тема очень обширная, и многих тем я не коснулся, только приоткрыв завесу. Желающие же могут углубляться до бесконечности.
Если у вас будут вопросы — постараюсь на них ответить.
Ссылки
ELF
DWARF
Автор: oco