2016-05-01 5 views
2

Ich versuche Assembly zu lernen, indem ich einfache Funktionen kompiliere und die Ausgabe betrachte.Warum referenziert gcc nicht den PLT für Funktionsaufrufe?

Ich suche Funktionen in anderen Bibliotheken aufrufen. Hier ist eine Spielzeug-C-Funktion, die eine Funktion an anderer Stelle definiert ruft:

void give_me_a_ptr(void*); 

void foo() { 
    give_me_a_ptr("foo"); 
} 

Hier ist die Montage von gcc erzeugt:

$ gcc -Wall -Wextra -g -O0 -c call_func.c 
$ objdump -d call_func.o 

call_func.o:  file format elf64-x86-64 

Disassembly of section .text: 

0000000000000000 <foo>: 
    0: 55      push %rbp 
    1: 48 89 e5    mov %rsp,%rbp 
    4: bf 00 00 00 00   mov $0x0,%edi 
    9: e8 00 00 00 00   callq e <foo+0xe> 
    e: 90      nop 
    f: 5d      pop %rbp 
    10: c3      retq 

Ich war so etwas wie call <[email protected]> erwartet. Warum springt dieser zu einer relativen Position, bevor er überhaupt weiß, wo give_me_a_ptr definiert ist?

Ich bin auch verwirrt von mov $0, %edi. Das sieht so aus, als würde es einen Nullzeiger übergeben - sicherlich wäre mov $address_of_string, %rdi hier richtig?

+0

Verwenden Sie 'objdump -dr', um Verschiebungseinträge anzuzeigen. – Jester

+1

FYI, 'clang-S' gibt dies anstelle Ihres mov 0x0/callq-Paares aus:' leaq L_.str (% rip),% rdi; callq _give_me_a_ptr'. –

+2

Wie für die PLT, die nur verwendet wird, wenn Sie PIC kompilieren, verwenden Sie das Flag "-fPIC". – Jester

Antwort

6

Sie sind nicht mit symbol-interposition Bau freigegeben (eine Nebenwirkung von -fPIC), so dass die call Zieladresse kann potenziell zur Verknüpfungszeit an eine Adresse in einer anderen Objektdatei aufgelöst werden, die statisch in die gleiche ausführbaren Datei verknüpft wird. (z.B. gcc foo.o bar.o).

Wenn jedoch das Symbol nur in einer Bibliothek gefunden ist, dass Sie dynamisch zu verknüpfen (gcc foo.o -lbar), die call hat durch die PLT werden indirected zu unterstützen.

Nun ist diese der schwierige Teil ist: without -fPIC or -fPIE, gcc emittiert noch asm, dass die Funktion direkt aufruft:

int puts(const char*);   // puts exists in libc, so we can link this example 
void call_puts(void) { puts("foo"); } 

    # gcc 5.3 -O3 (without -fPIC) 
    movl $.LC0, %edi  # absolute 32bit addressing: slightly smaller code, because static data is known to be in the low 2GB, in the default "small" code model 
    jmp  puts    # tail-call optimization. Same as call puts/ret, except for stack alignment 

Aber wenn man sich die verknüpften binären aussehen: (auf this Godbolt compiler explorer link, klicken Sie auf die „binäre“ Taste zwischen gcc -S ASM Ausgang und objdump -dr Demontage)

# disassembled linker output 
    mov $0x400654,%edi 
    jmpq 400490 <[email protected]> 

während eines Bindens war der Anruf zu puts „magische“ mit Indirektion ersetzt, um durch [email protected] und eine [email protected] Definition ist in der verknüpften ausführbaren Datei vorhanden.

Ich weiß nicht die Details, wie dies funktioniert, aber es ist bei Link-Zeit beim Link zu einer gemeinsam genutzten Bibliothek getan. Entscheidend ist, dass in den Header-Dateien nichts erforderlich ist, um den Funktionsprototyp als gemeinsam genutzte Bibliothek zu kennzeichnen. Sie erhalten die gleichen Ergebnisse von einschließlich <stdio.h> wie Sie von puts selbst zu deklarieren. (Dies ist sehr nicht empfohlen;.. Es ist wahrscheinlich legal für eine C-Implementierung nur zu arbeiten richtig mit den Erklärungen in Header kommt es vor, auf Linux zu arbeiten, obwohl)


Wenn ein positionsunabhängige ausführbare Kompilieren (mit -fPIE), springt die verknüpfte Binärdatei über den PLT zu puts, identisch wie ohne -fPIC. Allerdings Ausgabe der Compiler asm ist anders (versuchen Sie es selbst auf dem Godbolt Link oben):

call_puts: # compiled with -fPIE 
    leaq .LC0(%rip), %rdi  # RIP-relative addressing for static data 
    jmp  [email protected] 

Der Compiler Kräfte indirection durch die PLT für alle Anrufe auf Funktionen, die es nicht die Definition sehen können. Ich verstehe nicht warum. Im PIE-Modus kompilieren wir Code für eine ausführbare Datei und nicht für eine gemeinsam genutzte Bibliothek.Der Linker sollte in der Lage sein, mehrere Objektdateien in eine positionsunabhängige ausführbare Datei mit direkten Aufrufen zwischen in der ausführbaren Datei definierten Funktionen zu verknüpfen. Ich teste auf Linux (mein Desktop und godbolt), nicht OS X, wobei ich davon ausgehe, dass der Standard ist. Es könnte anders konfiguriert werden, IDK.


Mit -fPIC statt -fPIE, sind die Dinge noch schlimmer: sogar Anrufe auf globale Funktionen innerhalb derselben Übersetzungseinheit muss durch die PLT gehen definiert, symbol interposition zu unterstützen. (Z LD_PRELOAD=intercept_some_functions.so ./a.out)

Die Unterschiede zwischen -fPIC und -fPIE sind in erster Linie, dass PIE kein Symbol Zwischenschaltung für Funktionen in derselben Übersetzungseinheit annehmen kann, aber PIC kann es nicht. OS X erfordert positionsunabhängige ausführbare Dateien sowie gemeinsam genutzte Bibliotheken. Es gibt jedoch eine andere Möglichkeit, was der Compiler tun kann, wenn er Code für eine Bibliothek erstellt oder Code für eine ausführbare Datei erstellt.

Diese Godbolt example hat einige weitere Funktionen, die Dinge über PIC-und PIE-Modus, z. Das call_puts() kann nicht in eine andere Funktion im PIC-Modus, nur PIE.

Siehe auch: Shared object in Linux without symbol interposition, -fno-semantic-interposition error.


verwirrt durch mov $0, %edi

bei Demontage Ausgabe aus dem .o Sie suchen, wo Adressen werden nur 0s Platzhalter, der durch den Linker zur Verknüpfungszeit ersetzt werden, die Verlagerung basierend auf Informationen in der ELF-Objektdatei. Deshalb schlug @Leandros objdump -r vor.

In ähnlicher Weise ist die relative Verschiebung im call Maschinencode nur Nullen, weil der Linker es noch nicht ausgefüllt hat.

-1

Ich studiere noch diesen Linking-Prozess selbst, wollte aber etwas in meinen eigenen Worten wiederholen. Die PLT-bezogenen Benutzerfunktionsaufrufe werden möglicherweise nicht alle zu dem Zeitpunkt, zu dem die Ausführung beginnt, mit dem richtigen Code gefüllt. Dies könnte zu Beginn der Ausführung viel Zeit in Anspruch nehmen; und nicht alle Funktionsaufrufe, die von der PLT instrumentiert werden, könnten sogar verwendet werden. Bei einer "Lazy Binding" -Methode springt also beim ersten Aufruf einer Benutzerfunktion über den PLT-Code immer zuerst die PLT-Bindungsfunktion. Die Bindungsfunktion geht aus und findet die richtige Adresse für die Benutzerfunktion (ich denke von der GOT) und ersetzt dann den PLT-Eintrag (der auf die Bindungsfunktion verweist) durch den Code, der auf die Benutzerfunktion zeigt. Also wird jedesmal, wenn die Benutzerfunktion aufgerufen wird, die "faule" Bindungsfunktion nicht aufgerufen; stattdessen wird die Benutzerfunktion aufgerufen. Dies könnte der Grund sein, warum der PLT-Eintrag auf den ersten Blick seltsam aussieht; Es zeigt auf die Bindungsfunktion und nicht auf die Benutzerfunktion.

+0

Verzögerte Bindung tritt nach dem ersten Aufruf eines PLT-Eintrags auf und hat keinen Einfluss darauf, ob der Compiler "Aufrufputs" oder "Aufruf puts @ PLT" verwendet. Es ändert auch nicht die PLT, sondern modifiziert die GOT. Der PLT ist schreibgeschützt und verwendet indirekte JMP-Anweisungen in Form von 'puts @ PLT: jmp [puts @ GOTPLT]'. Anfänglich zeigt der Eintrag "puts @ GOTPLT" im GOT auf den Code, der den Lazy-Binding-Code aufruft, nachdem die gemeinsame Bibliothek gebunden wurde, zeigt er auf "puts" in der gemeinsam genutzten Bibliothek. –

Verwandte Themen