Есть ли какие-либо предпочтения, которые линкер дает статическим или динамическим символам? - PullRequest
0 голосов
/ 28 июня 2018

У меня есть два заголовка и два файла cpp:

//f1.h
int f1();

//f1.cpp
include "f1.h"
int f1() {return 1;}

//f2.h
int f2();

//f2.cpp
#include "f2.h"
#include "f1.h"
int f2() {return f1() + 1;}

//main.cpp
#include "f2.h"
int main() {return f2();}

Сначала я компилирую общий объект из f1 и f2 и создаю двоичный файл из main.cpp в зависимости от этого общего объекта:

g++ -c -fPIC -shared f1.cpp f2.cpp
g++ -shared -fPIC -o libf.so f2.o f1.o
g++ -o dynamic main.cpp libf.so

Теперь я внесу некоторые изменения в f1.cpp (скажем, f1 теперь возвращает 2):

//f1.cpp#
include "f1.h"
int f1() {return 2;}

И скомпилируйте двоичный файл следующим образом:

g++ -o semistatic main.cpp f1.cpp libf.so

Вопрос в том, будет ли «полуистатический» двоичный файл использовать определение f1() из libf (в котором f1 возвращает 1), или он будет использовать статически связанный символ (тот, в котором f1 возвращает * 1024) *)? Отличается ли это в разных системах, и могу ли я рассчитывать на то, что оно будет единообразным в одной системе?

1 Ответ

0 голосов
/ 30 июня 2018

Как уже указывалось, вы нарушаете правило одного определения. Это не конец света, но в этом случае нет никаких гарантий от C ++ - стандарта, что произойдет, и поведение зависит от деталей реализации компоновщика и загрузчика.

Цепочки инструментов и операционные системы сильно различаются, поэтому вышеприведенное даже не будет ссылаться на Windows Но если вы говорите о Linux с обычной парой компоновщик / загрузчик, то поведение будет заключаться в использовании измененной версии - и это будет для каждой установки Linux.

Так работает компоновщик / загрузчик в Linux (и это поведение широко используется, например, для LD_PRELOAD-trick ):

  • Символы в *.so слабы, и поэтому определение из *.so просто игнорируется, если компоновщик находит другое определение где-то еще (в вашем случае в обновленной версии f1.o).
  • во время выполнения загрузчик игнорирует определения из общего объекта, если символ уже связан, то есть известно другое определение. В вашем случае символ f1 (хорошо, из-за искажения имени у него будет другое имя, но давайте для простоты проигнорируем его) уже связан с определением, которое находится в основной программе и, таким образом, будет использоваться, когда f1 вызывается в *.so.

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

A: изменение видимости на скрытое.

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

__attribute__ ((visibility ("hidden")))
int f1() {return 1;}

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

B: выполнение f1 было встроенной функцией.

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

-fPIC предотвращает встраивание функции, которая не помечена inline, поэтому вышеприведенное верно только для функции, которая помечена как встроенная в явном виде.


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

...