Как избавиться от раздела DLL .reloc с помощью MinGW-gcc?

Я строю виртуальные таблицы вручную на C. При экспорте из DLL они создают много записей в своей таблице перемещений.
Пример objdump вывода:

Virtual Address: 00002000 Chunk size 24 (0x18) Number of fixups 8
    reloc    0 offset    0 [2000] HIGHLOW
    reloc    1 offset    4 [2004] HIGHLOW
    reloc    2 offset    8 [2008] HIGHLOW
    reloc    3 offset    c [200c] HIGHLOW
    reloc    4 offset   10 [2010] HIGHLOW
    reloc    5 offset   14 [2014] HIGHLOW
    reloc    6 offset   18 [2018] HIGHLOW
    reloc    7 offset   1c [201c] HIGHLOW

Есть ли способ избавиться от них, или это единственный способ в Windows?
Вот мои выводы на данный момент:

  1. в Visual Studio link есть опция /FIXED (которая делает именно то, что я хочу)
  2. есть это руководство , но большая часть из них применима только к gcc под Linux
  3. Я могу собрать DLL без -shared и вместо этого установить --image-base

Последнее действительно работает (секция .reloc не генерируется), но я считаю это крайне уродливым хаком, потому что на самом деле это уже не DLL.

Пояснение:

У меня складывается впечатление, что за этот вопрос проголосовали только отрицательно, потому что люди считают, что переезд — это хорошо. Я признаю, что они хороши в целом, но у меня есть очень конкретная цель. Я хочу показать, как динамический полиморфизм с vtables может быть достигнут в O (1), например:

struct IDerived {
    union {
        IBaseA asBaseA;
        struct {
            int (*foo)(Derived this); // inherited from BaseA
            ...
        };
    };
    union {
        IBaseB asBaseB;
        struct {
            int (*bar)(Derived this); // inherited from BaseB
            ...
        };
    };
    int (*baz)(Derived this);
    ...
};

struct Derived {
    const IDerived *iface;
    void *data;
};

extern void doSthWithBaseB(BaseB x);

void doSthWithDerived(Derived x) {
    x.iface->foo(x);
    doSthWithBaseB((BaseB){ &x.iface->asBaseB, x.data }) // high-level cast
}

Поскольку «высокоуровневое приведение» включает только арифметику указателя, это O (1) (в частности, линейный поиск не выполняется, как в Java).

Теперь вернемся к перемещениям: независимо от того, насколько низка стоимость, она оказывается равной O(n), поскольку каждый метод в каждом классе нуждается в обновлении. Вздох.

tl;dr
Есть ли подвеска к Microsoft /FIXED для GCC? Если нет, то какие флаги нужно установить в PE для достижения желаемого поведения?


person Philip    schedule 02.05.2016    source источник
comment
Почему минус? Пожалуйста, объясните в комментарии, чтобы я мог улучшить вопрос.   -  person Philip    schedule 02.05.2016
comment
Вы предполагаете, что они не нужны. Почему вы так предполагаете? Как вы проверили, что ваше предположение верно?   -  person Kuba hasn't forgotten Monica    schedule 02.05.2016
comment
@KubaOber Я думал, что это то, что делает -pie, создает независимый от позиции исполняемый файл. Мне кажется, это просто не относится к указателям функций в разделе .rdata. Но именно в этом мой вопрос (я отредактировал его, чтобы сделать это более понятным).   -  person Philip    schedule 02.05.2016
comment
Почему вы хотите избавиться от переездов? Параметр -pie по большей части игнорируется для целей Windows. Он делает прямо противоположное тому, что вы хотите, он говорит компоновщику создавать релокации для исполняемых файлов (.EXE). Обычно исполняемые файлы в Windows нельзя перемещать, перемещения позволяют перемещать их, как DLL. Обычно библиотеки DLL имеют перемещение, параметр Visual Studio /FIXED создает библиотеки DLL без них, предотвращая перемещение библиотеки DLL. Это предотвратит загрузку DLL Windows, если что-то уже находится по адресу, по которому должна быть загружена DLL.   -  person Ross Ridge    schedule 04.05.2016
comment
Несколько вариантов: (1.) статическая сборка (если позволяют лицензии) - PIC не требуется. (2.) Скомпилируйте с параметром -fvisibility=hidden, затем отметьте символы, которые необходимо явно экспортировать, с атрибутом видимости по умолчанию (я забыл синтаксис). (3.) пометьте символы, которые вы не хотите экспортировать, со скрытой видимостью атрибутов. или (4.) использовать скрипт компоновщика и файл карты   -  person technosaurus    schedule 06.05.2016


Ответы (2)


Хорошо, я сам нашел ответ:

Нужно использовать strip -R .reloc в DLL, а затем вручную добавить IMAGE_FILE_RELOCS_STRIPPED (0x0001) в поле Characteristics в заголовке PE. Это сработает.

Разумеется, его следует использовать с настраиваемым базовым адресом (-Wl,--image-base=...), иначе Windows не сможет загрузить DLL. контейнер сразу).

person Philip    schedule 11.05.2016

PIC не означает позиционно-независимые данные. Ваш код не зависит от позиции, но это влечет за собой затраты во время выполнения. Нет никакого волшебства, с помощью которого раздел данных может быть заполнен адресами функций во время компиляции/компоновки, поскольку они меняются во время выполнения - иначе PIE не указал бы затраты времени выполнения с самого начала. Компилятор, возможно, мог бы использовать какой-то другой тип указателя функции, который указывает на функции PIC и фиксируется перед вызовом, но это повлечет за собой дополнительные затраты на каждое разыменование указателя функции. Таким образом, компиляторы не делают этого по умолчанию.

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

Вместо виртуальной таблицы с указателями функций вы можете явно выполнить переход и надеяться, что переключение не будет реализовано с помощью виртуальной таблицы, созданной компилятором. Затем вызов преобразователя C_foo(&c, ...) заменит вызов c->vtable->foo(...).

typedef enum { C_type, D_type } type_t;


typedef struct {
  type_t type;
} C;

typedef struct {
  type_t type;
} D;

// Replaces the vtable
void C_foo_impl(int);
void D_foo_impl(int);
void C_foo(C * self, int i) {
  switch (self->type) {
  case C_type: return C_foo_impl(i);
  case D_type: return D_foo_impl(i);
  default: return;
  }
}

void C_init(C * self) {
  self->type = C_type;
}

void D_init(D * self) {
  C_init((C*)self);
  self->type = D_type;
}

void test(void) {
  C c;
  C_init(&c);
  D d;
  D_init(&d);
  C_foo(&c, 10); // calls c_foo_impl
  C_foo((C*)&d, 10); // calls d_foo_impl
}
person Kuba hasn't forgotten Monica    schedule 02.05.2016
comment
Каков (минималистский) путь? Создавать виртуальные таблицы во время выполнения (как это делает CPython) или полагаться на загрузчик для перемещения (как, я думаю, делает C++)? - person Philip; 02.05.2016
comment
Кроме того, где именно разница -fpic и -pie на GCC? - person Philip; 02.05.2016
comment
@PhilipDahnen Виртуальная таблица - это всего лишь один из способов реализации динамической диспетчеризации. В конце концов, вы точно знаете, какие функции вызывать, поэтому можете реализовать преобразователь самостоятельно. Почему вы так ненавидите переезды? - person Kuba hasn't forgotten Monica; 02.05.2016
comment
@PhilipDahnen Для изображения и пирога см. этот вопрос. TL;DR: PIE похож на PIC, но для исполняемых файлов и в некоторых случаях стоит немного дешевле. - person Kuba hasn't forgotten Monica; 02.05.2016
comment
Я не ненавижу их. Я просто хочу знать, есть ли более разумный способ, поскольку они несут затраты времени выполнения (как вы сказали). Ваше решение хорошее, но приводит к тому, что все функции необходимо импортировать. - person Philip; 02.05.2016
comment
@PhilipDahnen Это не имеет большого значения, подписи должны совпадать, чтобы вы могли макроизвести их. Редактировать: на самом деле вы не можете макросировать его, так что это две строки для каждого виртуального метода: одна, чтобы объявить его локально, другая, чтобы добавить его к коммутатору. - person Kuba hasn't forgotten Monica; 02.05.2016
comment
Давайте продолжим обсуждение в чате. - person Philip; 02.05.2016