#c #linker #shared-libraries #elf #abi
#c #компоновщик #разделяемые библиотеки #elf #abi
Вопрос:
Идиома pimpl обычно используется для того, чтобы разрешить изменение кода в динамически связанных библиотеках без нарушения совместимости с ABI и необходимости перекомпилировать весь код, который зависит от библиотеки.
В большинстве объяснений, которые я вижу, упоминается, что добавление новой частной переменной-члена изменяет смещения открытых и закрытых членов в классе. Для меня это имеет смысл. Чего я не понимаю, так это того, как на практике это фактически нарушает работу зависимых библиотек.
Я много читал о файлах ELF и о том, как на самом деле работает динамическое связывание, но я все еще не понимаю, как изменение размера класса в общей библиотеке может что-то изменить.
Например, вот тестовое приложение (a.out) Я написал, что использует code ( Interface::some_method
) из тестовой общей библиотеки (libInterface.so ):
aguthrie@ana:~/pimpl$ objdump -d -j .text a.out
08048874 <main>:
...
8048891: e8 b2 fe ff ff call 8048748 <_ZN9Interface11some_methodEv@plt>
При вызове some_method
используется таблица процедурных связей (PLT):
aguthrie@ana:~/pimpl$ objdump -d -j .plt a.out
08048748 <_ZN9Interface11some_methodEv@plt>:
8048748: ff 25 1c a0 04 08 jmp *0x804a01c
804874e: 68 38 00 00 00 push $0x38
8048753: e9 70 ff ff ff jmp 80486c8 <_init 0x30>
который впоследствии переходит в глобальную таблицу смещений (GOT), где содержится адрес 0x804a01c:
aguthrie@ana:~/pimpl$ readelf -x 24 a.out
Hex dump of section '.got.plt':
0x08049ff4 089f0408 00000000 00000000 de860408 ................
0x0804a004 ee860408 fe860408 0e870408 1e870408 ................
0x0804a014 2e870408 3e870408 4e870408 5e870408 ....>...N...^...
0x0804a024 6e870408 7e870408 8e870408 9e870408 n...~...........
0x0804a034 ae870408 ....
И тогда динамический компоновщик творит свое волшебство и просматривает все символы, содержащиеся в общих библиотеках в LD_LIBRARY_PATH, находит Interface::some_method
в libInterface.so и загружает свой код в GOT so при последующих вызовах some_method
, код в GOT на самом деле является сегментом кода из разделяемой библиотеки.
Или что-то в этом роде.
Но, учитывая вышесказанное, я все еще не понимаю, как здесь влияет размер класса общей библиотеки или его смещения методов. Насколько я могу судить, приведенные выше шаги не зависят от размера класса. Похоже, что в .out включено только символьное имя метода в библиотеке. Любые изменения в размере класса должны просто разрешаться во время выполнения, когда компоновщик загружает код в GOT, нет?
Чего мне здесь не хватает?
Ответ №1:
Основная проблема заключается в том, что при выделении нового экземпляра класса (либо в стеке, либо через new
) вызывающий код должен знать размер объекта. Если вы позже измените размер объекта (добавив закрытый элемент), это увеличит необходимый размер; однако ваши вызывающие пользователи все еще используют старый размер. Таким образом, вы в конечном итоге не выделяете достаточно места для хранения объекта, и конструктор объекта затем продолжает повреждать стек (или кучу), потому что он предполагает, что в нем достаточно места.
Кроме того, если у вас есть какие-либо встроенные функции-члены, их код (включая смещения переменных-членов) может быть встроен в вызывающий код. Если вы добавляете закрытые элементы в любом месте, кроме конца, эти смещения будут неверными, что также приведет к повреждению памяти (примечание: даже если вы добавляете в конец, несоответствие размера по-прежнему остается проблемой).
Комментарии:
1. Ага! Понял. Действительно, заглянув немного дальше в дизассемблирование перед вызовом ctor интерфейса, я вижу, что он выделяет пространство (в данном случае 4 байта) для объекта.