Struct über Array legen, alignment Warnung

OP #5841945
Lesenswert?

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
struct cmd_request
2
{
3
   uint16_t size;
4
   uint8_t type;
5
   uint16_t cmd;
6
   uint8_t data[CMD_REQUEST_DATA_SIZE];
7
} __attribute__ ((__packed__));
8
typedef struct cmd_request cmd_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.
Persönliche Seite #5841964
Lesenswert?

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):
1
struct cmd_request
2
{
3
   uint16_t size;
4
   uint16_t res1;
5
   uint8_t type;
6
   uint8_t res2;
7
   uint16_t cmd;
8
   uint8_t data[CMD_REQUEST_DATA_SIZE];
9
} __attribute__ ((__packed__));
10
typedef struct cmd_request cmd_request_t;
Persönliche Seite #5841991
Lesenswert?

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 
:)
OP #5842052
Lesenswert?

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
struct cmd_request
2
{
3
  uint16_t size;
4
  uint8_t *type;
5
  uint16_t *cmd;
6
  uint8_t *data;
7
};
8
typedef struct cmd_request cmd_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 :-)
Persönliche Seite #5842061
Lesenswert?

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:
1
#include <iostream>
2
#include <cstdint>
3
#include <cstddef>
4
#include <uSer/uSer.hh>
5

6
static constexpr std::size_t CMD_REQUEST_DATA_SIZE = 3;
7

8
struct cmd_request {
9
  USER_STRUCT(cmd_request)
10
  
11
  uint16_t size;
12
  uint8_t type;
13
  uint16_t cmd;
14
  uint8_t data [CMD_REQUEST_DATA_SIZE];
15

16
  USER_ENUM_MEM (size, type, cmd, data)
17
};
18

19
int main () {
20
  std::uint8_t rxBuffer [8] = { 3, 0, 42, 0xAD, 0xDE, 1, 2, 3 };
21
  cmd_request req;
22
  
23
  uSer::deserialize (rxBuffer, req);
24
  
25
  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.
Persönliche Seite #5842075
Lesenswert?

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
Gast #5842126
Lesenswert?

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.
OP #5842141
Lesenswert?

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 ;)
Persönliche Seite #5842147
Lesenswert?

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.
#5842163
Lesenswert?

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.
OP #5842176
Lesenswert?

OK.

Welchen Ansatz würdet ihr dann bevorzugen, wenn man die Daten mithilfe 
von serialize() / deserialize() verarbeitet:

1) mit Reserve-Byte
1
typedef struct cmd_request_t
2
{
3
   uint16_t size;
4
   uint8_t type;
5
   uint8_t reserved_0;
6
   uint16_t cmd;
7
   uint8_t data[CMD_REQUEST_DATA_SIZE];
8
} cmd_request_t;

2) Andere Reihenfolge
1
typedef struct cmd_request_t
2
{
3
   uint16_t size;
4
   uint16_t cmd;
5
   uint8_t type;
6
   uint8_t data[CMD_REQUEST_DATA_SIZE];
7
} cmd_request_t;
Persönliche Seite #5842177
Lesenswert?

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...
OP #5842196
Lesenswert?

Ich dachte was im Artikel steht wäre soweit ok.

Oder soll ich die Struct einfach so belassen:
1
typedef struct cmd_request_t
2
{
3
   uint16_t size;
4
   uint8_t type;
5
   uint16_t cmd;
6
   uint8_t data[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?
Persönliche Seite #5842197
Lesenswert?

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.
OP #5842206
Lesenswert?

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.
Gast #5842216
Lesenswert?

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?
OP #5842226
Lesenswert?

Hanna schrieb:
> UNION wird an dieser Stelle funktionieren, braucht keinen erweiterten
> Speicherplatz und kommt ohne Pointerverbiegerei hin.

Meinst du das etwa so:
1
typedef union foo_t
2
{
3
  uint8_t buf[1040];
4
  
5
  struct
6
  {
7
    uint16_t a;
8
    uint8_t b;
9
    uint16_t c;
10
    uint8_t d[512];
11
  };
12
} foo_t;

Weil, das geht so nicht. Da hast du ebenfalls das alignment Problem.
#5842229
Lesenswert?

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.
Persönliche Seite #5842230
Lesenswert?

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.
Gast #5842235
Lesenswert?

> 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
#5842238
Lesenswert?

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.
Persönliche Seite #5842242
Lesenswert?

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."
#5842268
Lesenswert?

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?
Gast #5842521
Lesenswert?

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.
#5842628
Lesenswert?

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?
#5842637
Lesenswert?

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.
Gast #5842721
Lesenswert?

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!
Gast #5842749
Lesenswert?

ich peil irgednwie das problem nicht

sollte das nicht so gehen :

typedef struct _attribute_ ((_packed_)){
   uint16_t size;
   uint8_t type;
   uint16_t cmd;
   uint8_t data[CMD_REQUEST_DATA_SIZE];
} cmd_request_t;

// buffer für daten
uint8_t buffer[1040];

// irgendwo pointer zuweisen
cmd_request_t *mybuf = (cmd_request_t*) buffer;

//lesen
uint16_t oldsize = mybuf->size;

//schreiben
mybuf->size = 0;


??
Gast #5842750
Lesenswert?

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.

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren