Глобальная оптимизация констант и размещение символов - PullRequest
0 голосов
/ 27 октября 2018

Я экспериментировал с gcc и clang, чтобы выяснить, могут ли они оптимизировать

#define SCOPE static
SCOPE const struct wrap_ { const int x; } ptr = { 42 /*==0x2a*/ };
SCOPE struct wrap { const struct wrap_ *ptr; } const w = { &ptr };
int ret_global(void) { return w.ptr->x; }

для возврата промежуточной константы.

Оказывается, они могут:

0000000000000010 <ret_global>:
   10:  b8 2a 00 00 00          mov    $0x2a,%eax
   15:  c3                      retq   

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

И это действительно так:

#!/bin/sh -eu
: ${CC:=gcc}
cat > lib.c <<EOF
int ret_42(void) { return 42; }

#define SCOPE 
SCOPE const struct wrap_ { const int x; } ptr = { 42 };
SCOPE struct wrap { const struct wrap_ *ptr; } const w = { &ptr };
int ret_global(void) { return w.ptr->x; }
int ret_fn_result(void) { return ret_42()+1; }
EOF

cat > lib_override.c <<EOF
int ret_42(void) { return 50; }

#define SCOPE
 SCOPE const struct wrap_ { const int x; } ptr = { 60 };
SCOPE struct wrap { const struct wrap_ *ptr; } const w = { &ptr };
EOF

cat > main.c <<EOF
#include <stdio.h>
int ret_42(void), ret_global(void), ret_fn_result(void);
struct wrap_ { const int x; };
extern struct wrap { const struct wrap_ *ptr; } const w;
int main(void)
{
    printf("ret_42()=%d\n", ret_42());
    printf("ret_fn_result()=%d\n", ret_fn_result());
    printf("ret_global()=%d\n", ret_global());
    printf("w.ptr->x=%d\n",w.ptr->x);
}
EOF
for c in *.c; do
    $CC -fpic -O2 $c -c
    #$CC -fpic -O2 $c -c -fno-semantic-interposition
done
$CC lib.o -o lib.so -shared
$CC lib_override.o -o lib_override.so -shared
$CC main.o $PWD/lib.so
export LD_LIBRARY_PATH=$PWD
./a.out
LD_PRELOAD=$PWD/lib_override.so ./a.out

выходы

ret_42()=42
ret_fn_result()=43
ret_global()=42
w.ptr->x=42
ret_42()=50
ret_fn_result()=51
ret_global()=42
w.ptr->x=60

Можно ли компилятору заменить ссылки для внешних глобальных переменных на промежуточные? Разве они не должны быть взаимозаменяемыми?


Редактировать:

Gcc не оптимизирует вызовы внешних функций (если не скомпилировано с -fno-semantic-interposition)
например, вызов ret_42() в int ret_fn_result(void) { return ret_42()+1; }, хотя, как и в случае ссылки на переменную extern global const, единственный способ изменить определение символа - это интерпозиция.

  0000000000000020 <ret_fn_result>:
  20:   48 83 ec 08             sub    $0x8,%rsp
  24:   e8 00 00 00 00          callq  29 <ret_fn_result+0x9>
  29:   48 83 c4 08             add    $0x8,%rsp
  2d:   83 c0 01                add    $0x1,%eax

Я всегда предполагал, что это должно допускать возможность вставки символов. Кстати, Clang их оптимизирует.

Интересно, где (если где-либо) написано, что ссылка на extern const w в ret_global() может быть оптимизирована до промежуточного, тогда как вызов на ret_42() в ret_fn_result не может.

В любом случае, кажется, что расположение символов ужасно несовместимо и ненадежно в разных компиляторах, если вы не устанавливаете границы единиц перевода. : / (Было бы хорошо, если бы просто все глобалы были последовательно вставляемыми, если не включен -fno-semantic-interposition, но можно только пожелать.)

Ответы [ 3 ]

0 голосов
/ 27 октября 2018

Вы можете использовать LD_DEBUG=bindings для отслеживания привязки символов. В этом случае он печатает (среди прочего):

 17570: binding file /tmp/lib.so [0] to /tmp/lib_override.so [0]: normal symbol `ptr'
 17570: binding file /tmp/lib_override.so [0] to /tmp/lib_override.so [0]: normal symbol `ptr'
 17570: binding file ./a.out [0] to /tmp/lib_override.so [0]: normal symbol `ret_42'
 17570: binding file ./a.out [0] to /tmp/lib_override.so [0]: normal symbol `ret_global'

Таким образом, объект ptr в lib.so действительно вставлен, но основная программа никогда не вызывает ret_global в исходной библиотеке. Вызов поступает на ret_global из предварительно загруженной библиотеки, потому что функция также вставлена.

0 голосов
/ 27 октября 2018

Согласно Что такое трюк LD_PRELOAD? , LD_PRELOAD - это переменная среды, которая позволяет пользователям загружать библиотеку до загрузки любой другой библиотеки, включая libc.so.

Из этого определения это означает 2 вещи:

  1. Библиотека, указанная в LD_PRELOAD, может перегружать символы из другой библиотеки.

  2. Однако, если указанная библиотека не содержит символа, другие библиотеки будут искать этот символ как обычно.

Здесь вы указали LD_PRELOAD как lib_override.so, он определяет int ret_42(void) и глобальную переменную ptr и w, но он не определяет int ret_global(void).

Таким образом, int ret_global(void) будет загружен из lib.so, и эта функция будет напрямую возвращать 42, поскольку компилятор не видит возможности, что ptr и w из lib.c могут быть изменены во время выполнения (они будут вставьте int const data section в elf, linux, гарантируя, что они не могут быть изменены во время выполнения аппаратной защитой памяти), поэтому компилятор оптимизировал это для непосредственного возврата 42.

Редактировать - тест:

Итак, я сделал некоторые изменения в вашем скрипте:

#!/bin/sh -eu
: ${CC:=gcc}
cat > lib.c <<EOF
int ret_42(void) { return 42; }

#define SCOPE
SCOPE const struct wrap_ { const int x; } ptr = { 42 };
SCOPE struct wrap { const struct wrap_ *ptr; } const w = { &ptr };
int ret_global(void) { return w.ptr->x; }
EOF

cat > lib_override.c <<EOF
int ret_42(void) { return 50; }

#define SCOPE
 SCOPE const struct wrap_ { const int x; } ptr = { 60 };
SCOPE struct wrap { const struct wrap_ *ptr; } const w = { &ptr };
int ret_global(void) { return w.ptr->x; }
EOF

cat > main.c <<EOF
#include <stdio.h>
int ret_42(void), ret_global(void);
struct wrap_ { const int x; };
extern struct wrap { const struct wrap_ *ptr; } const w;
int main(void)
{
    printf("ret_42()=%d\n", ret_42());
    printf("ret_global()=%d\n", ret_global());
    printf("w.ptr->x=%d\n",w.ptr->x);
}
EOF
for c in *.c; do gcc -fpic -O2 $c -c; done
$CC lib.o -o lib.so -shared
$CC lib_override.o -o lib_override.so -shared
$CC main.o $PWD/lib.so
export LD_LIBRARY_PATH=$PWD
./a.out
LD_PRELOAD=$PWD/lib_override.so ./a.out

И на этот раз он печатает:

ret_42()=42
ret_global()=42
w.ptr->x=42
ret_42()=50
ret_global()=60
w.ptr->x=60

Редактировать - вывод:

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

Редактировать - объяснение, почему int ret_global(void) перегружается, а ptr и w - нет.

Во-первых, я хочу указать на тип символов, определенных вами (используя приемы из Как мне перечислить символы в файле .so

Файл lib.so:

Symbol table '.dynsym' contains 13 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     5: 0000000000001110     6 FUNC    GLOBAL DEFAULT   12 ret_global
     6: 0000000000001120    17 FUNC    GLOBAL DEFAULT   12 ret_fn_result
     7: 000000000000114c     0 FUNC    GLOBAL DEFAULT   14 _fini
     8: 0000000000001100     6 FUNC    GLOBAL DEFAULT   12 ret_42
     9: 0000000000000200     4 OBJECT  GLOBAL DEFAULT    1 ptr
    10: 0000000000003018     8 OBJECT  GLOBAL DEFAULT   22 w

Symbol table '.symtab' contains 28 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
    23: 0000000000001100     6 FUNC    GLOBAL DEFAULT   12 ret_42
    24: 0000000000001110     6 FUNC    GLOBAL DEFAULT   12 ret_global
    25: 0000000000001120    17 FUNC    GLOBAL DEFAULT   12 ret_fn_result
    26: 0000000000003018     8 OBJECT  GLOBAL DEFAULT   22 w
    27: 0000000000000200     4 OBJECT  GLOBAL DEFAULT    1 ptr

Файл lib_override.so:

Symbol table '.dynsym' contains 11 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     6: 0000000000001100     6 FUNC    GLOBAL DEFAULT   12 ret_42
     7: 0000000000000200     4 OBJECT  GLOBAL DEFAULT    1 ptr
     8: 0000000000001108     0 FUNC    GLOBAL DEFAULT   13 _init
     9: 0000000000001120     0 FUNC    GLOBAL DEFAULT   14 _fini
    10: 0000000000003018     8 OBJECT  GLOBAL DEFAULT   22 w

Symbol table '.symtab' contains 26 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
    23: 0000000000001100     6 FUNC    GLOBAL DEFAULT   12 ret_42
    24: 0000000000003018     8 OBJECT  GLOBAL DEFAULT   22 w
    25: 0000000000000200     4 OBJECT  GLOBAL DEFAULT    1 ptr

Вы обнаружите, что, несмотря на то, что оба символа являются GLOBAL, все функции отмечены как тип FUNC, который является перегружаемым, в то время как все переменные имеют тип OBJECT. Тип OBJECT означает, что он не перегружен, поэтому компилятору не нужно использовать разрешение символов для получения данных.

Для получения дополнительной информации об этом, отметьте это: Что такое «условные» символы? .

0 голосов
/ 27 октября 2018

РЕДАКТИРОВАТЬ: Вопрос: I wonder where (if anywhere) it says that the reference to extern const w in ret_global() can be optimized to an intermediate while the call to ret_42() in ret_fn_result cannot.

TLDR; Логика этого поведения (по крайней мере, для GCC)

  • Оптимизация сворачивания констант компилятора, способная включать сложные константные переменные и структуры

  • Поведение компилятора по умолчанию для функций - экспорт. Если флаг -fvisibility=hidden не используется, все функции экспортируются. Поскольку любая определенная функция экспортируется, она не может быть встроенной. Таким образом, вызов ret_42 в ret_fn_result не может быть встроен. Включите -fvisibility=hidden, результат будет таким, как показано ниже.

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

  • Для этого объекта действуют другие флаги. Самые известные:

    • -Bsymbolic, -Bsymbolic-functions и --dynamic-list как для SO .

    • -fno-semantic-interposition

    • флаги оптимизации курса

Функция ret_fn_result, когда ret_42 скрыто, не экспортируется, а затем встроено.

0000000000001110 <ret_fn_result>:
    1110:   b8 2b 00 00 00          mov    $0x2b,%eax
    1115:   c3                      retq   

1054 * Технический * ШАГ # 1, тема определена в lib.c: SCOPE const struct wrap_ { const int x; } ptr = { 42 }; SCOPE struct wrap { const struct wrap_ *ptr; } const w = { &ptr }; int ret_global(void) { return w.ptr->x; } Когда lib.c компилируется, w.ptr->x оптимизируется до const. Таким образом, при постоянном сворачивании получается: $ object -T lib.so lib.so: file format elf64-x86-64 DYNAMIC SYMBOL TABLE: 0000000000000000 w D *UND* 0000000000000000 _ITM_deregisterTMCloneTable 0000000000000000 w D *UND* 0000000000000000 __gmon_start__ 0000000000000000 w D *UND* 0000000000000000 _ITM_registerTMCloneTable 0000000000000000 w DF *UND* 0000000000000000 GLIBC_2.2.5 __cxa_finalize 0000000000001110 g DF .text 0000000000000006 Base ret_42 0000000000002000 g DO .rodata 0000000000000004 Base ptr 0000000000001120 g DF .text 0000000000000006 Base ret_global 0000000000001130 g DF .text 0000000000000011 Base ret_fn_result 0000000000003e18 g DO .data.rel.ro 0000000000000008 Base w Где ptr и w установлены в rodata и data.rel.ro (потому что указатель const) соответственно. Постоянное сворачивание приводит к следующему коду: 0000000000001120 <ret_global>: 1120: b8 2a 00 00 00 mov $0x2a,%eax 1125: c3 retq Другая часть: int ret_42(void) { return 42; } int ret_fn_result(void) { return ret_42()+1; } Здесь ret_42 - это функция, так как не скрытая, это экспортируемая функция. Так что это code. И оба приводят к: 0000000000001110 <ret_42>: 1110: b8 2a 00 00 00 mov $0x2a,%eax 1115: c3 retq 0000000000001130 <ret_fn_result>: 1130: 48 83 ec 08 sub $0x8,%rsp 1134: e8 f7 fe ff ff callq 1030 <ret_42@plt> 1139: 48 83 c4 08 add $0x8,%rsp 113d: 83 c0 01 add $0x1,%eax 1140: c3 retq Учитывая, что компилятор знает только lib.c, мы закончили. Отложите lib.so в сторону. ШАГ # 2, компилировать lib_override.c: int ret_42(void) { return 50; } #define SCOPE SCOPE const struct wrap_ { const int x; } ptr = { 60 }; SCOPE struct wrap { const struct wrap_ *ptr; } const w = { &ptr }; Что просто: $ objdump -T lib_override.so lib_override.so: file format elf64-x86-64 DYNAMIC SYMBOL TABLE: 0000000000000000 w D *UND* 0000000000000000 _ITM_deregisterTMCloneTable 0000000000000000 w D *UND* 0000000000000000 __gmon_start__ 0000000000000000 w D *UND* 0000000000000000 _ITM_registerTMCloneTable 0000000000000000 w DF *UND* 0000000000000000 GLIBC_2.2.5 __cxa_finalize 00000000000010f0 g DF .text 0000000000000006 Base ret_42 0000000000002000 g DO .rodata 0000000000000004 Base ptr 0000000000003e58 g DO .data.rel.ro 0000000000000008 Base w Экспортируемые функции ret_42, а затем ptr и w устанавливаются в rodata и data.rel.ro (потому что указатель const) соответственно. Постоянное сворачивание приводит к следующему коду: 00000000000010f0 <ret_42>: 10f0: b8 32 00 00 00 mov $0x32,%eax 10f5: c3 retq ШАГ 3, скомпилируйте main.c, давайте сначала посмотрим на объект: $ objdump -t main.o # SKIPPED 0000000000000000 *UND* 0000000000000000 _GLOBAL_OFFSET_TABLE_ 0000000000000000 *UND* 0000000000000000 ret_42 0000000000000000 *UND* 0000000000000000 printf 0000000000000000 *UND* 0000000000000000 ret_fn_result 0000000000000000 *UND* 0000000000000000 ret_global 0000000000000000 *UND* 0000000000000000 w У нас все символы не определены. Поэтому они должны прийти откуда-то. Затем мы связываем по умолчанию с lib.so и кодом (printf и другие опущены): 0000000000001070 <main>: 1074: e8 c7 ff ff ff callq 1040 <ret_42@plt> 1089: e8 c2 ff ff ff callq 1050 <ret_fn_result@plt> 109e: e8 bd ff ff ff callq 1060 <ret_global@plt> 10b3: 48 8b 05 2e 2f 00 00 mov 0x2f2e(%rip),%rax # 3fe8 <w> Теперь у нас в руках lib.so, lib_override.so и a.out. Давайте просто позвоним a.out: main => ret_42 => lib.so => ​​ret_42 => return 42 main => ret_fn_result => lib.so => ​​ret_fn_result => return (lib.so => ​​ret_42 => return 42) + 1 main => ret_global => lib.so => ​​ret_global => вернуть родата 42 main => lib.so => ​​w.ptr-> x = rodata 42 Теперь давайте предварительно загрузим lib_override.so: main => ret_42 => lib_override.so => ​​ret_42 => return 50 main => ret_fn_result => lib.so => ​​ret_fn_result => return (lib_override.so => ​​ret_42 => return 50) + 1 main => ret_global => lib.so => ​​ret_global => return rodata 42 main => lib_override.so => ​​w.ptr-> x = rodata 60 Для 1: main вызовы ret_42 из lib_override.so, поскольку он предварительно загружен, ret_42 теперь разрешается в единицу в lib_override.so. Для 2: main вызывает ret_fn_result из lib.so, что вызывает ret_42, но из lib_override.so, потому что теперь оно разрешается до единицы в lib_override.so. Для 3: main вызывает ret_global из lib.so, что возвращает сложенную константу 42. Для 4: main читает внешний указатель, который указывает на lib_override.so, потому что он предварительно загружен. Наконец, как только lib.so генерируется со сложенными константами, которые встроены, никто не может требовать их переопределения. Если у вас есть намерение переопределить структуру данных, нужно определить ее другим способом (предоставить функции для управления ими, не использовать константы и т. Д.). Потому что при определении чего-то как константы намерение понятно, и компилятор делает то, что делает. Тогда, даже если этот же символ определен как не константный в main.c или другом месте, он не может быть unfolded обратно в lib.c. #!/bin/sh -eu : ${CC:=gcc} cat > lib.c <<EOF int ret_42(void) { return 42; } #define SCOPE SCOPE const struct wrap_ { const int x; } ptr = { 42 }; SCOPE struct wrap { const struct wrap_ *ptr; } const w = { &ptr }; int ret_global(void) { return w.ptr->x; } int ret_fn_result(void) { return ret_42()+1; } EOF cat > lib_override.c <<EOF int ret_42(void) { return 50; } #define SCOPE SCOPE const struct wrap_ { const int x; } ptr = { 60 }; SCOPE struct wrap { const struct wrap_ *ptr; } const w = { &ptr }; EOF cat > main.c <<EOF #include <stdio.h> int ret_42(void), ret_global(void), ret_fn_result(void); struct wrap_ { const int x; }; extern struct wrap { const struct wrap_ *ptr; } const w; int main(void) { printf("ret_42()=%d\n", ret_42()); printf("ret_fn_result()=%d\n", ret_fn_result()); printf("ret_global()=%d\n", ret_global()); printf("w.ptr->x=%d\n",w.ptr->x); } EOF for c in *.c; do gcc -fpic -O2 $c -c; done $CC lib.o -o lib.so -shared $CC lib_override.o -o lib_override.so -shared $CC main.o $PWD/lib.so export LD_LIBRARY_PATH=$PWD ./a.out LD_PRELOAD=$PWD/lib_override.so ./a.out

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...