Как 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 », если вы хотите узнать, как работает настоящий и современный компоновщик, вы можете начать здесь .

  • Что такое Microsoft Visual Studio, эквивалентная опции GCC ld - whall-archive
  • Используйте как статические, так и динамически связанные библиотеки в gcc
  • Передача gcc непосредственно для связывания библиотеки статически
  • Что означает «статически связанное» и «динамически связанное»?
  • Порядок инициализации статических переменных
  • Почему фатальная ошибка «LNK1104: невозможно открыть файл« C: \ Program.obj »возникает при компиляции проекта C ++ в Visual Studio?
  • Ошибки компоновщика при компиляции против glib ...?
  • Почему порядок, в котором связаны библиотеки, иногда вызывает ошибки в GCC?
  • Давайте будем гением компьютера.