Forum: Mikrocontroller und Digitale Elektronik Frage zu typedef bei Strukturen


von M. G. (ixil96)


Lesenswert?

Hallo,

was den Umgang mit Strukturen betrifft, da bin ich noch unsicher.
Daher möchte ich euch fragen, ob meine Annahme für dieses Beispiel so 
stimmt.

1. Durch die Typkonvertierung ist TEST nichts anderes als ein Zeiger auf 
0x40020000 (int-Zeiger)

2. Mittels typedef wird der Name TEST_TypeDef zu einem neuen Datentypen, 
in diesem Fall ein int-Zeiger auf 0x40020000

3. Der Zugriff auf die Strukturmember erfolgt mittels Test->Memberx
1
// Typkonvertierung auf eine Adresse - Die Struktur TEST_TypeDef startet an Adresse 0x4002 0000
2
3
#define TEST  ((TEST_TypeDef*) 0x40020000UL)
4
5
typedef struct
6
{
7
  volatile uint32_t Member1;    // 0x4002 0000 - 0x4002 0004
8
  volatile uint32_t Member2;      // 0x4002 0004 - 0x4002 0008
9
  volatile uint32_t Member3[3];   // 0x4002 0008 - 0x4002 0014
10
  volatile uint32_t Member4;      // 0x4002 0014 - 0x4002 0018
11
}TEST_TypeDef;            // Startadresse der Struktur: 0x4002 0000

Habe ich das so richtig verstanden?

von Tim T. (tim_taylor) Benutzerseite


Lesenswert?

M. G. schrieb:
> Hallo,
>
> was den Umgang mit Strukturen betrifft, da bin ich noch unsicher.
> Daher möchte ich euch fragen, ob meine Annahme für dieses Beispiel so
> stimmt.
>
> 1. Durch die Typkonvertierung ist TEST nichts anderes als ein Zeiger auf
> 0x40020000 (int-Zeiger)
>
> 2. Mittels typedef wird der Name TEST_TypeDef zu einem neuen Datentypen,
> in diesem Fall ein int-Zeiger auf 0x40020000
>
> 3. Der Zugriff auf die Strukturmember erfolgt mittels Test->Memberx
>
>
1
> // Typkonvertierung auf eine Adresse - Die Struktur TEST_TypeDef startet 
2
> an Adresse 0x4002 0000
3
> 
4
> #define TEST  ((TEST_TypeDef*) 0x40020000UL)
5
> 
6
> typedef struct
7
> {
8
>   volatile uint32_t Member1;    // 0x4002 0000 - 0x4002 0004
9
>   volatile uint32_t Member2;      // 0x4002 0004 - 0x4002 0008
10
>   volatile uint32_t Member3[3];   // 0x4002 0008 - 0x4002 0014
11
>   volatile uint32_t Member4;      // 0x4002 0014 - 0x4002 0018
12
> }TEST_TypeDef;            // Startadresse der Struktur: 0x4002 0000
13
>
>
> Habe ich das so richtig verstanden?

Ja, sollte passen, bis auf dass die Zeiger bei einem 64-Bit System eben 
64 Bit haben. Kannst es aber auch ganz einfach selber testen:
1
#include <stdio.h>
2
#include <stdint.h>
3
4
typedef struct {
5
  volatile uint32_t Member1;
6
  volatile uint32_t Member2;
7
  volatile uint32_t Member3[3];
8
  volatile uint32_t Member4;
9
} TEST_TypeDef;
10
11
int main(void) {
12
  
13
  TEST_TypeDef test;
14
  TEST_TypeDef *pointerauftest = &test;
15
16
  printf("Adresse von test: 0x%p\n", pointerauftest);
17
  printf("Adresse von test.Member1: 0x%p\n", &pointerauftest->Member1);
18
  printf("Adresse von test.Member2: 0x%p\n", &pointerauftest->Member2);
19
  printf("Adresse von test.Member3: 0x%p\n", &pointerauftest->Member3);
20
  printf("Adresse von test.Member4: 0x%p\n", &pointerauftest->Member4);
21
  
22
  return 0;
23
}

: Bearbeitet durch User
von Programmierer (Gast)


Lesenswert?

M. G. schrieb:
> 1. Durch die Typkonvertierung ist TEST nichts anderes als ein Zeiger auf
> 0x40020000 (int-Zeiger)

Nicht ganz, TEST ist ein Makro, d.h. überall wo du TEST schreibst wird 
das simpel textuell ersetzt zu "((TEST_TypeDef*) 0x40020000UL)". Also 
ein Zeiger vom Typ TEST_TypeDef* welcher auf 0x40020000UL zeigt.

M. G. schrieb:
> 2. Mittels typedef wird der Name TEST_TypeDef zu einem neuen Datentypen,
> in diesem Fall ein int-Zeiger auf 0x40020000

Nein, TEST_TypeDef ist dann ein Alias für den Typ dieses structs. Diese 
Schreibweise ist eine verkürzte Form von (ohne den Namen Foo):
1
struct Foo {
2
/* ... */
3
};
4
typedef struct Foo TEST_TypeDef;

Hier wird ein struct namens Foo definiert, d.h. man muss es in der Form 
"struct Foo" verwenden. Weil das aber umständlich ist, wird danach noch 
ein Alias namens "TEST_TypeDef" für "struct Foo" definiert. D.h. im 
weiteren Code braucht man das Schlüsselwort "struct" nicht mehr. Das 
kann man abkürzen zu:
1
typedef struct Foo {
2
/* ... */
3
} TEST_TypeDef;

Was exakt das gleiche bewirkt. Wenn man das "Foo" weglässt hat das 
struct keinen eigenen Namen mehr (es ist anonym), aber TEST_TypeDef ist 
dann ein Alias dafür. Wenn man im Code "TEST_TypeDef" nutzt meint man 
eben dieses struct, aber muss nicht jedes mal "struct" schreiben.

In C++ ist das übrigens unnötig, dort kann man einfach
1
struct TEST_TypeDef {
2
/* ... */
3
};
schreiben und kann das struct dann einfach über den Namen "TEST_TypeDef" 
verwenden ohne jedes Mal "struct TEST_TypeDef" schreiben zu müssen.

An dieser Stelle passiert noch nichts mit Integer-Werten, Zeigern oder 
Casting. Es wird nur ein struct-Typ mit Aliasnamen definiert. Die 
eigentliche Zeigerbastelei erfolgt dann im TEST-Makro.

M. G. schrieb:
> 3. Der Zugriff auf die Strukturmember erfolgt mittels Test->Memberx

Ja. Das ganze ist eine ziemlich schmutzige Konstruktion. Eine 
Integer-Konstante auf einen Zeigertyp zu casten ist in C eigentlich 
nicht erlaubt, aber es funktioniert "zufällig" mit den hier verwendeten 
Compilern.

von W.S. (Gast)


Lesenswert?

M. G. schrieb:
> 2. Mittels typedef wird der Name TEST_TypeDef zu einem neuen Datentypen,
> in diesem Fall ein int-Zeiger auf 0x40020000

Nein. So nicht.
typedef dient lediglich dazu, einem vorhandenen Typ einen weiteren 
Namen zu machen.

Also:
1
 struct ottokar
2
 { int  emil;
3
   char heinrich;
4
 };
Damit definiert man eine Struktur namens ottokar. Zum Deklarieren von 
Variablen dieses Typs muß man aber immer noch das Wort 'struct' 
dazuschreiben. Vielen Leuten ist das lästig.
Deswegen:
1
 typedef struct ottokar karlheinz;
2
oder
3
 typedef
4
 { int  emil;
5
   char heinrich;
6
 } karlheinz;
Hierbei erzeugt man im ersten Schritt eine namenlose Struktur und dann 
einen weiteren Namen dazu, hier 'karlheinz'.

So herum. Frag jetzt aber nicht warum so.

W.S.

von Tim T. (tim_taylor) Benutzerseite


Lesenswert?

Programmierer schrieb:
> M. G. schrieb:
>> 1. Durch die Typkonvertierung ist TEST nichts anderes als ein Zeiger auf
>> 0x40020000 (int-Zeiger)
>
> Nicht ganz, TEST ist ein Makro, d.h. überall wo du TEST schreibst wird
> das simpel textuell ersetzt zu "((TEST_TypeDef*) 0x40020000UL)". Also
> ein Zeiger vom Typ TEST_TypeDef* welcher auf 0x40020000UL zeigt.

Ein Makro das einen Zeiger vom Typ TEST_TypeDef auf eine Adresse 
(0x40020000) definiert. Also wo ist da der Widerspruch zum TO?

> M. G. schrieb:
>> 2. Mittels typedef wird der Name TEST_TypeDef zu einem neuen Datentypen,
>> in diesem Fall ein int-Zeiger auf 0x40020000
>
> Nein, TEST_TypeDef ist dann ein Alias für den Typ dieses structs. Diese
> Schreibweise ist eine verkürzte Form von (ohne den Namen Foo):
>
>
1
struct Foo {
2
> /* ... */
3
> };
4
> typedef struct Foo TEST_TypeDef;
>
> Hier wird ein struct namens Foo definiert, d.h. man muss es in der Form
> "struct Foo" verwenden. Weil das aber umständlich ist, wird danach noch
> ein Alias namens "TEST_TypeDef" für "struct Foo" definiert. D.h. im
> weiteren Code braucht man das Schlüsselwort "struct" nicht mehr. Das
> kann man abkürzen zu:
>
>
1
typedef struct Foo {
2
> /* ... */
3
> } TEST_TypeDef;

Also es wird demnach ein neuer Typ definiert. Nur das dieser eben 
erstmal nichts mit der Adresse zu tun hat, lag der TO auch hier richtig.

> Was exakt das gleiche bewirkt. Wenn man das "Foo" weglässt hat das
> struct keinen eigenen Namen mehr (es ist anonym), aber TEST_TypeDef ist
> dann ein Alias dafür. Wenn man im Code "TEST_TypeDef" nutzt meint man
> eben dieses struct, aber muss nicht jedes mal "struct" schreiben.
>
> In C++ ist das übrigens unnötig, dort kann man einfach
>
1
struct TEST_TypeDef {
2
> /* ... */
3
> };
> schreiben und kann das struct dann einfach über den Namen "TEST_TypeDef"
> verwenden ohne jedes Mal "struct TEST_TypeDef" schreiben zu müssen.
>
> An dieser Stelle passiert noch nichts mit Integer-Werten, Zeigern oder
> Casting. Es wird nur ein struct-Typ mit Aliasnamen definiert. Die
> eigentliche Zeigerbastelei erfolgt dann im TEST-Makro.

Ja, da lag der TO etwas daneben.

> M. G. schrieb:
>> 3. Der Zugriff auf die Strukturmember erfolgt mittels Test->Memberx
>
> Ja. Das ganze ist eine ziemlich schmutzige Konstruktion. Eine
> Integer-Konstante auf einen Zeigertyp zu casten ist in C eigentlich
> nicht erlaubt, aber es funktioniert "zufällig" mit den hier verwendeten
> Compilern.

Jein, das man einen Zeiger auf eine Adresse definiert ist jetzt nichts 
ungewöhnliches, nur wie der TO es macht ist unsauber.

von Tim T. (tim_taylor) Benutzerseite


Lesenswert?

W.S. schrieb:
> M. G. schrieb:
>> 2. Mittels typedef wird der Name TEST_TypeDef zu einem neuen Datentypen,
>> in diesem Fall ein int-Zeiger auf 0x40020000
>
> Nein. So nicht.
> typedef dient lediglich dazu, einem vorhandenen Typ einen weiteren
> Namen zu machen.

Haarspalterei. Natürlich wird das Konstrukt im Struct definiert, nur 
durch den Typedef wird es erst direkt (ohne struct Schlüsselwort) als 
Datentyp nutzbar. Aber wie gesagt, Haarspalterei.

: Bearbeitet durch User
von Klaus W. (mfgkw)


Lesenswert?

Tim T. schrieb:
> Programmierer schrieb:
>> M. G. schrieb:
>>> 1. Durch die Typkonvertierung ist TEST nichts anderes als ein Zeiger auf
>>> 0x40020000 (int-Zeiger)
>>
>> Nicht ganz, TEST ist ein Makro, d.h. überall wo du TEST schreibst wird
>> das simpel textuell ersetzt zu "((TEST_TypeDef*) 0x40020000UL)". Also
>> ein Zeiger vom Typ TEST_TypeDef* welcher auf 0x40020000UL zeigt.
>
> Ein Makro das einen Zeiger vom Typ TEST_TypeDef auf eine Adresse
> (0x40020000) definiert. Also wo ist da der Widerspruch zum TO?

Der TO meinte, daß er einen Zeiger auf int hat.
Er hat aber einen Zeiger auf TEST_TypeDef.

(Daß der wahrscheinlich auch auf die erste int in der struct zeigt, ist 
ein anderes Thema.)

von Tim T. (tim_taylor) Benutzerseite


Lesenswert?

Klaus W. schrieb:
> Der TO meinte, daß er einen Zeiger auf int hat.
> Er hat aber einen Zeiger auf TEST_TypeDef.
>
> (Daß der wahrscheinlich auch auf die erste int in der struct zeigt, ist
> ein anderes Thema.)

Ok, das ist natürlich richtig. Hatte den TO nur so verstanden das es im 
Prinzip ein Zeiger auf einen int(32) ist, in diesem Fall.

: Bearbeitet durch User
von Programmierer (Gast)


Lesenswert?

Tim T. schrieb:
> Also wo ist da der Widerspruch zum TO?

M. G. schrieb dass TEST ein Zeiger ist. TEST ist aber kein Zeiger, 
sondern ein Makro. Ja, Haarspalterei, aber das ist in bestimmten 
Situationen wichtig.

Tim T. schrieb:
> Jein, das man einen Zeiger auf eine Adresse definiert ist jetzt nichts
> ungewöhnliches,

Nur weil es oft so gemacht wird ist es noch lange nicht korrekt! Im 
Embedded-Umfeld ist das mangels guter Alternativen verbreitet, aber 
eigentlich nicht korrekt. ST macht das ja in der HAL sehr intensiv so. 
Einen Zeiger auf einen ausreichend großen Int-Typ und wieder zurück auf 
Zeiger zu casten ist erlaubt, aber wenn man den Zeiger dereferenziert 
muss man natürlich erst einen validen Zeiger auf einen zuvor allokierten 
Speicherbereich haben (z.B. per malloc). Das hat man hier halt nicht, 
sondern es wird eine Adresse "aus der Luft gegriffen". Das geht so in C 
eigentlich nicht. Es funktioniert "zufällig", weil der Compiler aus so 
etwas einen direkten Zugriff auf die Adresse macht, und an dieser 
Adresse wahrscheinlich eine Peripherie zu finden ist. Ein Compiler, der 
ein solches Programm aber nicht übersetzt oder irgendwelchen Blödsinn 
daraus macht, wäre aber konform zum C-Standard (aber im Embedded-Umfeld 
wohl nicht besonders nützlich). Korrekt ginge es, indem man die Adresse 
erst im Linkerscript zuweist oder den Zugriff über eine externe 
Assembler-Funktion umsetzt. Weil das aber unpraktisch ist und sich auch 
nicht so gut optimieren lässt, wird das nicht so gemacht.

Tim T. schrieb:
> Aber wie gesagt, Haarspalterei.

Naja, diese Haarspalterei bewirkt dass man das "struct"-Keyword 
weglassen kann. Also durchaus merklich.

Klaus W. schrieb:
> Der TO meinte, daß er einen Zeiger auf int hat.

Hm, es gibt im ganzen Code keinen int-Zeiger. Es gibt nur einen int-Wert 
(Literal) 0x40020000, der dann auf einen Zeiger vom Typ TEST_TypeDef 
gecastet wird. Also kein int-Zeiger.

von Klaus W. (mfgkw)


Lesenswert?

Programmierer schrieb:
> Klaus W. schrieb:
>> Der TO meinte, daß er einen Zeiger auf int hat.
>
> Hm, es gibt im ganzen Code keinen int-Zeiger. Es gibt nur einen int-Wert
> (Literal) 0x40020000, der dann auf einen Zeiger vom Typ TEST_TypeDef
> gecastet wird. Also kein int-Zeiger.

Ich habe ja auch nicht gesagt, daß er einen int-Zeiger hätte.

Das war der TO:

M. G. schrieb:
> 1. Durch die Typkonvertierung ist TEST nichts anderes als ein Zeiger auf
> 0x40020000 (int-Zeiger)

Meine Aussage war, daß das nicht stimmt und er einen Zeiger auf eine 
struct hat.

Falsch war allerdings von mir der Nachsatz:

Klaus W. schrieb:
> (Daß der wahrscheinlich auch auf die erste int in der struct zeigt, ist
> ein anderes Thema.)
Damit meinte ich die erste uint32_t in der struct.

von Tim T. (tim_taylor) Benutzerseite


Lesenswert?

Programmierer schrieb:
> Tim T. schrieb:
>> Also wo ist da der Widerspruch zum TO?
>
> M. G. schrieb dass TEST ein Zeiger ist. TEST ist aber kein Zeiger,
> sondern ein Makro. Ja, Haarspalterei, aber das ist in bestimmten
> Situationen wichtig.

Ob ich selber einen TEST Zeiger definiere oder es dem Präprozessor 
überlasse an jeder Stelle wo ich TEST schreibe die Adresse einzutragen, 
ist solange ich nicht versuche diesen vermeintlichen Zeiger im Programm 
zu ändern, wirklich belanglos.

ist doch nun wirklich
> Tim T. schrieb:
>> Jein, das man einen Zeiger auf eine Adresse definiert ist jetzt nichts
>> ungewöhnliches,
>
> Nur weil es oft so gemacht wird ist es noch lange nicht korrekt! Im
> Embedded-Umfeld ist das mangels guter Alternativen verbreitet, aber

Genau das.

> eigentlich nicht korrekt. ST macht das ja in der HAL sehr intensiv so.
> Einen Zeiger auf einen ausreichend großen Int-Typ und wieder zurück auf
> Zeiger zu casten ist erlaubt, aber wenn man den Zeiger dereferenziert
> muss man natürlich erst einen validen Zeiger auf einen zuvor allokierten
> Speicherbereich haben (z.B. per malloc). Das hat man hier halt nicht,
> sondern es wird eine Adresse "aus der Luft gegriffen". Das geht so in C

Klar, der Kontext muss stimmen aber das ist erstmal kein Grund zu 
behaupten das es nicht korrekt wäre. Es ist nur eben in den meisten 
Fällen, gerade bei Anfängern eben nicht korrekt weil es etwas anderes 
bewirkt als diese sich gedacht haben.

> eigentlich nicht. Es funktioniert "zufällig", weil der Compiler aus so
> etwas einen direkten Zugriff auf die Adresse macht, und an dieser
> Adresse wahrscheinlich eine Peripherie zu finden ist. Ein Compiler, der
> ein solches Programm aber nicht übersetzt oder irgendwelchen Blödsinn
> daraus macht, wäre aber konform zum C-Standard (aber im Embedded-Umfeld
> wohl nicht besonders nützlich). Korrekt ginge es, indem man die Adresse
> erst im Linkerscript zuweist oder den Zugriff über eine externe
> Assembler-Funktion umsetzt. Weil das aber unpraktisch ist und sich auch
> nicht so gut optimieren lässt, wird das nicht so gemacht.

Fakt ist C lässt Zugriffe auf fest definierte Adressen zu, ob das 
Betriebssystem das ebenfalls zulässt ist eine andere Geschichte oder ob 
da das Programm überhaupt dran darf.

> Tim T. schrieb:
>> Aber wie gesagt, Haarspalterei.
>
> Naja, diese Haarspalterei bewirkt dass man das "struct"-Keyword
> weglassen kann. Also durchaus merklich.

Der einzige Grund der mir spontan einfällt überhaupt ein struct über 
etwas anderes anzusprechen als über ein typedef auf das struct, ist wenn 
man bereits innerhalb des struct schon einen Pointer auf das struct 
braucht, dieses dort aber noch nicht über seinen durch typdef 
definierten typ ansprechbar ist. Z.B. bei verketteten Listen.

: Bearbeitet durch User
von Klaus W. (mfgkw)


Lesenswert?

Programmierer schrieb:
> Nur weil es oft so gemacht wird ist es noch lange nicht korrekt! Im
> Embedded-Umfeld ist das mangels guter Alternativen verbreitet, aber
> eigentlich nicht korrekt. ST macht das ja in der HAL sehr intensiv so.

Das macht zwangsläufig jeder so, der hardwarenah programmiert.
Nur so kommt man etwas heran, was irgendwo im Adreßraum eingeblendet 
ist.

Wer es nicht macht, programmiert nicht Controller, oder er läßt es 
machen und includet Headerdateien, in denen es so steht.

Daß man dann halt wissen muß, ob die ganzzahligen Literale als Adressen 
passen in der konkreten Situation, ist klar. Portabel ist das 
zwangsläufig nicht, aber trotzdem korrekt für ein bestimmtes System.

C ermöglicht den Spagat zwischen (1) low level und (2) schön+portabel.
Jetzt mit der reinen Lehre von schönem C zu kommen, macht nur im zweiten 
Fall Sinn, nicht in der Realität von (1). Solange man keinen besseren 
Vorschlag für (1) hat zumindest.

von Programmierer (Gast)


Lesenswert?

Tim T. schrieb:
> ist solange ich nicht versuche diesen vermeintlichen Zeiger im Programm
> zu ändern, wirklich belanglos

Naja, es kann auch Probleme mit dem Scope geben / Namenskollisionen etc. 
Ist hier aber nicht relevant. Haarspalterei ist gerade in C manchmal 
wichtig...

Tim T. schrieb:
> Klar, der Kontext muss stimmen aber das ist erstmal kein Grund zu
> behaupten das es nicht korrekt wäre.

Ja ne, korrekt ist was der C-Standard definiert. Und das tut er in 
diesem Fall nicht. Es funktioniert halt trotzdem. Das sollte man im 
Hinterkopf behalten.

Tim T. schrieb:
> Fakt ist C lässt Zugriffe auf fest definierte Adressen zu

Nein, der Standard definiert das nicht, aber es funktioniert in den 
meisten Umgebungen. Prinzipiell kann C (oder C++) auch ähnlich wie Java 
implementiert werden, wo Zeiger nur eine Referenznummer sind. Da kann 
man dann nicht einfach irgendwohin zugreifen. Allerdings gibt es keine 
Implementation die es so macht...

Klaus W. schrieb:
> Das macht zwangsläufig jeder so, der hardwarenah programmiert.
> Nur so kommt man etwas heran, was irgendwo im Adreßraum eingeblendet
> ist.

Wie gesagt gibt es Alternativen, aber die sind auch nicht das Gelbe vom 
Ei. Idealerweise müssten die Compiler ein Builtin zur Verfügung stellen, 
mit dem das geht, z.B. à la "__io_reg(0x40020000) = 42;". Das ist dann 
natürlich immer noch nicht portabel aber wenigstens nicht mehr UB 
sondern "nur" noch IB. Macht der AVR-GCC sowas nicht sogar?

von A. S. (Gast)


Lesenswert?

Tim T. schrieb:
>> Tim T. schrieb:
>>> Aber wie gesagt, Haarspalterei.
>>
>> Naja, diese Haarspalterei bewirkt dass man das "struct"-Keyword
>> weglassen kann. Also durchaus merklich.

Diesen Teil der "Haarspalterei" sollte man vielleicht ganz kurz für den 
TO aufdröseln, da er relativ unstrittig ist:
1
typedef int t0;       // erstellt einen typ t0, der äquivalent zu int ist
2
3
struct s1 {int a;};   // definiert ein struct s1
4
struct s1 myStruct1;  // Eine Variable davon  
5
typedef struct s1 t1; // "struct s1" kann ich durch t1 ersetzen.  
6
t1 myStruct1;         // das erlaubt eine kürzere Schreibweise
7
8
/* alternativ struct+typedef "vereint". Bewirkt genau das selbe */
9
typedef struct s1 {int dummy;} t1; 
10
11
/* wenn ich "struct s1" nicht brauche, darf ich "s1" weglassen */
12
typedef struct {int dummy;} t1;

Man braucht hier keinen typedef, er ist aber verbreitet.

Wenn man nur den Typedef nutzt, braucht das struct keinen Namen. Das 
wird (eher als Voodoo) trotzdem oft gemacht.

> Der einzige Grund der mir spontan einfällt überhaupt ein struct über
> etwas anderes anzusprechen als über ein typedef auf das struct,
Ein Vorteil bei "struct" ist nach Linus T., dass man direkt erkennt, 
dass es kein einfacher (kleiner) Skalar oder Pointer ist, sondern 
vielleicht ein mehrere (k)Bytes großes Objekt.

von Tim T. (tim_taylor) Benutzerseite


Lesenswert?

A. S. schrieb:
>> Der einzige Grund der mir spontan einfällt überhaupt ein struct über
>> etwas anderes anzusprechen als über ein typedef auf das struct,
> Ein Vorteil bei "struct" ist nach Linus T., dass man direkt erkennt,
> dass es kein einfacher (kleiner) Skalar oder Pointer ist, sondern
> vielleicht ein mehrere (k)Bytes großes Objekt.

Nagut, wenn Linus T. das sagt... Hail to the Chief!

Nee mal im Ernst, irgendwann sollte man eigentlich alle primitiven 
Datentypen kennen, so dass man bei einem anderen Datentyp als diesen 
eigentlich immer von einem größeren Konstrukt dahinter ausgehen kann.

von Programmierer (Gast)


Lesenswert?

Tim T. schrieb:
> Nagut, wenn Linus T. das sagt... Hail to the Chief!

Naja, die Meinungen des Linus T. über "gutes" Programmieren sind... 
kontrovers. Die sollte man nicht unreflektiert übernehmen.

Tim T. schrieb:
> Nee mal im Ernst, irgendwann sollte man eigentlich alle primitiven
> Datentypen kennen,

Sowas wie mode_t, time_t & Co sind auch nur Aliase für Integer-Typen, 
aber die kennt man auch nicht alle, vor allem wenn man mit diversen 
großen Frameworks & Libraries jongliert. Aber das ist auch egal, mit 
einem Mausklick in der IDE findet man die Definition des Typs und kann 
sich anschauen, wie groß er ist. Oder man startet den Debugger und lässt 
sich sizeof(Typ) ausgeben, wenn man es ganz genau wissen will.

von Tim T. (tim_taylor) Benutzerseite


Lesenswert?

Programmierer schrieb:
> Tim T. schrieb:
>> Nagut, wenn Linus T. das sagt... Hail to the Chief!
>
> Naja, die Meinungen des Linus T. über "gutes" Programmieren sind...
> kontrovers. Die sollte man nicht unreflektiert übernehmen.

Ja, manche verstehen Ironie und Sarkasmus und dann gibts noch die 
Anderen...

> Tim T. schrieb:
>> Nee mal im Ernst, irgendwann sollte man eigentlich alle primitiven
>> Datentypen kennen,
>
> Sowas wie mode_t, time_t & Co sind auch nur Aliase für Integer-Typen,
> aber die kennt man auch nicht alle, vor allem wenn man mit diversen

Naja, irgendwie hat man aber alle arithmetischen Typen irgendwann mal 
gesehen oder bei der Verwendung den drängenden Verdacht. Andererseits 
weis ich nicht wie oft mir z.B. beim tm struct schon das struct auf die 
nerven gegangen ist, so dass ich meistens direkt am Anfang dafür ein 
typedef mache.

> großen Frameworks & Libraries jongliert. Aber das ist auch egal, mit
> einem Mausklick in der IDE findet man die Definition des Typs und kann

Zum Beispiel.

> sich anschauen, wie groß er ist. Oder man startet den Debugger und lässt
> sich sizeof(Typ) ausgeben, wenn man es ganz genau wissen will.

Oder eben Oldschool, ja. Wobei wenn es wirklich wichtig ist (Leistung), 
ich mir jeden verwendeten Datentyp mehrmals anschaue und da fallen 
structs egal in welchem Typ sie stecken, direkt auf.

von Programmierer (Gast)


Lesenswert?

Tim T. schrieb:
> Ja, manche verstehen Ironie und Sarkasmus und dann gibts noch die
> Anderen...

Ich hatte das schon verstanden.

Noch ein Punkt: Es gibt auch kleine structs, die z.B. nur einen "int" 
enthalten. Das macht man z.B. als "strong typedef", d.h. ein Typ der an 
sich die gleiche Funktionalität wie z.B. ein "int" hat, aber nicht ohne 
weiteres in diesen umwandelbar ist. Interessant wird das eher in C++ im 
Kontext von Overloads und templates. Ein solcher kleiner Typ kann 
speichertechnisch aber genau wie ein "int" behandelt werden...

von A. S. (Gast)


Lesenswert?

Tim T. schrieb:
> Naja, irgendwie hat man aber alle arithmetischen Typen irgendwann mal
> gesehen oder bei der Verwendung den drängenden Verdacht.

Ist vielleicht auch Ansichtssache. Ich habe erst nach 20 Jahren 
angefangen, struct und enum nicht mehr zu typedeffen und finde das bei 
gemischten Datenfeldern gut. Andere nutzen Namenskonventionen oder gar 
ungarische Notation. Wieder andere haben Editoren die ganz andere 
mouseover-Anzeigen haben und finden andere Styles besser.

Das Argumente von Torvalds hat einigermaßen Einfluss/Verbreitung, daher 
hab ich es angeführt. Sonst jeder wie er mag.

von Darth Moan (Gast)


Lesenswert?

Programmierer schrieb:
> Tim T. schrieb:
>> Fakt ist C lässt Zugriffe auf fest definierte Adressen zu
>
> Nein, der Standard definiert das nicht, aber es funktioniert in den
> meisten Umgebungen. Prinzipiell kann C (oder C++) auch ähnlich wie Java
> implementiert werden, wo Zeiger nur eine Referenznummer sind. Da kann
> man dann nicht einfach irgendwohin zugreifen. Allerdings gibt es keine
> Implementation die es so macht...

Moin,
ich hätte dazu eine Frage. Wie kommst du auf UB?
Wenn ich mir den Standard anschaue (für mich ist C99 relevant),
dann finden sich 2 Punkte, die ich hier heranziehen würde.

Ich würde also IB vermuten und nicht UB. Es muss ja IB sein, sonst 
müsste der Standard vorschreiben wo was im Addressraum zu sein hat, und 
überhaupt was für ein Adressraum vorliegt usw.

Oder mache ich da noch einen Denkfehler?
Allzuoft schaue ich ja auch nicht in den Standard. Also könnte ich ja 
einer Fehlinterpretation aufgesessen sein.

6.3.2.3 Pointers

5 An integer may be converted to any pointer type. Except as previously 
specified, the result is implementation-defined, might not be correctly 
aligned, might not point to an entity of the referenced type, and might 
be a trap representation.56)

6.6 Constant Expressions

9 An address constant is a null pointer, a pointer to an lvalue 
designating an object of static storage duration, or a pointer to a 
function designator; it shall be created explicitly using the unary & 
operator or an integer constant cast to pointer type, or implicitly by 
the use of
an expression of array or function type. The array-subscript [] and 
member -access .
and -> operators, the address & and indirection * unary operators, and 
pointer casts may be used in the creation of an address constant, but 
the value of an object shall not be accessed by use of these operators.

von Programmierer (Gast)


Lesenswert?

In "6.5.3.2 Address and indirection operators" heißt es (N1256):

4. The unary * operator denotes indirection. [...] if it points to an 
object, the result is an lvalue designating the
object. If the operand has type ‘‘pointer to type’’, the result has type 
‘‘type’’. If an
invalid value has been assigned to the pointer, the behavior of the 
unary * operator is undefined .87)

In Fußnote 87: Among the invalid values for dereferencing a pointer by 
the unary * operator are a null pointer, an
address inappropriately aligned for the type of object pointed to, and 
the address of an object after the
end of its lifetime.

Daraus würde ich lesen, dass das Objekt also in seiner Lebzeit sein 
muss, damit man einen Zeiger darauf dereferenzieren darf. Der Zeiger 
zeigt aber auf überhaupt kein Objekt das irgendwie eine Lebzeit hätte 
(wie in 6.2.4 definiert), weil es nie mit malloc, static o.ä. allokiert 
wurde.

C verbietet ja sogar (6.5.8.5) zwei Zeiger zu vergleichen, die nicht in 
das selbe Aggregat (Array, struct) zeigen (UB). Über das "Niemandsland" 
zwischen den tatsächlich allokierten Objekten darf man also nicht mal 
nachdenken. Aber einen Zeiger dereferenzieren der irgendwohin zeigt?

von Klaus W. (mfgkw)


Lesenswert?

Ein Zeiger mit einer HW-Adresse ist doch keiner der Fälle, die du 
auflistest?
Außer bei der Adresse 0 muß noch das Alignment stimmen, das tut es ja 
wenn man die richtige Adresse einträgt.
Und das ist dann auch kein Objekt nach seiner Lebenszeit.

Also kein Fall für UB. Nur halt systemspezifisch und nicht portabel, 
also auf eigene Gefahr.

von Programmierer (Gast)


Lesenswert?

Klaus W. schrieb:
> Ein Zeiger mit einer HW-Adresse ist doch keiner der Fälle, die du
> auflistest?

Es ist aber auch kein legaler Fall in 6.2.4. HW-Adressen kommen in C 
überhaupt nicht vor.

Klaus W. schrieb:
> Und das ist dann auch kein Objekt nach seiner Lebenszeit.

Es ist einfach überhaupt kein Objekt, und auf ein nicht-existentes 
Objekt kann man auch nicht zugreifen. In 6.2.4 heißt es ja auch "If an 
object is referred to outside of its lifetime, the behavior is 
undefined.".

Wenn es IB wäre, müsste es in J.3 aufgeführt sein. 
Array-Out-Of-Bound-Zugriffe sind ja auch UB.

von Darth Moan (Gast)


Lesenswert?

Programmierer schrieb:
> In Fußnote 87: Among the invalid values for dereferencing a pointer by
> the unary * operator are a null pointer, an
> address inappropriately aligned for the type of object pointed to, and
> the address of an object after the
> end of its lifetime.

Das bedeutet doch aber nur, dass wenn ich zB eine HW (die immer da ist, 
und kein after the end of lifetime hat) unter einer falschen Adresse 
anzusprechen versuche, ist das UB. Wenn ich aber die korrekte Adresse 
verwende, ist es OK. Die korrekte Adresse kann aber zB von der MMU 
Konfig abhängen, die nicht im Scope des C Strandards ist. Das das dann 
zur Laufzeit stimmt, ist Aufgabe des Anwenders des Compilers, nicht die 
Aufgabe des Norm-Gremiums. Aber es stimmt, das dort von UB gesprochen 
wird. Werd ich mir merken. Wer weiss vofür das mal nützlich sein kann.

Programmierer schrieb:
> C verbietet ja sogar (6.5.8.5) zwei Zeiger zu vergleichen, die nicht in
> das selbe Aggregat (Array, struct) zeigen (UB). Über das "Niemandsland"
> zwischen den tatsächlich allokierten Objekten darf man also nicht mal
> nachdenken. Aber einen Zeiger dereferenzieren der irgendwohin zeigt?

Weil C nicht sicher stellen kann und will, dass immer und überall 
lineare Adressräume Verwendung finden müssen. Wenn man zB eine MMU hat, 
kann man die Objekte an beliebige Adressen setzen und alle Zwischenräume 
sind nicht da.
Und die MMU ist ggf. vom ausführenden Thread abhängig. Es kann also 
mehrere Objekte geben, die an der "Selben Adresse" liegen aber eben 
nicht vom Selben Thread angesprochen werden dürfen.
Wenn ich die Adresse eines Objekts (das vorher in dem Kontext angelegt 
wurde) mit & hole, kann soetwas nicht passieren. Also sollte man es 
tunlichst so machen. Bei HW ist die Adresse üblicherweise fest und nicht 
portabel. Ein AVR hat sein SPI DR mit Sicherheit auf einer anderen 
Adresse als ein STM32F7xx. Also kann das nicht im C Standard festgelegt 
sein.
Auch wenn man keine MMU hat aber vielleicht verschiedene Busse, dann 
kann die selbe Adresse auf einem Anderen Bus auf ein ganz anderes Objekt 
zeigen.
Wenn in dem Type irgendwie enthalten ist, auf welchem Bus das Objekt 
liegt,
können 2 verschieden Objekt Pointer den selben numerischen Wert haben, 
aber trotzdem auf verschiedene Objekte zeigen.

Die andere Frage ist, was soll ein Vergleich zweier Pointer aussagen?
Wenn sie beide auf (in) das gleiche Objekt zeigen, kann es eine 
sinnvolle Aussage haben. Aber was soll ein Vergleich zweier Pointer 
bringen, die auf unterschiedliche Objekte zeigen?

Wenn ich hingegen einen Pointer auf eine Bereichsüberschreitung prüfen 
will
kann die Aussage des Vergleichs zweier Pointer die auf etwas ganz anders 
zeigen sinnvoll sein. Das kann aber der C Standard nicht einfach so 
generell definieren, weill das von den Details der Zielplattform 
abhängt. Der Standard muss aber allgemein sein, sonnst bräuchte jeder uC 
seinen eigenen C Standard. Und jede Mögliche Konfiguration einer MMU 
würde dann ihren eigenen C Standard erzwingen.

Der Zeiger darf ja eben nicht irgendwohin zeigen, sonst ist es nicht 
definiert. Zumindest aus Sicht des Standard. In einem realen System kann 
es gewollt sein, das alle Zugriffe ausserhalb einer Reihe von 
definierten Objekten in einem Trap landen. Das kann der Standard auch 
nicht so einfach  festlegen. Das System kann aber durchaus verlangen, 
dass das TRAP Verhalten zur Laufzeit geprüft wird. Dann ist der Zugriff 
auf einen Pointer im (kontrollierten) Nirgendwo sinnvoll und richtig. 
Klar kann man als Standard festlegen, "ist mir doch egal, ist verboten, 
darf der Compiler nicht machen, basta". Aber dann bekommt man 
Schwierigkeiten den Standard beim Nutzer akzeptiert zu bekommen.

von Klaus W. (mfgkw)


Lesenswert?

Darth Moan schrieb:
> Die andere Frage ist, was soll ein Vergleich zweier Pointer aussagen?
> Wenn sie beide auf (in) das gleiche Objekt zeigen, kann es eine
> sinnvolle Aussage haben. Aber was soll ein Vergleich zweier Pointer
> bringen, die auf unterschiedliche Objekte zeigen?

Man könnte leichtsinnigerweise z.B. auf die Idee kommen, Zeiger auf 
Objekte als Sortierkriterium zu nehmen (zum Einsortieren in eine map) 
oder als Schlüssel (z.B. um einen Hashwert für eine Tabelle zu 
berechnen).

Hat man jetzt aber setgmentierten Speicher (volle Adresse besteht aus 
Segment und Offset) und die Segmente überschneiden sich, dann können 
mehrere Segment:Offset-Paare auf dieselbe Stelle im Speicher zeigen.

Wegen sowas ist es laut C-Standard nicht zulässig, Zeiger für etwas 
anderes zu verwenden als innerhalb eines Aggregats.

Hat aber doch alles nichts damit zu tun, einen Zeiger mit einer festen 
Adresse zu initialisieren, wenn man sie kennt?

von Programmierer (Gast)


Lesenswert?

Darth Moan schrieb:
> Das bedeutet doch aber nur, dass wenn ich zB eine HW (die immer da ist,
> und kein after the end of lifetime hat) unter einer falschen Adresse
> anzusprechen versuche, ist das UB. Wenn ich aber die korrekte Adresse
> verwende, ist es OK.

Alle existierenden Objekte müssen aber erst einmal im Code allokiert 
werden, damit sie eine Adresse haben. Das geschieht über malloc, lokale 
oder statische Allokation. Hardware-Register hingegen werden nirgendwo 
im Code "erzeugt", der C-Standard hat überhaupt kein Konzept für 
HW-Register, also gibt es aus Sicht des Standards keine Objekte an 
diesen Adressen, somit ist der Zugriff UB.

Wäre es IB, müssten alle Compiler ein konsistentes Verhalten für solche 
Zugriffe definieren und dokumentieren. Was bei so einem Zugriff 
passiert, kann der Compilerautor aber gar nicht wissen, weil das, wie du 
schon sagst, von der MMU und vielem anderen abhängt. Zugriff auf Adresse 
0x40000000 kann normal funktionieren, Zugriff auf Adresse 0x40000004 
kann abstürzen. Das kann unmöglich konsistent dokumentiert werden, daher 
ist es eben nicht IB.

von A. S. (Gast)


Lesenswert?

Darth Moan schrieb:
> Aber es stimmt, das dort von UB gesprochen
> wird. Werd ich mir merken. Wer weiss vofür das mal nützlich sein kann.

Hast Du denn eine Möglichkeit gefunden, wie der Zugriff auf (korrekte) 
HW-Adressen zu UB in C99 führen darf?

Dass davon abgesehen > oder < undefiniert (und sinnlos) sind bei 
verschiedenen Objekten ist m.E. klar, wenn man sich mögliche Ergebnisse 
überlegt.

von Darth Moan (Gast)


Lesenswert?

Programmierer schrieb:
> Wäre es IB, müssten alle Compiler ein konsistentes Verhalten für solche
> Zugriffe definieren und dokumentieren. Was bei so einem Zugriff
> passiert, kann der Compilerautor aber gar nicht wissen, weil das, wie du
> schon sagst, von der MMU und vielem anderen abhängt. Zugriff auf Adresse
> 0x40000000 kann normal funktionieren, Zugriff auf Adresse 0x40000004
> kann abstürzen. Das kann unmöglich konsistent dokumentiert werden, daher
> ist es eben nicht IB.

Deiner Meinung nach ist damit aber auch wirklich jeder Zugriff auf 
irgendeine Adresse immer UB. Denn jetzt nehme ein Objekt, das zB das 
system (OS oder Framework) mit malloc "erzeugt" hat, wie du es 
beschreibst. Jetzt ist die Adresse definitiv die Adresse des Objekts 
bzw. eine Elements davon. Nun greifst du in einem Thread lesend darauf 
zu, was du im implementierten System darfst, denn dein Thread darf die 
Konfig lesen (das sei jetzt hier mal für das Beispiel-System so 
angenommen). Und nun versuchst du zu schreiben und die MMU blockiert 
das, weil dein Thread das nicht darf.
Der Compiler kann niemals wissen wie eine MMU konfiguriert ist.
Das heisst, der Compiler weiss niemals und kann es daher niemals 
dokumentieren, ob dein Zugriff auf ein mallociertes Objekt über einen 
Pointer funktionieren wird oder nicht. Daher ist es deiner Meinung nach 
UB.
Und selbst wenn kein Pointer beim Zugriff involviert ist, kann der 
Compiler keine Aussage dazu machen, ob nicht doch eine MMU oder MPU 
diesen Zugriff zur Laufzeit verbietet. Wenn das eintritt, dann ist es 
deiner Meinung nach plötzlich UB. Ist die MMU aber anders konfiguriert 
ist es plötzlich alles konform. Das passt doch nicht.

Deiner Meinung nach müsste aber jeder Compiler alle Zugriffe die du über 
einen Pointer auf irgendein Objekt machst genau Dokumentieren, ob er, 
unter Umständen die er nicht kennen kann, funktioniert oder nicht.

Das IB ist eben, das der Compiler Code erzeugt, der auf der angegebenen 
Adresse so zuzuggreifen versucht, wie der type es erwarten lässt. Ob 
eine MMU zur Laufzeit eingreift, kann weder der Standard noch der 
Compiler definieren. Auch kann der Compiler nicht wissen, ob dein Objekt 
in der Zwischenzeit durch zB Strahlung korrumpiert wurde und nun der ECC 
check dich in einen TRAP schickt. In diesem Fall würde die ominöse 
Strahlung zur Laufzeit darüber entscheiden, ob es UB ist oder völlig 
korrekt.

Klaus W. schrieb:
> Man könnte leichtsinnigerweise z.B. auf die Idee kommen, Zeiger auf
> Objekte als Sortierkriterium zu nehmen (zum Einsortieren in eine map)
> oder als Schlüssel (z.B. um einen Hashwert für eine Tabelle zu
> berechnen).

Das implizierte doch einen linearen Adressraum, in dem die Objekte 
liegen müssten, oder? Das kannst du als Nutzer des Compilers festlegen 
(wenn deine HW das erlaubt), aber der Standard kann und will sich nicht 
darauf festlegen das C nur in linearen Adressräumen funktionieren 
soll/darf. Und weill er das nicht festschreiben will, ist das auf der 
Sprach-Ebene UB. Eine Implementierung, also ein Compiler, muss dann 
entscheiden, was er macht. Mit Fehlermeldung abbrechen und keinen Code 
erzeugen, oder ein IB umsetzen, das von einem linearen Adressraum 
ausgeht. Oder noch was anderes.
Aber der Compiler weiss im Voraus auch nicht, wo deine Objekte später im 
Adressraum liegen werden. Das macht entweder der Linker, oder das OS. 
Wenn es also wirklich verschiedene Objekte sind, weiss doch niemand so 
genau, wo die im Speicher zu liegen kommen. Aber der Standard definiert, 
das der Speicher (eines Objekts) mehr oder weniger zusammenhängend sein 
muss. Wenn du eine Struktur anlegst, die sagen wir mal 16 unsigned int 
enthält dann ist definiert, dass du den Speicher byte (oder Char?) weise 
ab der Start-Adresse lesen kannst so gross wie sizeof() das angibt, und 
du hast die gesammte Struktur gelesen. Es können alignment gaps in dem 
Speicher auftauchen, die von den einzelnem Elementen nicht belegt 
werden. aber die müssen in sizeof() enthalten sein. Es gibt auch keine 
Lücken in der Struktur in der dann andere Objekt zu liegen kommen 
könnten.

Also innerhalb eines Objekts ist der lineare Adressraum vorgeschrieben, 
ausserhalb aber nicht. Das ist eine Einschränkung der Sprache, nicht des 
Compilers. Denn die Sprache weiss nicht auf was für einer Plattform 
später mal gearbeitet werden soll. Der Compiler weiss immerhin schon auf 
welcher Plattform das Compilat ausgeführt werden soll. BTW, wenn ich auf 
die Idee käme, X86 Code auf einem Cortex M7 ausführen zu wollen, ist 
dass dann plötzlich auch UB in der Sprach-Ebene?
Das kann doch nicht sein.

A. S. schrieb:
> Hast Du denn eine Möglichkeit gefunden, wie der Zugriff auf (korrekte)
> HW-Adressen zu UB in C99 führen darf?

Äh, ich glaube nicht, aber ich versuche gerade die Argumente zu 
verarbeiten, aber mein Schieberegister scheint gerade überzulaufen. Ich 
glaube ich muss da nochmal drüber schlafen und nochmal alles nachlesen.
Wie geasgt, ich bin nicht geübt darin, die Spezifikation so ins Detail 
aufzudröseln.
Ich hatte mich hierauf bezogen:

The unary * operator denotes indirection. If the operand points to a 
function, the result is a function designator; if it points to an 
object, the result is an lvalue designating the object. If the operand 
has type ‘ ‘pointer to type’’, the result has type ‘ ‘type’’. If an 
invalid value has been assigned to the pointer , the behavior of the 
unary * operator is
undefined.

Hier wird gesagt das das Verhalten des * Operators undefined ist, wenn 
Die Adresse nicht korrrekt ist. Aber UB ist für mich immer, das (aus 
Sicht des Standards) alles passieren kann, auch ein neues Universum 
könnte entstehen.
Nur ob die Adresse korrekt ist, kann weder der Standard noch der 
Compiler irgendwie bestimmen.

von Programmierer (Gast)


Lesenswert?

Darth Moan schrieb:
> Und nun versuchst du zu schreiben und die MMU blockiert
> das, weil dein Thread das nicht darf.

Eine Implementation (also Hardware+OS+Standardlibrary) welche so etwas 
macht, ist nicht C-konform. Der C-Standard stellt nicht nur 
Anforderungen an das Programm, sondern auch an die Implementation, wie 
diese sich zu verhalten hat, wenn das Programm bestimmte Dinge tut. Wenn 
das Programm mit malloc() (und anderen Mechanismen) Speicher anfordert 
und dies nicht fehlschlägt, muss die Implementation allen Threads im 
Programm Schreib-*und*-Lesezugriff gewähren. Wenn die Implementation 
einem Thread den Zugriff darauf sperrt (indem z.B. das OS die MMU 
entsprechend konfiguriert), ist es keine konforme C-Implementation, und 
man kann dafür nicht in korrektem C programmieren. Denn der Zugriff auf 
korrekt allokierte Objekte, wie eben per malloc() erzeugt, ist eben 
kein UB.

Im Übrigen ist in C und C++ der Zugriff auf Objekte nie vom Thread 
abhängig - entweder können alle Threads zugreifen, oder keiner. Auch auf 
Thread-Local-Storage von anderen Threads kann man zugreifen, wenn man 
denn einen Zeiger darauf hat (ist natürlich kein besonders guter Stil). 
Natürlich gelten die Einschränkungen bzgl. Synchronisation (Data Races & 
Co). Mit anderen Worten: Ein OS, welches die MMU bei den 
unterschiedlichen Threads eines C- oder C++-Programms umkonfiguriert ist 
wahrscheinlich nicht C- bzw. C++-konform.

Also nochmal: Es ist ein Unterschied, ob man ein Objekt und den 
dazugehörigen Speicher per malloc o.ä. angefordert hat, oder ob zufällig 
an einer Adresse ein Peripherieregister herumliegt.

von Klaus W. (mfgkw)


Lesenswert?

Der C-Standard (egal welcher) kennt überhaupt keine Threads.
(Und das alles hat immer noch nichts mit der Frage zu tun.)

: Bearbeitet durch User
von Programmierer (Gast)


Lesenswert?

Klaus W. schrieb:
> Der C-Standard (egal welcher) kennt überhaupt keine Threads.

Äh doch, seit schlappen 11 Jahren:

https://en.cppreference.com/w/c/thread/thrd_create

von Darth Moan (Gast)


Lesenswert?

Programmierer schrieb:
> Klaus W. schrieb:
>> Der C-Standard (egal welcher) kennt überhaupt keine Threads.
>
> Äh doch, seit schlappen 11 Jahren:
>
> https://en.cppreference.com/w/c/thread/thrd_create

Eine Thread Support Library wird in ISO/IEC 9899:TC3 nirgendwo erwähnt.
Das Wort Thread kommt im gesammten Standard nicht vor.

Also die Argumentation das jeder immer auf alles Zugriff haben muss ist 
doch wieder inkonsistent. Denn man kann jederzeit einen normalen Pointer 
nach Pointer to const casten, bevor man ihn an einen Programmteil weiter 
gibt, der eben nicht darauf schreiben darf. Eine Erzwingung des 
Schreibschutzes über eine MMU/MPU ist nur eine Absicherung gegen 
Programmierfehler. Aber ein Programmierfehler kann ja kein UB auf 
Sprach-Ebene darstellen.

Aber die Ursprüngliche Frage war ja, ob der Standard dieses Konstukt als 
UB betrachtet: ((TEST_TypeDef*) 0x40020000UL).
Und das alleine ist erstmal von Standard OK:

An address constant is ... , a pointer to an lvalue
designating an object of static storage duration, ...;
it shall be created explicitly using ... or an *integer constant cast to 
pointer type*

Und genau das haben wir hier. Allerdings folgt dann noch das:
... and pointer casts may be used in the creation of an address 
constant, but the value of an object shall not be accessed by use of 
these operators.

Also könnte man argumentieren das Das nicht OK ist:

*((TEST_TypeDef*) 0x40020000UL) = 42;

Dabei meine ich keinen Type Missmatch von TEST_TypeDef zu 42.
Wenn es sich bei TEST_typeDef um eine Struktur handelt, muss natürlich 
eine element der Struktur selektiert werden, mum eine einzelnen Wert zu 
schreiben oder zu lesen.

Aber ich habe den Eindruck, das der genannte Halbsatz sich darauf 
bezieht, das zur Addressbildung keine andere Adresse dereferenziert 
werden darf.
Denn das wäre ja keine Konstante mehr.

von Programmierer (Gast)


Lesenswert?

Darth Moan schrieb:
> Eine Thread Support Library wird in ISO/IEC 9899:TC3 nirgendwo erwähnt.
> Das Wort Thread kommt im gesammten Standard nicht vor.

Das ist ja auch C99. Aktuell ist ISO/IEC 9899:2018  (C17). Im Draft 
n2310 kommt "Thread" 257x vor.

Darth Moan schrieb:
> Denn man kann jederzeit einen normalen Pointer
> nach Pointer to const casten, bevor man ihn an einen Programmteil weiter
> gibt, der eben nicht darauf schreiben darf.

Doch darf er, nachdem er wieder zurück castet. Besonders sauber ist das 
aber nicht.

Darth Moan schrieb:
> but the value of an object shall not be accessed by use of
> these operators.

Klingt für mich stark danach, dass man so einen Zeiger nicht 
dereferenzieren darf. Vermutlich sollte man diese Frage mal auf 
StackOverflow stellen, da bekommt man immer gute Antworten auf so etwas.

von Rolf M. (rmagnus)


Lesenswert?

Programmierer schrieb:
> Darth Moan schrieb:
>> but the value of an object shall not be accessed by use of
>> these operators.
>
> Klingt für mich stark danach, dass man so einen Zeiger nicht
> dereferenzieren darf.

Das bezieht sich nicht auf das Zeigerziel, sondern darauf, woraus der 
Zeiger erzeugt wird. Wenn man eine Adresskonstante erzeugen will, darf 
man dazu die genannten Operatoren verwenden, aber nicht, um den Wert 
eines Objekts zu ermitteln. Was man mit dem Zeiger danach machen darf, 
hat damit gar nichts zu tun.
Es hätte ja auch wenig Sinn, ausdrücklich zu erlauben, dass ein Zeiger 
so erzeugt werden kann, aber nicht, ihn dann auch zu benutzen.

von Darth Moan (Gast)


Lesenswert?

Programmierer schrieb:
> Darth Moan schrieb:
>> Denn man kann jederzeit einen normalen Pointer
>> nach Pointer to const casten, bevor man ihn an einen Programmteil weiter
>> gibt, der eben nicht darauf schreiben darf.
>
> Doch darf er, nachdem er wieder zurück castet. Besonders sauber ist das
> aber nicht.

Das ändert nichts an dem logische Konstrukt, das es Programmteile geben 
darf, die nicht schreiben dürfen. Das ist eine Frage der Anwendung und 
nicht der Sprach-Definition. Im übrigen kommt mindestens eine Warnung 
wenn man die Constness wegzucasten (man klingt das k.cke, das tut ja 
schon weh) versucht.
Aber im Standard habe ich auf die Schnelle nichts dazu gefunden, könnte 
also rein vom Compiler selbst kommen.

OK, in C18 scheint es eine Thread Lib zu geben. Tja, bis vor kurzem 
wurde ich in den Projekten sogar noch auf C89 festgenagelt, da kam mir 
C99 schon als hippe Neuerung vor. Aber nicht umsonst habe ich gleich 
gesagt, auf welchen Stand ich mich beziehe. Aber auch dort (C18) ist der 
gleiche Absatz zu "address constant" drin. Also ist das casten einer 
"interger Konstante" in einen "Pointer to type" immer noch gedeckt, auch 
in C18.

von Programmierer (Gast)


Lesenswert?

Rolf M. schrieb:
> Es hätte ja auch wenig Sinn, ausdrücklich zu erlauben, dass ein Zeiger
> so erzeugt werden kann, aber nicht, ihn dann auch zu benutzen.

Doch klar, um sowas wie ein "void* user_data" zum Ablegen von 
Integerdaten zu missbrauchen.

von Rolf M. (rmagnus)


Lesenswert?

Programmierer schrieb:
> Rolf M. schrieb:
>> Es hätte ja auch wenig Sinn, ausdrücklich zu erlauben, dass ein Zeiger
>> so erzeugt werden kann, aber nicht, ihn dann auch zu benutzen.
>
> Doch klar, um sowas wie ein "void* user_data" zum Ablegen von
> Integerdaten zu missbrauchen.

Das wäre aber eh undefiniertes Verhalten in C, denn ob ein Zeiger 
beliebige Integer-Werte aufnehmen kann, ist nicht definiert. Außerdem 
darf es "trap representations" geben, also Zeigerwerte, die irgendeine 
Art von Exception auslösen, die das Programm terminiert, selbst wenn man 
einen Zeiger mit so einem Wert nur befüllt.

Darth Moan schrieb:
> Das ändert nichts an dem logische Konstrukt, das es Programmteile geben
> darf, die nicht schreiben dürfen. Das ist eine Frage der Anwendung und
> nicht der Sprach-Definition. Im übrigen kommt mindestens eine Warnung
> wenn man die Constness wegzucasten (man klingt das k.cke, das tut ja
> schon weh) versucht.

Kenne ich nur bei Objekten, die initial bereits als const deklariert 
wurden. Bei einem Objekt, das nicht const ist, aber dessen Adresse dann 
über einen Zeiger auf const weitergereicht wird, darf man dort aus Sicht 
des C-Standards dieses const durchaus wieder wegcasten und das Objekt 
beschreiben.
Das das im Sinne der Programmlogik in der Regel schlechtes Design ist 
und vermieden werden soll, sollte klar sein.

> OK, in C18 scheint es eine Thread Lib zu geben. Tja, bis vor kurzem
> wurde ich in den Projekten sogar noch auf C89 festgenagelt, da kam mir
> C99 schon als hippe Neuerung vor.

Ja, so ein Festhalten an uralten längst überholten Standards gibt's 
leider zu oft.

von Darth Moan (Gast)


Lesenswert?

Rolf M. schrieb:
> Kenne ich nur bei Objekten, die initial bereits als const deklariert
> wurden. Bei einem Objekt, das nicht const ist, aber dessen Adresse dann
> über einen Zeiger auf const weitergereicht wird, darf man dort aus Sicht
> des C-Standards dieses const durchaus wieder wegcasten und das Objekt
> beschreiben.
> Das das im Sinne der Programmlogik in der Regel schlechtes Design ist
> und vermieden werden soll, sollte klar sein.

Ich dachte da eher an zB Treiber. Der TX Buffer wird als "pointer to 
const" übergeben, weil der Treiber da drinnen nicht zu schreiben hat.

Eine andere praktische und reale Anwendung ist ein System dass kein NOR 
Flash hat und komplett im RAM läuft. Da hat man dann 
Konfigurationsdaten, die jeder lesen darf, weil ja genau diese Daten 
spezifizieren, was gemacht werden soll. Im "normalen" uC System läge das 
im Flash und müsste nicht gegen einen einfachen Schreibzugriff geschützt 
werden, da der Flash nicht einfach so geschrieben werden kann. Im RAM 
basierten System wird das emuliert indem die Konfigurationsdaten im RAM 
per MMU/MPU gegegn überschreiben aus dem normalen Programmablauf 
geschützt wird.

Aber auch so kann es einen Programmteil geben, der die Objekte anlegen 
und verwalten darf. Aber andere Teile dürfen nur mit diesen Objekten 
Arbeiten, sie aber zB nicht veränern oder löschen. Das darf da eben nur 
der Kommander, nicht die Arbeitsknechte. Das kann man dann mit Pointer 
to const machen. Aber eine MMU/MPU kann das gegen Fehler absichern. Das 
ist doch nichts ungewöhnliches.

Aber diese Definitionen haben ja nichts mit dem C Standard zu tun. Das 
sind Anwendungsdefinitionen bzw. Anforderungen, die die Anwendung eben 
erfüllen muss.

von Mombert H. (mh_mh)


Lesenswert?

Darth Moan schrieb:
> Aber auch so kann es einen Programmteil geben, der die Objekte anlegen
> und verwalten darf. Aber andere Teile dürfen nur mit diesen Objekten
> Arbeiten, sie aber zB nicht veränern oder löschen. Das darf da eben nur
> der Kommander, nicht die Arbeitsknechte. Das kann man dann mit Pointer
> to const machen. Aber eine MMU/MPU kann das gegen Fehler absichern. Das
> ist doch nichts ungewöhnliches.
>
> Aber diese Definitionen haben ja nichts mit dem C Standard zu tun. Das
> sind Anwendungsdefinitionen bzw. Anforderungen, die die Anwendung eben
> erfüllen muss.

Und wie genau willst du dieses "Absichern gegen Fehler mit einer 
MMU/MPU" in C umsetzen?

von Programmierer (Gast)


Lesenswert?

Darth Moan schrieb:
> Das kann man dann mit Pointer
> to const machen. Aber eine MMU/MPU kann das gegen Fehler absichern. Das
> ist doch nichts ungewöhnliches.

Das ist aber von C nicht vorgesehen und abgedeckt. Im Prinzip zerstört 
man sich absichtlich die C-Komformität wenn man verhindert, dass 
bestimmte Programmteile auf bestimmte Daten zugreifen.

von Darth Moan (Gast)


Lesenswert?

Mombert H. schrieb:
> Darth Moan schrieb:
>> Aber auch so kann es einen Programmteil geben, der die Objekte anlegen
>> und verwalten darf. Aber andere Teile dürfen nur mit diesen Objekten
>> Arbeiten, sie aber zB nicht veränern oder löschen. Das darf da eben nur
>> der Kommander, nicht die Arbeitsknechte. Das kann man dann mit Pointer
>> to const machen. Aber eine MMU/MPU kann das gegen Fehler absichern. Das
>> ist doch nichts ungewöhnliches.
>>
>> Aber diese Definitionen haben ja nichts mit dem C Standard zu tun. Das
>> sind Anwendungsdefinitionen bzw. Anforderungen, die die Anwendung eben
>> erfüllen muss.
>
> Und wie genau willst du dieses "Absichern gegen Fehler mit einer
> MMU/MPU" in C umsetzen?

Ich weiss jetzt nicht genau wie du die Frage meinst. Im C Standard kann 
man sowas nicht machen. Denn dann würde C nur auf Platforman möglich 
sein die MMU/MPU mit den spezifizierten Eigendschaften haben. Da wären 
wir dann wieder dabei, das jede Platform ihren eigenen C Standard 
bräuchte.

Und eine MMU/MPU mit einem in C geschriebenen Programm zu konfigurieren 
ist doch jetzt nicht der Hit oder? Oder verpeile ich die Frage jetzt 
völlig?

von Darth Moan (Gast)


Lesenswert?

Programmierer schrieb:
> Darth Moan schrieb:
>> Das kann man dann mit Pointer
>> to const machen. Aber eine MMU/MPU kann das gegen Fehler absichern. Das
>> ist doch nichts ungewöhnliches.
>
> Das ist aber von C nicht vorgesehen und abgedeckt. Im Prinzip zerstört
> man sich absichtlich die C-Komformität wenn man verhindert, dass
> bestimmte Programmteile auf bestimmte Daten zugreifen.

Wo findest du im C Standard, dass niemand eine MMU benutzen darf, um 
Anforderungen an die zu erstellende Anwendung umzusetzen?

von Programmierer (Gast)


Lesenswert?

Darth Moan schrieb:
> Wo findest du im C Standard, dass niemand eine MMU benutzen darf, um
> Anforderungen an die zu erstellende Anwendung umzusetzen?

Du DARFST die MMU natürlich schon benutzen. Da kommt nicht die 
Sprachpolizei. ABER deine Umgebung ist dann ggf. nicht mehr C-konform. 
Das folgert alleine schon daraus, dass der Sprachstandard dir erlaubt, 
auf alle Variablen immer zuzugreifen. Im bereits zitierten Kapitel übers 
Dereferenzieren und über Lifetimes steht praktisch, dass alle "normal" 
angelegten Variablen immer lesbar sind, und nicht dass es 
Einschränkungen durch MMUs oder was auch immer geben kann. In einer 
konformen C-Umgebung darf hier z.B. der 2. Zugriff auf "x" nicht 
irgendwie abstürzen:
1
int x;
2
3
int main () {
4
  printf("x: %d\n", x);
5
  configureMMU ();
6
  printf("x: %d\n", x);
7
}

Wenn aber configureMMU den Zugriff auf "x" blockiert, scheitert das 2. 
printf. Der C-Standard erfordert aber, dass alle Variablen innerhalb 
ihrer Lifetime immer zugänglich sind. Somit ist das keine C-konforme 
Umgebung mehr.

Die interessante Frage ist natürlich: Was passiert in "configureMMU"? 
Sofern dort nur vom C-Standard abgedecktes steht, kann man natürlich 
auch keine MMU konfigurieren. Dort muss also so etwas stehen wie ein 
Syscall, Inline-Assembly, oder Zugriff auf Memory-Mapped Register oder 
MMU-Tabellen. Letzteres kann sogar genau über so einen 
Adresse-auf-Zeiger-Cast wie im Ausgangspost geschehen. Das ist natürlich 
vom Standard nicht vorgesehen, man nutzt hier eine Eigenheit der 
Implementation aus um die Konformität zu beeinträchtigen.

Das heißt natürlich nicht, dass das grundsätzlich "falsch" ist - man 
kann das trotzdem gewinnbringend einsetzen, aber es ist eben am Standard 
vorbei. Letztlich nutzen aber 99% der C-Programme nichtstandardisierte 
Dinge wie spezielle Syscalls, Libraries, Protokolle, I/O-Methoden um 
irgendwas Sinnvolles zu machen. Dies schränkt aber in den Meisten Fällen 
nicht die C-Konformität der Implementation ein. Der Zugriff auf direkte 
Adressen ist zwar UB, funktioniert auf Embedded-Compilern aber, und 
führt auch üblicherweise nicht zu Problemen.

Man könnte den Zugriff auf Cortex-M auf solche Peripherie-Register mit 
Inline-Assembly machen, so in der Art:
1
inline uint32_t readReg32 (uint32_t addr) {
2
  uint32_t reg;
3
  __asm__ ("ldr %[oreg], [%[addr]]" : [oreg] "=r" (reg) : [addr] "r" (addr));
4
  return reg;
5
}
Das wäre dann "nur" noch IB. Das Keyword "__ asm __" ist vom Standard 
nicht definiert, aber weil es mit zwei Unterstrichen beginnt darf der 
Compiler es als Extension anbieten ohne den Standard zu verletzen. Die 
Funktion vom Inline-Assembly ist natürlich sowieso IB. So kommt man ohne 
UB an Register-Werte. Man müsste allerdings noch Varianten für andere 
Registergrößen sowie mit und ohne "volatile" vorsehen (für Register, bei 
denen das Auslesen einen Seiteneffekt hat). In bestimmten Fällen lässt 
sich das aber nicht so gut optimieren (insb. Berechnung der Adresse bei 
den erwähnten Register-Structs), weshalb das in der Praxis auch nicht so 
toll ist.

von Darth Moan (Gast)


Lesenswert?

Programmierer schrieb:
> Das folgert alleine schon daraus, dass der Sprachstandard dir erlaubt,
> auf alle Variablen immer zuzugreifen.

Diese Aussage in ihrer Absolutheit stimmt doch so überhaupt nicht.
1
void foo(void)
2
{
3
   const int slati = 42;
4
5
   slati++;
6
}
Hier muss der Compiler verweigern. Weil das inkrementieren von slati 
verboten ist.
Der code der Funktion selbst muss aber die 42 in slati reinschreiben, 
wenn die lokale Variable auf dem Stack angelegt wird.
1
void bar(const char *SrcPtr)
2
{
3
    *SrcPtr = '0';
4
}
Auch hier muss der Compiler verweigern. Das Verlangt der Standard.
Ausserdem bringst du die Ebenen durcheinander.
Zur Ausführungszeit kann ich kein UB in der Sprachdefinition erzeugen.
Die Folge von UB in der Sprachdefinition ist, dass der Compiler nicht 
weiss, was er umsetzen soll. Wie ich vorher schon geschrieben habe, muss 
ein Compiler jetzt entscheiden, was er tut. Naheliegend ist es da, mit 
einer Fehlermeldung die Übersetzung abzubrechen. Eine defekte 
Ausführungseinheit kann nicht dazu führen, das ein Zeitsprung passiert 
und der Compiler mit einer Fehlermeldung abbricht, zwei Jahre bevor das 
Programm auf einer defekten HW ausgeführt werden sollte.

Ausserdem wiederholst du die Behauptung, dass die Dereferenzierung eines 
Pointers aus einer "address constant" UB ist. Die Belegung dieser 
Behauptung durch den Standard bist du immer noch schuldig geblieben.

Alles was du mit deinem Beispiel belegt hast, ist die Erkenntnis, das 
eine falsch konfigurierte MMU/MPU dazu führen kann, das ein 
standardkonformes Programm möglicherweise nicht mehr korrekt ausgeführt 
werden könnte.
Das gleiche tritt ein, wenn ich versuche, den Microcode eines modernen 
i7 während des Programmablaufs mit einer schönen grossen Axt 
umzuschreiben. Das führt aber nicht dazu, dass das Programm bzw. dessen 
Source Code jetzt plötzlich nicht mehr standardkonform wäre.

Ich schlug vor die MMU/MPU zB dazu zu benutzen, die festgelegte 
Zugriffsseinschränkung zu erzwingen, um Fehler detektieren und abfangen 
zu können. Ein const char *SrcPtr darf nicht schreibend benutzt werden. 
Wenn ein Programm das trotzdem zu tun versucht, ist es nicht 
standardkonform und der Compiler mus sich weigern das umzusetzen. Wenn 
es Dir gelingt, den Compiler und seine Checks auszutricksen und doch auf 
diesen Speicher zu schreiben, ist dein trickreiches Programm nicht mehr 
anforderungskonform. Ob es noch standardkonform ist, ist dabei nochmal 
eine andere Frage.
Die MMU/MPU kann dann aber ggf. verhindern, dass dein trickreiches 
Programm gegen die Anwendungs-Anforderungen verstösst.
Sie kann aber nicht erzwingen, dass dein trickreiches Programm nicht 
mehr standardkonform ist. Ob dein trickreiches Programm standardkonform 
ist oder nicht, ist vollkommen unabhängig davon, ob eine MMU/MPU zur 
Laufzeit die einhaltung der Anwendungs-Anforderungen erzwingt oder 
nicht.

von Programmierer (Gast)


Lesenswert?

Darth Moan schrieb:
> Hier muss der Compiler verweigern. Weil das inkrementieren von slati
> verboten ist.

Ich schrieb nur "zugreifen". Und das kann auch "nur lesen" bedeuten.

Darth Moan schrieb:
> Zur Ausführungszeit kann ich kein UB in der Sprachdefinition erzeugen.

Doch:
1
int div (int a, int b) {
2
  return a/b;
3
}
4
5
int main (int argc) {
6
  div (1, argc);
7
}
Ob UB eintritt oder nicht, entscheidet sich zur Laufzeit. Kein Compiler 
wird hier eine Fehlermeldung geben.

Darth Moan schrieb:
> Naheliegend ist es da, mit
> einer Fehlermeldung die Übersetzung abzubrechen.

Das tut er da wo möglich, aber das ist es oft nicht. Oder warum sonst 
haben so viele Programme Buffer-Overflow-Lücken?

Darth Moan schrieb:
> Die Belegung dieser
> Behauptung durch den Standard bist du immer noch schuldig geblieben.

Wie gesagt das gleiche Argument wie zu Anfang: Es gibt an der Adresse 
kein Objekt, das eine Lifetime hat. Nur weil man eine "address constant" 
hat, kann man noch lange nicht derefernezieren. So genau kenne ich mich 
mit dem Standard aber nicht aus. Für mich macht es aber keinen Sinn, 
einerseits alles mögliche an "fiesen" Zugriffen zu verbieten, keine 
Möglichkeit zu bieten irgendwelche Adressen festzulegen, alle 
Speicherverwaltung dem Linker oder der Laufzeitumgebung zu überlassen, 
aber dann das Schreiben an beliebige Adressen zu erlauben. IB hieße ja 
auch "erlaubt", was wie gesagt ein konsistentes Verhalten und eine 
Dokumentation bedeuten würde, was aber kaum machbar sein dürfte.

Darth Moan schrieb:
> Ich schlug vor die MMU/MPU zB dazu zu benutzen, die festgelegte
> Zugriffsseinschränkung zu erzwingen, um Fehler detektieren und abfangen
> zu können. Ein const char *SrcPtr darf nicht schreibend benutzt werden.

Ja das kann man machen. Viele Compiler legen statisch allokierte 
Konstanden in einen Bereich (z.B. .rodata), der vom OS schreibgeschützt 
gemacht wird. Dann kann man auch String-Literale nicht überschreiben. 
Ich hätte mich klarer ausdrücken sollen: Wenn man mit der MMU das Lesen 
von Variablen verbietet, oder das Schreiben von nicht-const-Daten, ist 
es nicht mehr C-konform. Ich hatte gedacht dass das klar ist.

von Darth Moan (Gast)


Lesenswert?

Programmierer schrieb:
> So genau kenne ich mich
> mit dem Standard aber nicht aus. Für mich macht es aber keinen Sinn,
> einerseits alles mögliche an "fiesen" Zugriffen zu verbieten, keine
> Möglichkeit zu bieten irgendwelche Adressen festzulegen, alle
> Speicherverwaltung dem Linker oder der Laufzeitumgebung zu überlassen,
> aber dann das Schreiben an beliebige Adressen zu erlauben.

Hmm, ich glaube jetzt kommen wir der Sache näher. Weil es dir nicht 
sinnvoll erscheint muss es UB sein?

Das sehe ich anders. Nur der Standard definiert was UB ist, ob mir das 
gefällt oder nicht.
Mir gefallen machne Dinge nicht in C. Aber ob ich in der Lage wäre eine 
bessere Lösung zu definieren, die in allen Belangen konsistent und 
universell einsetzbar ist?

Das Konzept der Lifetime eines Objekts entsteht ganz automatisch, wenn 
man mit lokalen Variablen auf dem Stack arbeitet. Würde es dieses 
Konzept nicht geben, also es würde nur globale, immer existierende 
Variablen geben, dann würde man ein Konzept der "Initialisiertheit" 
brauchen. Und genau das hat man ja bei HW.
Vielleicht nicht immer, aber der Nutzer muss selber darauf achten, das 
er Zugriffe auf HW Register so macht, dass er keinen Murks macht. Ein 
Compiler kann niemals sicher stellen, das der User die HW Register 
richtig benutzt.

Das gilt aber nicht nur für HW sondern auch für alle Datenobjekte. Wenn 
ich ein Programm schreibe, das die Tabelle der Sonnenauf und 
-Untergangszeiten in eine FFT Lib füttert und dann die Analog Samples in 
die Dämmerungssteuerung schiebt, wird der Compiler eben nicht mit einem 
Fehler abbrechen, nur weil ich die falschen Daten verwurstet habe. 
Lediglich formale Typ-Prüfungen könnten da Warnings oder einen Error 
schmeissen.

Der Compiler muss auch in keinster Weise die HW die hinter einer 
bestimmten Adresse liegen soll beschreiben. Wenn er sein Verhalten 
Dokumntieren will, dann beschreibt er das er die Zugriffe auf feste 
Adressen eben genauso umsetzt, wie der type es erwarten lässt. Er muss 
nie und nimmer dokumentieren, was dann tatsächlich passiert oder 
passieren soll.
Das obliegt dem Anwender (des Compilers). Im oben genannten Beispiel 
darf ich auch nicht erwarten, dass die FFT über die Tabellendaten 
irgendetwas sinnvolles ausgibt. Für den Compiler kann das aber alles 
standardkonform sein.

Allerdings muss ich wohl meine hammerharte Definition von UB aufgeben.
Wobei ich bei dem Beispiel der Division aber auch sagen muss, das der 
Compiler auch da entscheidet, was er macht. Ich habe schon Code 
durchgesteppt der vor jeder division auf 0 prüft, und ein kontrolliertes 
und dokumentiertes Ergebnis in diesem Fall liefert. Das heisst, es tritt 
eben doch zur Laufzeit kein UB auf. Jedenfalls nicht aus Sicht des 
Compilers. Weil der Compiler festlegt und dokumentiert, was passiert 
wenn der User eine UB operation provoziert. Oder er überlässt es der HW, 
wenn das geht. Die Cortexe haben, glaube ich, einen Divisionsbefehl. 
Dieser legt für sich fest, was bei einer Division durch 0 raus kommt.
Wenn es sowas nicht gibt, oder aber gesetzte Optionen sagen, er soll die 
Division zu fuss machen, dann wird er einen entsprechenden 
Divisionsalgorithmus einsetzen, der genauso definiert auf eine Division 
durch 0 reagiert. Was auch immer diese Reaktion sein mag. Auch ein TRAP 
kann an dieser Stelle die genau gewünschte Reaktion sein. Wenn ich als 
User mit dieser Reaktion unzufrieden bin, kann ich immer noch vorher 
versuchen, diesen Fall abzufangen und meine eigene Reaktion zu 
implementieren.

von Oliver S. (oliverso)


Lesenswert?

Darth Moan schrieb:
> Hier muss der Compiler verweigern. Weil das inkrementieren von slati
> verboten ist.
> Der code der Funktion selbst muss aber die 42 in slati reinschreiben,
> wenn die lokale Variable auf dem Stack angelegt wird.

Das allerdings führt direkt zur Frage, welcher Code da was tun soll, 
wenn der Compiler sich weigert, welchen zu erzeugen.

Oliver

von Programmierer (Gast)


Lesenswert?

Darth Moan schrieb:
>>
>
> Hmm, ich glaube jetzt kommen wir der Sache näher. Weil es dir nicht
> sinnvoll erscheint muss es UB sein?

Natürlich nicht. Ich glaube nur, dass das das Verhalten ist, was vom 
C-Standard gewollt ist.

Darth Moan schrieb:
> Würde es dieses Konzept nicht geben, also es würde nur globale, immer
> existierende Variablen geben

Naja, und dynamisch allokierte Variablen (malloc).

Darth Moan schrieb:
> Das gilt aber nicht nur für HW sondern auch für alle Datenobjekte. Wenn
> ich ein Programm schreibe...

Da geht es aber um die Funktion eines Programms, welches im Standard 
definierte Mechanismen nutzt, wie Variablen, Arrays oder arithmetische 
Operationen. Nirgendwo im Standard ist aber definiert, dass man auf 
beliebige Adressen zugreifen darf.

Darth Moan schrieb:
> Wenn er sein Verhalten Dokumntieren will, dann beschreibt er das er die
> Zugriffe auf feste Adressen eben genauso umsetzt, wie der type es
> erwarten lässt.

Das erscheint mir aber sehr schwamming im Vergleich zu den anderen 
IB-Fällen. Die anderen IB-Aspekte sind so Sachen wie die Anzahl der Bits 
von "int", das Vorzeichenformat von signed Integern, Alignment von 
Datentypen, wie NULL definiert ist usw. Alles Dinge, die der Standard 
nicht vorgeben kann, die der Compilerautor aber klar, konsistent und 
einfach festlegen und dokumentieren kann. An viele dieser Dinge kann 
sich ein Programm auch dynamisch anpassen, also z.B. sizeof(int) 
abfragen und entsprechend reagieren. Und dazu soll auch der Zugriff auf 
X-beliebige Adressen gehören, welcher kaum definierbar/fassbar ist? Und 
wie gesagt: In der Liste der IB-Aspekte im Standard taucht der Zugriff 
auf beliebige Adressen nicht auf. Wie kann es also IB sein?

Darth Moan schrieb:
> Allerdings muss ich wohl meine hammerharte Definition von UB aufgeben.

Die Bedeutung von UB und IB ist im Standard vorgegeben.

von Mombert H. (mh_mh)


Lesenswert?

Darth Moan schrieb:
> Programmierer schrieb:
>> So genau kenne ich mich
>> mit dem Standard aber nicht aus. Für mich macht es aber keinen Sinn,
>> einerseits alles mögliche an "fiesen" Zugriffen zu verbieten, keine
>> Möglichkeit zu bieten irgendwelche Adressen festzulegen, alle
>> Speicherverwaltung dem Linker oder der Laufzeitumgebung zu überlassen,
>> aber dann das Schreiben an beliebige Adressen zu erlauben.
>
> Hmm, ich glaube jetzt kommen wir der Sache näher. Weil es dir nicht
> sinnvoll erscheint muss es UB sein?
> Das sehe ich anders. Nur der Standard definiert was UB ist, ob mir das
> gefällt oder nicht.
Wo genau steht steht denn, dass es kein UB ist?

Die Logik, die Programmierer beschreibt ist in kurz:
- Zugriff auf ein Objekt außerhalb seiner Lebenszeit ist UB [a].
- Die Lebenszeit eines Objekts ist entweder durch seine storage duration 
[b] definiert, oder wann der Speicher reserviert ist [c].
- Das hier referenzierte "Objekt" hat keine storage duration, da keine 
declaration existiert [d].
- Es ist kein Speicher reserviert, da keine Definition existiert [e].

Aus N2176 (C17 Draft?)
[a]
1
6.2.4 2) [...] If an object is referred to outside of its lifetime, the behavior is undefined. [...]
[b]
1
6.2.4 1) An object has a storage duration that determines its lifetime. [...]
[c]
1
6.2.4 2) The lifetime of an object is the portion of program execution during which storage is guaranteed to be reserved for it.
[d] Die einzig passende storage duration ist static:
1
6.2.4 3) An object whose identifier is declared without the storage-class specifier _Thread_local , and either with external or internal linkage or with the storage-class specifier static, has static storage duration. Its lifetime is the entire execution of the program and its stored value is initialized only once, prior to program startup.
[e] Soweit ich finden konnte die einzige Möglichkeit Speicher zu 
reservieren:
1
6.7 5) A declaration specifies the interpretation and attributes of a set of identifiers. A definition of an identifier is a declaration for that identifier that:
2
— for an object, causes storage to be reserved for that object;
3
[...]

von Rolf M. (rmagnus)


Lesenswert?

Darth Moan schrieb:
> Zur Ausführungszeit kann ich kein UB in der Sprachdefinition erzeugen.
> Die Folge von UB in der Sprachdefinition ist, dass der Compiler nicht
> weiss, was er umsetzen soll.

Nein, ganz im Gegenteil. Es gibt Fälle, wo es auch zur Compilezeit 
erkannt werden kann, aber eigentlich geht es bei UB primär gerade um 
Fehler, die erst zur Laufzeit auftreten. Genau deshalb gibt es ja UB. 
Der Compiler soll nicht dazu gezwungen werden, teure Laufzeit-Checks 
einzubauen, um bestimmte Fehler abzufangen. Sonst könnte C dafür ja 
einfach verlangen, dass der Compiler immer mit Fehler abbricht. Der 
Compiler darf davon ausgehen, dass du nie UB auslöst und braucht diesen 
Fall daher ganz einfach nicht zu berücksichtigen.

> Wie ich vorher schon geschrieben habe, muss ein Compiler jetzt
> entscheiden, was er tut.

Gerade das nicht. UB ist genau das Gegenteil davon. Er muss gar nichts 
entscheiden, sondern kann einfach "den Dingen ihren Lauf lassen". Denn 
ihn zu einer Entscheidung zu zwingen, hieße ja, das Verhalten zu 
definieren. Nur bei Dingen, die implementation-defined oder unspecified 
sind, muss der Compiler etwas entscheiden.

> Ich schlug vor die MMU/MPU zB dazu zu benutzen, die festgelegte
> Zugriffsseinschränkung zu erzwingen, um Fehler detektieren und abfangen
> zu können. Ein const char *SrcPtr darf nicht schreibend benutzt werden.

Den Zeiger darf man sehr wohl beschreiben. Ob man auch das bescheiben 
darf, worauf er zeigt, hängt davon ab, ob das Ziel ursprünglich als 
const deklariert wurde oder nicht. Natürlich darf man es nicht direkt 
über diesen Zeiger selbst beschreiben, aber man darf es dennoch 
beschreiben. Also:
1
const char a = 5;
2
char b = 5;
3
4
int main()
5
{
6
    const char* pa = &a;
7
    const char* pb = &b;
8
9
    *pa = 3;         // in C verboten
10
    *(char*)pa = 3;  // undefined behavior in C, da a const
11
12
    *pb = 3;         // in C verboten
13
    *(char*)pb = 3;  // durch C erlaubt, da b nicht const ist
14
}

> Wenn es Dir gelingt, den Compiler und seine Checks auszutricksen und doch
> auf diesen Speicher zu schreiben, ist dein trickreiches Programm nicht
> mehr anforderungskonform.

Viel tricksen muss man da nicht. Man muss lediglich dem Compiler sagen, 
dass man jetzt doch schreiben will, indem man castet.

> Ob es noch standardkonform ist, ist dabei nochmal eine andere Frage.

Aber genau um die Frage geht es, denn:

> Die MMU/MPU kann dann aber ggf. verhindern, dass dein trickreiches
> Programm gegen die Anwendungs-Anforderungen verstösst.

Sofern deine MMU/MPU es irgendwie schafft, die letzte der Zuweisungen in 
meinem Beispielcode zu unterbinden (was ich eher nicht glaube) wäre das 
Programm standardkonform, nicht aber das Eingreifen durch die MMU, da 
das laut C funktionieren muss.

> Sie kann aber nicht erzwingen, dass dein trickreiches Programm nicht
> mehr standardkonform ist. Ob dein trickreiches Programm standardkonform
> ist oder nicht, ist vollkommen unabhängig davon, ob eine MMU/MPU zur
> Laufzeit die einhaltung der Anwendungs-Anforderungen erzwingt oder
> nicht.

Es ging aber um die Frage, ob das Erzwingen durch die MMU 
standardkonform ist oder nicht.

Darth Moan schrieb:
> Wobei ich bei dem Beispiel der Division aber auch sagen muss, das der
> Compiler auch da entscheidet, was er macht. Ich habe schon Code
> durchgesteppt der vor jeder division auf 0 prüft, und ein kontrolliertes
> und dokumentiertes Ergebnis in diesem Fall liefert.

Das darf der Compiler durchaus machen, wenn er will, aber er muss nicht. 
Er kann auch einfach davon ausgehen, dass der zweite Operand niemals 0 
ist und sich einen feuchten drum kümmern, was das System macht, wenn das 
doch mal auftritt.

> Das heisst, es tritt eben doch zur Laufzeit kein UB auf. Jedenfalls nicht
> aus Sicht des Compilers.

UB ist aber kein Konzept des Compilers, sondern der Sprache, und aus 
deren Sicht tritt da UB auf, egal was der Compiler draus macht. UB heißt 
nur, dass der C-Standard keinerlei Aussage mehr darüber trifft, was mit 
dem Programm passiert, wenn man es auslöst. Auch ein Abfangen des Falls 
durch den Compiler mit kontrolliertem Absturz fällt darunter. Das 
Programm muss nicht zwingend etwas unvorhergesehenes tun, damit es UB 
ist.

: Bearbeitet durch User
von A. S. (Gast)


Lesenswert?

Mombert H. schrieb:
> [e] Soweit ich finden konnte die einzige Möglichkeit Speicher zu
> reservieren:
> 6.7 5) A...
Also hängt die Frage, ob der Zugriff auf HW-Register per gecasteter 
Adresse UB ist daran, ob diese Register C-konform reserviert sind. Hier 
also, ob das Environment z.b. per Datenblatt und Berücksichtigung im 
linker-mapping Register als "reserviert" gelten lassen kann.

Und es gibt keinen direkten Hinweis auf UB (nur über den Ausschluss über 
e)

Verstehe ich Dich richtig?

von Mombert H. (mh_mh)


Lesenswert?

A. S. schrieb:
> Also hängt die Frage, ob der Zugriff auf HW-Register per gecasteter
> Adresse UB ist daran, ob diese Register C-konform reserviert sind.

Dann braucht man aber den Cast nicht mehr, da man Adresse nicht mehr 
explizit braucht oder die Adresse des Registers direkt nehmen kann.

von A. S. (Gast)


Lesenswert?

Mombert H. schrieb:
> Dann braucht man aber den Cast nicht mehr, da man Adresse nicht mehr
> explizit braucht oder die Adresse des Registers direkt nehmen kann.

Verstehe ich nicht. Es ging doch um die Frage, ob die Nutzung einer 
festen (und richtigen) Adresse UB ist. Also z.B.
1
struct sType *p  =   (struct sType*) 0x4000;  /* als Ptr */
2
struct sType foo = *((struct sType*) 0x4000); /* oder direkt */

von Mombert H. (mh_mh)


Lesenswert?

A. S. schrieb:
> Mombert H. schrieb:
>> Dann braucht man aber den Cast nicht mehr, da man Adresse nicht mehr
>> explizit braucht oder die Adresse des Registers direkt nehmen kann.
>
> Verstehe ich nicht. Es ging doch um die Frage, ob die Nutzung einer
> festen (und richtigen) Adresse UB ist. Also z.B.
> struct sType *p  =   (struct sType*) 0x4000;  /* als Ptr */
> struct sType foo = *((struct sType*) 0x4000); /* oder direkt */

Damit das funktioniert, muss an der Stelle aber schon ein Objekt vom Typ 
struct sType existieren. Dann reicht ein
1
extern struct sType baa;
2
struct sType *p = &baa;
3
struct sType foo = baa;

von A. S. (Gast)


Lesenswert?

Mombert H. schrieb:
> Damit das funktioniert, muss an der Stelle aber schon ein Objekt vom Typ
> struct sType existieren. Dann reicht ein extern struct sType baa;

Sorry, ich dachte Du bezogst Dich auf die Stellen, die Du zitiert 
hattest. Da ging es genau um die Frage, z.B. bei HW-Registern. Nicht 
darum, wie man Adressen von Variablen verwendet, das hatte ich als 
bekannt vorausgesetzt (und ist ein anderes Thema).

von Mombert H. (mh_mh)


Lesenswert?

A. S. schrieb:
> Mombert H. schrieb:
>> Damit das funktioniert, muss an der Stelle aber schon ein Objekt vom Typ
>> struct sType existieren. Dann reicht einextern struct sType baa;
>
> Sorry, ich dachte Du bezogst Dich auf die Stellen, dass Du zitiert
> hattest. Da ging es genau um die Frage, z.B. bei HW-Registern. Nicht
> darum, wie man Ptr von Variablen verwendet, das hatte ich als bekannt
> vorausgesetzt.

Vielleicht hast du es nicht verstanden, baa ist das HW-Register.

von A. S. (Gast)


Lesenswert?

Mombert H. schrieb:
> Vielleicht hast du es nicht verstanden, baa ist das HW-Register

OK. Dann zeige doch bitte den Code in C, wie baa auf Adresse 0x10000 
gelegt wird. Dass es über Linker-Segmente/Symbole oder Built-ins etc. 
geht, geschenkt. Das war auch nicht Thema.

von Mombert H. (mh_mh)


Lesenswert?

A. S. schrieb:
> Dass es über Linker-Segmente/Symbole oder Built-ins etc.
> geht, geschenkt.
Das ist aber der einzige Weg, der ohne UB funktioniert.

von A. S. (Gast)


Lesenswert?

Mombert H. schrieb:
> Das ist aber der einzige Weg, der ohne UB funktioniert.

Wir drehen uns im Kreis.

Ich habe Deine gute Analyse zusammengefasst und gefragt, ob ich Dich 
richtig verstehe:

A. S. schrieb:
> Also hängt die Frage, ob der Zugriff auf HW-Register per gecasteter
> Adresse UB ist daran, ob diese Register C-konform reserviert sind.
...
> Und es gibt keinen direkten Hinweis auf UB (nur über den Ausschluss über
> e)

Und Du kommst jetzt mit einem Beispiel, wie man ohne gecasteter Adresse 
auskommt.

von Mombert H. (mh_mh)


Lesenswert?

A. S. schrieb:
> Mombert H. schrieb:
>> Das ist aber der einzige Weg, der ohne UB funktioniert.
>
> Wir drehen uns im Kreis.
>
> Ich habe Deine gute Analyse zusammengefasst und gefragt, ob ich Dich
> richtig verstehe:
> [...]
> Und Du kommst jetzt mit einem Beispiel, wie man ohne gecasteter Adresse
> auskommt.

Ja wir drehen uns im Kreis. Der Fall, wo man ohne gecastete Adresse 
auskommt, ist gleichzeitig der Fall, wo man ohne UB casten darf.

von A. S. (Gast)


Lesenswert?

Mombert H. schrieb:
> Ja wir drehen uns im Kreis. Der Fall, wo man ohne gecastete Adresse
> auskommt, ist gleichzeitig der Fall, wo man ohne UB casten darf.
Das ist klar. Nur hast Du suggeriert, dass das casten einer festen 
Adresse UB wäre ohne das zu belegen. Bzw. nur indirekt über [e], wo Du 
eine externe reservierung der HW ausschließt.

von Mombert H. (mh_mh)


Lesenswert?

A. S. schrieb:
> Mombert H. schrieb:
>> Ja wir drehen uns im Kreis. Der Fall, wo man ohne gecastete Adresse
>> auskommt, ist gleichzeitig der Fall, wo man ohne UB casten darf.
> Das ist klar. Nur hast Du suggeriert, dass das casten einer festen
> Adresse UB wäre ohne das zu belegen.
Ich habe nicht suggeriert. Wenn du mich falsch verstanden hast, habe ich 
mich schlecht ausgedrückt und/oder es ist ein Problem auf deiner Seite.

A. S. schrieb:
> Bzw. nur indirekt über [e], wo Du
> eine externe reservierung der HW ausschließt.
Ich habe nichts ausgeschlossen. Das ist der einzige Absatz im Standard, 
den ich dazu gefunden habe und wie ich diesen Absatz interpretiere. Du 
kannst gerne selbst im Standard lesen und einen anderen Weg finden.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.