Рассмотрим этот код:
// 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 $1, %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, Clang встраивает это. Оба все еще ищут last
из полученного. Для компиляции в Linux, правильно ли clang сделать эту оптимизацию, а gcc упускает простое встраивание, или неверно clang, чтобы сделать эту оптимизацию, или это просто проблема QoI?
Я не думаю, что в стандарте так много деталей. Это просто говорит о том, что примерно, если символ имеет внешнюю связь в разных единицах перевода, это один и тот же символ. Это делает верную версию 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 $0x1,%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 $0x1,%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
, как next
является глобально видимым, gcc должен учитывать возможность того, что next
может быть вставлен. Так что он использует PLT. Когда используешь -fPIE
однако вам не разрешено вставлять символы, поэтому 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-относительную, встроена в оба компилятора:
// 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;
}
По сути, это явно означает, что, несмотря на то, что мы делаем их доступными в глобальном масштабе, мы используем скрытую версию этих символов, которая будет игнорировать любое взаимное расположение. Затем оба компилятора полностью оптимизируют доступ независимо от переданных параметров.
Других решений пока нет …