Что такое расширение файла ELF? Расширение файла ELF Elf файл

Версия этого ответа с хорошим TOC и большим количеством контента: http://www.cirosantilli.com/elf-hello-world (нажмите здесь ограничение 30k char)

Стандарты

ELF задается LSB:

  • core generic: http://refspecs.linuxfoundation.org/LSB_4.1.0/LSB-Core-generic/LSB-Core-generic/elf-generic.html
  • core AMD64: http://refspecs.linuxfoundation.org/LSB_4.1.0/LSB-Core-AMD64/LSB-Core-AMD64/book1.html

LSB в основном ссылается на другие стандарты с незначительными расширениями, в частности:

    generic (оба по SCO):

    • System V ABI 4.1 (1997) http://www.sco.com/developers/devspecs/gabi41.pdf , не 64 бит, хотя для него зарезервировано магическое число. То же самое для основных файлов.
    • System V ABI Update DRAFT 17 (2003) http://www.sco.com/developers/gabi/2003-12-17/contents.html добавляет 64 бит. Только обновляет главы 4 и 5 предыдущего документа: остальные остаются в силе и по-прежнему ссылаются.
  • специфическая архитектура:

    • IA-32: http://refspecs.linuxfoundation.org/LSB_4.1.0/LSB-Core-IA32/LSB-Core-IA32/elf-ia32.html указывает в основном на http://www.sco.com/developers/devspecs/abi386-4.pdf
    • AMD64: http://refspecs.linuxfoundation.org/LSB_4.1.0/LSB-Core-AMD64/LSB-Core-AMD64/elf-amd64.html , в основном указывает на http://www.x86-64.org/documentation/abi.pdf

Удобное резюме можно найти по адресу:

Его структура может быть рассмотрена с помощью удобных для пользователя способов, таких как readelf и objdump .

Создать пример

Позвольте сломать минимальный исполняемый пример Linux x86-64:

Section .data hello_world db "Hello world!", 10 hello_world_len equ $ - hello_world section .text global _start _start: mov rax, 1 mov rdi, 1 mov rsi, hello_world mov rdx, hello_world_len syscall mov rax, 60 mov rdi, 0 syscall

Скомпилировано с помощью

Nasm -w+all -f elf64 -o "hello_world.o" "hello_world.asm" ld -o "hello_world.out" "hello_world.o"

  • NASM 2.10.09
  • Binutils версия 2.24 (содержит ld)
  • Ubuntu 14.04

Мы не используем программу на C, так как это осложнит анализ, который будет уровнем 2: -)

шестнадцатеричных представлений бинарных

hd hello_world.o hd hello_world.out

Глобальная файловая структура

Файл ELF содержит следующие части:

  • Заголовок ELF. Указывает на позицию таблицы заголовка раздела и таблицы заголовков программ.

    Таблица заголовков разделов (необязательно в исполняемом файле). Каждый из них имеет заголовки секций e_shnum , каждый из которых указывает на положение раздела.

    N разделов с N <= e_shnum (необязательно в исполняемом файле)

    Таблица заголовков программ (только для исполняемых файлов). Каждый из них имеет e_phnum заголовки программ, каждый из которых указывает на положение сегмента.

    N сегментов, с N <= e_phnum (необязательно в исполняемом файле)

Порядок этих частей не фиксирован: единственная фиксированная вещь - это заголовок ELF, который должен быть первым в файле: Общие документы говорят:

Заголовок ELF

Самый простой способ наблюдать за заголовком:

Readelf -h hello_world.o readelf -h hello_world.out

Байт в объектном файле:

00000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 |.ELF............| 00000010 01 00 3e 00 01 00 00 00 00 00 00 00 00 00 00 00 |..>.............| 00000020 00 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00 |........@.......| 00000030 00 00 00 00 40 00 00 00 00 00 40 00 07 00 03 00 |....@.....@.....|

00000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 |.ELF............| 00000010 02 00 3e 00 01 00 00 00 b0 00 40 00 00 00 00 00 |..>.......@.....| 00000020 40 00 00 00 00 00 00 00 10 01 00 00 00 00 00 00 |@...............| 00000030 00 00 00 00 40 00 38 00 02 00 40 00 06 00 03 00 |[email protected]...@.....|

Представленная структура:

Typedef struct { unsigned char e_ident; Elf64_Half e_type; Elf64_Half e_machine; Elf64_Word e_version; Elf64_Addr e_entry; Elf64_Off e_phoff; Elf64_Off e_shoff; Elf64_Word e_flags; Elf64_Half e_ehsize; Elf64_Half e_phentsize; Elf64_Half e_phnum; Elf64_Half e_shentsize; Elf64_Half e_shnum; Elf64_Half e_shstrndx; } Elf64_Ehdr;

Распад вручную:

    0 0: EI_MAG = 7f 45 4c 46 = 0x7f "E", "L", "F" : магическое число ELF

    0 4: EI_CLASS = 02 = ELFCLASS64: 64-разрядный эльф

    0 5: EI_DATA = 01 = ELFDATA2LSB: данные большого конца

    0 6: EI_VERSION = 01: версия формата

    0 7: EI_OSABI (только в 2003 году) = 00 = ELFOSABI_NONE: нет расширений.

    0 8: EI_PAD = 8x 00: зарезервированные байты. Должно быть установлено в 0.

    1 0: e_type = 01 00 = 1 (big endian) = ET_REl: перемещаемый формат

    В исполняемом файле 02 00 для ET_EXEC .

    1 2: e_machine = 3e 00 = 62 = EM_X86_64: архитектура AMD64

    1 4: e_version = 01 00 00 00: должно быть 1

    1 8: e_entry = 8x 00: точка ввода адреса выполнения, или 0, если не применимо, как для объектного файла, так как нет точки входа.

    В исполняемом файле это b0 00 40 00 00 00 00 00 . TODO: что еще мы можем установить? Ядро, кажется, помещает IP непосредственно в это значение, оно не является жестко запрограммированным.

    2 0: e_phoff = 8x 00: смещение таблицы заголовка программы, 0, если нет.

    40 00 00 00 в исполняемом файле, то есть он начинается сразу после заголовка ELF.

    2 8: e_shoff = 40 7x 00 = 0x40: смещение файла таблицы заголовка раздела, 0, если нет.

    3 0: e_flags = 00 00 00 00 TODO. Специально для Arch.

    3 4: e_ehsize = 40 00: размер этого заголовка эльфа. Почему это поле? Как это может измениться?

    3 6: e_phentsize = 00 00: размер каждого заголовка программы, 0, если нет.

    38 00 в исполняемом файле: длина файла составляет 56 байтов

    3 8: e_phnum = 00 00: количество записей заголовка программы, 0, если нет.

    02 00 в исполняемом файле: есть 2 записи.

    3 A: e_shentsize и e_shnum = 40 00 07 00: размер заголовка раздела и количество записей

Таблица заголовков разделов

Массив структур Elf64_Shdr .

Каждая запись содержит метаданные о данном разделе.

e_shoff заголовка ELF дает здесь начальную позицию, 0x40.

e_shentsize и e_shnum из заголовка ELF говорят, что у нас есть 7 записей, каждый длиной 0x40 .

Таким образом, таблица берет байты от 0x40 до 0x40 + 7 + 0x40 - 1 = 0x1FF.

Названия некоторых разделов зарезервированы для определенных типов разделов: http://www.sco.com/developers/gabi/2003-12-17/ch4.sheader.html#special_sections , например. .text требуется тип SHT_PROGBITS и SHF_ALLOC + SHF_EXECINSTR

readelf -S hello_world.o:

There are 7 section headers, starting at offset 0x40: Section Headers: Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .data PROGBITS 0000000000000000 00000200 000000000000000d 0000000000000000 WA 0 0 4 [ 2] .text PROGBITS 0000000000000000 00000210 0000000000000027 0000000000000000 AX 0 0 16 [ 3] .shstrtab STRTAB 0000000000000000 00000240 0000000000000032 0000000000000000 0 0 1 [ 4] .symtab SYMTAB 0000000000000000 00000280 00000000000000a8 0000000000000018 5 6 4 [ 5] .strtab STRTAB 0000000000000000 00000330 0000000000000034 0000000000000000 0 0 1 [ 6] .rela.text RELA 0000000000000000 00000370 0000000000000018 0000000000000018 4 2 4 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings), l (large) I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific)

struct , представленный каждой записью:

Typedef struct { Elf64_Word sh_name; Elf64_Word sh_type; Elf64_Xword sh_flags; Elf64_Addr sh_addr; Elf64_Off sh_offset; Elf64_Xword sh_size; Elf64_Word sh_link; Elf64_Word sh_info; Elf64_Xword sh_addralign; Elf64_Xword sh_entsize; } Elf64_Shdr;

Разделы

Раздел индекса 0

Содержится в байтах от 0x40 до 0x7F.

Первый раздел всегда волшебный: http://www.sco.com/developers/gabi/2003-12-17/ch4.sheader.html говорит:

Если количество секций больше или равно SHN_LORESERVE (0xff00), e_shnum имеет значение SHN_UNDEF (0), а фактическое количество записей таблицы заголовков разделов содержится в поле sh_size заголовка раздела с индексом 0 (в противном случае член sh_size начальной записи содержит 0).

В разделе Figure 4-7: Special Section Indexes есть другие магические разделы.

В индексе 0, SHT_NULL является обязательным. Существуют ли для этого другие виды использования: Какая польза от раздела SHT_NULL в ELF? ?

.data раздел

Data - это раздел 1:

00000080 01 00 00 00 01 00 00 00 03 00 00 00 00 00 00 00 |................| 00000090 00 00 00 00 00 00 00 00 00 02 00 00 00 00 00 00 |................| 000000a0 0d 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000000b0 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|

    Здесь 1 говорит, что имя этого раздела начинается с первого символа этого раздела и заканчивается на первом символе NUL, составляя строку.data .

    Data - одно из имен разделов, которое имеет предопределенное значение http://www.sco.com/developers/gabi/2003-12-17/ch4.strtab.html

    В этих разделах хранятся инициализированные данные, которые вносят вклад в образ памяти программы.

  • 80 4: sh_type = 01 00 00 00: SHT_PROGBITS: содержимое раздела не задано ELF, только тем, как программа интерпретирует его. Нормально, так как a .data .

    80 8: sh_flags = 03 7x 00: SHF_ALLOC и SHF_EXECINSTR: http://www.sco.com/developers/gabi/2003-12-17/ch4.sheader.html#sh_flags , как требуется из раздела.data

    90 0: sh_addr = 8x 00: в каком виртуальном адресе раздел будет помещен во время выполнения, 0 если не помещен

    90 8: sh_offset = 00 02 00 00 00 00 00 00 = 0x200: количество байт от начала программы до первого байта в этом разделе

    a0 0: sh_size = 0d 00 00 00 00 00 00 00

    Если взять 0xD байт, начиная с sh_offset 200, мы видим:

    00000200 48 65 6c 6c 6f 20 77 6f 72 6c 64 21 0a 00 |Hello world!.. |

    AHA! Итак, наша строка "Hello world!" находится в разделе данных, как мы сказали, это на NASM.

    Как только мы закончим hd , мы рассмотрим это как:

    Readelf -x .data hello_world.o

    который выводит:

    Hex dump of section ".data": 0x00000000 48656c6c 6f20776f 726c6421 0a Hello world!.

    NASM устанавливает достойные свойства для этого раздела, потому что он магически относится к.data: http://www.nasm.us/doc/nasmdoc7.html#section-7.9.2

    Также обратите внимание, что это был неправильный выбор раздела: хороший компилятор C поместил бы строку в.rodata вместо этого, потому что он доступен только для чтения, и это позволит продолжить оптимизацию ОС.

    a0 8: sh_link и sh_info = 8x 0: не применяются к типу этого раздела. http://www.sco.com/developers/gabi/2003-12-17/ch4.sheader.html#special_sections

    b0 0: sh_addralign = 04 = TODO: зачем это выравнивание необходимо? Это только для sh_addr , а также для символов внутри sh_addr ?

    b0 8: sh_entsize = 00 = раздел не содержит таблицы. Если!= 0, это означает, что раздел содержит таблицу записей фиксированного размера. В этом файле мы видим из вывода readelf , что это имеет место для разделов.symtab и.rela.text .

.text раздел

Теперь, когда мы сделали один раздел вручную, дайте выпускнику и используйте readelf -S других разделов.

Name Type Address Offset Size EntSize Flags Link Info Align [ 2] .text PROGBITS 0000000000000000 00000210 0000000000000027 0000000000000000 AX 0 0 16

Text является исполняемым, но не доступен для записи: если мы попытаемся написать ему Linux segfaults. Посмотрим, действительно ли у нас есть код:

Objdump -d hello_world.o

Hello_world.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000 <_start>: 0: b8 01 00 00 00 mov $0x1,%eax 5: bf 01 00 00 00 mov $0x1,%edi a: 48 be 00 00 00 00 00 movabs $0x0,%rsi 11: 00 00 00 14: ba 0d 00 00 00 mov $0xd,%edx 19: 0f 05 syscall 1b: b8 3c 00 00 00 mov $0x3c,%eax 20: bf 00 00 00 00 mov $0x0,%edi 25: 0f 05 syscall

Если мы имеем grep b8 01 00 00 на hd , мы видим, что это происходит только в 00000210 , что и говорится в этом разделе. И размер равен 27, что также соответствует. Поэтому мы должны говорить о правильном разделе.

Это выглядит как правильный код: a write , за которым следует exit .

Самая интересная часть - это строка a , которая делает:

Movabs $0x0,%rsi

передать адрес строки в системный вызов. В настоящее время 0x0 является просто заполнителем. После связывания произойдет его изменение:

4000ba: 48 be d8 00 60 00 00 movabs $0x6000d8,%rsi

Эта модификация возможна из-за данных раздела.rela.text .

SHT_STRTAB

Разделы с sh_type == SHT_STRTAB называются строковыми таблицами.

Такие разделы используются другими разделами, когда имена строк должны использоваться. В разделе "Использование" говорится:

  • какую строку они используют
  • что такое индекс в таблице целевых строк, где начинается строка

Так, например, мы могли бы иметь строковую таблицу, содержащую: TODO: нужно ли начинать с \0 ?

Data: \0 a b c \0 d e f \0 Index: 0 1 2 3 4 5 6 7 8

И если другой раздел хочет использовать строку d e f , они должны указывать на индекс 5 этого раздела (буква d).

Известные строковые таблицы:

  • .shstrtab
  • .strtab

.shstrtab

Тип раздела: sh_type == SHT_STRTAB .

Общее имя: строка заголовка заголовка раздела.

Имя раздела.shstrtab зарезервировано. В стандарте говорится:

В этом разделе содержатся имена разделов.

В этом разделе указывается поле e_shstrnd самого заголовка ELF.

Индексы строк этого раздела указываются полем sh_name заголовков разделов, которые обозначают строки.

В этом разделе не указано SHF_ALLOC , поэтому оно не будет отображаться в исполняемой программе.

Readelf -x .shstrtab hello_world.o

Hex dump of section ".shstrtab": 0x00000000 002e6461 7461002e 74657874 002e7368 ..data..text..sh 0x00000010 73747274 6162002e 73796d74 6162002e strtab..symtab.. 0x00000020 73747274 6162002e 72656c61 2e746578 strtab..rela.tex 0x00000030 7400 t.

Данные в этом разделе имеют фиксированный формат: http://www.sco.com/developers/gabi/2003-12-17/ch4.strtab.html

Если мы посмотрим на имена других разделов, мы увидим, что все они содержат числа, например. секция.text имеет номер 7 .

Затем каждая строка заканчивается, когда найден первый символ NUL, например. символ 12 \0 сразу после.text\0 .

.symtab

Тип раздела: sh_type == SHT_SYMTAB .

Общее имя: таблица символов.

Сначала отметим, что:

  • sh_link = 5
  • sh_info = 6

В разделе SHT_SYMTAB эти числа означают, что:

  • Строки
  • которые дают имена символов, находятся в разделе 5, .strtab
  • данные перемещения находятся в разделе 6, .rela.text

Хороший инструмент высокого уровня для разборки этого раздела:

Nm hello_world.o

который дает:

0000000000000000 T _start 0000000000000000 d hello_world 000000000000000d a hello_world_len

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

Readelf -s hello_world.o

который дает:

Symbol table ".symtab" contains 7 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS hello_world.asm 2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 3: 0000000000000000 0 SECTION LOCAL DEFAULT 2 4: 0000000000000000 0 NOTYPE LOCAL DEFAULT 1 hello_world 5: 000000000000000d 0 NOTYPE LOCAL DEFAULT ABS hello_world_len 6: 0000000000000000 0 NOTYPE GLOBAL DEFAULT 2 _start

Бинарный формат таблицы документируется на http://www.sco.com/developers/gabi/2003-12-17/ch4.symtab.html

Readelf -x .symtab hello_world.o

Что дает:

Hex dump of section ".symtab": 0x00000000 00000000 00000000 00000000 00000000 ................ 0x00000010 00000000 00000000 01000000 0400f1ff ................ 0x00000020 00000000 00000000 00000000 00000000 ................ 0x00000030 00000000 03000100 00000000 00000000 ................ 0x00000040 00000000 00000000 00000000 03000200 ................ 0x00000050 00000000 00000000 00000000 00000000 ................ 0x00000060 11000000 00000100 00000000 00000000 ................ 0x00000070 00000000 00000000 1d000000 0000f1ff ................ 0x00000080 0d000000 00000000 00000000 00000000 ................ 0x00000090 2d000000 10000200 00000000 00000000 -............... 0x000000a0 00000000 00000000 ........

Записи имеют тип:

Typedef struct { Elf64_Word st_name; unsigned char st_info; unsigned char st_other; Elf64_Half st_shndx; Elf64_Addr st_value; Elf64_Xword st_size; } Elf64_Sym;

Как и в таблице разделов, первая запись волшебна и задана фиксированными бессмысленными значениями.

Запись 1 имеет ELF64_R_TYPE == STT_FILE . ELF64_R_TYPE продолжается внутри st_info .

Байт-анализ:

    10 8: st_name = 01000000 = символ 1 в.strtab , который до следующего \0 делает hello_world.asm

    Этот фрагмент информационного файла может использоваться компоновщиком для определения того, какие сегменты сегмента идут.

    10 12: st_info = 04

    Bits 0-3 = ELF64_R_TYPE = Type = 4 = STT_FILE: основная цель этой записи - использовать st_name для указания имени файла, сгенерированного этим объектным файлом.

    Биты 4-7 = ELF64_ST_BIND = Binding = 0 = STB_LOCAL . Требуемое значение для STT_FILE .

    10 13: st_shndx = Таблица символов Таблица заголовков Индекс = f1ff = SHN_ABS . Требуется для STT_FILE .

    20 0: st_value = 8x 00: требуется для значения для STT_FILE

    20 8: st_size = 8x 00: нет выделенного размера

Теперь из readelf мы быстро интерпретируем остальные.

STT_SECTION

Есть два таких элемента, один указывает на.data , а другой на.text (индексы раздела 1 и 2).

Num: Value Size Type Bind Vis Ndx Name 2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 3: 0000000000000000 0 SECTION LOCAL DEFAULT 2

TODO, какова их цель?

STT_NOTYPE

Затем введите наиболее важные символы:

Num: Value Size Type Bind Vis Ndx Name 4: 0000000000000000 0 NOTYPE LOCAL DEFAULT 1 hello_world 5: 000000000000000d 0 NOTYPE LOCAL DEFAULT ABS hello_world_len 6: 0000000000000000 0 NOTYPE GLOBAL DEFAULT 2 _start Строка

hello_world находится в разделе.data (индекс 1). Это значение равно 0: он указывает на первый байт этого раздела.

Start отмечен видимостью GLOBAL , так как мы написали:

Global _start

в NASM. Это необходимо, так как оно должно рассматриваться как точка входа. В отличие от C, по умолчанию метки NASM являются локальными.

hello_world_len указывает на специальный st_shndx == SHN_ABS == 0xF1FF .

0xF1FF выбирается так, чтобы не противоречить другим разделам.

st_value == 0xD == 13 , который является значением, которое мы сохранили там на сборке: длина строки Hello World! .

Это означает, что перемещение не повлияет на это значение: оно является константой.

Это небольшая оптимизация, которую делает наш ассемблер для нас и имеет поддержку ELF.

Если бы мы использовали адрес hello_world_len в любом месте, ассемблер не смог бы пометить его как SHN_ABS , и позже у компоновщика будет дополнительное перемещение.

SHT_SYMTAB в исполняемом файле

По умолчанию NASM размещает.symtab в исполняемом файле.

Это используется только для отладки. Без символов мы полностью слепы и должны все перепроектировать.

Вы можете удалить его с помощью objcopy , и исполняемый файл все равно будет работать. Такие исполняемые файлы называются разделенными исполняемыми файлами.

.strtab

Удерживает строки для таблицы символов.

В этом разделе sh_type == SHT_STRTAB .

Указывается на sh_link == 5 раздела.symtab .

Readelf -x .strtab hello_world.o

Hex dump of section ".strtab": 0x00000000 0068656c 6c6f5f77 6f726c64 2e61736d .hello_world.asm 0x00000010 0068656c 6c6f5f77 6f726c64 0068656c .hello_world.hel 0x00000020 6c6f5f77 6f726c64 5f6c656e 005f7374 lo_world_len._st 0x00000030 61727400 art.

Это означает, что это ограничение уровня ELF, что глобальные переменные не могут содержать символы NUL.

.rela.text

Тип раздела: sh_type == SHT_RELA .

Общее имя: раздел перемещения.

Rela.text содержит данные перемещения, в которых указано, как адрес должен быть изменен, когда последний исполняемый файл связан. Это указывает на байты текстовой области, которые должны быть изменены, когда связывание происходит с указанием правильных мест памяти.

В основном, он преобразует текст объекта, содержащий адрес заполнителя 0x0:

A: 48 be 00 00 00 00 00 movabs $0x0,%rsi 11: 00 00 00

к фактическому исполняемому коду, содержащему окончательный 0x6000d8:

4000ba: 48 be d8 00 60 00 00 movabs $0x6000d8,%rsi 4000c1: 00 00 00

Указывалось sh_info = 6 раздела.symtab .

readelf -r hello_world.o дает:

Relocation section ".rela.text" at offset 0x3b0 contains 1 entries: Offset Info Type Sym. Value Sym. Name + Addend 00000000000c 000200000001 R_X86_64_64 0000000000000000 .data + 0

Раздел не существует в исполняемом файле.

Фактические байты:

00000370 0c 00 00 00 00 00 00 00 01 00 00 00 02 00 00 00 |................| 00000380 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|

Представленный struct:

Typedef struct { Elf64_Addr r_offset; Elf64_Xword r_info; Elf64_Sxword r_addend; } Elf64_Rela;

    370 0: r_offset = 0xC: адрес в адрес.text , адрес которого будет изменен

    370 8: r_info = 0x200000001. Содержит 2 поля:

    • ELF64_R_TYPE = 0x1: значение зависит от точной архитектуры.
    • ELF64_R_SYM = 0x2: индекс раздела, на который указывает адрес, поэтому.data , который находится в индексе 2.

    AMD64 ABI говорит, что тип 1 называется R_X86_64_64 и что он представляет операцию S + A где:

    • S: значение символа в объектном файле, здесь 0 , потому что мы указываем на 00 00 00 00 00 00 00 00 из movabs $0x0,%rsi
    • a: добавление, присутствующее в поле r_added

    Этот адрес добавляется в раздел, в котором работает перемещение.

    Эта операция перемещения действует на 8 байтов.

    380 0: r_addend = 0

Таким образом, в нашем примере мы заключаем, что новый адрес будет: S + A = .data + 0 , и, таким образом, первое в разделе данных.

Таблица заголовков программ

Отображается только в исполняемом файле.

Содержит информацию о том, как исполняемый файл должен быть помещен в виртуальную память процесса.

Исполняемый файл создается объектным файлом компоновщиком. Основные задания, которые выполняет компоновщик:

    определите, какие разделы объектных файлов войдут в какие сегменты исполняемого файла.

    В Binutils это сводится к анализу компоновщика script и работе с множеством значений по умолчанию.

    Вы можете получить компоновщик script, используемый с ld --verbose , и установить пользовательский с ld -T .

    выполнять перемещение по текстовым разделам. Это зависит от того, как несколько разделов помещаются в память.

readelf -l hello_world.out дает:

Elf file type is EXEC (Executable file) Entry point 0x4000b0 There are 2 program headers, starting at offset 64 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 0x00000000000000d7 0x00000000000000d7 R E 200000 LOAD 0x00000000000000d8 0x00000000006000d8 0x00000000006000d8 0x000000000000000d 0x000000000000000d RW 200000 Section to Segment mapping: Segment Sections... 00 .text 01 .data

В заголовке ELF e_phoff , e_phnum и e_phentsize сказали нам, что есть 2 заголовка программы, которые начинаются с 0x40 и длиной 0x38 байтов каждый, поэтому они:

00000040 01 00 00 00 05 00 00 00 00 00 00 00 00 00 00 00 |................| 00000050 00 00 40 00 00 00 00 00 00 00 40 00 00 00 00 00 |..@.......@.....| 00000060 d7 00 00 00 00 00 00 00 d7 00 00 00 00 00 00 00 |................| 00000070 00 00 20 00 00 00 00 00 |.. ..... |

00000070 01 00 00 00 06 00 00 00 | ........| 00000080 d8 00 00 00 00 00 00 00 d8 00 60 00 00 00 00 00 |..........`.....| 00000090 d8 00 60 00 00 00 00 00 0d 00 00 00 00 00 00 00 |..`.............| 000000a0 0d 00 00 00 00 00 00 00 00 00 20 00 00 00 00 00 |.......... .....| typedef struct { Elf64_Word p_type; Elf64_Word p_flags; Elf64_Off p_offset; Elf64_Addr p_vaddr; Elf64_Addr p_paddr; Elf64_Xword p_filesz; Elf64_Xword p_memsz; Elf64_Xword p_align; } Elf64_Phdr;

Пробой первого:

  • 40 0: p_type = 01 00 00 00 = PT_LOAD: TODO. Я думаю, это означает, что он будет загружен в память. Другие типы могут не обязательно быть.
  • 40 4: p_flags = 05 00 00 00 = выполнять и читать разрешения, не писать TODO
  • 40 8: p_offset = 8x 00 TODO: что это? Похоже на смещения от начала сегментов. Но это будет означать, что некоторые сегменты переплетаются? С ним можно немного поиграть: gcc -Wl,-Ttext-segment=0x400030 hello_world.c
  • 50 0: p_vaddr = 00 00 40 00 00 00 00 00: начальный адрес виртуальной памяти для загрузки этого сегмента в
  • 50 8: p_paddr = 00 00 40 00 00 00 00 00: начальный физический адрес для загрузки в память. Только вопросы для систем, в которых программа может установить физический адрес. В противном случае, как и в системах System V, может быть что угодно. Кажется, что NASM просто скопирует p_vaddrr
  • 60 0: p_filesz = d7 00 00 00 00 00 00 00: TODO vs p_memsz
  • 60 8: p_memsz = d7 00 00 00 00 00 00 00: TODO
  • 70 0: p_align = 00 00 20 00 00 00 00 00: 0 или 1 означает, что никакого выравнивания не требуется TODO, что это значит? в противном случае избыточно с другими полями

Вторая аналогична.

Section to Segment mapping:

раздела readelf говорит нам, что:

  • 0 - сегмент.text . Ага, поэтому он является исполняемым и не доступен для записи.
  • 1 - сегмент.data .

В данном обзоре мы будем говорить только о 32-х битной версии этого формата, ибо 64-х битная нам пока ни к чему.

Любой файл формата ELF (в том числе и объектные модули этого формата) состоит из следующих частей:

  • Заголовок ELF файла;
  • Таблица программных секций (в объектных модулях может отсутствовать);
  • Секции ELF файла;
  • Таблица секций (в выполняемом модуле может отсутствовать);
  • Ради производительности в формате ELF не используются битовые поля. И все структуры обычно выравниваются на 4 байта.

Теперь рассмотрим типы, используемые в заголовках ELF файлов:

Теперь рассмотрим заголовок файла:

#define EI_NIDENT 16 struct elf32_hdr { unsigned char e_ident; Elf32_Half e_type; Elf32_Half e_machine; Elf32_Word e_version; Elf32_Addr e_entry; /* Entry point */ 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; };

Массив e_ident содержит в себе информацию о системе и состоит из нескольких подполей.

Struct { unsigned char ei_magic; unsigned char ei_class; unsigned char ei_data; unsigned char ei_version; unsigned char ei_pad; }

  • ei_magic - постоянное значение для всех ELF файлов, равное { 0x7f, "E", "L", "F"}
  • ei_class - класс ELF файла (1 - 32 бита, 2 - 64 бита который мы не рассматриваем)
  • ei_data - определяет порядок следования байт для данного файла (этот порядок зависит от платформы и может быть прямым (LSB или 1) или обратным (MSB или 2)) Для процессоров Intel допустимо только значение 1.
  • ei_version - достаточно бесполезное поле, и если не равно 1 (EV_CURRENT) то файл считается некорректным.

В поле ei_pad операционные системы хранят свою идентификационную информацию. Это поле может быть пустым. Для нас оно тоже не важно.

Поле заголовка e_type может содержать несколько значений, для выполняемых файлов оно должно быть ET_EXEC равное 2

e_machine - определяет процессор на котором может работать данный выполняемый файл (Для нас допустимо значение EM_386 равное 3)

Поле e_version соответствует полю ei_version из заголовка.

Поле e_entry определяет стартовый адрес программы, который перед стартом программы размещается в eip.

Поле e_phoff определяет смещение от начала файла, по которому располагается таблица программных секций, используемая для загрузки программ в память.

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

Поле e_phentsize определяет размер записи в таблице программных секций.

И поле e_phnum определяет количество записей в таблице программных секций.

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

Теперь про программные секции. Формат записи таблицы программных секций таков:

Struct elf32_phdr { 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; };

Подробнее о полях.

  • p_type - определяет тип программной секции. Может принимать несколько значений, но нас интересует только одно. PT_LOAD (1). Если секция именно этого типа, то она предназначена для загрузки в память.
  • p_offset - определяет смещение в файле, с которого начинается данная секция.
  • p_vaddr - определяет виртуальный адрес, по которому эта секция должна быть загружена в память.
  • p_paddr - определяет физический адрес, по которому необходимо загружать данную секцию. Это поле не обязательно должно использоваться и имеет смысл лишь для некоторых платформ.
  • p_filesz - определяет размер секции в файле.
  • p_memsz - определяет размер секции в памяти. Это значение может быть больше предыдущего. Поле p_flag определяет тип доступа к секциям в памяти. Некоторые секции допускается выполнять, некоторые записывать. Для чтения в существующих системах доступны все.

Загрузка формата ELF.

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

Int LoadELF (unsigned char *bin) { struct elf32_hdr *EH = (struct elf32_hdr *)bin; struct elf32_phdr *EPH; if (EH->e_ident != 0x7f || // Контролируем MAGIC EH->e_ident != "E" || EH->e_ident != "L" || EH->e_ident != "F" || EH->e_ident != ELFCLASS32 || // Контролируем класс EH->e_ident != ELFDATA2LSB || // порядок байт EH->e_ident != EV_CURRENT || // версию EH->e_type != ET_EXEC || // тип EH->e_machine != EM_386 || // платформу EH->e_version != EV_CURRENT) // и снова версию, на всякий случай return ELF_WRONG; EPH = (struct elf32_phdr *)(bin + EH->e_phoff); while (EH->e_phnum--) { if (EPH->p_type == PT_LOAD) memcpy (EPH->p_vaddr, bin + EPH->p_offset, EPH->p_filesz); EPH = (struct elf32_phdr *)((unsigned char *)EPH + EH->e_phentsize)); } return ELF_OK; }

По серьезному стоит еще проанализировать поля EPH->p_flags, и расставить на соответствующие страницы права доступа, да и просто копирование здесь не подойдет, но это уже не относится к формату, а к распределению памяти. Поэтому сейчас об этом не будем говорить.

Формат PE.

Во многом он аналогичен формату ELF, ну и не удивительно, там так же должны быть секции, доступные для загрузки.

Как и все в Microsoft:) формат PE базируется на формате EXE. Структура файла такова:

  • 00h - EXE заголовок (не буду его рассматривать, он стар как Дос. :)
  • 20h - OEM заголовок (ничего существенного в нем нет);
  • 3сh - смещение реального PE заголовка в файле (dword).
  • таблица перемещения stub;
  • stub;
  • PE заголовок;
  • таблица объектов;
  • объекты файла;

stub - это программа, выполняющаяся в реальном режиме и производящая какие-либо предварительные действия. Может и отсутствовать, но иногда может быть нужна.

Нас интересует немного другое, заголовок PE.

Структура его такая:

Struct pe_hdr { unsigned long pe_sign; unsigned short pe_cputype; unsigned short pe_objnum; unsigned long pe_time; unsigned long pe_cofftbl_off; unsigned long pe_cofftbl_size; unsigned short pe_nthdr_size; unsigned short pe_flags; unsigned short pe_magic; unsigned short pe_link_ver; unsigned long pe_code_size; unsigned long pe_idata_size; unsigned long pe_udata_size; unsigned long pe_entry; unsigned long pe_code_base; unsigned long pe_data_base; unsigned long pe_image_base; unsigned long pe_obj_align; unsigned long pe_file_align; // ... ну и еще много всякого, неважного. };

Много всякого там находится. Достаточно сказать, что размер этого заголовка - 248 байт.

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

Таблица объектов следует непосредственно после PE заголовка. Записи в этой таблице имеют следующий формат:

Struct pe_ohdr { unsigned char o_name; unsigned long o_vsize; unsigned long o_vaddr; unsigned long o_psize; unsigned long o_poff; unsigned char o_reserved; unsigned long o_flags; };

  • o_name - имя секции, для загрузки абсолютно безразлично;
  • o_vsize - размер секции в памяти;
  • o_vaddr - адрес в памяти относительно ImageBase;
  • o_psize - размер секции в файле;
  • o_poff - смещение секции в файле;
  • o_flags - флаги секции;

Вот на флагах стоит остановиться поподробнее.

  • 00000004h - используется для кода с 16 битными смещениями
  • 00000020h - секция кода
  • 00000040h - секция инициализированных данных
  • 00000080h - секция неинициализированных данных
  • 00000200h - комментарии или любой другой тип информации
  • 00000400h - оверлейная секция
  • 00000800h - не будет являться частью образа программы
  • 00001000h - общие данные
  • 00500000h - выравнивание по умолчанию, если не указано иное
  • 02000000h - может быть выгружен из памяти
  • 04000000h - не кэшируется
  • 08000000h - не подвергается страничному преобразованию
  • 10000000h - разделяемый
  • 20000000h - выполнимый
  • 40000000h - можно читать
  • 80000000h - можно писать

Опять таки не буду с разделяемыми и оверлейными секциями, нас интересуют код, данные и права доступа.

В общем, этой информации уже достаточно для загрузки бинарного файла.

Загрузка формата PE.

int LoadPE (unsigned char *bin) { struct elf32_hdr *PH = (struct pe_hdr *) (bin + *((unsigned long *)&bin)); // Конечно комбинация не из понятных... просто берем dword по смещению 0x3c // И вычисляем адрес PE заголовка в образе файла struct elf32_phdr *POH; if (PH == NULL || // Контролируем указатель PH->pe_sign != 0x4550 || // сигнатура PE {"P", "E", 0, 0} PH->pe_cputype != 0x14c || // i386 (PH->pe_flags & 2) == 0) // файл нельзя запускать! return PE_WRONG; POH = (struct pe_ohdr *)((unsigned char *)PH + 0xf8); while (PH->pe_obj_num--) { if ((POH->p_flags & 0x60) != 0) // либо код либо инициализированные данные memcpy (PE->pe_image_base + POH->o_vaddr, bin + POH->o_poff, POH->o_psize); POH = (struct pe_ohdr *)((unsigned char *)POH + sizeof (struct pe_ohdr)); } return PE_OK; }

Это опять таки не готовая программа, а алгоритм загрузки.

И опять таки многие моменты не освещаются, так как выходят за пределы темы.

Но теперь стоит немного поговорить про существующие системные особенности.

Системные особенности.

Не смотря на гибкость средств защиты, имеющихся в процессорах (защита на уровне таблиц дескрипторов, защита на уровне сегментов, защита на уровне страниц) в существующих системах (как в Windows, так и в Unix) полноценено используется только страничная защита, которая хотя и может уберечь код от записи, но не может уберечь данные от выполнения. (Может быть, с этим и связано изобилие уязвимостей систем?)

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

В связи с этим все модули линкуются не с начальных адресов, а с достаточно большим смещением в сегменте. В Windows используется базовый адрес в сегменте - 0x400000, в юникс (Linux или FreeBSD) - 0x8048000.

Некоторые особенности так же связаны со страничной организацией памяти.

ELF файлы линкуются таким образом, что границы и размеры секций приходятся на 4-х килобайтные блоки файла.

А в PE формате, не смотря на то, что сам формат позволяет выравнивать секции на 512 байт, используется выравнивание секций на 4к, меньшее выравнивание в Windows не считается корректным.

При наличии на компьютере установленной антивирусной программы можносканировать все файлы на компьютере, а также каждый файл в отдельности . Можно выполнить сканирование любого файла, щелкнув правой кнопкой мыши на файл и выбрав соответствующую опцию для выполнения проверки файла на наличие вирусов.

Например, на данном рисунке выделен файл my-file.elf , далее необходимо щелкнуть правой кнопкой мыши по этому файлу, и в меню файла выбрать опцию «сканировать с помощью AVG» . При выборе данного параметра откроется AVG Antivirus, который выполнит проверку данного файла на наличие вирусов.


Иногда ошибка может возникнуть в результате неверной установки программного обеспечения , что может быть связано с проблемой, возникшей в процессе установки. Это может помешать вашей операционной системе связать ваш файл ELF с правильным прикладным программным средством , оказывая влияние на так называемые «ассоциации расширений файлов» .

Иногда простая переустановка Dolphin (emulator) может решить вашу проблему, правильно связав ELF с Dolphin (emulator). В других случаях проблемы с файловыми ассоциациями могут возникнуть в результате плохого программирования программного обеспечения разработчиком, и вам может потребоваться связаться с разработчиком для получения дополнительной помощи.


Совет: Попробуйте обновить Dolphin (emulator) до последней версии, чтобы убедиться, что установлены последние исправления и обновления.


Это может показаться слишком очевидным, но зачастую непосредственно сам файл ELF может являться причиной проблемы . Если вы получили файл через вложение электронной почты или загрузили его с веб-сайта, и процесс загрузки был прерван (например, отключение питания или по другой причине), файл может повредиться . Если возможно, попробуйте получить новую копию файла ELF и попытайтесь открыть его снова.


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


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

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


Совет: Если при попытке открыть файл ELF вы получаете сообщение об ошибке, связанной с.SYS file , проблема, вероятно, может быть связана с поврежденными или устаревшими драйверами устройств , которые необходимо обновить. Данный процесс можно облегчить посредством использования программного обеспечения для обновления драйверов, такого как DriverDoc .


Если шаги не решили проблему , и у вас все еще возникают проблемы с открытием файлов ELF, это может быть связано с отсутствием доступных системных ресурсов . Для некоторых версий файлов ELF могут потребоваться значительный объем ресурсов (например, память/ОЗУ, вычислительная мощность) для надлежащего открытия на вашем компьютере. Такая проблема встречается достаточно часто, если вы используете достаточно старое компьютерное аппаратное обеспечение и одновременно гораздо более новую операционную систему.

Такая проблема может возникнуть, когда компьютеру трудно справиться с заданием, так как операционная система (и другие службы, работающие в фоновом режиме) могут потреблять слишком много ресурсов для открытия файла ELF . Попробуйте закрыть все приложения на вашем ПК, прежде чем открывать Nintendo Wii Game File. Освободив все доступные ресурсы на вашем компьютере вы обеспечите налучшие условия для попытки открыть файл ELF.


Если вы выполнили все описанные выше шаги , а ваш файл ELF по-прежнему не открывается, может потребоваться выполнить обновление оборудования . В большинстве случаев, даже при использовании старых версий оборудования, вычислительная мощность может по-прежнему быть более чем достаточной для большинства пользовательских приложений (если вы не выполняете много ресурсоемкой работы процессора, такой как 3D-рендеринг, финансовое/научное моделирование или интенсивная мультимедийная работа). Таким образом, вполне вероятно, что вашему компьютеру не хватает необходимого объема памяти (чаще называемой «ОЗУ», или оперативной памятью) для выполнения задачи открытия файла.

Стандартные средства разработки компилируют вашу программу в файл 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). Каждый узел имеет свой тэг (тип) и список атрибутов (свойств), описывающих узел. Атрибуты могут содержать все, что угодно, например, данные или ссылки на другие узлы. Кроме того, существует информация, хранящаяся вне дерева.
Узлы делятся на два основных типа: узлы, описывающие данные, и узлы, описывающие код.
Узлы, описывающие данные:
  1. Типы данных:
    • Базовые типы данных (узел с типом DW_TAG_base_type), например такие как тип int в C.
    • Составные типы данных (указатели и т.д.)
    • Массивы
    • Структуры, классы, объединения, интерфейсы
  2. Объекты данных:
    • константы
    • параметры функций
    • переменные
    • и т.д.
Каждый объект данных имеет атрибут DW_AT_location, который указывает, как вычисляется адрес, по которому находится данные. Например переменная может иметь фиксированный адрес, находиться в регистре или на стеке, быть членом класса или объекта. Этот адрес может вычисляться довольно сложным образом, поэтому стандарт предусматривает так называемые Location Expressions, которые могут содержать последовательность операторов специальной внутренней стековой машины.
Узлы, описывающие код:
  1. Процедуры (функции) - узлы с тэгом DW_TAG_subprogram. Узлы-потомки могут содержать описания переменных - параметров функции и локальных переменных функции.
  2. Единица компиляции (Compilation Unit). Содержит информацию программе и является родителем всех остальных узлов.
Информация, описанная выше, находится в секциях ".debug_info" и ".debug_abbrev".
Другая информация:
  • Информация о номерах строк (секция ".debug_line")
  • Информация о макросах (секция ".debug_macinfo")
  • Информация о формате фрейма (Call Frame Information) (секция ".debug_frame")

Создание ELF

Создавать файлы в формате EFL мы будем при помощи библиотеки libelf из пакета elfutils . В сети есть хорошая статья по использованию libelf - LibELF by Example (к сожалению, созданию файлов в ней описано очень кратко) а также документация .
Создание файла состоит из нескольких этапов:
  1. Инициализация libelf
  2. Создание заголовка файла (ELF Header)
  3. Создание заголовка программы (Program Header Table)
  4. Создание секций
  5. Запись файла
Рассмотрим этапы подробнее
Инициализация 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; 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 = 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);

Эта секция создается обычным образом, только 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 \0 n a m e \0 v a r \0
Запись файла
Итак, заголовки и секции уже сформированы, теперь их нужно записать в файл и завершить работу с libelf. Запись производит функция elf_update:
off_t elf_update(Elf *elf, Elf_Cmd cmd);
  • elf - дескриптор
  • cmd - команда, должна быть равна ELF_C_WRITE для записи.
Функция возвращает -1 при ошибке. Текст ошибки можно получить вызвав функцию elf_errmsg(-1), которая возвратит указатель на строку с ошибкой.
Заканчиваем работу с библиотекой функцией elf_end, которой передаем наш дескриптор. Осталось только закрыть ранее открытый файл.
Однако наш созданный файл не содержит отладочной информации, которую мы добавим в следующем разделе.

Создание DWARF

Создавать отладочную информацию будем с помощью библиотеки , в комплекте с которой идет pdf-файл с документацией (libdwarf2p.1.pdf - A Producer Library Interface to DWARF).
Создание отладочной информации состоит из таких этапов:
  1. Создание узлов (DIE - Debugging Information Entry)
  2. Создание атрибутов узла
  3. Создание типов данных
  4. Создание процедур (функций)
Рассмотрим этапы подробнее
Инициализация 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)
  • 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 - собственно значение атрибута (имя узла)

Функции 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_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 - длина процедуры в байтах
Функция вернет 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)
  • 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)

Функция 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)
  • opcode - код операции, константа DW_OP_хххх
  • val1, val2 - параметры инструкции (см. Стандарт)

Для явного задания адреса переменной вместо предыдущей должна использоваться функция 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

Формат ELF имеет файлы нескольких типов, которые до сих пор мы называли по-разному, например, исполняемый файл или объектный файл. Тем не менее стандарт ELF различает следующие типы:

1. Перемещаемый файл (relocatable file), хранящий инструкции и данные, которые могут быть связаны с другими объектными файлами. Результатом такого связывания может быть исполняемый файл или разделяемый объектный файл.

2. Разделяемый объектный файл (shared object file) также содержит инструкции и данные, но может быть использован двумя способами. В первом случае, он может быть связан с другими перемещаемыми файлами и разделяемыми объектными файлами, в результате будет создан новый объектный файл. Во втором случае, при запуске программы на выполнение операционная система может динамически связать его с исполняемым файлом программы, в результате чего будет создан исполняемый образ программы. В последнем случае речь идет о разделяемых библиотеках.

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

На рис. 2.4 приведена структура исполняемого файла, с помощью которого операционная система может создать образ программы и запустить программу на выполнение.

Рис. 2.4 . Структура исполняемого файла в формате ELF

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

Поскольку заголовок ELF-файла определяет его структуру, рассмотрим его более подробно (табл. 2.4).

Таблица 2.3 . Поля заголовка ELF-файла

Поле Описание
е_ident Массив байт, каждый из которых определяет некоторую общую характеристику файла: формат файла (ELF), номер версии, архитектуру системы (32-разрядная или 64-разрядная) и т.д.
e_type Тип файла, поскольку формат ELF поддерживает несколько типов
e_machine Архитектура аппаратной платформы, для которой создан данный файл. В табл. 2.4 приведены возможные значения этого поля
e_version Номер версии ELF-формата. Обычно определяется как EV_CURRENC (текущая), что означает последнюю версию
e_entry Виртуальный адрес, по которому системой будет передано управление после загрузки программы (точка входа)
e_phoff Расположение (смещение от начала файла) таблицы заголовков программы
е_shoff Расположение таблицы заголовков секций
е_ehsize Размер заголовка
e_phentsize Размер каждого заголовка программы
e_phnum Число заголовков программы
e_shentsize Размер каждого заголовка сегмента (секции)
е_shnum Число заголовков сегментов (секций)
e_shstrndx Расположение сегмента, содержащего таблицу строк

Таблица 2.4 . Значения поля e_machine заголовка ELF-файла

Значение Аппаратная платформа
ЕМ_М32 AT&T WE 32100
ЕМ_SPARC Sun SPARC
ЕМ_386 Intel 80386
ЕМ_68K Motorola 68000
EM_88K Motorola 88000
ЕМ_486 Intel 80486
ЕМ_860 Intel i860
ЕМ_MIPS MIPS RS3000 Big-Endian
EM_MIPS_RS3_LE MIPS RS3000 Little-Endian
EM_RS6000 RS6000
EM_PA_RISC PA-RISC
EM_nCUBE nCUBE
EM_VPP500 Fujitsu VPP500
EM_SPARC32PLUS Sun SPARC 32+

Информация, содержащаяся в таблице заголовков программы, указывает ядру, как создать образ процесса из сегментов. Большинство сегментов копируются (отображаются) в память и представляют собой соответствующие сегменты процесса при его выполнении, например, сегменты кода или данных.

Каждый заголовок сегмента программы описывает один сегмент и содержит следующую информацию:

Тип сегмента и действия операционной системы с данным сегментом

Расположение сегмента в файле

Стартовый адрес сегмента в виртуальной памяти процесса

Размер сегмента в файле

Размер сегмента в памяти

Флаги доступа к сегменту (запись, чтение, выполнение)

Часть сегментов имеет тип LOAD, предписывающий ядру при запуске программы на выполнение создать соответствующие этим сегментам структуры данных, называемые областями , определяющие непрерывные участки виртуальной памяти процесса и связанные с ними атрибуты. Сегмент, расположение которого в ELF-файле указано в соответствующем заголовке программы, будет отображен в созданную область, виртуальный адрес начала которой также указан в заголовке программы. К сегментам такого типа относятся, например, сегменты, содержащие инструкции программы (код) и ее данные. Если размер сегмента меньше размера области, неиспользованное пространство может быть заполнено нулями. Такой механизм, в частности используется при создании неинициализированных данных процесса (BSS). Подробнее об областях мы поговорим в главе 3.

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

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

Мы еще вернемся к формату ELF в главе 3 при обсуждении организации виртуальной памяти процесса, а пока перейдем к следующему распространенному формату - COFF.

Из книги Искусство программирования для Unix автора Реймонд Эрик Стивен

Из книги Самоучитель работы на компьютере автора Колисниченко Денис Николаевич

Из книги Реферат, курсовая, диплом на компьютере автора Баловсяк Надежда Васильевна

5.2.6. Формат Windows INI Многие программы в Microsoft Windows используют текстовый формат данных, подобный фрагменту, приведенному в примере 5.6. В данном примере необязательные ресурсы с именами account, directory, numeric_id и developer связываются с именованными проектами python, sng, f etchmail и py-howto. В записи

Из книги Новейший самоучитель работы на компьютере автора Белунцов Валерий

14.5.3. Формат ячейки Формат задает, как будет отображаться значение ячейки. Формат тесно связан с типом данных ячейки. Тип задаете вы сами. Если вы ввели число, то это числовой тип данных. Excel сама старается определить формат по типу данных. Например, если вы ввели текст, то

Из книги Искусство программирования для Unix автора Реймонд Эрик Стивен

Формат PDF PDF расшифровывается как Portable Document Format (портативный формат документа). Этот формат был создан специально для ликвидации проблем с отображением информации в файлах. Его преимущество состоит в том, что, во-первых, документ, сохраненный в формате PDF, будет одинаково

Из книги TCP/IP Архитектура, протоколы, реализация (включая IP версии 6 и IP Security) автора Фейт Сидни М

Формат файлов Когда пользователь начинает работу с каким-либо файлом, системе необходимо знать, в каком формате он записан и с помощью какой программы его нужно открывать. Например, если файл содержит обычный текст, то он может быть прочитан в любой текстовой программе

Из книги Яндекс для всех автора Абрамзон М. Г.

5.2.2. Формат RFC 822 Метаформат RFC 822 происходит от текстового формата сообщений электронной почты в Internet. RFC 822 является основным Internet RFC-стандартом, описывающим данный формат (впоследствии заменен RFC 2822). Формат MIME (Multipurpose Internet Media Extension - многоцелевые расширения Internet)

Из книги Macromedia Flash Professional 8. Графика и анимация автора Дронов В. А.

5.2.3. Формат Cookie-Jar Формат cookie-jar используется программой fortune(1) для собственной базы данных случайных цитат. Он подходит для записей, которые представляют собой просто блоки неструктурированного текста. В качестве разделителя записей в данном формате применяется символ

Из книги Компьютерная обработка звука автора Загуменнов Александр Петрович

5.2.4. Формат record-jar Разделители записей формата cookie-jar хорошо сочетаются с метаформатом RFC 822 для записей, образующих формат, который в данной книге называется "record-jar". Иногда требуется текстовый формат, поддерживающий множественные записи с различным набором явных имен

Из книги Операционная система UNIX автора Робачевский Андрей М.

5.2.6. Формат Windows INI Многие программы в Microsoft Windows используют текстовый формат данных, подобный фрагменту, приведенному в примере 5.6. В данном примере необязательные ресурсы с именами account, directory, numeric_id и developer связываются с именованными проектами python, sng, fetchmail и py-howto. В записи

Из книги Офисный компьютер для женщин автора Пастернак Евгения

19.5 Обобщенный формат URL Обобщая вышесказанное, отметим, что:? URL начинается с указания используемого протокола доступа.? Для всех приложений, кроме сетевых новостей и электронной почты, далее следует разделитель://.? Затем указывается имя хоста сервера.? Наконец

Из книги автора

3.3.1. Формат RSS Читать новости сайтов можно по-разному. Самый простой способ - заходить время от времени на сайт и просматривать новые сообщения. Можно поставить программу, которая подключается к новостному каналу и сама получает заголовки или аннотации новостей, по

Из книги автора

Формат MP3 Формат MP3 был создан для распространения музыкальных файлов, сжатых кодеком MPEG 1 level 3. В настоящее время - самый популярный формат распространения музыки через Интернет, и не только. Поддерживается абсолютно всеми программами записи и обработки звука, за

Из книги автора

Формат MP3 Метод сжатия звука, а также формат сжатых звуковых файлов, предложенный международной организацией MPEG (Moving Pictures Experts Group – Экспертная группа по видеозаписи), основан на перцептуальном кодировании звука. Работы по созданию эффективных алгоритмов кодирования

Из книги автора

Формат ELF Формат ELF имеет файлы нескольких типов, которые до сих пор мы называли по-разному, например, исполняемый файл или объектный файл. Тем не менее стандарт ELF различает следующие типы:1. Перемещаемый файл (relocatable file), хранящий инструкции и данные, которые могут быть

Из книги автора

Формат чисел Наконец-то добрались до формата чисел. Я уже не раз о нем упоминала, теперь разложу все по полочкам (хотя общий смысл вы уже могли понять).Числа в Excel могут отображаться в различных форматах. В этом разделе мы поговорим о том, какие существуют форматы чисел и как