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
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
>}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
typedefstruct{
5
volatileuint32_tMember1;
6
volatileuint32_tMember2;
7
volatileuint32_tMember3[3];
8
volatileuint32_tMember4;
9
}TEST_TypeDef;
10
11
intmain(void){
12
13
TEST_TypeDeftest;
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);
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
structFoo{
2
/* ... */
3
};
4
typedefstructFooTEST_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
typedefstructFoo{
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
structTEST_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.
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
structottokar
2
{intemil;
3
charheinrich;
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
typedefstructottokarkarlheinz;
2
oder
3
typedef
4
{intemil;
5
charheinrich;
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.
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
structFoo{
2
>/* ... */
3
>};
4
>typedefstructFooTEST_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
typedefstructFoo{
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
structTEST_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.
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.
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.)
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.
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.
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.
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.
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.
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?
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
typedefintt0;// erstellt einen typ t0, der äquivalent zu int ist
2
3
structs1{inta;};// definiert ein struct s1
4
structs1myStruct1;// Eine Variable davon
5
typedefstructs1t1;// "struct s1" kann ich durch t1 ersetzen.
6
t1myStruct1;// das erlaubt eine kürzere Schreibweise
7
8
/* alternativ struct+typedef "vereint". Bewirkt genau das selbe */
9
typedefstructs1{intdummy;}t1;
10
11
/* wenn ich "struct s1" nicht brauche, darf ich "s1" weglassen */
12
typedefstruct{intdummy;}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.
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.
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.
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.
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...
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.
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.
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?
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.
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.
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.
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?
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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?
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.
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?
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?
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
intx;
2
3
intmain(){
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:
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.
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
voidfoo(void)
2
{
3
constintslati=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
voidbar(constchar*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.
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
intdiv(inta,intb){
2
returna/b;
3
}
4
5
intmain(intargc){
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.
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.
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
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.
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;
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
constchara=5;
2
charb=5;
3
4
intmain()
5
{
6
constchar*pa=&a;
7
constchar*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.
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?
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.
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
structsType*p=(structsType*)0x4000;/* als Ptr */
2
structsTypefoo=*((structsType*)0x4000);/* oder direkt */
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
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).
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.
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.
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.
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.
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.
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.