Как добавление закрытой переменной-члена нарушает совместимость C ++ ABI? - PullRequest
12 голосов
/ 08 октября 2011

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

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

Я много читал об ELF-файлах и о том, как на самом деле работает динамическое связывание, но я до сих пор не понимаю, как меняетсяразмер класса в разделяемой библиотеке может привести к поломке.

Например, я написал тестовое приложение (a.out), которое использует код (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 и так далее.при последующих вызовах some_method код в GOT на самом деле является сегментом кода из общей библиотеки.

Или что-то в этом роде.

Но, учитывая вышесказанное, я все еще не понимаю, как размер класса разделяемой библиотеки или смещения ее методов здесь играют роль.Насколько я могу судить, описанные выше шаги не зависят от размера класса.Похоже, только символ имя метода в библиотеке включен в a.out.Любые изменения в размере класса должны быть разрешены только во время выполнения, когда компоновщик загружает код в GOT, нет?

Что мне здесь не хватает?

1 Ответ

16 голосов
/ 08 октября 2011

Основная проблема заключается в том, что при выделении нового экземпляра класса (либо в стеке, либо через new) вызывающему коду необходимо знать размер объекта. Если позже вы измените размер объекта (добавив закрытый элемент), это увеличит необходимый размер; однако ваши абоненты все еще используют старый размер. Таким образом, вы в конечном итоге не выделяете достаточно места для хранения объекта, и конструктор объекта затем продолжает повреждать стек (или кучу), поскольку предполагает, что у него достаточно места.

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

...