Подтвердить что ты не робот

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

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

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

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

например. Вот тестовое приложение (a.out), которое я написал, которое использует код (Interface::some_method) из тестовой общей библиотеки (libInterface.so):

[email protected]:~/pimpl$ objdump -d -j .text a.out 
08048874 <main>:
...
 8048891:   e8 b2 fe ff ff          call   8048748 <[email protected]>

В вызове some_method используется таблица процедур (PLT):

[email protected]:~/pimpl$ objdump -d -j .plt a.out 

08048748 <[email protected]>:
 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:

[email protected]:~/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 на самом деле является сегментом кода из общей библиотеки.

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

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

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

4b9b3361

Ответ 1

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

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