Как добавление частной переменной-члена нарушает совместимость с C ABI?

#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 байта) для объекта.