2017-08-30 64 views
8

Rozważmy kod:GCC vs brzękiem: inline funkcję z -fPIC

// foo.cxx 
int last; 

int next() { 
    return ++last; 
} 

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

podczas kompilacji z gcc 7.2:

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

ten emituje:

next(): 
    movq [email protected](%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 

Jednakże, gdy kompilacja tego samego kodu z tymi samymi flagami przy użyciu języka 3.9 zamiast:

next():        # @next() 
    movq [email protected](%rip), %rcx 
    movl (%rcx), %eax 
    incl %eax 
    movl %eax, (%rcx) 
    retq 

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

gcc wywołania next() za pośrednictwem PLT, clang podkreśla je. Oba nadal szukają last z GOT. Do kompilacji na Linuksie, jest clang prawo, aby ta optymalizacja i gcc brakuje na łatwej inline, lub jest błędny clang, aby dokonać tej optymalizacji, czy jest to kwestia czysto QoI?

Odpowiedz

13

Nie sądzę, że standard idzie w tak wiele szczegółów. Mówi jedynie, że z grubsza, jeśli symbol ma zewnętrzne powiązanie w różnych jednostkach tłumaczeniowych, jest to ten sam symbol. To sprawia, że ​​wersja klangowa jest poprawna.

Od tego momentu, zgodnie z moją najlepszą wiedzą, jesteśmy poza standardem. Wybory kompilatorów różnią się w zależności od tego, co uważają za przydatne.

Zauważ, że g++ -c -std=c++11 -O3 -fPIE wyjścia:

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 

Więc GCC robi wiedzieć, jak zoptymalizować tego. Po prostu nie wybiera, kiedy używasz -fPIC. Ale dlaczego? Widzę tylko jedno wytłumaczenie: pozwól na przesłonięcie symbolu podczas dynamicznego łączenia i spójrz spójnie efekty. Ta technika znana jest pod nazwą symbol interposition.

W udostępnionej biblioteki, jeśli index rozmowy next, jak next jest widoczne globalnie, gcc musi rozważyć możliwość, że next może zostać wstawiona. Więc używa PLT. Podczas korzystania z -fPIE nie można jednak wstawiać symboli, więc gcc włącza optymalizację.

Czy błąd jest nieprawidłowy? Nie. Ale gcc wydaje się zapewniać lepsze wsparcie dla interpozycji symboli, co jest przydatne przy instrumentowaniu kodu. Czyni to kosztem pewnych kosztów ogólnych, jeśli zamiast tego używa się -fPIC zamiast -fPIE do budowania jego pliku wykonywalnego.


Dodatkowe uwagi:

W this blog entry z jednego z gcc programistów, wspomina, po zakończeniu postu:

Porównując niektóre odniesienia do dzyń, zauważyłem, że szczęk faktycznie ignoruj ​​reguły interpolacji ELF. Chociaż jest to błąd, postanowiłem dodać flagę GCC, aby uzyskać podobne zachowanie. Jeśli interpozycja nie jest pożądana, oficjalną odpowiedzią ELF jest użycie ukrytej widoczności i jeśli symbol ma zostać wyeksportowany, zdefiniuj alias. Nie zawsze jest to praktyczne zadanie ręczne.

Po tym wyjściu wylądował mnie na x86-64 ABI spec. W sekcji 3.5.5 nakazuje, aby wszystkie funkcje wywołujące widoczne na całym świecie symbole musiały przejść przez PLT (idzie to aż do określenia dokładnej sekwencji instrukcji do użycia w zależności od modelu pamięci).

Tak więc, chociaż nie narusza standardu C++, ignorowanie interpozycji semantycznej wydaje się naruszać ABI.


Ostatnie słowo: nie wiedziałem, gdzie to umieścić, ale może cię zainteresować. Oszczędzę ci wysypisk, ale moje testy z opcji objdump i kompilatora wykazały, że:

Na gcc stronie rzeczy:

  • gcc -fPIC: uzyskuje dostęp do last przechodzi przez GOT, wzywa do next() przechodzi przez PLT.
  • gcc -fPIC -fno-semantic-interposition: last przechodzi przez GOT, next() jest zakreślony.
  • gcc -fPIE:last jest względne IP, next() jest wstawione.
  • -fPIE implikuje -fno-semantic-interposition

Na stronie dzyń rzeczy:

  • clang -fPIC:last przechodzi przez GOT, next() jest inlined.
  • clang -fPIE:last przechodzi przez GOT, next() jest wstawiony.

I zmodyfikowana wersja, która kompiluje do IP-krewnego inlined obu kompilatorów:

// 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; 
} 

Zasadniczo, to wyraźnie zaznacza, że ​​pomimo ich udostępnianie na całym świecie, używamy ukryty wersję tych symboli, które będą zignorować każdy rodzaj interpozycji. Oba kompilatory następnie w pełni optymalizują dostęp, niezależnie od przekazanych opcji.

+0

@Barry> Cieszę się, że okazało się to przydatne, nauczyłem się interesujących rzeczy badających to pytanie. Ciekawiło mnie również "ostatnie" i wykonałem kilka testów; dodał kilka ustaleń do postu. – spectras

+0

Jaka jest przewaga atrybutu aliasu nad 'widocznością (" ukryty ")'? – Barry

+0

@Barry> to, co tylko po to, aby atrybut był widoczny, tak aby semantyczna pozostała równoważna, ale z powodu braku wsparcia interpozycji. Oczywiście, jeśli nie ma zamiaru ich eksportować, można po prostu zlikwidować aliasy i mieć widoczność ("ukryte"). Nie testowałem tego, ale uważam, że kompilowanie z '-fvisibility = hidden -fvisibility-inlines-hidden' i ręczne oznaczanie odpowiednich symboli za pomocą' __attribute __ ((widoczność ("default"))) powinno dać ten sam wynik. – spectras