Spielereien mit Funktionspointern
-
Hi,
Ein Kommilitone und ich sind heute beim lernen auf die Funktionspointer gestoßen. Im Embedded-Bereich wurden sie in unserem Beispiel dazu genutzt beim Systemstart eine Methode, die an einer festen Adresse im Speicher liegt, aufzurufen.
Da wir hier mit Visual Studio keine Funktion mit fester Adresse haben (oder geht das?) haben wir uns die Adresse eben geholt:
#include <stdio.h> void test1 (){ printf("test1a\n"); printf("test1b\n"); } void test2 (){ printf("test2a\n"); printf("test2b\n"); } int main (){ void (*funktionspointer)(); funktionspointer = (void(*)())&test1; (*funktionspointer)(); return 0; }
Damit wird test1() korrekt aufgerufen.
Nun haben wir uns gefragt, wenn wir schon den Pointer auf den Anfang der Funktion haben, können wir dann nicht auch Befehle überspringen? Bitte fragt nicht nach dem Sinn oder Anwendungsmöglichkeiten, wir wollen einfach nur wissen ob C das zulässt.Wir haben mit ein paar Debugausgaben herausgefunden, dass test1() genau 5 Speicheradressen belegt. Nun haben wir versucht den Funktionspointer zu erhöhen, um das erste printf aus test1 zu überspringen:
#include <stdio.h> void test1 (){ printf("test1a\n"); printf("test1b\n"); } void test2 (){ printf("test2a\n"); printf("test2b\n"); } int main (){ int adresse = (int)&test1; int neu = adresse + 1; void (*funktionspointer)(); funktionspointer = (void(*)())neu; //funktionspointer++; //diese Zeile gäbe einen Compile-Error, offenbar sind Funktionspointer speziell geschützt? (*funktionspointer)(); return 0; }
Egal um wieviel wir "neu" erhöhten, es gab immer einen Laufzeitfehler. Wieso? Bei "adresse + 5" wird wiederum test2() korrekt aufgerufen.
Unsere Recherche hat leider nichts ergeben, das Thema ist wohl dazu zu speziell.
System:
Windows XP - 32 Bit
Visual Studio 2008
getestet mit dem Visual Studios Compiler und GCCDanke für jede Hilfe!
Gruß
-
orderline schrieb:
Nun haben wir uns gefragt, wenn wir schon den Pointer auf den Anfang der Funktion haben, können wir dann nicht auch Befehle überspringen?
Nein. Was der Compiler aus der Funktion macht ist sehr compilerspezifisch und hängt von vielen Faktoren ab, z.B. Optimierungsstufe.
Außerdem hat eine Funktion normalerweise einen Initialisierungsblock, den man nicht überspringen sollte. Und überhaupt, auch wenn du es nicht hören wolltest, ganz ganz schlechte Idee
-
orderline schrieb:
Egal um wieviel wir "neu" erhöhten, es gab immer einen Laufzeitfehler. Wieso?
Bei den meisten Architekturen sehen die ersten paar Befehle einer Funktion meist so aus, dass man sich Platz auf dem Stack beschafft (eine einfache Subtraktion auf den Stackpointer), um dort lokale Variablen ablegen zu können. Wird dieser Befehl einfach übersprungen, schreiben die nächsten Instruktionen in Speicher, der für was anderes benutzt wird. Dabei knallt es dann natürlich.
Eine andere Möglichkeit (bei dir eher zutreffend) sieht so aus: Beim Aufruf einer Funktion muss eine Rücksprungadresse gesichert werden, damit es nach Ausführung der Funktion dort weitergeht, wo es weitergehen soll. Viele Architekturen haben hierfür ein spezielles Register. Wenn die aufgerufene Funktion nun wiederum eine Funktion aufruft (test1 ruft printf auf), würde die Rücksprungadresse im dafür vorgesehenen Register überschrieben werden. Daher muss test1 vor dem Funktionsaufruf dafür sorgen, dass die Rücksprungadresse woanders gespeichert wird. Hierfür nimmt man dann wieder den Stack.
Nun überspringst du das Speichern der Rücksprungadresse auf dem Stack. Die letzten paar Instruktionen der Funktion werden aber trotzdem versuchen die Adresse wieder vom Stack zu laden. Da sie dort aber nicht liegt, wird irgendein Unfugsdatum vom Stack geholt und in den Program Counter geladen. Was dann passiert, kannst du dir ja ausmalen.
Das ganze ist aber in jedem Fall abhängig von der verwendeten Architektur und Calling Convention. Auf C-Ebene kannst du hier keine zuverlässigen Annahmen treffen. Es gibt keinerlei Möglichkeit im Voraus zu sagen, in wievielen Maschinenbefehlen ein beliebiges C-Statement resultiert. Das ist abhängig von sovielen Variablen, dass du da zwar sicherlich informierte Schätzungen abgeben kannst, aber das ganze niemals so zuverlässig voraussagen könntest, um sinnvoll Arithmetik mit Funktionspointern zu betreiben. Aus dem Grund lässt die Sprache das auch nicht zu.
-
Hat das einen Grund warum du den Ausdruck
funktionspointer = (void(*)())&test1;
so ausdrückst, statt direkt
funktionspointer = test1;
Ich frage aus reiner Neugier. Denn von der Übersicht her ist der Ausdruck vor dem Funktionsnamen besser zu vernachlässigen, da der Compiler den Ausruck automatisch ersetzt. Den Adressoperator würde ich noch als sinnvoll erachten als Neuling, aber der Rest ist schon sehr unübersichtlich und unnötig oder?
-
Hallo,
vielen Dank für die Antworten.
Klar ist es eine schlechte Idee, ich käme auch nie auf die Idee das für irgendwas "sinnvolles" einsetzen zu wollen. Wir sind nur einfach zur Zeit dabei mal zu sehen was C zulässt und was nicht.
Die Geschichte mit dem Stackpointer, Rücksprungadresse usw klingt natürlich logisch, da hätten wir auch selbst drauf kommen können.
Danke!
-
Hi
TheRulor schrieb:
Hat das einen Grund warum du den Ausdruck
funktionspointer = (void(*)())&test1;
so ausdrückst, statt direkt
funktionspointer = test1;
Ich frage aus reiner Neugier. Denn von der Übersicht her ist der Ausdruck vor dem Funktionsnamen besser zu vernachlässigen, da der Compiler den Ausruck automatisch ersetzt. Den Adressoperator würde ich noch als sinnvoll erachten als Neuling, aber der Rest ist schon sehr unübersichtlich und unnötig oder?
Ich habe es gerade getestet.
Es funktioniert auch, aber der "Rest" ist halt die korrekte Schreibweise für den Cast, oder?
Wenn du es nicht ausschreibst macht wohl der Compiler den Cast.
Ob der dann jetzt nur zufällig richtig ist oder ob er das merkt was du eigentlich willst kann ich aber auch nicht beantworten.
-
orderline schrieb:
Ich habe es gerade getestet.
Es funktioniert auch, aber der "Rest" ist halt die korrekte Schreibweise für den Cast, oder?
Wenn du es nicht ausschreibst macht wohl der Compiler den Cast.
Ob der dann jetzt nur zufällig richtig ist oder ob er das merkt was du eigentlich willst kann ich aber auch nicht beantworten.Es ist sogar eher schädlich, wenn du so einen unnötigen Cast hinschreibst. Beispiel:
int add(int a, int b) { return a+b; } int main(void) { int (*fp)(int,int); fp = add; return add(5,-5); }
Ändert man jetzt die Signatur von add:
long add(long a, long b) { return a+b; } int main(void) { int (*fp)(int,int); fp = add; // <-- wird das hier nicht mehr kompilieren return add(5,-5); }
was auch gut so ist. Baust du da noch einen Cast ein:
long add(long a, long b) { return a+b; } int main(void) { int (*fp)(int,int); fp = (int(*)(int,int)) &add; // <-- wird das hier kompilieren return add(5,-5); // <-- und das hier undefiniertes Verhalten hervorrufen. }
Es kompiliert dann zwar, ist aber falsch, weil die Typen gar nicht zusammen passen. Da wär mir ein Fehler beim Kompilieren lieber, statt zur Laufzeit undefiniertes Verhalten hervorzurufen.