Überschreiben Sie einen Funktionsaufruf in C.

71

Ich möchte bestimmte Funktionsaufrufe an verschiedene APIs überschreiben, um die Aufrufe zu protokollieren, aber ich möchte möglicherweise auch Daten bearbeiten, bevor sie an die eigentliche Funktion gesendet werden.

Angenommen, ich verwende eine Funktion, die getObjectNamein meinem Quellcode tausende Male aufgerufen wird . Ich möchte diese Funktion manchmal vorübergehend überschreiben, weil ich das Verhalten dieser Funktion ändern möchte, um das unterschiedliche Ergebnis zu sehen.

Ich erstelle eine neue Quelldatei wie folgt:

Ich kompiliere alle meine anderen Quellen wie gewohnt, verknüpfe sie jedoch zuerst mit dieser Funktion, bevor ich sie mit der API-Bibliothek verknüpfe. Dies funktioniert gut, außer dass ich die reale Funktion in meiner überschreibenden Funktion offensichtlich nicht aufrufen kann.

Gibt es eine einfachere Möglichkeit, eine Funktion zu "überschreiben", ohne Fehler / Warnungen beim Verknüpfen / Kompilieren zu erhalten? Idealerweise möchte ich in der Lage sein, die Funktion zu überschreiben, indem ich nur ein oder zwei zusätzliche Dateien kompiliere und verknüpfe, anstatt mit Verknüpfungsoptionen herumzuspielen oder den tatsächlichen Quellcode meines Programms zu ändern.

Dreamlax
quelle
@dreamlax, wir wechseln jetzt von den allgemeinen (C) zu spezifischen (gcc / linux) Lösungen. Es wäre eine gute Idee, zu klären, worauf Sie laufen, um die Antworten besser zielen zu können.
Paxdiablo
1
Nun, ich entwickle unter Linux, aber die Ziele sind Mac OS, Linux und Windows. Einer der Gründe, warum ich Funktionen überschreiben möchte, ist, dass ich vermute, dass sie sich auf verschiedenen Betriebssystemen unterschiedlich verhalten.
Dreamlax

Antworten:

75

Wenn Sie die Aufrufe nur für Ihre Quelle erfassen / ändern möchten, besteht die einfachste Lösung darin, eine Header-Datei ( intercept.h) zusammenzustellen mit:

und implementieren Sie die Funktion wie folgt (in intercept.cder nicht enthalten ist intercept.h):

Stellen Sie dann sicher, dass jede Quelldatei, in der Sie den Anruf abfangen möchten, Folgendes enthält:

oben.

Wenn Sie dann mit " -DINTERCEPT" kompilieren , rufen alle Dateien Ihre Funktion auf und nicht die echte, und Ihre Funktion kann weiterhin die echte aufrufen.

Das Kompilieren ohne " -DINTERCEPT" verhindert das Abfangen.

Es ist etwas schwieriger, wenn Sie alle Anrufe abfangen möchten (nicht nur die von Ihrer Quelle) - dies kann im Allgemeinen durch dynamisches Laden und Auflösen der realen Funktion (mit dlload-und dlsym-Typ von Anrufen) erfolgen, aber ich denke nicht, dass dies bei Ihnen erforderlich ist Fall.

paxdiablo
quelle
1
Verwenden Sie ein Flag zur Kompilierungszeit, um das Abfangen zu steuern (siehe aktualisierte Antwort). Auch dies kann zur Laufzeit erfolgen. Sie müssen es nur in myGetObjectName () erkennen und immer getObjectName aufrufen, wenn das Laufzeitflag gesetzt ist (dh immer noch abfangen, aber das Verhalten ändern).
Paxdiablo
12
Sie können die Option "-include file" verwenden, damit GCC die Datei automatisch einschließt. Sie müssen dann nicht einmal eine Datei berühren :)
Johannes Schaub - litb
1
Das automatische Einschließen kann jedoch zu Problemen beim Einschließen der ursprünglichen API-Datei führen. Ihre Erklärungen werden ebenfalls ersetzt. nicht zu einfach :)
Johannes Schaub - litb
1
Ich mag diese Lösung am besten, da sie auf jedem C-Compiler funktioniert. Es ist der bewährte Weg, der auch seit Jahrzehnten verwendet wird. Ziemlich einfach.
Craig S
2
Dies funktioniert hervorragend für Unit-Test-Suites (wie die Verwendung von CGreen), bei denen Sie einige Abhängigkeiten innerhalb einer Funktion stubben möchten. War eine großartige Antwort für meine Zwecke.
Nrjohnstone
84

Unter gcc können Sie unter gcc das --wrapLinker-Flag wie folgt verwenden:

und definieren Sie Ihre Funktion als:

Dadurch wird sichergestellt, dass alle Aufrufe an getObjectName()Ihre Wrapper-Funktion umgeleitet werden (zur Verbindungszeit). Dieses sehr nützliche Flag fehlt jedoch in gcc unter Mac OS X.

Denken Sie daran, die Wrapper-Funktion mit zu deklarieren, extern "C"wenn Sie mit g ++ kompilieren.

codelogic
quelle
3
Das ist ein schöner Weg. wusste nichts davon. aber wenn ich die Manpage richtig lese, sollte es "__real_getObjectName (anObject)" sein; Dies wird vom Linker an getObjectName weitergeleitet. Andernfalls rufen Sie __wrap_getObjectName erneut rekursiv auf. oder fehlt mir etwas
Johannes Schaub - litb
Sie haben Recht, es muss __real_getObjectName sein, danke. Ich hätte die Manpage noch einmal überprüfen sollen :)
codelogic
1
Ich bin enttäuscht, dass ld unter Mac OS X die --wrapFlagge nicht unterstützt .
Daryl Spitzer
3
Der Compiler liefert übrigens nicht die Deklaration des __real * -Symbols. Sie müssen es selbst mit deklarieren extern char *__real_getObjectName(object *anObject).
Brendan
37

Sie können eine Funktion mit LD_PRELOADtrick - see überschreiben man ld.so. Sie kompilieren die gemeinsam genutzte Bibliothek mit Ihrer Funktion und starten die Binärdatei (Sie müssen die Binärdatei sogar nicht ändern!) Wie LD_PRELOAD=mylib.so myprog.

Im Hauptteil Ihrer Funktion (in der gemeinsam genutzten Bibliothek) schreiben Sie wie folgt:

Sie können jede Funktion aus der gemeinsam genutzten Bibliothek überschreiben, auch aus stdlib, ohne das Programm zu ändern / neu zu kompilieren, sodass Sie den Trick für Programme ausführen können, für die Sie keine Quelle haben. Ist es nicht schön

qrdl
quelle
2
Nein, Sie können auf diese Weise nur eine von einer gemeinsam genutzten Bibliothek bereitgestellte Funktion überschreiben .
Chris Stratton
2
@ChrisStratton Sie haben Recht, Syscall kann auf diese Weise nicht überschrieben werden. Ich habe meine Antwort bearbeitet.
Qrdl
26

Wenn Sie GCC verwenden, können Sie Ihre Funktion festlegen weak. Diese können durch nicht schwache Funktionen überschrieben werden:

test.c :

Was tut es?

test1.c :

Was tut es?

Leider funktioniert das bei anderen Compilern nicht. Sie können jedoch die schwachen Deklarationen, die überschreibbare Funktionen enthalten, in ihrer eigenen Datei haben und nur ein Include in die API-Implementierungsdateien einfügen, wenn Sie mit GCC kompilieren:

schwachdecls.h :

functions.c :

Der Nachteil davon ist, dass es nicht ganz funktioniert, ohne etwas mit den API-Dateien zu tun (diese drei Zeilen und die Schwachstellen werden benötigt). Sobald Sie diese Änderung vorgenommen haben, können Funktionen leicht überschrieben werden, indem Sie eine globale Definition in eine Datei schreiben und diese verknüpfen.

Johannes Schaub - litb
quelle
1
Das würde eine Änderung der API erfordern, nicht wahr?
Dreamlax
Ihr Funktionsname ist derselbe. ändert auch nicht die ABI oder API in irgendeiner Weise. Fügen Sie beim Verknüpfen einfach die überschreibende Datei hinzu, und die nicht schwache Funktion wird aufgerufen. libc / pthread machen diesen Trick: Wenn pthread eingebunden ist, werden seine threadsicheren Funktionen anstelle der schwachen von libc
Johannes Schaub - verwendet. Litb
Ich habe einen Link hinzugefügt. Ich weiß nicht, ob es zu Ihren Zielen passt (dh ob Sie mit diesem GCC-Ding leben können. Wenn msvc etwas Ähnliches hat, können Sie es SCHWACH definieren). aber wenn unter Linux, würde ich diesen verwenden (vielleicht gibt es sogar einen besseren Weg. Ich habe keine Ahnung. Schauen Sie sich auch die Versionierung an).
Johannes Schaub - litb
11

Es ist häufig wünschenswert, das Verhalten vorhandener Codebasen durch Umschließen oder Ersetzen von Funktionen zu ändern. Wenn das Bearbeiten des Quellcodes dieser Funktionen eine praktikable Option ist, kann dies ein unkomplizierter Vorgang sein. Wenn die Quelle der Funktionen nicht bearbeitet werden kann (z. B. wenn die Funktionen von der System-C-Bibliothek bereitgestellt werden), sind alternative Techniken erforderlich. Hier präsentieren wir solche Techniken für UNIX-, Windows- und Macintosh OS X-Plattformen.

Dies ist eine großartige PDF-Datei, die beschreibt, wie dies unter OS X, Linux und Windows gemacht wurde.

Es gibt keine erstaunlichen Tricks, die hier nicht dokumentiert wurden (dies ist übrigens eine erstaunliche Reihe von Antworten) ... aber es ist eine schöne Lektüre.

Abfangen beliebiger Funktionen auf Windows-, UNIX- und Macintosh OS X-Plattformen (2004) von Daniel S. Myers und Adam L. Bazinet .

Sie können das PDF direkt von einem anderen Speicherort herunterladen (aus Redundanzgründen) .

Und schließlich, sollten die beiden vorherigen Quellen irgendwie in Flammen aufgehen, hier ein Google-Suchergebnis dafür .

Kuba hat Monica nicht vergessen
quelle
9

Sie können einen Funktionszeiger als globale Variable definieren. Die Syntax des Anrufers würde sich nicht ändern. Wenn Ihr Programm gestartet wird, kann es überprüfen, ob ein Befehlszeilenflag oder eine Umgebungsvariable gesetzt ist, um die Protokollierung zu aktivieren. Speichern Sie dann den ursprünglichen Wert des Funktionszeigers und ersetzen Sie ihn durch Ihre Protokollierungsfunktion. Sie würden keinen speziellen Build "Protokollierung aktiviert" benötigen. Benutzer können die Protokollierung "vor Ort" aktivieren.

Sie müssen in der Lage sein, den Quellcode des Anrufers zu ändern, nicht jedoch den Angerufenen (dies würde also funktionieren, wenn Sie Bibliotheken von Drittanbietern aufrufen).

foo.h:

foo.cpp:

Chris Peterson
quelle
1
Ich habe über diese Methode nachgedacht, aber es erfordert eine Änderung des Quellcodes, was ich nicht wirklich tun möchte, es sei denn, ich muss. Dies hat jedoch den zusätzlichen Vorteil, dass zur Laufzeit gewechselt wird.
Dreamlax
4

Aufbauend auf der Antwort von @Johannes Schaub mit einer Lösung, die für Code geeignet ist, den Sie nicht besitzen.

Alias ​​die Funktion, die Sie überschreiben möchten, auf eine schwach definierte Funktion und implementieren Sie sie dann selbst neu.

override.h

foo.c

override.c

Verwenden Sie musterspezifische Variablenwerte in Ihrem Makefile, um das Compiler-Flag hinzuzufügen -include override.h.

Nebenbei: Vielleicht können Sie auch -D 'foo(x) __attribute__((weak))foo(x)'Ihre Makros definieren.

Kompilieren Sie die Datei und verknüpfen Sie sie mit Ihrer Neuimplementierung ( override.c).

  • Auf diese Weise können Sie eine einzelne Funktion aus einer beliebigen Quelldatei überschreiben, ohne den Code ändern zu müssen.

  • Der Nachteil ist, dass Sie für jede Datei, die Sie überschreiben möchten, eine separate Header-Datei verwenden müssen.

Vaughan
quelle
3

Es gibt auch eine schwierige Methode, dies im Linker mit zwei Stub-Bibliotheken zu tun.

Bibliothek Nr. 1 ist mit der Hostbibliothek verknüpft und zeigt das Symbol an, das unter einem anderen Namen neu definiert wird.

Bibliothek Nr. 2 ist mit Bibliothek Nr. 1 verknüpft, wobei der Aufruf abgefangen und die neu definierte Version in Bibliothek Nr. 1 aufgerufen wird.

Seien Sie sehr vorsichtig mit Linkbestellungen hier, sonst funktioniert es nicht.

Joshua
quelle
Klingt schwierig, vermeidet jedoch das Ändern der Quelle. Sehr schöner Vorschlag.
Dreamlax
Ich glaube nicht, dass Sie getObjectName zwingen können, ohne dlopen / dlsym-Tricks in eine bestimmte Bibliothek zu wechseln.
Paxdiablo
Jede Verknüpfungszeitoperation, die sich in der Hostbibliothek hinzieht, führt zu einem mehrfach definierten Symbol.
Paxdiablo
Beispiel: Wenn Sie gegen lib2 verlinken, benötigt der Linker l1getObj (den neu definierten Namen). Für l1getObj ist jedoch getObj erforderlich (das sich bereits in l2 befindet, damit der Linker die Hostobjekte nicht einbringt) - dies führt zu einer unendlichen Rekursion.
Paxdiablo
1
Das Verhalten, wenn die Bibliotheken eher dynamisch als statisch sind, ist besonders merkwürdig. Ich habe es zufällig entdeckt.
Joshua
0

Sie können auch eine gemeinsam genutzte Bibliothek (Unix) oder eine DLL (Windows) verwenden, um dies zu tun (wäre ein kleiner Leistungsverlust). Sie können dann die DLL / so ändern, dass sie geladen wird (eine Version für das Debuggen, eine Version für das Nicht-Debuggen).

Ich habe in der Vergangenheit etwas Ähnliches getan (nicht um das zu erreichen, was Sie erreichen wollen, aber die Grundvoraussetzung ist dieselbe) und es hat gut geklappt.

[Bearbeiten basierend auf OP-Kommentar]

Einer der Gründe, warum ich Funktionen überschreiben möchte, ist, dass ich vermute, dass sie sich auf verschiedenen Betriebssystemen unterschiedlich verhalten.

Es gibt zwei gängige Methoden (die ich kenne), um damit umzugehen, die gemeinsame lib / dll-Methode oder das Schreiben verschiedener Implementierungen, mit denen Sie verknüpfen.

Für beide Lösungen (gemeinsam genutzte Bibliotheken oder unterschiedliche Verknüpfungen) hätten Sie foo_linux.c, foo_osx.c, foo_win32.c (oder besser Linux / foo.c, osx / foo.c und win32 / foo.c) und dann kompilieren und mit dem entsprechenden verknüpfen.

Wenn Sie sowohl nach unterschiedlichem Code für unterschiedliche Plattformen als auch nach Debug -vs- Release suchen, würde ich wahrscheinlich die gemeinsam genutzte lib / DLL-Lösung wählen, da diese am flexibelsten ist.

TofuBeer
quelle