Abstrakte void *

mogbo

Banned
Hallo,
bin bei C etwas in Richtung Pointerspielereien abgerutscht um mein Verständnis zu verbessern und bin mittlerweile echt fasziniert was der Compiler alles mitmacht :)
Code:
int
main(int argc, char *argv[])
{
    if (argc != 2)
        err(EXIT_FAILURE, "argc != 2");

    void *c_ptr, *s_ptr;
    if ((s_ptr = malloc(strlen(argv[1]) + 1)) == NULL)
        err(EXIT_FAILURE, "Malloc failure");

    if ((c_ptr = malloc(sizeof(unsigned long))) == NULL)
        err(EXIT_FAILURE, "Malloc failure");

    /* Speicheradresse als unsigned long ablegen */
    *((unsigned long *) c_ptr) = s_ptr;

    /* Adresse aus Zahl lesen und mit String beschreiben */
    snprintf((char *) *((unsigned long *) c_ptr), strlen(argv[1]) + 1, "%s", argv[1]);

    /* Zahl als Adresse ausgeben und String lesen der Zielposition */
    printf("%s\n", (char *) *((unsigned long *) c_ptr));

    /* freezero ueber Adresse, die in einer Zahl beschrieben wird */
    freezero(*((unsigned long *) c_ptr), strlen(argv[1]) + 1);

    return EXIT_SUCCESS;
}
Gibts fuer sowas Anwendungen oder ist das eine Spielerei, die man sich ganz schnell wieder abgewöhnen sollte?
 
Zumindest das hier solltest du sofort wieder vergessen:
Code:
*((unsigned long *) c_ptr) = s_ptr;

Der Pointer ist s_ptr ist so lang wie die Speicheradressen auf der Architektur, auf der das Programm compiled wurde. Ein 'unsigned long' ist ebenfalls unterschiedlich lang, auf einem LP64 System (also bis auf ein paar obskure Systeme alles außer Windows) 64 Bit und auf einem LLP64 System (also Windows) 32 Bit... Dabei ist vor allem der Windows 64 Bit Fall sehr problematisch, du steckst dort einen 64 Bit Pointer in ein 32 Bit 'unsigned long'. Das Reinstecken geht oft sogar noch, auf wenn ein besserer Compiler schon meckern sollte. Das wieder rausziehen führt dann aber zur Katastrophe, da die halbe Speicheradresse fehlt. Das ist das klassische Problem bei Portierungen von alten Code auf moderne 64 Bit Plattformen.

Grundsätzlich sollte man solche Konstrukte vermeiden. Denn wenn wir irgendwann einmal 128 Bit Plattformen bekommen sollten, fliegt das alles wieder auseinander. Wenn es denn sein muss, sollte man zumindest sicherstellen, dass die Speicheradressen in den jeweiligen Datentyp hineinpassen und es so im Code abbilden, dass sich dort leicht Änderungen vornehmen lassen.
 
Der Pointer ist s_ptr ist so lang wie die Speicheradressen auf der Architektur, auf der das Programm compiled wurde. Ein 'unsigned long' ist ebenfalls unterschiedlich lang

Hmm, ich sehe in dem Code einen Cast auf einen Zeiger auf einen unsigned long. Das sollte daher schon passen. Der Sinn erschließt sich natürlich dennoch nicht - man braucht einen Zeiger nie casten, außer man übergibt ihn einer Funktion, die einen bestimmten Typ erwartet.

Rob
 
Hmm, ich sehe in dem Code einen Cast auf einen Zeiger auf einen unsigned long.
Ja, du hast recht. Solange da nur auf einen Zeiger und nicht den Datentypen selbst gecastet wird, funktioniert es natürlich. Wenn aber der Datentyp selbst ins Spiel kommt, wird es problematisch.
 
Moin,
hab sogar eine Anwendung gefunden, auch wenn sie ein bischen abwägig ist - struct mit void * der auf einen Speicher mit Infos zeigt, weitere Variablen + Pointer liegen im Offset des Speichers (Anhang, wem langweilig ist, kanns ja mal lesen).

Das die Anwendung völliger Unsinn ist, ist mir klar :), hatte nur gestern kein Internet und somit viel Zeit, danke Telekom


Das ist das klassische Problem bei Portierungen von alten Code auf moderne 64 Bit Plattformen.
Werd ich mal im Hinterkopf behalten, hab mir nie wirklich Gedanken über Portierbarkeit auf andere Plattformen gemacht (oder wenige).

Jeder Cast weiter rechts ist unnütz.
Code:
void *ptr;
if ((ptr = (char *) malloc(sizeof(strlen(argv[1]) + 1) == NULL)
    err(EXIT_FAILURE, "Malloc failure");
Las mal auf Stackoverflow, dass ich void * möglichst casten sollte, sah den Sinn darin auch nicht, habs aber auch nicht wirklich hinterfragt, da es den Code oft lesbarer macht. Hab das evtl zu stark verallgemeinert?

"...der Zeiger des Zeigers, der auf den Zeiger zeigt..."
So langsam muss ich nichtmehr viel drüber nachdenken, wie ich Zeiger in den Code verbaue, jetzt machts leider Spaß mit Zeiger auf Zeiger zu zeigen, die auf weitere Zeiger zeigen :). Ist hoffentlich nur ne Phase
 

Anhänge

  • programm.txt
    3,1 KB · Aufrufe: 298
Zuletzt bearbeitet:
C ist _nicht_ Java. :) [Gemeint ist die Position der Deklarationen von automat. Variablen.]
 

Anhänge

  • korrektur.c.txt
    3,1 KB · Aufrufe: 284
Das die Anwendung völliger Unsinn ist, ist mir klar :), hatte nur gestern kein Internet und somit viel Zeit, danke Telekom
<derail> Wenn man mal mit Leuten redet, die wirklich "coolen Scheiss" bauen, dann geht das meist/nur mit offline Ruhezeiten. Dieses ganze Bing und Bang (chat/mail/lala) ist absolut kontraproduktiv.

Leider zieht ja nun online/WLAN nun auch in den Flieger ein.. :}
 
Wenn man mal mit Leuten redet, die wirklich "coolen Scheiss" bauen, dann geht das meist/nur mit offline Ruhezeiten.
Kenn leider keine "richtigen" Programmierer, bei uns gibts kaum Nerds :ugly:

C ist _nicht_ Java. :) [Gemeint ist die Position der Deklarationen von automat. Variablen.]
War interessant zu lesen:
Code:
(void)snprintf(s_ptr, *((u_long *) u_ptr) + 1, "%s", argv[i]);
Funktionen mit einem void casten, wenn ich das return nicht nutze, war bei mir nie wirklich auf dem Schirm, macht aber Sinn, danke

Den Anwendegrund für u_long musst du mir erklären, gings dir dabei um die Schreibarbeit oder mache ich mit sowas den Code portierbarer?

Code:
current = head = node_alloc();
Muss zu meiner Schande gestehen, ich wusste garnicht, dass das geht :zitter:


Code:
// deins
s_ptr = (char *) malloc(*((u_long *) u_ptr) + 1);
        if (s_ptr == NULL)
            errx(EX_OSERR, "%s: malloc(3)", __func__);

// meins
if ((s_ptr = (char *) malloc(*((u_long *) u_ptr) + 1) == NULL)
    errx(EX_OSERR, "%s: malloc(3)", __func__);
Ist denke ich eher persönliche Preferenz?


Code:
s_ptr = (char *) malloc(*((u_long *) u_ptr) + 1);
Code:
if ((ptr = malloc(sizeof(struct node))) == NULL)
Das der struct node * nicht gecastet wird ist denke ich auch eher Zufall?

Vielen Dank für die Arbeit, konnte mir ein paar Sinnvolle Dinge abschauen :)
 
Kenn leider keine "richtigen" Programmierer, bei uns gibts kaum Nerds :ugly:
Den Anwendegrund für u_long musst du mir erklären, gings dir dabei um die Schreibarbeit oder mache ich mit sowas den Code portierbarer?
In der Kuerze liegt die Wuerze.

Code:
// deins
s_ptr = (char *) malloc(*((u_long *) u_ptr) + 1);
        if (s_ptr == NULL)
            errx(EX_OSERR, "%s: malloc(3)", __func__);

// meins
if ((s_ptr = (char *) malloc(*((u_long *) u_ptr) + 1) == NULL)
    errx(EX_OSERR, "%s: malloc(3)", __func__);
Ist denke ich eher persönliche Preferenz?

Korrekt, bei mir war es eher zufaelllig gewaehlt.

Code:
s_ptr = (char *) malloc(*((u_long *) u_ptr) + 1);
Code:
if ((ptr = malloc(sizeof(struct node))) == NULL)
Das der struct node * nicht gecastet wird ist denke ich auch eher Zufall?

Noe, weil die Implementierung von malloc(3) einen generischen Zeiger auf allozierten Speicher zurueckgibt.

Wo es mir jetzt auffaellt, ware es sogar besser alle Vorkommnisse von malloc(3) [im Quelltext] durch calloc(3) zu ersetzen,
Code:
static void *
node_alloc(void)
{
    void *ptr

    if ((ptr = calloc(1, sizeof(struct node))) == NULL)
        errx(EX_OSERR, "%s: malloc(3)", __func__);

    return (ptr);
}
weil der allozierte Block mit 0 initialisiert wird. Wiso? Damit die Informationsbasis bzgl. Consistency-Checks nicht durcheinander gebracht werde, weil es nicht so toll ist den "Inhalt" von uninitialisierten "Speicher" bzw. den Wert oder Belegung bzgl. irgendwelcher Annahmen oder Constraints zu pruefen, wenn der "Inhalt" vom ["neuen"] Speichersegment mit nem zufaelligen Wert oder einem Wert aus vorangegangener Nutzung [durch eine andere Systemkomponente oder Programm, was irgendwann vorher durch free(3) freigegeben wurde] belegt ist.

Ich beobachtete bei Java-Developer_innen [ist _nicht_ boese oder als "Diskriminierung" gemeint, falls in dem Forum Personen unterwegs sind, die Java als ein Werkzeug fuer das Problemloesen verwenden oder gezwungen sind bzw. damit von Arbeitgeber_in gefoltert werden, wobei ich selbst auch gerne Java benuetze, da schoen abstrakte Sprache mit netten Libraries bzw. Frameworks, aber der GC ist *pita*] oftmals die Unsitte eine Variable dort zu deklarieren, wo diese im Kontext eines Stackframes [oder Position im Quelltext von einem Funktionenkoerper] der JVM tatsaechlich das erste mal erscheine bzw. im Kontext von Programmfluss gebraucht werde.

Es ist mMn besser bzw. haette mehr Stil automat. Variablen am Anfang des Funktionenkoerpers [nach Groesse und Verwendung] zu deklarieren, weil es vereinfacht den Quelltext zu verstehen.

Einen Prozess mit exit(3) zu beenden ist "sauberer", weil dadurch Exception handliing via atexit(3) registrierter Callback-Funktionen moeglich erscheint, siehe u. A. Kapitel 8.5 in "Advanced Programming in the UNIX Environment" bzw. hier.
 
Zuletzt bearbeitet von einem Moderator:
Wo es mir jetzt auffaellt, ware es sogar besser alle Vorkommnisse von malloc(3) [im Quelltext] durch calloc(3) zu ersetzen,
Hier stehe ich stark im Zwiespalt, bei OpenBSD wird freezero gelebt, dann wieder genullten Speicher anzufordern wirkt auf mich doppelt gemobbelt.

Ich versuche immer vom Code die Qualität möglichst hoch zu halten, heißt für mich:
- zweckmäßiger RAM-Verbrauch
- unnötiges nur, wenn der Code lesbarer wird
- alles richtig machen (sicher)

Ich weiß 50 Byte calloc werden mir in 99,9 % der Anwendungen keinen wirklichen Performanceverlust bringen, ich weiß aber dass hier etwas mehr Leistung möchte als unbedingt nötig, nun müsste ich einen guten Grund fürs calloc kennen sehe aber keinen Vorteil, wenn ich den Speicher im Nachhinein mit Zahlen und Pointern einer festen Größe fülle. Habe ich hier einen Denkfehler? Speicher der dynamisch gefüllt wird ist natürlich ein anderes Thema.

Damit die Informationsbasis bzgl. Consistency-Checks nicht durcheinandergebracht werde, weil es nicht so toll ist den "Inhalt" von uninitialisierten "Speicher" bzw. den Wert oder Belegung bzgl. irgendwelcher Annahmen oder Constraints zu pruefen, wenn der "Inhalt" vom ["neuen"] Speichersegment mit nem zufaelligen Wert oder einem Wert aus vorangegangener Nutzung [durch eine andere Systemkomponente oder Programm, was durch free(3) freigegeben wurde] belegt ist.
Kann das nicht sogar ein Nachteil sein, zB. das ein Overflow nicht auffällt, da der Speicher ohnehin immer zufällig am Ende des Strings genullt wurde, bis der Input einmal zufällig etwas länger ist und die Null somit fehlt?
 
Zuletzt bearbeitet:
Es ist mMn besser bzw. haette mehr Stil automat. Variablen am Anfang des Funktionenkoerpers [nach Groesse und Verwendung] zu deklarieren, weil es vereinfacht den Quelltext zu verstehen.
Hehe, die Anwendung dient auch eher zum Abstrakten Anwenden von Pointern, würde so kein Programm auf Anwender loslassen. Ich lerne lustigerweise beim debuggen von abstrakten Anwendungen schneller, als beim abarbeiten logischer Übungen.
 
Hier stehe ich stark im Zwiespalt, bei OpenBSD wird freezero gelebt, dann wieder genullten Speicher anzufordern wirkt auf mich doppelt gemobbelt.

Ich versuche immer vom Code die Qualität möglichst hoch zu halten, heißt für mich:
- zweckmäßiger RAM-Verbrauch
- unnötiges nur, wenn der Code lesbarer wird
- alles richtig machen (sicher)

Ich weiß, 50 Byte calloc werden mir in 99,9 % der Anwendungen keinen wirklichen Performanceverlust bringen, ich weiß aber, dass hier etwas mehr Leistung möchte, als unbedingt nötig, nun müsste ich einen guten Grund fürs calloc kennen, sehe aber keinen Vorteil, wenn ich den Speicher im Nachhinein mit Zahlen und Pointern einer festen Größe fülle. Habe ich hier einen Denkfehler? Speicher der dynamisch gefüllt wird ist natürlich ein anderes Thema.


Kann das nicht sogar ein Nachteil sein, zB. das ein Overflow nicht auffällt, da der Speicher ohnehin immer zufällig am Ende des Strings genullt wurde, bis der Input einmal zufällig etwas länger ist und die Null somit fehlt?

Du haeltst dich fuer _unfehlbar_ und verzichtest auf Peer-Review?

Es geht nicht darum, ob sich die Laufzeit bzgl. Effektivitaet dem Idealbild der Effizienz [mit moeglichst groszer Wahrscheinlichkeit] naehert, sondern darum, dasz das Problem hinreichend geloest wird [siehe insbesonders das, was Eric E. Raymond als Richtlinien definierte].
 
Zuletzt bearbeitet von einem Moderator:
Hehe, die Anwendung dient auch eher zum Abstrakten Anwenden von Pointern, würde so kein Programm auf Anwender loslassen. Ich lerne lustigerweise beim debuggen von abstrakten Anwendungen schneller, als beim abarbeiten logischer Übungen.

Das mag sein, man sollte stets sich dazu anhalten es nicht schleifen zu lassen, da Ordnung sowiso das halbe Leben ist. ;)
 
Rob Pike:
Code:
Regel 6: Es gibt keine Regel 6.
:ugly:

Es geht nicht darum, ob die Laufzeit bzgl. Effektivitaet sich dem Idealbild der Effizienz [mit moeglichst groszer Wahrscheinlichkeit] naehert, sondenr darum, dasz das Problem hinreichend geloest wird
Bin bei calloc grade wirklich im Zwiespalt, es gibt Fehler die ich mit der Anwendung von calloc evtl. "nie" gefunden hätte. Muss mir darüber mal mehr Gedanken machen

Danke für https://de.wikipedia.org/wiki/Unix-Philosophie wirklich sehr lesenswert!
 
Es geht nicht darum, ob die Laufzeit bzgl. Effektivitaet sich dem Idealbild der Effizienz [mit moeglichst groszer Wahrscheinlichkeit] naehert, sondenr darum, dasz das Problem hinreichend geloest wird [siehe insbesonders das, was Eric E. Raymond als Richtlinien definierte].
Der Auffassung kann man sein, aber man kann auch argumentieren dass eine solche Sicht eine Ausrede für träge und fette Software ist. Im übrigen versteckt man die ernsthafte Frage nach der Effizens hinter dem Wort "hinreichend" denn was bedeutet das jeweils genau? Kurz und gut: Das sind so Aussagen auf die man gut verzichten kann...
 
Rob Pike:
Bin bei calloc grade wirklich im Zwiespalt, es gibt Fehler die ich mit der Anwendung von calloc evtl. "nie" gefunden hätte. Muss mir darüber mal mehr Gedanken machen

Es geht [auch] um das Prinzip, ungenutzte Variablen mit Default-Werten zu initialisieren, die im Rahmen eines Konzeptes [welches Constraints] fuer Programm spezifiziert wurden.

Will ja nicht gehaessig sein: ein UNIX(tm)-Derivat ist mehr als nur ein Linux-Kernel *grusel*, da erwarte man ja auch mehr "Ordnung". *scnr*
 
Der Auffassung kann man sein, aber man kann auch argumentieren dass eine solche Sicht eine Ausrede für träge und fette Software ist. Im übrigen versteckt man die ernsthafte Frage nach der Effizens hinter dem Wort "hinreichend" denn was bedeutet das jeweils genau? Kurz und gut: Das sind so Aussagen auf die man gut verzichten kann...

Das mit dem verfetteten Code ist ein sehr guter Einwand, den ich nachvollziehen kann und zustimme, wobei im Blickfeld von _Safety_ [bspw. Software fuer Pumpen oder im Kontext von embedded Systems auf off-shore Bohrplattform, etc. ..] kaeme man nicht darum herum etwas "praeziser" bzw. mit mehr "bloat" zu entwickeln?

Ich will mich auch nicht streiten, weil der von mir erwaehnte Use-case bzgl. Safety [not Security] sehr "konstruiert" ist und ich definitiv _nicht_ die Weiszheit mit Loeffeln gegessen habe. :)
 
"Früher (tm)" gab es die Sprache "B" [1]. Es kannte keinerlei Typen. Durch diese Schwäche mussten Zugriffe auf Variablen vom Programmierer gemacht werden. Wenn die Anzahl der Variablen steigt, wird das mehr und mehr anfällig. Diese Fehler fallen (wenn überhaupt) nur zur Laufzeit auf mit undefiniertem Verhalten.

In C gab es dann die Strukturen. Ein "dev->my_info" wird vom Compiler geprüft, dass es In der Struktur "device" (worauf dev ein pointer ist) ein my_info gibt. Es ist im Prinzip ein "Lade 2 Byte von der Adresse &dev mit dem Offset xyz". Ein void* umgeht das komplett. Wenn du vom OS einen Block mit malloc holst, kann hier alles drin stehen (theoretisch). Du kannst nicht sagen, ob der Wert von dir stammt und gültig ist. Eine 0, zeigt das viel deutlicher an. Etliche OS machen auch einen MemoryFence mit einem Memory break Point um einen dynamisch allokierten Bereich (aber ich denke gerade das macht unter Windows + MSVC der Compiler bzw. die Runtime im Debug Mode). Damit gibt es dann eine Debug Assertion wenn du in den Fence kommst. Unter gdb gibt es hier die Memory Break Points.

Mit C++ wird das RAII [2] Pattern zelebriert. Es ist eines der zentralsten Konzepte. Damit wird beim Ressource allokieren auch die Ressource initialisiert. Siehe Artikel ;)

[1] https://de.wikipedia.org/wiki/B_(Programmiersprache)
[2] https://de.wikipedia.org/wiki/Ressourcenbelegung_ist_Initialisierung
[3] http://sites.harvard.edu/~lib113/reference/c/c_history.html

Dennis Ritchie schrieb:
The Problems of B
The machines on which we first used BCPL and then B were word-addressed, and these languages' single data type, the `cell,' comfortably equated with the hardware machine word. The advent of the PDP-11 exposed several inadequacies of B's semantic model. First, its character-handling mechanisms, inherited with few changes from BCPL, were clumsy: using library procedures to spread packed strings into individual cells and then repack, or to access and replace individual characters, began to feel awkward, even silly, on a byte-oriented machine.
Second, although the original PDP-11 did not provide for floating-point arithmetic, the manufacturer promised that it would soon be available. Floating-point operations had been added to BCPL in our Multics and GCOS compilers by defining special operators, but the mechanism was possible only because on the relevant machines, a single word was large enough to contain a floating-point number; this was not true on the 16-bit PDP-11.
Finally, the B and BCPL model implied overhead in dealing with pointers: the language rules, by defining a pointer as an index in an array of words, forced pointers to be represented as word indices. Each pointer reference generated a run-time scale conversion from the pointer to the byte address expected by the hardware.
For all these reasons, it seemed that a typing scheme was necessary to cope with characters and byte addressing, and to prepare for the coming floating-point hardware. Other issues, particularly type safety and interface checking, did not seem as important then as they became later.
Aside from the problems with the language itself, the B compiler's threaded-code technique yielded programs so much slower than their assembly-language counterparts that we discounted the possibility of recoding the operating system or its central utilities in B.
In 1971 I began to extend the B language by adding a character type and also rewrote its compiler to generate PDP-11 machine instructions instead of threaded code. Thus the transition from B to C was contemporaneous with the creation of a compiler capable of producing programs fast and small enough to compete with assembly language. I called the slightly-extended language NB, for `new B.'
 
Mit C++ wird das RAII [2] Pattern zelebriert. Es ist eines der zentralsten Konzepte. Damit wird beim Ressource allokieren auch die Ressource initialisiert. Siehe Artikel ;)
Muss es denn immmer gleich C++ sein? Also das geht auch [fantastisch] mit C. </ironic> :)


Thx, wieder etwas gelernt bzw. das RAII-Pattern [als solches] kannte ich [als Begriff bzw. in dieser Form] noch nicht.
 
Zurück
Oben