Guten Abend,
ich versuche mich gerade an einer Art "Code Review" meines eigenen
Quelltextes. Dass das nur mittelmäßig gut funktioniert, ist klar. Aber
ich habe halt nichts Besseres.
Ich habe im Speicher ein relativ großes Byte Array, das als
Allzweckpuffer dient:
1
/* Allzweckpuffer.
2
Regel: Nur die Funktion, die hineingeschrieben hat, darf auch wieder daraus lesen. */
3
#define AUXBUFFERSIZE_BYTES 8192
4
uint8_tglAuxbuffer[AUXBUFFERSIZE_BYTES];
Bislang habe ich, wenn ich den Puffer gefüllt oder ausgelesen habe,
immer brav jedes Byte einzeln behandelt. Der Compiler ist natürlich so
schlau, daraus wieder Ganz- oder Halbwortzugriffe zu machen, wenn es
möglich ist. Aber der Quelltext ist natürlich immer noch unübersichtlich
und damit fehlerträchtig, wenn man jedes Wort beim Schreiben zerlegt und
beim Wiederlesen zusammensetzt.
Ist es erlaubt®, ein Byte Array mit einem z.B. 16-Bit-Zeiger als Alias
zu nutzen?
1
uint16_t*mybuffer=(uint16_t*)glAuxbuffer;
2
uint_fast16_tbufferSize=AUXBUFFERSIZE_BYTES/2;
3
for(inti=0;i<bufferSize;i++)
4
{
5
*(myBuffer++)=giveMeASixteenBitValue(i);
6
}
Eine Warnung bekomme ich nicht. Klar, die ist ja auch totgecastet. Das
Assembler-Listing sieht auch passend aus. Aber ich kenne keine C-Regel,
die das erlaubt.
Lass das besser bleiben. Auf ein Array von Bytes greift man Byte-weise
zu. Alles andere stiftet nur Verwirrung und provoziert Probleme.
Du geht ja auch nicht im Straßenverkehr hin und setzt den Blinks links
wenn du nach rechts fahren willst.
Walter T. schrieb:> Ist es erlaubt®, ein Byte Array mit einem z.B. 16-Bit-Zeiger als Alias> zu nutzen?
Natürlich ist es "erlaubt". Bloß nicht portabel. Das funktioniert so nur
bei little endian Maschinen. Und dann auch nicht unbedingt immer.
endianess und alignment könnten das Gebäude zum Einsturz bringen.
c-hater schrieb:> Das funktioniert so nur> bei little endian Maschinen
Warum? Der Adresszeiger liegt immer vorn.
Norbert schrieb:> Wenn man mal von möglichen alignment Problemen absieht
Du meinst das Byte wäre Word-Aligned und ich verschenke Speicherplatz?
Okay...es geht bei beiden Beiträgen Richtung "ungutes Gefühl". Dann ist
wohl ein union das Mittel der Wahl.
c-hater schrieb:> Norbert schrieb:>>> Wenn man mal von möglichen alignment Problemen absieht, sehe ich nicht>> das geringste Problem darin.>> Dann musst du noch viel lernen.
Gerne, erklär mal bitte.
mitlesa schrieb:> Würde sagen dass der Zeiger bei einer von beiden Endianess-> Versionen falsch steht.
Es sei denn du hast die Byte-Order and die Endianess angepasst.
Walter T. schrieb:> Warum? Der Adresszeiger liegt immer vorn.
Ja. Genau das ist ja das Problem. "Vorn" ist halt bei big endian was
anderes als bei little endian.
> Dann ist> wohl ein union das Mittel der Wahl.
Sicher nicht. Da stellt sich exakt das gleiche Problem. Nicht portabel.
Ein byte Array darf an beliebigen Positionen im RAM liegen, weil man
Byte-weise darauf zugreift.
Bei vielen Mikrocontrollern müssen 16 bit Werte aber an geraden
Adressen liegen, sonst können sie nicht darauf zugreifen.
Bei anderen wiederum sollte es so sein, weil sie sonst deutlich
langsamer werden. Was blöd ist, weil dieser Code ja aus der Idee heraus
kam, ihn zu beschleunigen.
Wenn das Array z.B. an Adresse 101 beginnt, und ich das erste Word (16
Bit) davon lesen will, habe ich schon verloren. Es sei denn, ich weiß
mit Sicherheit, dass beim vorliegenden Mikrocontroller das aligning
keine Rolle spielt. Das wäre aber eher eine Ausnahme als der Regelfall.
Viel wichtiger ist mir aber die Lesbarkeit des Codes. Wenn ich Birnen
verarbeiten will, dann benutze ich nicht Befehle für Äpfel. Wenn ich
beim Bäcker vier halbe Roggenbrote bestelle, dann will ich nicht zwei
ganze Brote bekommen. Wir sind hier nicht beim Fußball, wo das
Antäuschen (nur gegenüber dem Gegner) eine sinnvolle Taktik ist.
Walter T. schrieb:> uint16_t *mybuffer = (uint16_t *) glAuxbuffer;> uint_fast16_t bufferSize = AUXBUFFERSIZE_BYTES/2;> for( int i = 0; i<bufferSize; i++)> {> *(myBuffer++) = giveMeASixteenBitValue(i);> }
Du kannst so nicht wissen, dass sizeof(uint_fast16_t) = 2 ist.
Es ist die schnellste Version für 16 Bit. Das können auch 32 Bit sein.
Oder 24, ....
Aber schreib jetzt nicht statt der 2 sizeof(uint_fast16_t) sondern
sizeof(bufferSize)
Warum machst du keine union, da wird dann schon passend auf das
Alignement geachtet - oder du nimmst Compiler-Erweiterungen.
c-hater schrieb:> Walter T. schrieb:>>> Ist es erlaubt®, ein Byte Array mit einem z.B. 16-Bit-Zeiger als Alias>> zu nutzen?>> Natürlich ist es "erlaubt". Bloß nicht portabel. Das funktioniert so nur> bei little endian Maschinen. Und dann auch nicht unbedingt immer.
Es ist verboten.
1
6.5 Expressions
2
7) An object shall have its stored value accessed only by an lvalue expression that has one of the following types:89)
3
— a type compatible with the effective type of the object,
4
— a qualified version of a type compatible with the effective type of the object,
5
— a type that is the signed or unsigned type corresponding to the effective type of the object,
6
— a type that is the signed or unsigned type corresponding to a qualified version of the effective
7
type of the object,
8
— an aggregate or union type that includes one of the aforementioned types among its members
9
(including, recursively, a member of a subaggregate or contained union), or
Stefan ⛄ F. schrieb:> Ein byte Array darf an beliebigen Positionen im RAM liegen, weil man> Byte-weise darauf zugreift.
Liegt es aber nicht. Ich habe in meinem ganzen Assembler-Listing kein
einziges nicht Wort-ausgerichtetes Byte-Array gesehen.
Ich wüßte auch nicht, warum es das geben solle. Es liegt schließlich
hinter einem Zeiger in Wortbreite.
c-hater schrieb:> Sicher nicht. Da stellt sich exakt das gleiche Problem. Nicht portabel.
Hier definitiv: Doch. Ich darf beim union immer das auslesen, was ich
auch geschrieben habe. Genau dafür ist es da.
Walter T. schrieb:> Hier definitiv: Doch. Ich darf beim union immer das auslesen, was ich> auch geschrieben habe. Genau dafür ist es da.
Klar. Und es knallt genau dann, wenn du den Inhalt der Union zwischen
Schreiben und Lesen von einer Maschine auf eine andere überträgst und
diese Maschinen unterschiedliche endianess haben.
Das kann doch nicht so schwer zu begreifen sein?
Aber das Beispiel mit dem Übertragen auf eine andere Maschine ist nur
der Extremfall, wo es sofort klar wird. Viel gefährlicher ist, dass
solche Konstrukte gern dazu eingesetzt werden, aus Bytes größere Werte
größerer Datentypen "zusammenzubauen" (oder auch für die umgekehrte
Operation).
Der Code, der das tut, ist halt nicht portabel. Er funktioniert nur bei
einer endianess, derselbe Code auf einer Maschine mit anderer
endianess macht Bullshit.
Genau das ist mit portabel gemeint: der Code sollte unabhängig von der
endianess funktionieren.
Walter T. schrieb:> Liegt es aber nicht. Ich habe in meinem ganzen Assembler-Listing kein> einziges nicht Wort-ausgerichtetes Byte-Array gesehen.
Das ist ja schön für dich, aber darauf ist kein Verlass.
> Es liegt schließlich hinter einem Zeiger in Wortbreite.
Auch darauf ist kein Verlass. Wo die Daten im Speicher abgelegt werden
und in welcher Reihenfolge, das ist nicht festgelegt.
Die Programmiersprache C sichert viele Eigenschaften zu und lässt einige
offen, damit sie auf jeder Plattform einigermaßen performant umgesetzt
werden kann. Du verlässt dich hier auf eine Eigenschaft, die nicht
zugesichert wurde.
"Na los, du bist nicht zu schwer! Seile halten immer doppelt so viel als
auf der Verpackung steht."
c-hater schrieb:> Klar. Und es knallt genau dann, wenn du den Inhalt der Union zwischen> Schreiben und Lesen von einer Maschine auf eine andere überträgst und> diese Maschinen unterschiedliche endianess haben.
Wie soll das gehen? Wenn ich Daten zwischen Maschinen austausche,
brauche ich ein Protokoll, und da muss ich natürlich auf Network Byte
Order achten.
Und wenn ich das Programm von einem auf den anderen Rechner übertrage,
wird das union von jedem im Programmverlauf frisch befüllt und wieder in
der Reihenfolge ausgelesen, wie es geschrieben wurde.
Ich habe zwar schon vom Begriff "Bit rot" gehört, aber das ist
lächerlich. Bei jedem union kann ich auf jeder Maschine jedes Datum so
auslesen, wie ich es gespeichert habe - solange der Speicher in Ordnung
ist.
Walter T. schrieb:> Ich wüßte auch nicht, warum es das geben solle. Es liegt schließlich> hinter einem Zeiger in Wortbreite.
Nein. tut es nicht. Ein Array ist kein struct aus einem Zeiger und einem
Datenteil, sondern ein Array ist eine feste Adresse, hinter der X Bytes
Speicher reserviert sind. Es ist an Speicher also nur der Array-Inhalt
belegt.
Ich habe mit sowas schon bei einem Compilerupdate schon Probleme gehabt,
als ein Array aus uint8_t über uint32_t angesprochen wurde. Wenn Du
sowas machen willst, benutz das Alignment-Attribut für das Array.
Walter T. schrieb:> Wie soll das gehen?
Spiele es doch einfach mal real durch. Dann verstehst vielleicht sogar
du das Problem.
Naja, an little endian Maschinen herrscht kein Mangel, bei big endian
wird's mittlerweile etwas eng...
c-hater schrieb:> Spiele es doch einfach mal real durch.
Kann es sein, dass Du die zweite Zeile im Listing nicht gelesen hast?
Walter T. schrieb:> /* Allzweckpuffer.> Regel: Nur die Funktion, die hineingeschrieben hat, darf auch wieder> daraus lesen. */
Es geht nicht um type punning. Es geht darum, jeweils innerhalb von
einer Funktion (evtl. mit Unterfunktionen) Daten aufzubewahren.
(Hauptsächlich entpackte oder generierte Daten kurz puffern.)
Nop schrieb:> [...] sondern ein Array ist eine feste Adresse, hinter der X Bytes> Speicher reserviert sind. Es ist an Speicher also nur der Array-Inhalt> belegt.>> Ich habe mit sowas schon bei einem Compilerupdate schon Probleme gehabt,> als ein Array aus uint8_t über uint32_t angesprochen wurde. Wenn Du> sowas machen willst, benutz das Alignment-Attribut für das Array.
Das klingt sinnvoll und plausibel. Danke!
Mittlerweile wird hier ja nun wirklich verzweifelt nach Gründen gesucht,
warum man dies und das nicht... jetzt sind wir schon bei Rechner zu
Rechner Transfer.
Fakt ist: Wir haben einen Speicherbereich auf den man mit Hilfe von
Pointern zugreifen möchte. Alignment vorausgesetzt, kann man auf den
Beginn des Speicherbereiches mit (u)int8_t *,(u)int16_t *,(u)int32_t
*,... zugreifen. Das ist ein Mechanismus mit dem schon im letzten
Jahrtausend gearbeitet wurde, vermutlich auch noch im Nächsten.
Man sollte/darf nicht mit einem Pointer schreiben und mit einem anderen
lesen, das wäre in der Tat ein Problem. Endianess wurde schon erwähnt.
Doch wenn man mit uintxx_t * schreibt und liest, passiert präzise gar
nichts Schlimmes. <--Punkt
Ich verwende für general purpose Speicher eine typedef BYTE, das i.d.R.
auf char lautet. Damit ist ein cast erlaubt und OK (typedef, damit
erkennbar ist, dass es kein uint8_t oder so ist, sondern g.p.Speicher).
Du kannst auch ohne union beliebige Typen auf den Speicher abbilden,
solange Du das zurückliest, was Du reingeschrieben hast, WENN Dein Array
für alle Typen korrekt aligned ist.
Endianess spielt keine Rolle, es sei denn, Du willst da selber Zahlen
zusammenbasteln.
Speichergröße für Deine Typen spielt auch keine Rolle, solange Du halt
die Arraygrenzen nicht verlässt.
Also ja, mach dass, wenn Du das Allignment nötigenfalls sicherstellen
kannst.
Es ist portabel, erlaubt, gut und üblich.
Ein malloc liefert einen void Pointer auf einen Speicherbereich,
den void Pointer castet man sich selbst passend.
Was zum Henker spricht dagegen mal mit einen uint8_t* zu lesen und zu
schreiben, mal mit einem uint16_t* zu lesen und zu schreiben?
Norbert schrieb:> Ein malloc liefert einen void Pointer auf einen Speicherbereich,> den void Pointer castet man sich selbst passend.> Was zum Henker spricht dagegen mal mit einen uint8_t* zu lesen und zu> schreiben, mal mit einem uint16_t* zu lesen und zu schreiben?
Der Standard, wie oben zitiert.
Norbert schrieb:> Was zum Henker spricht dagegen
Nichts, weil malloc immer das Aligment für den größten möglichen
Datentyp anwendet. Auf einem PC ist die Adresse immer durch 8 teilbar.
Du meine Güte!
uint16_t *ptr16 = malloc(1024);
uint8_t *ptr8 = (uint8_t *)ptr16; (**)
Beide zeigen präzise auf die gleiche Stelle, incrementieren aber anders.
(**)Von mir aus auch mit reinterpret_cast<uint8_t *>
Norbert schrieb:> Doch wenn man mit uintxx_t * schreibt und liest, passiert präzise gar> nichts Schlimmes. <--Punkt
So ist es. Solange man zwischen Schreiben und Lesen den Datentyp nicht
wechselt, gibt es auch kein Problem.
Norbert schrieb:> Du meine Güte!> uint16_t *ptr16 = malloc(1024);> uint8_t *ptr8 = (uint8_t *)ptr16; (**)> Beide zeigen präzise auf die gleiche Stelle, incrementieren aber anders.>> (**)Von mir aus auch mit reinterpret_cast<uint8_t *>
So rum ist erlaubt, wie du oben oder im Standard nachlesen kannst
mh schrieb:> So rum ist erlaubt, wie du oben oder im Standard nachlesen kannst
Nun, erlaubt ist beides, jedoch garantiert problemlos ist nur von
stärker nach schwächer. (Alignment)
1
A pointer to one type may be converted to a pointer to another type. The resulting pointer may cause addressing exceptions if the subject pointer does not refer to an object suitably aligned in storage. It is guaranteed that a pointer to an object may be converted to a pointer to an object whose type requires less or equally strict storage alignment and back again without change.
Norbert schrieb:> mh schrieb:>>> So rum ist erlaubt, wie du oben oder im Standard nachlesen kannst>> Nun, erlaubt ist beides, jedoch garantiert problemlos ist nur von> stärker nach schwächer. (Alignment)> A pointer to one type may be converted to a pointer to another type. The> resulting pointer may cause addressing exceptions if the subject pointer> does not refer to an object suitably aligned in storage. It is> guaranteed that a pointer to an object may be converted to a pointer to> an object whose type requires less or equally strict storage alignment> and back again without change.
Das erlaubt dir den Pointer zu konvertieren, aber sagt dir nicht, was du
mit dem Pointer machen darfst. Wie gesagt, ich habe oben den relevanten
Teil zitiert.
mh schrieb:> Norbert schrieb:>> mh schrieb:> Das erlaubt dir den Pointer zu konvertieren, aber sagt dir nicht, was du> mit dem Pointer machen darfst. Wie gesagt, ich habe oben den relevanten> Teil zitiert.
Wenn ich einen /oder mehrere) Pointer konvertiere und diese dann nicht
benutze, würde ich mir schon ernsthafte Sorgen machen;-)
Es wird aber explizit erwähnt das es zu Exceptions kommen könnte, das
würde aber nur passieren wenn solche Pointer auch verwendet würden.
Somit wird impliziert das diese Pointer auch verwendet werden können.
Ansonsten wäre die ganze Passage obsolet.
Norbert schrieb:> ...
Du kannst glauben was du willst. Solange du aber nicht erklären kannst,
warum das explizite Verbot, das im Standard steht und oben zitiert
wurde, nicht gilt, bleibt es verboten.
mh schrieb:> Es ist verboten.6.5 Expressions> 7) An object shall have its stored value accessed only by an lvalue> expression that has one of the following types:89)> ...
Fehler in der Argumentation: Es wird hier nicht auf den gespeicherten
Wert der uint8_t zugegriffen.
Al Fine schrieb:> mh schrieb:>> Es ist verboten.6.5 Expressions>> 7) An object shall have its stored value accessed only by an lvalue>> expression that has one of the following types:89)>> ...>> Fehler in der Argumentation: Es wird hier nicht auf den gespeicherten> Wert der uint8_t zugegriffen.
Was meinst du mit "hier"? In dem Beispiel des TO wird es das sehr wohl.
Die Lebenszeit eines Objekt beginnt, wenn der Speicher garantiert zur
Verfügung steht. Ein Objekt hat während seiner Lebenszeit einen Wert.
Dieser Wert mag "indeterminate" sein, dann ist der lesende Zugriff UB.
Norbert schrieb:> Es wird aber explizit erwähnt das es zu Exceptions kommen könnte, das> würde aber nur passieren wenn solche Pointer auch verwendet würden.> Somit wird impliziert das diese Pointer auch verwendet werden können.
Die neuere Fassung hat da eine andere Meinung
1
A pointer to an object type may be converted to a pointer to a different object type. If the resulting
2
pointer is not correctly aligned for the referenced type, the behavior is undefined. Otherwise,
3
when converted back again, the result shall compare equal to the original pointer. When a pointer to
4
an object is converted to a pointer to a character type, the result points to the lowest addressed byte
5
of the object. Successive increments of the result, up to the size of the object, yield pointers to the
https://gustedt.wordpress.com/2016/08/17/effective-types-and-aliasing/
- auf ein char array darf man nur per char* zugreifen.
- auf ein int16 array darf man per int16* und per char* zugreifen.
- auf ein int32 array darf man per int32* und per char* zugreifen.
- ...
Das sollte die eingangsfrage klären. Andererseits kann man auch mit
-fno-strict-aliasing kompilieren und alles kann einem egal sein
(alignment kommt auch noch ins spiel).
Für speicher der über malloc abgeholt wurde, gelten nochmal andere
regeln, siehe link.
aliasing schrieb:> Für speicher der über malloc abgeholt wurde, gelten nochmal andere> regeln, siehe link.
Das müsste durch 6.5.6 (der Absatz vor den oben zitierten
aliasing-Regeln 6.5.7) abgehandelt werden.
mh schrieb:> Was meinst du mit "hier"? In dem Beispiel des TO wird es das sehr wohl.
Wird es nicht. Das wäre doch in C++ gemäß deiner Argumentation
undefined, also nicht das, was da gemacht wird.
mh schrieb:> Die Lebenszeit eines Objekt beginnt, wenn der Speicher garantiert zur> Verfügung steht.
In C++ beginnt (korrekterweise) ein Konstruktor die Lebenszeit eines
Objekts: Einen von malloc erhaltenen Zeiger zu casten und dann einfach
Werte zuzuweisen, ist immer undefined.
In C gibt es das Konstrukt aber so nicht. Alles, was im Code des TO
steht, ist, dass uint16_t-Objekte an bestimmte Stellen geschrieben
werden. Das ist, glaube ich, kein Zugriff auf die uint8_t.
Al Fine schrieb:> mh schrieb:>> Die Lebenszeit eines Objekt beginnt, wenn der Speicher garantiert zur>> Verfügung steht.>> In C++ beginnt (korrekterweise) ein Konstruktor die Lebenszeit eines> Objekts: Einen von malloc erhaltenen Zeiger zu casten und dann einfach> Werte zuzuweisen, ist immer undefined.>> In C gibt es das Konstrukt aber so nicht. Alles, was im Code des TO> steht, ist, dass uint16_t-Objekte an bestimmte Stellen geschrieben> werden. Das ist, glaube ich, kein Zugriff auf die uint8_t.
Magst du vielleicht erstmal im Standard nachlesen, bevor du so etwas
behauptest? Natürlich gibt es auch in C die "Lebenszeit eines Objekts".
Aus ISO/IEC9899:2017 (das müsste der letzte offizielle C17-Draft sein):
1
6.2.4 Storage durations of objects
2
2) The lifetime of an object is the portion of program execution during which storage is guaranteed to be reserved for it.
mh schrieb:> Magst du vielleicht erstmal im Standard nachlesen, bevor du so etwas> behauptest? Natürlich gibt es auch in C die "Lebenszeit eines Objekts".
Aber eben nicht explizit durch Konstruktoren und Destruktoren
eingegrenzt. Wie sollte man sich das vorstellen?
Al Fine schrieb:> mh schrieb:>> Magst du vielleicht erstmal im Standard nachlesen, bevor du so etwas>> behauptest? Natürlich gibt es auch in C die "Lebenszeit eines Objekts".>> Aber eben nicht explizit durch Konstruktoren und Destruktoren> eingegrenzt. Wie sollte man sich das vorstellen?void *p = malloc(256);> // was lebt hier?
Nichts! p zeigt auf "allocated memory" mit "no declared type".
> uint8_t* p8 = p; // jetzt lebt da ein uint8_t?
Nein!. p8 zeigt auf den gleichen "allocated memory" mit "no declared
type".
Erst wenn mit einem geeigneten Lvalue darauf zugegriffen wird gibt es
einen "effective type". Ab diesem Zeitpunkt "lebt" dort etwas. Das
bezieht sich aber auf "allocated memory", hat also nichts mit dem
ursprünglichen Beispiel des TO zu tun.
mh schrieb:> Das bezieht sich aber auf "allocated memory", hat also nichts mit dem> ursprünglichen Beispiel des TO zu tun.
Das kommt darauf an, wie das malloc() implementiert ist, denke ich.
Al Fine schrieb:> mh schrieb:>> Das bezieht sich aber auf "allocated memory", hat also nichts mit dem>> ursprünglichen Beispiel des TO zu tun.>> Das kommt darauf an, wie das malloc() implementiert ist, denke ich.
Klar, wenn sich die malloc Implementation nicht an den Standard hält ...
mh schrieb:> Achso, das steht übrigens in dem schon genannten Absatz 6.5.6
Ja, richtig. Ich gebe dir hiermit recht, dass die lifetime der uint8_t
da schon begonnen hat, deswegen der code undefined ist.
Al Fine schrieb:> mh schrieb:>> Achso, das steht übrigens in dem schon genannten Absatz 6.5.6>> Ja, richtig. Ich gebe dir hiermit recht, dass die lifetime der uint8_t> da schon begonnen hat, deswegen der code undefined ist.
Ach so: aber eben deshalb, weil dort deshalb kein uint16_t objekt liegen
kann.
mh schrieb:> Ich hoffe, dass du es richtig verstanden hast. Ich kann allerdings deine> Argumentation nicht nachvollziehen.
Tja, theoretische Gedankenspiele... Warum sollte man ein Array uint8_t
deklarieren, wenn man uint16_t speichern will?
Al Fine schrieb:> Tja, theoretische Gedankenspiele... Warum sollte man ein Array uint8_t> deklarieren, wenn man uint16_t speichern will?
Beispielsweise oben genannter Klassiker: Weil man in einem STM32F103C8
einen 8kB-Puffer nicht unbedingt zweimal unterbringen kann und will.
mh schrieb:> Es ist verboten.>> [ sinnvolle Stelle aus dem Standard ]
Den Teil hatte ich gestern übersehen. Damit ist ja eigentlich alles
geklärt. Ich wechsel auf ein Union.