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

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

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

3 ответа

РЕДАКТИРОВАТЬ: я переместил этот ответ в дубликат: /questions/14358707/chto-delayut-linkeryi/14358720#14358720

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

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

0) Введение

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

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

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

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

Пререквизиты: минимальное понимание:

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

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

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). Текст из.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

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

Но ждать! Как может компилятор знать где "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и те, кто знает кодировку команд x86-64, заметят, что это кодирует 64-битную адресную часть инструкции.

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

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

    Это поле фактически зависит от процессора и, таким образом, задокументировано в разделе 4.4 "Перемещение" для 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). Текст из.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 в порядке байтов) вместо 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  R E    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: результат компиляции различных единиц перевода (грубо, предварительно обработанные исходные файлы)

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

Другие вопросы по тегам