Как C ++-связь работает на практике?

Как C ++-связь работает на практике? Я ищу подробное объяснение того, как происходит связь, а не какие команды делают связь.

Уже есть аналогичный вопрос о компиляции, который не затрагивает слишком много деталей: как работает процесс компиляции / связывания?

EDIT : я переместил этот ответ в дубликат: https://stackoverflow.com/a/33690144/895245

В этом ответе основное внимание уделяется перемещению адресов , которое является одной из важнейших функций связывания.

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

0) Введение

Реферат: перемещение редактирует раздел .text объектных файлов для перевода:

  • адрес объектного файла
  • в конечный адрес исполняемого файла

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

  • разрешать неопределенные символы, такие как объявленные неопределенные функции
  • не сталкиваться с несколькими разделами .text и .data из нескольких объектных файлов

Предпосылки: минимальное понимание:

  • assembly x86-64 или IA-32
  • глобальная структура файла ELF. Я сделал учебник для этого

Связывание не имеет ничего общего с C или C ++: компиляторы просто генерируют объектные файлы. Затем компоновщик берет их как входные данные, даже не зная, какой язык их компилировал. Это может быть и Фортран.

Итак, чтобы уменьшить кору, давайте изучим мир приветствия ELF Linux NASM x86-64:

 section .data hello_world db "Hello world!", 10 section .text global _start _start: ; sys_write mov rax, 1 mov rdi, 1 mov rsi, hello_world mov rdx, 13 syscall ; sys_exit mov rax, 60 mov rdi, 0 syscall 

скомпилированы и собраны:

 nasm -o hello_world.o hello_world.asm ld -o hello_world.out hello_world.o 

с NASM 2.10.09.

1) .text of .o

Сначала мы декомпилируем секцию .text объектного файла:

 objdump -d hello_world.o 

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

 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 

ключевые черты:

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

который должен переместить адрес строки hello world в регистр rsi , который передается системному вызову записи.

Но ждать! Как компилятор может знать, где "Hello world!" закончится в памяти при загрузке программы?

Ну, это не так, особенно после того, как мы свяжемся с группой файлов .o вместе с несколькими разделами .data .

Только компоновщик может это сделать, поскольку только у него будут все эти объектные файлы.

Итак, компилятор просто:

  • помещает значение заполнителя 0x0 на скомпилированный вывод
  • дает дополнительную информацию компоновщику о том, как изменить скомпилированный код с хорошими адресами

Эта «дополнительная информация» содержится в разделе .rela.text объектного файла

2) .rela.text

.rela.text означает «перемещение раздела .text».

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

Мы можем разобрать раздел .rela.text с помощью:

 readelf -r hello_world.o 

который содержит;

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

Формат этого раздела зафиксирован в документе: http://www.sco.com/developers/gabi/2003-12-17/ch4.reloc.html.

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

Упрощая немного, для этой конкретной строки мы имеем следующую информацию:

  • Offset = C : каков первый байт в .text который изменяется в этой записи.

    Если мы movabs $0x0,%rsi на декомпилированный текст, это точно внутри критических movabs $0x0,%rsi , а те, которые знают кодировку команд x86-64, заметят, что это кодирует 64-битную адресную часть инструкции.

  • Name = .data : адрес указывает на раздел .data

  • Type = R_X86_64_64 , который указывает, что именно нужно сделать для перевода адреса.

    Это поле на самом деле зависит от процессора и, таким образом, задокументировано в подразделе 4.464 «Перемещение» для расширения AMD64 System V ABI .

    В этом документе говорится, что R_X86_64_64 делает:

    • Field = word64 : 8 байтов, таким образом 00 00 00 00 00 00 00 00 по адресу 0xC

    • Calculation = S + A

      • Sзначение по адресу, который перемещается, таким образом, 00 00 00 00 00 00 00 00
      • A – это сложение, которое здесь 0 . Это поле ввода перемещения.

      Итак, S + A == 0 и мы переместимся на самый первый адрес раздела .data .

3) .text of .out

Теперь давайте посмотрим на текстовую область исполняемого файла ld сгенерированного для нас:

 objdump -d hello_world.out 

дает:

 00000000004000b0 <_start>: 4000b0: b8 01 00 00 00 mov $0x1,%eax 4000b5: bf 01 00 00 00 mov $0x1,%edi 4000ba: 48 be d8 00 60 00 00 movabs $0x6000d8,%rsi 4000c1: 00 00 00 4000c4: ba 0d 00 00 00 mov $0xd,%edx 4000c9: 0f 05 syscall 4000cb: b8 3c 00 00 00 mov $0x3c,%eax 4000d0: bf 00 00 00 00 mov $0x0,%edi 4000d5: 0f 05 syscall 

Таким образом, единственное, что изменилось из объектного файла, – это критические строки:

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

которые теперь указывают на адрес 0x6000d8 ( d8 00 60 00 00 00 00 00 в little-endian) вместо 0x0 .

Правильно ли это место для строки hello_world ?

Чтобы решить, мы должны проверить заголовки программ, которые сообщают Linux, где загружать каждый раздел.

Мы разбираем их с помощью:

 readelf -l hello_world.out 

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

 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 0x00000000000000d7 0x00000000000000d7 RE 200000 LOAD 0x00000000000000d8 0x00000000006000d8 0x00000000006000d8 0x000000000000000d 0x000000000000000d RW 200000 Section to Segment mapping: Segment Sections... 00 .text 01 .data 

Это говорит нам, что раздел .data , который является вторым, начинается с VirtAddr = 0x06000d8 .

И единственное, что есть в разделе данных, это наша мировая мировая строка.

На самом деле, можно сказать, что связь довольно проста.

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

Очевидно, что компоновщику необходимо создать файл, который соответствует определенному формату (обычно формат ELF в Unix) и будет разделять различные категории кода / данных на разные разделы файла, но это просто отправка.

Я знаю о двух осложнениях:

  • необходимость дедупликации символов: некоторые символы присутствуют в нескольких объектных файлах, и только один должен сделать это в создаваемой библиотеке / исполняемом файле; это задача компоновщика включать только одно из определений

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

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

Помимо уже упомянутых « Linkers and Loaders », если вы хотите узнать, как работает настоящий и современный компоновщик, вы можете начать здесь .

  • Используйте как статические, так и динамически связанные библиотеки в gcc
  • Могу ли я смешивать статические и общедоступные библиотеки при связывании?
  • ошибка LNK2038: обнаружено несоответствие для '_ITERATOR_DEBUG_LEVEL': значение '0' не соответствует значению '2' в main.obj
  • Порядок инициализации статических переменных
  • Передача gcc непосредственно для связывания библиотеки статически
  • Определение переменной в файлах заголовков
  • ошибка LNK2019: неразрешенный внешний символ _main, указанный в функции ___tmainCRTStartup
  • Как удалить пакет из Laravel с помощью композитора?
  • Interesting Posts

    Поиск значения enums по его описанию Атрибут

    объявлять функцию шаблона шаблона classа шаблона

    Как сделать полупрозрачный слой PNG в Google Maps в IE8?

    jQuery получить все div, которые не имеют атрибута classа

    Как узнать, когда UITableView завершил ReloadData?

    Где я могу получить «полезный» алгоритм бинарного поиска C ++?

    Изменение типа сети от Неопознанной сети до частной сети в соединении с OpenVPN

    Как компьютер знает, что ПК не был правильно выключен?

    Прослушивание ACTION_SCREEN_OFF

    Где находится JAVA_HOME на macOS Sierra (10.12), Эль Капитан (10.11), Йосемити (10.10), Маверикс (10.9), Горный лев (10.8) или Лекс OSX (10.7)?

    Можно ли перенести процесс переднего плана на задний план без приостановки (control + z)?

    Менеджер загрузок Chrome возобновляет загрузку, если соединение потеряно или закрыто?

    Что означает «Метод» ~ ‘объекта’ ~ ‘failed?

    Две OSX-компьютеры в физической близости: параметры?

    Свернуть и захватить повторяющийся шаблон в выражении с одним выражением

    Давайте будем гением компьютера.