gcc vs clang: вставка функции с помощью-fPIC

рассмотрим этот код:

// foo.cxx
int last;

int next() {
  return ++last;
}

int index(int scale) {
  return next() << scale;
}

при компиляции с gcc 7.2:

$ g++ -std=c++11 -O3 -fPIC

серия:

next():
    movq    last@GOTPCREL(%rip), %rdx
    movl    (%rdx), %eax
    addl    , %eax
    movl    %eax, (%rdx)
    ret
index(int):
    pushq   %rbx
    movl    %edi, %ebx
    call    next()@PLT    ## next() not inlined, call through PLT
    movl    %ebx, %ecx
    sall    %cl, %eax
    popq    %rbx
    ret

однако при компиляции того же кода с теми же флагами с использованием clang 3.9 вместо этого:

next():                               # @next()
    movq    last@GOTPCREL(%rip), %rcx
    movl    (%rcx), %eax
    incl    %eax
    movl    %eax, (%rcx)
    retq

index(int):                              # @index(int)
    movq    last@GOTPCREL(%rip), %rcx
    movl    (%rcx), %eax
    incl    %eax              ## next() was inlined!
    movl    %eax, (%rcx)
    movl    %edi, %ecx
    shll    %cl, %eax
    retq

вызовы gcc next() через PLT, лязг inlines его. Оба по-прежнему lookup last из GOT. Для компиляции в linux, правильно ли clang, чтобы сделать эту оптимизацию, и gcc отсутствует на простой вставке, или неправильно clang, чтобы сделать это оптимизация, или это чисто проблема QoI?

1 ответов


я не думаю, что стандарт входит в эту деталь. Он просто говорит, что если символ имеет внешнюю связь в разных единицах перевода, это один и тот же символ. Это делает версию clang правильной.

С этого момента, насколько мне известно, мы вышли из стандарта. Выбор компиляторов отличается от того, что они считают полезным -fPIC выход.

отметим, что g++ -c -std=c++11 -O3 -fPIE выходы:

0000000000000000 <_Z4nextv>:
   0:   8b 05 00 00 00 00       mov    0x0(%rip),%eax        # 6 <_Z4nextv+0x6>
   6:   83 c0 01                add    x1,%eax
   9:   89 05 00 00 00 00       mov    %eax,0x0(%rip)        # f <_Z4nextv+0xf>
   f:   c3                      retq   

0000000000000010 <_Z5indexi>:
  10:   8b 05 00 00 00 00       mov    0x0(%rip),%eax        # 16 <_Z5indexi+0x6>
  16:   89 f9                   mov    %edi,%ecx
  18:   83 c0 01                add    x1,%eax
  1b:   89 05 00 00 00 00       mov    %eax,0x0(%rip)        # 21 <_Z5indexi+0x11>
  21:   d3 e0                   shl    %cl,%eax
  23:   c3                      retq

так GCC тут знаю как оптимизировать это. Он просто выбирает не при использовании -fPIC. Но почему? Я вижу только одно объяснение: сделать возможным переопределение символа во время динамической компоновки и последовательно видеть эффекты. Техника известна как символ взаиморасположение.

в общей библиотеке, если index звонки next, as next глобально виден, gcc должен рассмотреть возможность того, что next можно было бы вмешаться. Поэтому он использует PLT. При использовании , вы не разрешается вставлять символы, поэтому gcc включает оптимизацию.

так лязгать не так? Нет. Но gcc, похоже, обеспечивает лучшую поддержку интерпозиции символов, что удобно для инструментирования кода. Это происходит за счет некоторых накладных расходов, если используется -fPIC вместо -fPIE для создания его исполняемого файла.


дополнительно:

на эта запись в блоге от одного из разработчиков gcc, упоминает он, в конце после:

сравнивая некоторые тесты с clang, я заметил, что clang фактически игнорирует правила интерпозиции ELF. Хотя это ошибка, я решил добавить -fno-semantic-interposition флаг для GCC, чтобы получить подобное поведение. Если интерпозиция нежелательна, официальный ответ ELF должен использовать скрытую видимость, и если символ должен быть экспортирован, определите псевдоним. Это не всегда практично делать вручную.

следуя этой зацепке, я приземлился на x86-64 ABI spec. В разделе 3.5.5 он предписывает, что все функции, вызывающие глобально видимые символы, должны проходить через PLT (он доходит до определения точной последовательности команд для использования в зависимости от модели памяти).

таким образом, хотя он не нарушает стандарт C++, игнорирование семантического интерпозиции, похоже, нарушает ABI.


последнее слово: не знал, куда это положить, но это может вас заинтересовать. Я избавлю тебя от свалок., но мои тесты с параметрами objdump и компилятора показали, что:

на стороне gcc вещей:

  • gcc -fPIC: обращается к last идет через GOT, звонки в next() проходит через PLT.
  • gcc -fPIC -fno-semantic-interposition: last проходит гот,next() встроена.
  • gcc -fPIE: last является IP-относительным,next() встроена.
  • -fPIE подразумевает -fno-semantic-interposition

на стороне лязга вещей:

  • clang -fPIC: last проходит гот,next() встроена.
  • clang -fPIE: last проходит гот,next() встроена.

и модифицированная версия, которая компилируется в IP-relative, встроена в оба компилятора:

// foo.cxx
int last_ __attribute__((visibility("hidden")));
extern int last __attribute__((alias("last_")));

int __attribute__((visibility("hidden"))) next_()
{
  return ++last_;
}
// This one is ugly, because alias needs the mangled name. Could extern "C" next_ instead.
extern int next() __attribute__((alias("_Z5next_v")));

int index(int scale) {
  return next_() << scale;
}

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