Hallo zusammen,
folgende Situation.
System:
ARM Cortex M4 und Atmel Studio.
Ich erhalte einen Datensatz (Befehl), dieser liegt dann in einem 1040
Byte großem Buffer.
Um diese Daten nun einfacher verarbeiten zu können (Protokollabsprache)
und nicht jedes mal mit einem Index zugreifen zu müssen, habe ich mir
z.B. folgende Struct aufgebaut:
1
structcmd_request
2
{
3
uint16_tsize;
4
uint8_ttype;
5
uint16_tcmd;
6
uint8_tdata[CMD_REQUEST_DATA_SIZE];
7
}__attribute__((__packed__));
8
typedefstructcmd_requestcmd_request_t;
Diese lege ich nun als Pointer auf die Adresse des Buffers.
Soweit so gut, funktioniert auch alles.
Natürlich bekomme ich vom Compiler die Warnung:
"packed attribute causes inefficient alignment for 'size'"
Ist ja auch verständlich,
aber wie könnte ich dieses Problem umgehen, evtl. anders lösen?
Habt ihr da eine Idee?
Weiterhin möchte ich doppelte Datenhaltung vermeiden,
sonst könnte man die Struct umbauen und die Werte einzeln zuweisen.
Wenn ich __PACKED für Serialisierung verwende, sind meine Arrays immer
aligned. Ggf. fülle ich mit padding (Reserved) auf, damit das Alignment
wieder passt. Somit braucht der Compiler das auch nicht umzuschieben bei
der Auswertung, sonst bekommst du einen Haufen ineffizienter BYTE
accesses.
Und - wie auch bei dir: Das Array mit der variablen Länge kommt immer
ans Ende. Sachen wie Size kommen immer an den Anfang.
Dein Array, unter Beibehaltung der Reihenfolge (ggf. kannst auch
umsortieren):
Adam P. schrieb:> Diese lege ich nun als Pointer auf die Adresse des Buffers.> Soweit so gut, funktioniert auch alles.
Du darfst in C nicht einfach wahllos Pointer auf Objekte unpassenden
Typs zeigen lassen, der Zugriff durch diese ruft undefiniertes Verhalten
hervor!
Adam P. schrieb:> Ja das ist mir bekannt...jedoch hat das doppelte Datenhaltung zur folge.
Wie empfängst du die Daten denn? Falls jedes Byte einzeln einliest,
kannst du die µSer-Bibliothek mit einem Input-Iterator benutzen. Dann
werden die Bytes einzeln in das struct eingefüllt.
Jemand schrieb:> Du darfst in C nicht einfach wahllos Pointer auf Objekte unpassenden> Typs zeigen lassen
Ist doch gar nicht wahllos... mir ist doch bekannt wie die Daten zu
interpretieren sind.
Niklas G. schrieb:> Dann> werden die Bytes einzeln in das struct eingefüllt.
Ja, aber ich wollte ja die Struct nicht befüllen sondern diese als
"Maske" über einen Buffer legen um einen komfortablen Zugriff zu haben.
So war zumindest die Grundidee.
Nachtrag: Du musst deinen Empfangsbuffer ebenfalls mit __align(4) an die
richtige Position packen (falls der Compiler das nicht eh schon macht,
aber sicher ist sicher).
Adam P. schrieb:> Ich erhalte sie per USB mit DMA und einer Callback.
Ok, dann wird es komplizierter.
Adam P. schrieb:> Ja, aber ich wollte ja die Struct nicht befüllen sondern diese als> "Maske" über einen Buffer legen um einen komfortablen Zugriff zu haben.> So war zumindest die Grundidee.
Das ist in C und C++ nicht wohldefiniert / portabel möglich. Die
doppelte Datenhaltung wäre für Dinge wie Byte-Reihenfolge-Tauschen
sowieso teilweise erforderlich.
Nach dem Konvertieren kannst du ja den "raw" Puffer direkt wieder für
Empfang per DMA benutzen. Somit ist die Datenhaltung nur so Halb-Doppelt
:)
Niklas G. schrieb:> Nach dem Konvertieren kannst du ja den "raw" Puffer direkt wieder für> Empfang per DMA benutzen. Somit ist die Datenhaltung nur so Halb-Doppelt> :)
So hatte ich es ja bis jetzt auch.
Nun dachte ich ok, dann mach ich eine Serialisierung aber nutze Zeiger:
1
structcmd_request
2
{
3
uint16_tsize;
4
uint8_t*type;
5
uint16_t*cmd;
6
uint8_t*data;
7
};
8
typedefstructcmd_requestcmd_request_t;
9
10
...
11
12
cmd.size=1024;
13
14
cmd.type=&buf[0];
15
cmd.cmd=(uint16_t*)&buf[1];
16
cmd.data=&buf[3];
Aber da gefällt ihm der cast nicht:
"cast increases required alignment of target type"
Ich glaub ich werde die "doppelte" Datenhaltung beibehalten, entferne
die " _attribute_ ((_packed_))" und bastel mir die serialize() /
deserialize() Funktionen.
Alles nicht so wirklich zufriedenstellend :-)
Adam P. schrieb:> Aber da gefällt ihm der cast nicht:> "cast increases required alignment of target type"
Du weist ja auch Pointer zu!
So war das wohl gemeint:
1
cmd.size=1024;
2
3
cmd.type=buf[0];
4
cmd.cmd=buf[1]|(((uint16_t)buf[2])<<8);
5
memcpy(cmd.data,buf+3,cmd.size);
Wichtig hier: Bitshifts statt cast auf uint16_t* verwenden. Dadurch wird
es wohldefiniert und portabel.
Mithilfe der µSer-Bibliothek ginge es so:
std::cout<<"cmd_request: size="<<req.size<<", type="<<(int)req.type<<", cmd=0x"<<std::hex<<req.cmd<<", data = {"<<(int)req.data[0]<<", "<<(int)req.data[1]<<", "<<(int)req.data[2]<<"}"<<std::endl;
26
}
Wozu brauchst du überhaupt das "size"? Bei USB-Paketen ist doch die
Größe immer bekannt. Du kannst doch, genau wie bei Control Endpoints,
die Daten mit einem nicht-vollständigen oder leeren Paket abschließen.
Adam P. schrieb:> Ja, schau dir doch die Struct Definition an, da sind Pointer drin.
Achso, ja aber wozu? Wenn du die auf den "raw" Puffer zeigen lässt, ist
zur ursprünglichen Version kaum was gewonnen.
Adam P. schrieb:> Ist aber C++.
Es muss nur die 1 Datei mit dem "deserialize" Aufruf mit C++ kompiliert
werden; der Rest des Projekts kann C bleiben:
https://erlkoenig90.github.io/uSer-doc/html/tutorial.html#CCompat
Adam P. schrieb:> Niklas G. schrieb:>> Dann>> werden die Bytes einzeln in das struct eingefüllt.>> Ja, aber ich wollte ja die Struct nicht befüllen sondern diese als> "Maske" über einen Buffer legen um einen komfortablen Zugriff zu haben.> So war zumindest die Grundidee.
So etwas hat man früher öfters gemacht. War dann Jahre später immer die
Ursache von Problemen, wenn sich Compiler oder CPU (32->64 Bit) geändert
haben.
Deshalb macht man das heute nicht mehr, sondern serialisiert sauber und
freut sich in 10 Jahren, dass es immer noch geht.
Random .. schrieb:> Lässt sich denn das Protokoll nicht mehr ändern?
:-D
Klar, wenn es nur an mir liegen würde, sofort. Aber leider geht das
nicht so einfach.
Aber beim nächsten mal werde ich genau auf sowas achten.
Ich meine, das mit der "sauberen" Serialisierung ist ja gar nicht so
wild,
ich dachte nur, dass es auch sauber mit Zeigern einer Struct geht,
leider scheint dies nicht so gängig zu sein.
Aber deshalb habe ich ja auch in die Runde gefragt ;)
Adam P. schrieb:> ich dachte nur, dass es auch sauber mit Zeigern einer Struct geht,> leider scheint dies nicht so gängig zu sein.
Damit vermeidest du nur das Padding-Problem, aber ggf. hast du immer
noch Probleme mit Alignment, Byte-Reihenfolge und Vorzeichen-Formaten.
Zusätzlich verschwendest du damit noch Speicher für die Zeiger. Da
kannst du dir besser inline-Getter-Funktionen bauen, welche den Zeiger
"live" berechnen - das wird vermutlich Laufzeit und Speicher sparen,
aber dennoch o.g. Probleme haben.
Adam P. schrieb:> Ist ja auch verständlich,> aber wie könnte ich dieses Problem umgehen, evtl. anders lösen?> Habt ihr da eine Idee?
Indem Du Dein Struct NICHT packed machst (Todsünde) und stattdessen
einfach die Member alle so umsortierst daß sie automatisch von selbst
schon aligned sind.
Lies das: http://www.catb.org/esr/structure-packing/
Und dann machst Du eine union aus byte array und struct um
sichertzstellen daß das byte array mindestens so streng aligned ist wie
es das struct erfordert.
Bernd K. schrieb:> Und dann machst Du eine union aus byte array und struct um> sichertzstellen daß das byte array mindestens so streng aligned ist wie> es das struct erfordert.
Kannst du bei solchen Hinweisen vielleicht dazu schreiben, dass ein
solches Vorgehen in C plattform-abhängig und nicht portabel, und in C++
ganz verboten ist?
chrieb im Beitrag #5842163:
> Lies das: http://www.catb.org/esr/structure-packing/
Hier geht es aber lediglich darum, Speicher zu sparen, und nicht um
Serialisierung/Konvertierung...
Bernd K. schrieb:> Indem Du Dein Struct NICHT packed machst (Todsünde) und stattdessen> einfach die Member alle so umsortierst daß sie automatisch von selbst> schon aligned sind.
Er kann das Protokoll nicht ändern.
Adam P. schrieb:> Welchen Ansatz würdet ihr dann bevorzugen,
Keinen von beiden, weil der Compiler es immer noch kaputtoptimieren kann
(außer bei unions in C) und das auf Big-Endian-Systemen immer noch
schief geht.
Hanna schrieb:> Was du suchst sind UNIONS
Die sind ausschließlich zum Speicher Sparen, aber nicht zum Daten
Konvertieren gemacht. Wie auch schon im vom 1. Beitrag verlinkten
Artikel erläutert. Einfach mal den Thread lesen...
Niklas G. schrieb:> Keinen von beidenNiklas G. schrieb:> Wie auch schon im vom 1. Beitrag verlinkten> Artikel erläutert. Einfach mal den Thread lesen...
Jetzt bin ich verwirrt.
Ich dachte was im Artikel steht wäre soweit ok.
Oder soll ich die Struct einfach so belassen:
1
typedefstructcmd_request_t
2
{
3
uint16_tsize;
4
uint8_ttype;
5
uint16_tcmd;
6
uint8_tdata[CMD_REQUEST_DATA_SIZE];
7
}cmd_request_t;
Da werden dann halt "leer-bytes" eingefügt, was mich ja durch das
Serialisieren aber nicht weiter stören sollte. Da ich ja die Werte
direkt zuweise (falls nötig mit Bit-Shifting), sehe ich das richtig?
Adam P. schrieb:> Jetzt bin ich verwirrt.
Achso, du meintest, welchen Ansatz wählen wenn man sowieso explizit
serialisiert (Bitshifts & Co)?
Dann auf jeden Fall den 2. Ansatz. Reservierte Bytes braucht man in
structs nur, wenn man sie "direkt" ohne Serialisierung schickt/einliest;
der Compiler baut solches Padding automatisch ein, solange man eben
nicht "packed" verwendet, was man sowieso nicht tun sollte. Ohne
"packed" ist das "reserved_0" unnütz.
Da CMD_REQUEST_DATA_SIZE wahrscheinlich > 1 ist und "data" auch aligned
ist, wird der Speicherverbrauch in beiden Fällen gleich sein.
Niklas G. schrieb:> Achso, du meintest, welchen Ansatz wählen wenn man sowieso explizit> serialisiert (Bitshifts & Co)?>> Dann auf jeden Fall den 2. Ansatz.
Ja genau, so meinte ich das.
Adam P. schrieb:> Oder soll ich die Struct einfach so belassen:
Da würde er es ja automatisch einfügen, aber würde mich ja auch net
stören.
Wäre halt von der logischen Datenreihenfolge her identisch.
In welcher ANSI C ist denn definiert, dass eine UNION nur zum Speicher
sparen verwednet werden kann?
Faktisch gibst du dem ein und selben Speicherbereich 2 oder mehrere
Variablennamen etc.
Für was das letztlich Verwendung findet ist dem Programmierer
überlassen.
Fakt ist:
UNION wird an dieser Stelle funktionieren, braucht keinen erweiterten
Speicherplatz und kommt ohne Pointerverbiegerei hin.
Also was will man mehr?
Hanna schrieb:> UNION wird an dieser Stelle funktionieren, braucht keinen erweiterten> Speicherplatz und kommt ohne Pointerverbiegerei hin.
Meinst du das etwa so:
1
typedefunionfoo_t
2
{
3
uint8_tbuf[1040];
4
5
struct
6
{
7
uint16_ta;
8
uint8_tb;
9
uint16_tc;
10
uint8_td[512];
11
};
12
}foo_t;
Weil, das geht so nicht. Da hast du ebenfalls das alignment Problem.
Niklas G. schrieb:> und in C++> ganz verboten ist?
Es ist nicht explizit erlaubt weil keine Klausel enthalten ist die das
wörtlich erlaubt. Verboten ist es jedoch nicht!
Aber aus der Anforderung im jeweiligen ABI wie ein struct
zwingenderweise physikalisch anzuordnen ist und aus der Garantie im C++
Standard daß "common initial members" immer übereinander zu liegen
kommen folgt aber im logischem Schluß daß structs in einer Union am
ersten Member bündig ausgerichtet übereinander liegen müssen. Und aus
den Anforderungen des jeweiligen ABI folgt auch das Alignment des
gesamten Gebildes.
Daraus kann man nun logisch schließen daß Type-Punning gemäß des
Datenlayouts der jeweiligen Platform immer funktionieren wird, es ist
also durchaus garantiert, wenn auch nur indirekt!
Diese logische Verkettung zweier Garantien zur (unausgesprochenen weil
impliziten Garantie des Type-Punning) reduziert also das ganze Problem
auf die Betrachtung wie die Daten auf dem jeweiligen ABI physikalisch im
Speicher angeordnet sein werden. Und das kann man für die jeweiligen
angedachten Zielplattform(en) schwarz auf weiß nachlesen und
glücklicherweise steht da heute auch fast überall diesbezüglich das
selbe drin, also wird es in der Praxis (von der wir hier sprechen)
eigentlich ganz einfach.
Zukünftige Abweichungen kann man mit ein paar strategisch gut plazierten
statischen asserts zur Compilezeit abfangen wenn man ein
Über-Perfektionist sein will, dann muss an der Stelle halt in 50 oder
100 Jahren wenn bekannt ist wie die neuen Prozessoren die mit allen
Traditionen brechen funktionieren nochmal jemand kurz Hand anlegen. Aber
wahrscheinlich ist C(++) bis dahin dann sowieso obsolet.
Hanna schrieb:> In welcher ANSI C ist denn definiert, dass eine UNION nur zum Speicher> sparen verwednet werden kann?
Es gibt nur ein "ANSI C". Welches ziemlich veraltet ist. Es ist in C wie
gesagt erlaubt (IIRC aber nur in neueren Standards, nicht in ANSI-C),
unions zum "Konvertieren" zu nutzen, aber in keinem Standard ist ein
garantiertes Verhalten definiert.
Hanna schrieb:> UNION wird an dieser Stelle funktionieren
Definiere "funktionieren".
Hanna schrieb:> Also was will man mehr?
Eine Version, deren Funktionsfähigkeit vom Standard garantiert ist, und
welche auf allen Plattformen und Compilern funktioniert, und vielleicht
sogar C++-aufwärtskompatibel ist.
> UNION wird an dieser Stelle funktionieren, braucht keinen erweiterten> Speicherplatz und kommt ohne Pointerverbiegerei hin.
Das sehe ich auch so. Das funktioniert und habe ich schon oft selbst
benutzt.
Das Problem ist natuerlich das es Probleme geben kann wenn man seinen
Source in 10Jahren mal auf einen ganz anderen Controller weiterverwenden
will und sich auf die gute alte Funktionalitaet verlaesst!
Und natuerlich rasten die ganzen Konzernprogrammierer hier aus weil sie
die Erfahrung bereits gemacht haben. :-)
Andererseits wenn man weiss was man macht und es nur fuer einen kleinen
Controller ist wo man nur wenig Resourcen hat wieso nicht. Erzieht
wenigstens die ganzen Abschreiber im Internetzeitalter zum selber
denken. .-)
Olaf
Niklas G. schrieb:> Es ist in C wie> gesagt erlaubt (IIRC aber nur in neueren Standards, nicht in ANSI-C)
Es wurde ein klärender Satz hinzufügt der die implizit bereits logisch
herleitbare Erlaubnis nochmal explizit wörtlich erwähnt hat damit der
eilige Leser nicht so lange drüber nachdeken muß.
Im C++-Standard hat man das wohl bislang noch nicht für nötig befunden
oder schlichtweg vergessen. Ist ja eigentlich auch redundant.
Niklas G. schrieb:> Hier steht was anderes:>> https://stackoverflow.com/a/11996970
Bitte präzisiere oder zitiere den entsprechenden Satz, das ist ein
ganzer Stackoverflow-Thread, den kenn ich schon, worauf nimmst Du
konkret Bezug?
PS: Big-Endian-Plattformen existieren, und für diese muss man bei allen
sich aufs Speicherlayout verlassenden Varianten dann #ifdefs und
Byte-Tauschen einbauen, wobei die #ifdef-Bedingung selbst nur mit
compilerspezifischen Erweiterungen möglich ist (z.B:
_ORDER_BIG_ENDIAN_ beim GCC).
Diese Tauscherei ist dann auch nicht mehr Aufwand als es direkt richtig
mit Bitshifts zu machen.
Bernd K. schrieb:> worauf nimmst Du> konkret Bezug?
"That is, although we can legitimately form an lvalue to a non-active
union member (which is why assigning to a non-active member without
construction is ok) it is considered to be uninitialized."
Adam P. schrieb:> mir ist doch bekannt wie die Daten zu interpretieren sind.
Das ist absolut irrelevant. Wäre sonst auch implementationsdefiniert,
nicht undefiniert.
Dann will ichs mal anders formulieren: Ist einem der Anwesenden auch nur
ein einziger Fall bekannt bei dem Type-Punning mit Unions in der Praxis
nicht wie erwartet funktioniert hat obwohl alle Anforderungen des ABI
bezüglich Datenlayout, Endianness und Alignment auf der betreffenden
Plattform sorgfältig recherchiert und buchstabengetreu erfüllt waren?
Also ein Fall in dem der Compiler quasi gesagt hat "Du hast zwar alle
Offsets richtig ausgerechnet und eigentlich müssten Deine Daten genau
dort liegen wo Du gerade zugreifen willst aber ich habe jetzt keine Lust
weil das im Standard nicht wörtlich von mir verlangt wird und deshalb
mach ich jetzt einfach mal was ganz anderes, auch wenn das mehr Aufwand
für mich bedeutet!", sowas in der Art?
Bernd K. schrieb:> Dann will ichs mal anders formulieren: Ist einem der Anwesenden auch nur> ein einziger Fall bekannt bei dem Type-Punning mit Unions in der Praxis> nicht wie erwartet funktioniert hat obwohl alle Anforderungen des ABI> bezüglich Datenlayout, Endianness und Alignment auf der betreffenden> Plattform sorgfältig recherchiert und buchstabengetreu erfüllt waren?
Wenn sich alle im Straßenverkehr korrekt verhalten würden, bräuchte man
keinen Sicherheitsgurt.
Mir hat er einmal das Leben gerettet.
Wenn es eine potentielle Gefahrenstelle gibt, dann umschifft man die.
Und die einfachste Art ist die Serialisierung.
PittyJ schrieb:> Wenn es eine potentielle Gefahrenstelle gibt
Das war die Frage. Ist es eine Gefahrenstelle (rein logisch kann es
keine sein denn das ABI diktiert(!) haarklein das Speicherlayout einer
Union), oder ist es nur eine unbegründete Befürchtung weil niemand
offizielles bislang bestätigt hat daß es erlaubt ist (und die ganze Zeit
schon war) wie es bei C geschehen ist?
Es **ist** erlaubt.
Allerdings (vornehmlich, weil es nun mal Big- und Little-Endian-Systeme
gibt) implementation defined. Also so, wie es sich der Compiler- (bzw.
Plattformhersteller gedacht hat).
Wenn das nicht so funktionieren würde, würden Generationen von
TCP-Stacks (s. struct sockaddr) auf die Nase fallen.
Markus F. schrieb:> Es **ist** erlaubt.>[…]> Wenn das nicht so funktionieren würde, würden Generationen von> TCP-Stacks (s. struct sockaddr) auf die Nase fallen.
Ja, das ist so krass erlaubt, dass im Linux Kernel extra die
Aliasing-Regeln für solche Spielereien by-default gelockert sind.
Muss unglaublich C-konform sein, dieser Code!
Jemand schrieb:> Ja, das ist so krass erlaubt, dass im Linux Kernel extra die> Aliasing-Regeln für solche Spielereien by-default gelockert sind.
... und strict aliasing hat was genau mit unions zu tun?
Markus F. schrieb:> Jemand schrieb:>> Ja, das ist so krass erlaubt, dass im Linux Kernel extra die>> Aliasing-Regeln für solche Spielereien by-default gelockert sind.>> ... und strict aliasing hat was genau mit unions zu tun?
Was hat dein Post mit Unions zu tun? Du zitierst nichts und Linux nutzt
für sockaddr Pointer-Casts.