Datentyp von Adressen? & Pointerarithmetik overflow?!


  • Mod

    Differenz zwischen zwei Zeigern: ptrdiff_t
    Datentyp von Adressen: Zeiger auf Objekt auf das man den Adressoperator angewendet hat.

    Wenn es unbedingt sein muss, und ich betone, dass du höchstwahrscheinlich etwas krass falsch machst, wenn du dies außerhalb der Programmierung von ganz low-leveligen Sachen benutzt, gibt es noch (u)intptr_t, welches ein Integer ist, der mindestens den ganzen Wertebereich eines void-Pointers verlustfrei abbilden kann.

    Funzel schrieb:

    PS. Pointer werden mathematisch doch genauso behandelt wie ints, oder?!

    Nein, überhaupt nicht. Zu Pointern kann man Integer addieren und man kann die Differenz von zwei Pointern berechnen. Das ist alles, was man mit Pointern rechnen kann. Und die Regeln für diese Rechnungen sind ganz andere, als wenn man den Zeiger im Sinne eines uintptr_t interpretieren würde und mit diesem die Rechnung durchführen würde.



  • Aus dem Grund kannst du zu einem (ungecasteten) void-Pointer auch nichts addieren.

    Dann war das natürlich ein unglückliches Beispiel.
    Wie wärs dann hiermit:

    char k = 5;
    unsigned long h = 5;
    float *a = &xyz + k;
    float *b = &xyz + h;
    float *c = &xyz + 5; // oder 5u etc.. ?
    

    Was soll der +-= Operator machen?

    Entweder "+=" oder "-=".

    Es könnte ja sein, dass der Speicher von "0" beginnend aufgefüllt wird. Wenn also mein Programm gestartet wird, wären die ersten paar Adressen schon belegt (vom OS etc.). Dann könnte ich mir sicher sein, dass kein Pointer in meinem Programm auf z.B. 0x0, oder 0x1 zeigt und bei "-= 5" den Wertebereich verlässt...
    Entsprechendes könnte für das andere Ende des Speichers gelten.
    So stell ich mir das vor..

    Und jetzt frage ich mich natürlich nach welchen Regeln die typedefs erstellt wurden..?



  • SeppJ schrieb:

    Wenn es unbedingt sein muss, und ich betone, dass du höchstwahrscheinlich etwas krass falsch machst, wenn du dies außerhalb der Programmierung von ganz low-leveligen Sachen benutzt, gibt es noch (u)intptr_t, welches ein Integer ist, der mindestens den ganzen Wertebereich eines void-Pointers verlustfrei abbilden kann.

    Beim Parsen von Binärdaten (MZ-/ELF-Headern, Netzwerkpaketen etc.) hat man nicht den Luxus, einfach eine Struktur für den Zugriff draufzuknallen. Stattdessen hat man dann die Anzahl der Bytes ab Beginn eines Referenzpunktes. Auf diesen muss man dann die Anzahl der Bytes addieren, damit man zu den Daten kommt. Der Check, um zu prüfen, ob sich die Daten innerhalb des Pakets befinden, sieht dann so aus:

    uintptr_t begin_uintptr = (uintptr_t)begin_ptr;
    uintptr_t end_uintptr   = (uintptr_t)end_ptr;
    uintptr_t new_uintptr   = begin_uintptr + bytes;
    
    if(new_uintptr  < begin_uintptr)
    || new_uintptr >= end_uintptr)
        return EOVERFLOW;
    

    Der erste Check prüft auf Overflow der Adresse. Wenn man das nicht tut, kann am Ende der Wert überlaufen, und der zweite Check, der prüft, ob die Adresse noch im Bereich liegt, kann das nicht merken. Um nicht undefinierte Pointerarithmetik zu machen muss man in uintptr_t s casten. Das würde ich nicht unbedingt low-levelig bezeichnen.

    @TE:
    Und nicht size_t mit uintptr_t verwechseln, da gibt es subtile Unterschiede zwischen den Typen. Nimm für Pointerberechnungen immer uintptr_t .



  • Was ist denn xyz?

    Wenn du auf Speicher zugreifen möchtest, der dir (deinem Programm) nicht gehört, hast du UB.
    Da darf alles passieren.

    Wenn du auf deinem System ein bestimmtes Verhalten ermittelt hast, dann kann das bei einem anderen System, Compiler, Compilerversion wieder anders sein.

    float y[3] = { 1, 2, 3];
    float *fp = &y[1];
    

    Dann greifst du mit *(fp+1) auf y[2] und mit *(fp-1) auf y[0] zu.

    float x = 1, y = 2, z = 3;
    float *fp = &y;
    

    Dann kann es sein, dass du mit *(fp+1) auf z und mit *(fp-1) auf x zugreifen kannst.


  • Mod

    dachschaden schrieb:

    uintptr_t begin_uintptr = (uintptr_t)begin_ptr;
    uintptr_t end_uintptr   = (uintptr_t)end_ptr;
    uintptr_t new_uintptr   = begin_uintptr + bytes;
    
    if(new_uintptr  < begin_uintptr)
    || new_uintptr >= end_uintptr)
        return EOVERFLOW;
    

    Der erste Check prüft auf Overflow der Adresse. Wenn man das nicht tut, kann am Ende der Wert überlaufen, und der zweite Check, der prüft, ob die Adresse noch im Bereich liegt, kann das nicht merken. Um nicht undefinierte Pointerarithmetik zu machen muss man in uintptr_t s casten. Das würde ich nicht unbedingt low-levelig bezeichnen.

    Warum nicht end_ptr - begin_ptr >= bytes ?



  • Was ist denn xyz?

    Wenn du auf Speicher zugreifen möchtest, der dir (deinem Programm) nicht gehört, hast du UB.
    Da darf alles passieren.

    xyz sei z.B. ein Array. Ist aber nicht wichtig, da ich nicht auf Speicher zugreife, der mir nicht gehört. Es geht mir nur darum, dass ich z.B. bei einem Zwischenschritt einer Rechnung mit Pointern, bei dem nicht dereferenziert wird, nicht den Wertebereich verlasse oder überlaufe.

    char k = 5;
    unsigned long h = 5;
    float *a = &xyz + k;
    float *b = &xyz + h;
    float *c = &xyz + 5; // oder 5u etc.. ?
    

    Also was ist "besser"/performanter, - a oder b? Und welches Literal nehme ich bei c ??



  • SeppJ schrieb:

    Warum nicht end_ptr - begin_ptr >= bytes ?

    Weil ich keine Mutmaßungen darüber treffe, was begin_ptr und end_ptr für einen Typ haben.

    #include <stdint.h>
    #include <stdio.h>
    
    typedef uint32_t* myptr;
    
    int main(void)
    {
            myptr a = (myptr)0x1,b = (myptr)0x4;
            uintptr_t ua = (uintptr_t)a, ub = (uintptr_t)b;
    
            printf("%lx|%lx\n",b - a,ub - ua);
            return 0;
    }
    

    Ergebnis:

    0|3
    

    Und wenn myptr als void* definiert wird, kompiliert es nicht einmal mehr mit Visual Studio.

    error C2036: "myptr": Unbekannte Größe
    

  • Mod

    dachschaden schrieb:

    SeppJ schrieb:

    Warum nicht end_ptr - begin_ptr >= bytes ?

    Weil ich keine Mutmaßungen darüber treffe, was begin_ptr und end_ptr für einen Typ haben.

    Wieso nicht? Wenn es darum geht, irgendwelche Einzelbyteoffsets zu einem Pointer hinzu zu rechnen, dann ist der einzig korrekte Typ für diesen Pointer char* . Und es ist sogar völlig legitim, einen anderen Pointer zu char* zu casten, um solche Operationen durchzuführen.


  • Mod

    Funzel schrieb:

    char k = 5;
    unsigned long h = 5;
    float *a = &xyz + k;
    float *b = &xyz + h;
    float *c = &xyz + 5; // oder 5u etc.. ?
    

    Also was ist "besser"/performanter, - a oder b?

    Möglicherweise b, aber aus anderen Gründen als du denkst.

    Und welches Literal nehme ich bei c ??

    Wie sollte das eine Rolle spielen? Meinst du, der Compiler würde Code erzeugen, um den Typ eines unpassenden Literals zur Laufzeit zu konvertieren, anstatt gleich den passenden Typ ins Programm einzubauen?



  • SeppJ schrieb:

    Wieso nicht? Wenn es darum geht, irgendwelche Einzelbyteoffsets zu einem Pointer hinzu zu rechnen, dann ist der einzig korrekte Typ für diesen Pointer char* . Und es ist sogar völlig legitim, einen anderen Pointer zu char* zu casten, um solche Operationen durchzuführen.

    Das war ein verkürztes Beispiel. Im Land der Realität habe ich einen Zeiger auf irgendein struct , mit Header-Daten, in denen sich das Offset befindet, welches man auf den Zeiger auf struct addieren muss. ELF mag das ganz besonders. Oder IP. Oder TCP. Oder sogar HTTP mit seiner Chunk-basierten Übertragung.

    EDIT: Und wenn man auf Frickelcode genau wenig Lust hat wie ich, dann baut man sich vorher noch Code, der auf Overflow prüfen kann. Da verwendet man dann direkt uintptr im Makro oder in einer eingebundenen Funktion.


Anmelden zum Antworten