Что такое частичное связывание в GNU Linker?

Лучшее объяснение, которое я смог найти, было из официального документа:

-r --relocateable Сгенерировать перемещаемый вывод - т.е. сгенерировать выходной файл, который в свою очередь может служить входом для ld. Это часто называют частичное связывание. В качестве побочного эффекта в средах, которые поддерживают стандартные магические числа Unix, этот параметр также устанавливает выходной файл магическое число для OMAGIC. Если эта опция не указана, абсолютная файл создается. При связывании программ на С++ эта опция не будет разрешать ссылки на конструкторы; для этого используйте -Ur. Этот параметр делает то же, что и `-i '.

Мне особенно интересно узнать, что происходит с символами, присутствующими в входах в компоновщик. Возьмем конкретный случай, когда у меня есть статическая библиотека libstatic.a, которая содержит один объектный файл component.o. Теперь я хочу создать еще одну статическую библиотеку libfinal.a, которая будет работать как интерфейс libstatic.a. Я использую эту команду для ее создания:

ld -r -o libfinal.a wrapper.o -L. -lstatic

Где wrapper.o предоставляет эксклюзивные API для вызова функций, определенных в libstatic.a

Будет ли libfinal.a быть только объединенный архив с wrapper.o и component.o или всеми ссылками, которые могут быть - разрешено между wrapper.o и component.o разрешено (связывание), а затем помещается в libfinal.a?

Edit_1: Обновление вопроса на основе достигнутого прогресса: objdump библиотеки компонентов libstatic.a (objdump -D libstatic.a) показывает разделы .text отдельно для каждой функции (как и ожидалось). Если в объединенной библиотеке libfinal.a, которая была создана путем частичного связывания (флаг -r), существует только один раздел .text. Я предполагаю, что это означает, что произошло внутреннее связывание, и это не просто создание простого архива.

Ответ 1

Пример минимального запуска

Здесь я приведу минимальный пример и скомпилирую его двумя способами для получения функционально идентичных исполняемых файлов:

  • один объединенный файл f12.c без частичной ссылки на f12.o
  • два отдельных f1.c и f2.c, которые сначала частично связаны в f12_r.o

main.c

#include <assert.h>
#include <stdlib.h>

int f_1_2(void);
int f_2_1(void);

int main(void) {
    assert(f_1_2() + f_2_1() == 5);
    return EXIT_SUCCESS;
}

f1.c

#include "f1.h"

f2.c

#include "f2.h"

f12.c

#include "f1.h"
#include "f2.h"

f1.h

int f_2(void);

int f_1_2(void) {
    return f_2() + 1;
}

int f_1(void) {
    return 1;
}

f2.h

int f_1(void);

int f_2_1(void) {
    return f_1() + 1;
}

int f_2(void) {
    return 2;
}

run.sh

#!/usr/bin/env bash
set -eux
cflags='-ggdb3 -std=c99 -O0 -fPIE -pie'
gcc $cflags -c -o f1.o f1.c
gcc $cflags -c -o f2.o f2.c
gcc $cflags -c -o f12.o f12.c
ld -o f12_r.o -r f1.o f2.o
gcc $cflags -c -o main.o main.c
gcc $cflags -o main.out f12.o main.o
gcc $cflags -o main_r.out f12_r.o main.o
./main.out
./main_r.out

GitHub upstream.

Если мы попробуем то же самое, но без ld -r, мы получим последние предупреждения:

+ ld -o f12_r.o f1.o f2.o
ld: warning: cannot find entry symbol _start; defaulting to 0000000000401000
+ gcc -ggdb3 -std=c99 -O0 -fPIE -pie -o main_r.out f12_r.o main.o
/usr/bin/ld: error in f12_r.o(.eh_frame); no .eh_frame_hdr table will be created

ни один из них не заставляет инструмент выходить не равным 0, а финальный исполняемый файл все еще выполняется, поэтому я не уверен, насколько он плох. ТОДО понимаю.

Двоичный анализ

Если вы не знакомы с перемещением, сначала прочтите это: Что делают компоновщики?

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

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

Я подтвердил это с помощью:

objdump -S f12.o
objdump -S f12_r.o

оба из которых производят идентичные выходные данные, которые содержат:

int f_1_2(void) {
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
    return f_2() + 1;
   4:   e8 00 00 00 00          callq  9 <f_1_2+0x9>
   9:   83 c0 01                add    $0x1,%eax
}
   c:   5d                      pop    %rbp
   d:   c3                      retq

поэтому мы видим, что вызов f_1_2 еще не был разрешен ни в одном из случаев, поскольку адрес относительного смещения по-прежнему равен 0: e8 00 00 00 00 (e8 - это код операции).

Это также научило меня, что GCC не разрешает вызовы функций до окончательной ссылки, либо обоснование TODO, возможно ли принудительно разрешить его?

Benchmark

Я сравнил LD и GOLD по адресу: Замена ld золотом - есть опыт?, поэтому я решил использовать его повторно, чтобы посмотреть, не приведет ли частичная ссылка к какому-либо ускорению ссылки.

Я сгенерировал тестовые объекты с помощью этого сценария:

./generate-objects 100 1000 100

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

mv main.o ..
ld -o partial.o -r *.o
time gcc               partial.o ../main.o
time gcc -fuse-ld=gold partial.o ../main.o

Результаты в виде настенных часов в секундах были следующими:

          No partial link   Partial link
No Gold   6.15              5.756
Gold      4.06              4.457

Поэтому:

  • разница во времени существует, но она не очень значительна
  • без золота он шел быстрее, а с золотом - медленнее!

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

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

Пример из практики: ядро Linux

Ядро Linux является одним из примеров большого проекта, в котором раньше использовались инкрементные ссылки, поэтому, возможно, мы сможем что-то из этого извлечь.

С тех пор он переместился в ar T тонкие архивы, как показано на: https://unix.stackexchange.com/questions/5518/what-is-the-difference-between-the-following-kernel-makefile-terms-vmlinux-vml/482978#482978

Первоначальная фиксация и обоснование находятся по адресу: a5967db9af51a84f5e181600954714a9e4c69f1f (включена в v4.9), сообщение о фиксации которого гласит:

ld -r is an incremental link used to create built-in.o files in build
subdirectories. It produces relocatable object files containing all
its input files, and these are are then pulled together and relocated
in the final link. Aside from the bloat, this constrains the final
link relocations, which has bitten large powerpc builds with
unresolvable relocations in the final link.

это также упоминается в Documentation/process/changes.rst:

Binutils
--------

The build system has, as of 4.13, switched to using thin archives ('ar T')
rather than incremental linking ('ld -r') for built-in.a intermediate steps.
This requires binutils 2.20 or newer.

TODO: узнайте, когда было введено инкрементное связывание, и посмотрите, есть ли минимальный тестовый пример, который мы можем использовать, чтобы увидеть его ускорение: https://unix.stackexchange.com/questions/491312/why-does-the-linux-kernel-build-system-use-incremental-linking-or-ar-t-thin-arch

Протестировано на Ubuntu 18.10, GCC 8.2.0, ноутбуке Lenovo ThinkPad P51, процессоре Intel Core i7-7820HQ (4 ядра /8 потоков), 2x оперативной памяти Samsung M471A2K43BB1-CRC (2x 16 ГБ), Samsung MZVLB512HAJQ-000L7 SSD (3000 МБ/с)).

Ответ 2

ld создает исполняемые файлы и общие библиотеки, а не архивы объектных файлов (файлы .a).

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


Опция

-r, --relocateable полезна, когда вы хотите разрешить определенные (неразрешенные) символы .so и создать еще один .so.