Hallo,
ich bastle gerade an einem Headerfile für einen Controller, wo ich die
einzelnen Register wie folgt bitweise definiere:
1
#define un const volatile uint32_t
2
#define ro const volatile uint32_t
3
#define rw volatile uint32_t
4
#define wo volatile uint32_t
5
6
/* Registerdefinition */
7
unionUART_DR_u{
8
rwreg;
9
struct{
10
unsignedDATA:8;
11
unsignedconstFE:1;
12
unsignedconstPE:1;
13
unsignedconstBE:1;
14
unsignedconstOE:1;
15
};
16
};
17
/* ... */
18
19
/* Peripheriedefinition */
20
struct{
21
unionUART_DR_uDR;
22
unionUART_RSR_uRSR;
23
unRes0[4];
24
/* ... */
25
};
26
27
/* Instanzen */
28
#define UART0 ((struct UART_s*)0x4000C000)
29
#define UART1 ((struct UART_s*)0x4000D000)
Damit kann ich mittels UART0->DR.DATA auf die einzelnen Felder
zugreifen, aber auch mittels UART0->DR.reg auf das ganze Register. Das
finde ich beinahe schön.
Gibt es eine Möglichkeit, die Register so zu definieren, dass ich auf
das ".reg" verzichten kann, also dass UART0->DR das gesamte Register und
UART0->DR.DATA das einzelne Feld beschreibt?
Gibt es die Möglichkeit, "write-only"-Felder so zu markieren, dass ich
bei Lesezugriffen eine Warnung bekomme? Für "read-only"-Felder gibt es
ja const.
Kann ich in der Peripheriedefinition namenlose Feldelemente als
Platzhalter für reservierte Bereiche definieren?
Vielen Dank,
Svenska
> Wofür genau könnte write-only denn sinnvoll sein??
Wenn im Datenblatt steht, dass ein Register nicht gelesen werden darf,
dann hätte ich gerne eine Warnung, wenn ich es trotzdem versuche.
H.Joachim S. schrieb:> Wofür genau könnte write-only denn sinnvoll sein??
Datenregister von µC-Peripherie wär ein möglicherweise sinnvolles Ziel,
z.B. UDR der AVR-UART: Lesezugriff ließt vom Receiver, Schreibzugriff
schreibt auf den Transmitter.
S. R. schrieb:> #define un const volatile uint32_t> #define ro const volatile uint32_t> #define rw volatile uint32_t> #define wo volatile uint32_t
Bitte benutze typedef.
S. R. schrieb:> Damit kann ich mittels UART0->DR.DATA auf die einzelnen Felder> zugreifen, aber auch mittels UART0->DR.reg auf das ganze Register. Das> finde ich beinahe schön.
Sei dir bewusst, dass das nach Standard undefiniertes Verhalten erzeugt.
Viele Compiler unterstützen jedoch dieses "Feature". Das Handbuch des
Compilers hilft da weiter.
Die mit const deklarierten Variablen können über reg jedoch geschrieben
werden. Durch Optimierungen des Compilers können dabei komische Effekte
auftreten. Falls die Werte durch die Hardware geändert werden können,
sollten diese ebenfalls nicht als const deklariert werden (gleiches
Problem). In diesem Fall müsste so oder so die union als volatile
deklariert werden.
S. R. schrieb:> #define UART0 ((struct UART_s*)0x4000C000)> #define UART1 ((struct UART_s*)0x4000D000)
volatile nötig oder nicht?
S. R. schrieb:> Gibt es eine Möglichkeit, die Register so zu definieren, dass ich auf> das ".reg" verzichten kann, also dass UART0->DR das gesamte Register und> UART0->DR.DATA das einzelne Feld beschreibt?
Nicht in C.
S. R. schrieb:> Gibt es die Möglichkeit, "write-only"-Felder so zu markieren, dass ich> bei Lesezugriffen eine Warnung bekomme?
Nein. Du könntest für deine union Funktionen bereitstellen und nicht
mehr direkt auf die Member zugreifen.
S. R. schrieb:> Kann ich in der Peripheriedefinition namenlose Feldelemente als> Platzhalter für reservierte Bereiche definieren?
Ja, durch einen unnamed member:
1
structfoo{
2
unsignedintA:5;
3
unsignedint:4;
4
unsignedintC:3;
5
};
Beachte dabei Alignment und Padding, das Compilerhandbuch hilft weiter.
> Da ja, aber doch nicht als struct im RAM?
Wo siehst du in meinem Beispiel eine "struct im RAM"?
> #define UART0 ((struct UART_s*)0x4000C000)
Das ist ein Zeiger auf eine Struktur, die an einer fixen Adresse (in
diesem Fall dem UART0-Peripherieblock) liegt. Nicht im RAM.
Be S. schrieb:> Bitte benutze typedef.
Einverstanden. ;-)
>> Damit kann ich mittels UART0->DR.DATA auf die einzelnen Felder>> zugreifen, aber auch mittels UART0->DR.reg auf das ganze Register.> Sei dir bewusst, dass das nach Standard undefiniertes Verhalten erzeugt.
Das ist mir klar, aber auf der Ebene kann ich damit leben. Der Code ist
ohnehin nicht portabel, da hardware-spezifisch.
> Die mit const deklarierten Variablen können über reg jedoch geschrieben> werden.
Logisch, wobei "reg" auch const ist, wenn das gesamte Register read-only
ist.
>> #define UART0 ((struct UART_s*)0x4000C000)>> #define UART1 ((struct UART_s*)0x4000D000)> volatile nötig oder nicht?
Hmm, das ist eine gute Frage. Im Augenblick ist jeder einzelne Member
volatile (wegen den #defines/typedefs oben), daher habe ich mir das
gespart.
Wenn ich die Struktur selbst volatile mache, kann ich mir das für die
einzelnen Member sparen, richtig? Wie interagiert das mit "const", ist
das dann auch "const volatile" und ist für mich nur lesbar, für die
Hardware aber schreibbar?
>> Gibt es eine Möglichkeit, die Register so zu definieren, dass ich auf>> das ".reg" verzichten kann, also dass UART0->DR das gesamte Register und>> UART0->DR.DATA das einzelne Feld beschreibt?> Nicht in C.
Gut, das war eigentliche Frage.
>> Gibt es die Möglichkeit, "write-only"-Felder so zu markieren, dass ich>> bei Lesezugriffen eine Warnung bekomme?> Nein. Du könntest für deine union Funktionen bereitstellen und nicht> mehr direkt auf die Member zugreifen.
Schade, aber dann ist das eben so.
>> Kann ich in der Peripheriedefinition namenlose Feldelemente als>> Platzhalter für reservierte Bereiche definieren?>> Ja, durch einen unnamed member:> struct foo{> unsigned int A : 5;> unsigned int : 4;> unsigned int C : 3;> };> Beachte dabei Alignment und Padding, das Compilerhandbuch hilft weiter.
Das gilt für Bitfelder, aber kann ich sowas auch für normale Member
einer Struct machen? Zum Beispiel hat die UART am Offset 0x10 kein
Register, daher lege ich da ein Dummy-Array rein.
Vielen Dank erstmal,
Svenska
S. R. schrieb:> Wenn ich die Struktur selbst volatile mache, kann ich mir das für die> einzelnen Member sparen, richtig?
Richtig.
S. R. schrieb:> Wie interagiert das mit "const", ist> das dann auch "const volatile" und ist für mich nur lesbar, für die> Hardware aber schreibbar?
const und volatile schliessen sich gegenseitig aus. Falls der Wert aber
von der Hardware verändert werden kann, musst du die Variable zwingend
mit volatile deklarieren. Ansonsten wird der Compiler optimieren und den
Wert evt. einmalig bei Programmstart aus dem Register lesen.
S. R. schrieb:> Das gilt für Bitfelder, aber kann ich sowas auch für normale Member> einer Struct machen?
unnamed member gibts nur bei Bitfeldern. Bei normalen Strukturen müsste
man das über Dummies erledigen.
EDIT:
Wenn man Member von Strukturen als const oder volatile deklariert, holt
man sich meistens mehr Probleme in Boot, als man sonst schon hat.
Entweder die gesamte Struktur mit const oder volatile deklarieren oder
gar nichts.
Be S. schrieb:> Falls die Werte durch die Hardware geändert werden können,> sollten diese ebenfalls nicht als const deklariert werden (gleiches> Problem). In diesem Fall müsste so oder so die union als volatile> deklariert werden.
Const und volatile schließen sich nicht gegenseitig aus. Du hast genau
den Fall beschrieben, für den tatsächlich eine Typqualifizierung mittels
"const volatile" gedacht ist.
> Wenn man Member von Strukturen als const oder volatile deklariert, holt> man sich meistens mehr Probleme in Boot, als man sonst schon hat.> Entweder die gesamte Struktur mit const oder volatile deklarieren oder> gar nichts.
Volle Zustimmung!
Andreas S. schrieb:> Const und volatile schließen sich nicht gegenseitig aus. Du hast genau> den Fall beschrieben, für den tatsächlich eine Typqualifizierung mittels> "const volatile" gedacht ist.
Danke für die Berichtigung, da war ich falsch informiert. Ist auch so im
Standard beschrieben (Kapitel 6.7.3):
> EXAMPLE 1 An object declared> extern const volatile int real_time_clock;> may be modifiable by hardware, but cannot be assigned to, incremented,> or decremented.
S. R. schrieb:> amit kann ich mittels UART0->DR.DATA auf die einzelnen Felder> zugreifen, aber auch mittels UART0->DR.reg auf das ganze Register.
Du weißt aber schon, daß die Anordnung der Bitfields im
zugrundeliegenden Datentyp nicht im C-Standard definiert ist? Der
Compiler darf die legen, wie er lustig ist. Also vom MSB her damit
anfangen, oder auch vom LSB.
Bitfields sind für diese Anwendung verführerisch, aber grundfalsch.
Die einzige Ausnahme, wo man eine Union mit einem Bitfield machen kann,
ist zum Zwecke des Kopierens des ganzen Bitfields, und zum Vergleichen
auf Identität. Und selbst dabei sollte man sich überzeugen, daß die
Datentypen wirklich gleich breit sind.
Nop schrieb:> Du weißt aber schon, daß die Anordnung der Bitfields im> zugrundeliegenden Datentyp nicht im C-Standard definiert ist? Der> Compiler darf die legen, wie er lustig ist. Also vom MSB her damit> anfangen, oder auch vom LSB.
Sowas ist im (E)ABI definiert. Der Compiler wählt ja auch nicht nach
Gutdünken, ob ein int nun 2, 4 oder 8 Bytes groß ist, abhängig von
Wochentag und Mondphase :-)
> Bitfields sind für diese Anwendung verführerisch, aber grundfalsch.
ACK.
Johann L. schrieb:> Sowas ist im (E)ABI definiert.
Nein, das kann jeder Compiler nach Gutdünken tun. Hat ja mit ABI auch
nichts zu tun. Das gibt witzige Effekte, wenn z.B. der Hersteller eines
Chips sowas aus Inkomptetenz im Referenzcode per Bitfields macht, man
aber dann einen anderen Compiler benutzt.
Oder wenn der Compiler seine Meinung spontan beim nächsten Release
ändert, was er ja darf. Speziell die GCC-Leutchen sind bekannt dafür,
daß es sie nicht interessiert, wieviel ohnehin zweifelhaften
Produktivcode sie zerbrechen, wenn der Standard es erlaubt. Siehe
Torvalds' Anfälle zum Thema GCC.
> ACK.
Eben drum. (:
S. R. schrieb:> Das ist mir klar, aber auf der Ebene kann ich damit leben. Der Code ist> ohnehin nicht portabel, da hardware-spezifisch.
bis zum nächsten Complier-Release, der es anders macht. Weil er was
"Optimieren" kann :)
Hallo,
>> Das gilt für Bitfelder, aber kann ich sowas auch für normale Member>> einer Struct machen?> unnamed member gibts nur bei Bitfeldern. Bei normalen Strukturen müsste> man das über Dummies erledigen.
Schade, aber dann wohl nicht zu ändern.
> EDIT:> Wenn man Member von Strukturen als const oder volatile deklariert, holt> man sich meistens mehr Probleme in Boot, als man sonst schon hat.> Entweder die gesamte Struktur mit const oder volatile deklarieren oder> gar nichts.
Wenn ich die Struktur als volatile deklariere und den einzelnen Member
als const, dann habe ich den gleichen Effekt, wie wenn ich den Member
als const volatile deklarieren würde, richtig?
Dann baue ich das mal um.
Nop schrieb:> Du weißt aber schon, daß die Anordnung der Bitfields im> zugrundeliegenden Datentyp nicht im C-Standard definiert ist? Der> Compiler darf die legen, wie er lustig ist. Also vom MSB her damit> anfangen, oder auch vom LSB.
Mein arm-none-eabi-gcc fängt beim LSB an und ich gehe mal stark davon
aus, dass das auch so bleiben wird. ;-)
> Bitfields sind für diese Anwendung verführerisch, aber grundfalsch.
Du würdest also eine #define-Orgie nehmen, die die einzelnen Felder für
jedes Register ausmaskiert? Habe ich auch mal gemacht, finde ich aber
eher furchtbar. An den Stellen, wo man es machen muss, kann man ja
immernoch auf das ganze Register zugreifen.
Nop schrieb:> Nein, das kann jeder Compiler nach Gutdünken tun. Hat ja mit ABI auch> nichts zu tun. Das gibt witzige Effekte, wenn z.B. der Hersteller eines> Chips sowas aus Inkomptetenz im Referenzcode per Bitfields macht, man> aber dann einen anderen Compiler benutzt.
Hast du Beispiele?
> Oder wenn der Compiler seine Meinung spontan beim nächsten Release> ändert, was er ja darf.
Hast du Beispiele zu Bitfeldern?
Wenn das nämlich wirklich so furchtbar ist, dann könnte ich auch die
tausend #defines schreiben, aber das wollte ich eher vermeiden. Lieber
stelle ich den Header komplett auf C++ mit einer handgeklopften
Bitfeld-Klasse um, damit ich in der Beschreibung möglichst nah am
Datenblatt bin.
Du hast natürlich vollkommen Recht, dass ich mich mit meiner Struktur
sehr tief in schlechtdefinierte Gegenden wage, aber wenn das Risiko nur
theoretisch ist, dann akzeptiere ich das freiwillig.
Gruß,
Svenska
S. R. schrieb:> Du würdest also eine #define-Orgie nehmen, die die einzelnen Felder für> jedes Register ausmaskiert?
So macht man das embedded halt. Außer wenn Dein Controller Bitbanding
kann (nicht Bitbanging). Es wird übrigens noch viel "besser", wenn Du
bedenkst, daß Lesezugriffe auf Register selbige mitunter auch verändern
können. Ein Schreibzugriff per Bitfield macht Dir im Hintergrund eine
Lese-Operation, die Du nicht "siehst".
Deswegen liest man das Register in einen passenden Integer (auf ARM
sinnigerweise uint32_t) und arbeitet dann auf dieser Variablen damit.
> Hast du Beispiele?http://embeddedgurus.com/stack-overflow/2009/12/a-c-test-the-0x10-best-questions-for-would-be-embedded-programmers/
"I recently had the misfortune to look at a driver written by Infineon
for one of their more complex communications chip. It used bit fields,
and was completely useless because my compiler implemented the bit
fields the other way around. The moral – never let a non-embedded person
anywhere near a real piece of hardware!"
> Hast du Beispiele zu Bitfeldern?
Du meinst, wozu man sie einsetzen kann? Man sollte das so gut wie
überhaupt nicht tun:
http://embeddedgurus.com/stack-overflow/2009/10/effective-c-tip-6-creating-a-flags-variable/#comment-2390
Einzige Ausnahme: Man hat eine Menge Bool'scher Variablen und möchte
gerne Speicher sparen. Dafür sind sie geeignet und auch gedacht, dann
machen sie den Quelltext lesbarer.
> Wenn das nämlich wirklich so furchtbar ist, dann könnte ich auch die> tausend #defines schreiben, aber das wollte ich eher vermeiden.
Es hat seinen Grund, daß man mit einer Menge Defines arbeitet, und mit
Bitmasken.
> Du hast natürlich vollkommen Recht, dass ich mich mit meiner Struktur> sehr tief in schlechtdefinierte Gegenden wage, aber wenn das Risiko nur> theoretisch ist, dann akzeptiere ich das freiwillig.
Es kann gute Gründe geben, wieso man jenseits des C-Standards
programmiert. Aber "gefällt mir halt so" ist keiner. Die Gegenden sind
auch nicht schlecht definiert, sie sind undefiniert.
Und ganz besonders, wenn Du GCC einsetzt! Bei kommerziellen Compilern
können es sich die Hersteller nicht leisten, den funktionierenden, wenn
auch schlechten Code ihrer Kunden mit einem Update zu zerbrechen. GCC
ist aber nicht kommerziell, und es ist ihnen vollkommen egal, was mit
C-Code ist, der nicht nach Standard arbeitet.
Ich mag es außerdem, wenn ich einen neuen GCC nach dem ersten
Bugfix-Release einfach einsetzen kann, ohne erstmal rumzurätseln, welche
Option ich jetzt schon wieder setzen muß, damit mein Code immer noch
läuft.
Hallo,
> Ein Schreibzugriff per Bitfield macht Dir im Hintergrund eine> Lese-Operation, die Du nicht "siehst".
Das stimmt.
> "I recently had the misfortune to look at a driver written by Infineon> for one of their more complex communications chip. It used bit fields,> and was completely useless because my compiler implemented the bit> fields the other way around.
oO...
>> Du hast natürlich vollkommen Recht, dass ich mich mit meiner Struktur>> sehr tief in schlechtdefinierte Gegenden wage, aber wenn das Risiko nur>> theoretisch ist, dann akzeptiere ich das freiwillig.
Du hast mich überzeugt (insbesondere das zwingende RMW).
Dann also doch wieder zurück zu den #defines. Seufz.
Danke für den Input,
Gruß
Nop schrieb:> Und ganz besonders, wenn Du GCC einsetzt! Bei kommerziellen Compilern> können es sich die Hersteller nicht leisten, den funktionierenden, wenn> auch schlechten Code ihrer Kunden mit einem Update zu zerbrechen. GCC> ist aber nicht kommerziell, und es ist ihnen vollkommen egal, was mit> C-Code ist, der nicht nach Standard arbeitet.
Das mit den Bitfeldern, die über Ports und SFRs gelegt werden, machen
die Compiler für die PICs schon lange so. Dabei ist der XC16 (und auch
der XC32) der GCC. Die Fa. Microchip, die den Compiler offiziell ihren
gewerblichen Kunden anbietet, ist also im Gegensatzt zu dir der Meinung,
der GCC wird sich nicht soweit ändern, daß der Code ihrer Großkunden
sowie geschätzt hunderttausende Zeilen ihrer Prozessorspezifischen
Headerfiles obsolet werden.
MfG Klaus
Nop schrieb:> Oder wenn der Compiler seine Meinung spontan beim nächsten Release> ändert, was er ja darf. Speziell die GCC-Leutchen sind bekannt dafür,
Kannst du dafür ein konkretes Beispiel nennen?
In welcher GCC-Version für welche Architektur hat sich das
Bitfield-Layout "einfach so" geändert?
Klaus schrieb:> Die Fa. Microchip, die den Compiler offiziell ihren> gewerblichen Kunden anbietet, ist also im Gegensatzt zu dir der Meinung,
.. daß man den C-Standard ignorieren darf. Compliert doch, läuft doch,
alles gut. Das sind genau diejenigen, die sich irgendwann wundern, wieso
es nicht mehr läuft. Im Übrigen zeigt das auch, daß die Firma Mikrochip
keine ernsthaften Kunden mit der Softwareseite adressieren will, sondern
nur Hobbybastler.
Denn z.B. MISRA-Konformität ist damit nicht zu erreichen. Ich würde
sowas in einem Codereview schlichtweg durchfallen lassen. Der Rest der
Codebasis wird nämlich mit derselben Einstellung geschrieben worden
sein. Will man also bei entsprechenden Produkten was machen, dann kann
man den Microchip-Code direkt wegwerfen und neu coden.
Infineon, siehe zitierter Teil, war auch der Meinung, daß Bitfields da
ne tolle Idee sind. Hardwarefirmen machen oft saumäßigen Code, das ist
nichts Neues. Möglicherweise halt an den billigstmöglichen Anbieter
irgendwo in Versklavistan outgesourced.
@ Johann:
> Kannst du dafür ein konkretes Beispiel nennen?
Das bezog sich darauf, daß GCC erwiesenermaßen bereits Bestandscode
zerbrochen hat. Deren Reaktion war dann "aber der Standard erlaubt das
doch". Siehe die Rants von Torvalds zum Thema GCC 4.9. Davor compilierte
auch alles, und es lief. Selbe Einstellung wie bei Microchip und
Infineon.
Ob die auch auf den Gedanken kommen, mit den Bitfields was zu machen,
weiß ich nicht. Tatsache ist aber, wenn man deren Einstellung zu
nicht-standardkonformem Code kennt, dann ist es eine wirklich schlechte
Idee, GCC für solche Zwecke einzusetzen.
Denkbar wäre es z.B., daß man auf ARM bei Optimierung auf
Geschwindigkeit immer 32bit-Ints zugrundelegt, weil das schneller geht
als etwa 8 bit. Bei Optimierung auf Größe hingegen könnte man das
durchaus mit 8 bit machen, wenn es paßt. C-Compiler müssen nicht das
tun, was der Programmierer sagt, solange das Ergebnis funktional im
Rahmen des Standards äquivalent ist.
Abgesehen davon ist es auch mit nachsichtigeren Compilerschreibern eine
grundsätzlich schlechte Idee, sich nicht an den C-Standard zu halten.
Mit der Ausnahme, daß man Dinge tatsächlich braucht, die in C so nicht
möglich sind - beispielsweise Erweiterungen. attribute naked, used,
interrupt, section und dergleichen.
Nop schrieb:> @ Johann:>>> Kannst du dafür ein konkretes Beispiel nennen?>> Das bezog sich darauf, daß GCC erwiesenermaßen bereits Bestandscode> zerbrochen hat.
Weil es einen Bug gab, der dann behoben wurde? (möglich, weil Software
nun mal Bugs haben kann, die aber auch behoben werden können)
Oder weil der "Bestandscode" nicht Standard-konform war und auf einmal
anderer Code erzeugt wurde als in der Phantasie des Autors? (nach meiner
Erfahrung mit Abstand die wahrscheinlichste Alternative, der Autor wird
dann gerne zum Rumpelstielzchen)
Oder weil die GCC-Entwickler einfach mal so das Bitfeld-Layout geändert
haben weil sie Langeweile und nix besseres zu tun hatten? (absolut
unwahrscheinlich)
> Deren Reaktion war dann "aber der Standard erlaubt das doch".> Siehe die Rants von Torvalds zum Thema GCC 4.9. Davor compilierte> auch alles, und es lief.
Du meinst vermutlich "Memory corruption due to word sharing"
https://gcc.gnu.org/ml/gcc/2012-02/msg00005.html> Ob die auch auf den Gedanken kommen, mit den Bitfields was zu machen,> weiß ich nicht. Tatsache ist aber, wenn man deren Einstellung zu> nicht-standardkonformem Code kennt, dann ist es eine wirklich> schlechte Idee, GCC für solche Zwecke einzusetzen.
Was bitte soll denn das Kriterium sein wenn nicht ein Standard oder
explizite Zusicherungen des Compilers, bei GCC z.B:
- dass Type Punning per Unions funktioniert
- dass die Zugriffsbreite auf volatile Bitfelder per
-fstrict-volatile-bitfields durch den Basistyp des Bitfelds
ausgedrückt wird.
Letzteres ist schon deshalb problematisch weil garnicht sichergestellt
ist, dass eine Architektur entsprechende Befehle überhaupt HAT oder
ausführen kann ohne eine Trap zu generieren.
Oder einfach Torvalds "compiler should not do something stupid"?
> Denkbar wäre es z.B., daß man auf ARM bei Optimierung auf> Geschwindigkeit immer 32bit-Ints zugrundelegt, weil das schneller geht> als etwa 8 bit.
Siehe -fstrict-volatile-bitfields. Wie gesagt geht es dabei nicht ums
Layout (das ist Sache des (E)ABI), sondern darum, welche Zugriffe
(nicht) erzeugt werden.
http://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html#index-fstrict-volatile-bitfields-1217
Johann L. schrieb:> Weil es einen Bug gab, der dann behoben wurde?
Nein. Erstens ist das beim GCC schon sehr selten. Wenn etwas nicht geht,
ist es in aller Regel der C-Programmierer, der etwas verkehrt macht.
Zweitens beheben die GCC-Leute tatsächliche Bugs ja auch. Drittens bauen
sie Bugs nicht bewußt ein - Zerbrechen von nicht standardkonformem
Produktivcode aber schon. Also sollte man sich entweder strikt an den
Standard halten oder nicht GCC nutzen.
> Oder weil der "Bestandscode" nicht Standard-konform war und auf einmal> anderer Code erzeugt wurde als in der Phantasie des Autors?
Genau das. Registermapping mit Bitfields ist übrigens auch nicht
standardkonform. "Aber das funktioniert doch" ist schlichtweg kein
Argument, weil das in anderen Dingen auch schon eine Fehlannahme war.
> Was bitte soll denn das Kriterium sein wenn nicht ein Standard
Sag ich ja.
> - dass Type Punning per Unions funktioniert
Logisch, C99 halt. Wobei der Teil im Standard etwas lax notiert ist und
es von daher schon gut ist, wenn der Compiler das explizit klarstellt.
> Oder einfach Torvalds "compiler should not do something stupid"?
Ja, ein echter Brüller, nicht wahr? Zumal es eigentlich gut ist, daß GCC
da endlich mal mit pointer aliasing aus dem Knick kam. Genau das ist
nämlich ein wichtiger Grund, wieso Fortran bei numerischen Berechnungen
immer noch schneller ist als C. Weil Aliasing bei Fortran schlichtweg
nicht drin ist. Mit den heutigen Caches, wo die Speicheranbindung ein
Flaschenhals ist, merkt man das bei entsprechender Software auch.
Nop schrieb:> Also sollte man sich entweder strikt an den> Standard halten oder nicht GCC nutzen.
Damit verbietest du den GCC für jede Form von Kernelprogrammierung (und
im Übrigen auch jeden anderen C-Compiler). Gratuliere.
>> Oder einfach Torvalds "compiler should not do something stupid"?> Ja, ein echter Brüller, nicht wahr?
Ich darf davon ausgehen, dass der Autopilot im Flugzeug keinen groben
Unfug treibt, ja. Wenn ich diese Annahme nicht mehr treffen darf, dann
darf ich keinen Autopiloten mehr benutzen.
> Zumal es eigentlich gut ist, daß GCC da endlich mal> mit pointer aliasing aus dem Knick kam.
Es wäre vielleicht mal gut, einen C-Compiler zu schreiben, der jede Form
von undefiniertem (oder implementation-defined) Code zu einem NOP
optimiert, und zwar rekursiv.
Damit kann man zwar keinen real existierenden Code mehr bauen, aber man
wäre standardkonform.
S. R. schrieb:> Nop schrieb:>> Also sollte man sich entweder strikt an den>> Standard halten oder nicht GCC nutzen.>> Damit verbietest du den GCC für jede Form von Kernelprogrammierung (und> im Übrigen auch jeden anderen C-Compiler). Gratuliere.
Das ist doch Blödsinn, es ist absolut kein Problem einen Kernel zu
schreiben, ohne die C-Regeln zu verletzen. Die Initialisierung von
Prozessorspezifischem wie z.B. wechseln in den Protected Mode der CPU
oder speichern der Stackframe beim Taskswitch muss sowieso in ASM
geschrieben werden, das linkt man dann einfach zum C-Code dazu. Der
grössere Rest, der noch übrig bleibt, kann absolut Problemlos in C
gelöst werden, ohne den Standard verletzen zu müssen. Oder kannst du dir
irgendein Beispiel überlegen, bei dem man den Standard verletzen muss?
S. R. schrieb:>> Zumal es eigentlich gut ist, daß GCC da endlich mal>> mit pointer aliasing aus dem Knick kam.>> Es wäre vielleicht mal gut, einen C-Compiler zu schreiben, der jede Form> von undefiniertem (oder implementation-defined) Code zu einem NOP> optimiert, und zwar rekursiv.
Ich finde was der GCC mit dem pointer aliasing ebenfalls gut. Es
steigert die Portabilität des Codes. Häufig warnt der GCC ja sogar, wenn
man in die Alias falle tappt, das genügt mir föllig. Ich weiss noch
damals, als ich gemerkt habe wie wenig die Implementierung von Bitfelder
vom Standard vorgeschrieben wird. Wenn der GCC nicht etwas anderes
gemacht hätte, als ich erwartete, wäre es mir nie aufgefallen. Und was
habe ich dann gemacht? Ich habe mich nicht beschwert, sondern meinen
Code korrigiert, alle Bitfelder raus. Ich finde es immer wieder gut,
wenn beim GCC neue solche Dinge dazu kommen und der Code nichtmehr
Kompiliert, weil man ja so Clever war, -Werror zu setzen. Dann meckere
ich aber nicht über GCC, sondern behebe den Fehler im Code.
S. R. schrieb:> Damit verbietest du den GCC für jede Form von Kernelprogrammierung (und> im Übrigen auch jeden anderen C-Compiler). Gratuliere.
Nee. Embedded gibt's haufenweise RTOS, die nicht über undefiniertes
Verhalten laufen. C ist doch für dieses Level von Programmierung
überhaupt erst geschaffen worden.
Für ein paar Dinge nimmt man dann noch Assembler, weil die
"C-Modellmaschine" mitunter zuviel abstrahiert. Auch beim STM32 geht der
eine oder andere exception-handler nicht in C zu programmieren.
> Es wäre vielleicht mal gut, einen C-Compiler zu schreiben, der jede Form> von undefiniertem (oder implementation-defined) Code zu einem NOP> optimiert, und zwar rekursiv.
Lies Dir mal die dreiteilige Serie von Chris Lattner zum Thema
"undefined behaviour in C" durch, ist schon sehr interessant. Zwar ist
das aus der LLVM-Ecke, aber GCC hat ja mit denselben technischen
Aspekten zu tun.
http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html
Das ist nicht so einfach, wie es aussieht, jedes undefinierte Verhalten
als Fehler zu erkennen. In C ist aus sehr gutem Grund viel undefiniert,
nämlich weil es schnell sein soll, und zwar auf unterschiedlicher
Hardware.
> Damit kann man zwar keinen real existierenden Code mehr bauen, aber man> wäre standardkonform.
GCC hat die neuen, sehr genialen sanitiser-Features für undefiniertes
Verhalten. Der Nachteil ist für embedded bare metal leider, daß es nicht
wirklich einfach zu benutzen ist, wenn man nicht von vornherein zwischen
Applikation und Hardware-Layer trennt. Tut man das aber, dann kann man
zumindest die Applikation z.B. unter Linux auf undefiniertes Verhalten
testen.
S. R. schrieb:> /* Peripheriedefinition */> struct {> union UART_DR_u DR;> union UART_RSR_u RSR;> un Res0[4];> /* ... */> };
Warum um himmelswillen bloß so etwas? manchmal schreibst du ganz
vernünftig - und dann kommt plötzlich sowas daher. Also, gibt's einen
echten Grund für derartige Akrobatik? Ich sehe keinen. Schreib dir nen
richtigen seriellen Handler mit einem hardwareunabhängigen Headerfile -
und zwar für jeden seriellen Port einen separaten, damit du nicht in
switch (uart) { case uart1... case uart2... usw. dich verzetteln mußt.
Wenn du es so anpackst, brauchst du obige Eiertänze überhaupt nicht,
dein Code wird stabil und übersichtlich und wartbar und du hast final
mehr Freude am Leben.
W.S.
Nop schrieb:> Zerbrechen von nicht standardkonformem Produktivcode aber schon.
ROFL. Was will man denn da groß "zerbrechen".
Wenn Murx wie x = x++ nicht mehr "funktioniert", dann wird ein anderer
Compilerhersteller gesucht? Anstatt den Murx zu beheben?
Daniel A. schrieb:> Das ist doch Blödsinn, es ist absolut kein Problem einen Kernel zu> schreiben, ohne die C-Regeln zu verletzen.
Natürlich kann man alle C-Regeln einhalten, wenn man die Teile des
Kernels, wo man sie nicht einhalten könnte, in Assembler programmiert.
Aber das geht gerade am Sinn vorbei.
Im Übrigen hat ARM die Cortex-M extra so entworfe, dass man den
Startup-Code vollständig in C schreiben kann.
> Oder kannst du dir irgendein Beispiel überlegen, bei dem man den> Standard verletzen muss?
Linus hatte im verlinkten Thread dazu was geschrieben:
LT> The paper C standard can never promise what a kernel expects.
LT> There are tons of unspecified things that a compiler could do,
LT> including moving memory around behind our back as long as it moves
LT> it back. Because it's all "invisible" in the virtual C machine
LT> in the absence of volatiles.
LT> The fact that the kernel has things like SMP coherency requirements
LT> is simply not covered by the standard. There are tons of other
LT> things not covered by the standard too that are just
LT> "that's what we need".
Die ganze Diskussion zu den C11/C++11 Memory Models läuft beispielsweise
darauf hinaus, dass der Compiler niemals mehr als eine Compilation Unit
am Stück sieht und damit niemals das wahre Ausmaß seines generierten
Codes vollständig einschätzen kann.
Nop schrieb:> Lies Dir mal die dreiteilige Serie von Chris Lattner zum Thema> "undefined behaviour in C" durch, ist schon sehr interessant. Zwar ist> das aus der LLVM-Ecke, aber GCC hat ja mit denselben technischen> Aspekten zu tun.
Habe ich. Von ihm kam ja die Idee eines Compilers, der jedes
undefinierte Verhalten rigoros erkennt und darauf optimiert. Das muss
weder schnell geschehen noch muss der generierte Code besonders gut
sein.
Da ein C-Programm, welches an irgendeiner Stelle undefiniertes Verhalten
benutzt, schon vor seiner eigenen Ausführung(!) undefiniert ist, könnte
man an so einem Compiler schön sehen, wie sinnvoll eine zu enge
Auslegung des Standards ist.
So etwas ähnliches könnte man auch auf Betriebssystemsebene machen,
indem man z.B. jeden ungültigen Bibliotheksaufruf (memcpy() mit
überlappenden Bereichen, ...) sofort mit abort() quittiert.
Ich halte es da ein bisschen wie Linus, denn realer Code ist am Ende nie
standard-compliant. Es gibt da auch ein schönes Beispiel, wo gcc ein
leeres Programm kompiliert und dabei selbst undefined behaviour
ausnutzt.
W.S. schrieb:> Schreib dir nen richtigen seriellen Handler mit einem> hardwareunabhängigen Headerfile - und zwar für jeden> seriellen Port einen separaten, ...
Der serielle Handler (eher UART-Treiber) muss auf irgendeiner Ebene mit
Registern arbeiten. Genau da bin ich und hätte gerne sprechende Namen,
nämlich genau die aus dem Datenblatt. Die stehen in meinem Headerfile.
Um den Code, der die Register dann anspricht, ging es mir an dieser
Stelle noch garnicht. Der besteht im Augenblick aus
__attribute__((constructor)) void uart0_init();
uint8_t uart0_rx(void);
void uart0_tx(uint8_t);
und wird in einem separaten (hardware-unabhängigen) Headerfile
beschrieben. Da das Gesamtsystem sowieso nur Spielerei ist (als
Vorbereitung für andere Architekturen), reichen mir blockierende
PIO-Routinen aus.
Johann L. schrieb:> ROFL. Was will man denn da groß "zerbrechen".
Wieso, Du fandest gerade eben undefinierten Code noch ganz toll mit dem
"Argument", läuft doch (bisher). ICH habe von vornherein gesagt "lieber
nicht". Aber schön, wenn wir da inzwischen übereinstimmen, hat ja ne
Weile gedauert.
S. R. schrieb:> Natürlich kann man alle C-Regeln einhalten, wenn man die Teile des> Kernels, wo man sie nicht einhalten könnte, in Assembler programmiert.
Assembler braucht man nicht, um undefinierten C-Code zu vermeiden,
sondern weil C z.B. direkte Manipulationen von Registern nicht erlaubt.
Sowas wie einen exception handler für stack overflow kann man nicht in C
schreiben.
Gib doch mal ein Beispiel, wo man undefinierten Code schreiben "muß", um
Assembler zu vermeiden.
> Das muss weder schnell geschehen noch muss der generierte Code besonders> gut sein.
Man könnte ja auch einfach eine Sprache nehmen, die dafür entworfen
wurde. C ist das nicht.
> Im Übrigen hat ARM die Cortex-M extra so entworfe, dass man den> Startup-Code vollständig in C schreiben kann.
Undefinierten Code braucht man dazu aber nicht. Wäre auch ungeschickt,
weil Keil, IAR und GCC dazu garantiert verschiedene Meinungen hätten. Im
Übrigen geht der Speichertest auch nicht in C - zumindest den Teil, wo
der Stack reinsoll, muß man in Assembler machen.
> denn realer Code ist am Ende nie standard-compliant.
Fehler macht jeder, keine Frage. Das heißt aber nicht, daß man sie
gezielt einbauen sollte, denn nichts anderes ist undefinierter Code
nunmal.
Nop schrieb:> Gib doch mal ein Beispiel, wo man undefinierten Code schreiben "muß", um> Assembler zu vermeiden.
Du hast doch schon zwei genannt:
- Speichertest
- Stack-Overflow Exception Handler
>> [ Compiler, der undefiniertes Verhalten gezielt ausnutzt ]>> Das muss weder schnell geschehen noch muss der generierte Code besonders>> gut sein.> Man könnte ja auch einfach eine Sprache nehmen, die dafür entworfen> wurde. C ist das nicht.
Entweder, du nimmst den Standard als das Maß aller Dinge, oder du nimmst
den Standard als Richtlinie, von der auch abgewichen werden kann.
Im ersten Fall solltest du über einen Compiler mit dazu passender
Standardbibliothek, die gezielt undefiniertes Verhalten finden und
möglichst idiotisch ausnutzen, um möglichst viele Bugs zu triggern, doch
froh sein?
> Fehler macht jeder, keine Frage. Das heißt aber nicht, daß man sie> gezielt einbauen sollte, denn nichts anderes ist undefinierter Code> nunmal.
Der Unterschied zwischen "implementation-defined" und "undefined" ist,
dass man sich auf ersteres ebenfalls verlassen kann, wenn man denn will.
Das ist nicht zwingend ein Fehler.
S. R. schrieb:> Du hast doch schon zwei genannt:> - Speichertest> - Stack-Overflow Exception Handler
Die kann man überhaupt nicht in C schreiben, weder mit definiertem noch
mit undefiniertem Code. Also bleibt meine Frage bestehen - Beispiele für
die Notwendigkeit von undefiniertem Verhalten?
> Entweder, du nimmst den Standard als das Maß aller Dinge, oder du nimmst> den Standard als Richtlinie, von der auch abgewichen werden kann.
Ersteres. Letzteres zerbricht Dir nämlich früher oder später ohnehin.
Die einzigen Abweichungen, die ich akzeptabel finde, sind
compilerspezifische Erweiterungen, die dann aber auch wohldefiniert
sind. Zu denken wäre hier an die __attribute-Erweiterungen. interrupt,
naked, used, section, was die sind, die ich so brauche. Das geht
embedded oftmals nicht anders, ist aber auch nicht undefiniert.
Auch #pragma kann vereinzelt sinnvoll sein, etwa um enge Code-Abschnitte
mit O3 zu optimieren, wenn Messungen sie als Hotspot ausweisen. Dort
kann man auch mit builtin-expect arbeiten.
Natürlich alles über defines gekapselt. Bei anderen Compilern kann man
diese Dinge dann erstmal zu "nicht da" definen, so daß der Code halt
etwas langsamer läuft, bis man die entsprechende Formulierung für den
anderen Compiler dort einbaut.
> Im ersten Fall solltest du über einen Compiler mit dazu passender> Standardbibliothek, die gezielt undefiniertes Verhalten finden und> möglichst idiotisch ausnutzen, um möglichst viele Bugs zu triggern, doch> froh sein?
So einfach ist es nicht. Es geht vielmehr so herum, daß der Compiler
ANNIMMT, der Code sei definiert, und nur für diesen Fall seine Ausgabe
korrekt macht. Die undefinierten Fälle kann er getrost ignorieren.
Deswegen ist das Ergebnis schneller UND korrekt, wenn der Code definiert
war. Das ist nunmal das Wesen von C.
Praktisches Beispiel: Shift Overflow. Shift um mehr die die Wortbreite
ist undefiniert. Wenn man (32bit) eine 1 um 31 bit nach links shiftet,
und dann separat nochmal um 1 nach links shiftet, das gibt 0. Intuitiv
würde man erwarten, daß auch 0 kriegt, wenn man gleich um 32 shiftet.
Nun hat aber z.B. der Opcode auf x86-32 für den Shiftbetrag nur 5 Bits,
womit sich nur 0-31 darstellen lassen. 32 ergibt damit 0 in den
untersten 5 bit, also ein Shift um 0, womit die Ausgangs-1 gar nicht
geshiftet wird - und 1 rauskommt.
Wollte man das definiert auf allen CPUs haben, müßte man Laufzeit-Checks
verbauen, was langsamer wäre. Deswegen undefined.
Daß die Konsequenz dann haufenweise "dieser buffer overflow wurde Ihnen
präsentiert mit freundlicher Unterstützung der Programmiersprache C"
ist, steht auf einem anderen Blatt. Wer bereit ist, Performance zu
opfern und dabei mehr Zuverlässigkeit zu gewinnen, für den sehe ich C
einfach als eine schlechte Wahl an. Dann sollte man sich doch gleich
eine dafü gedachte Sprache nehmen. ADA wäre eine Möglichkeit, und für
hochkritische Systeme wie z.B. Triebwerksteuerung eines Flugzeugs
sicherlich eine bessere Grundentscheidung als ausgerechnet C.
Zum Finden von Bugs durch undefiniertes Verhalten habe ich ja schon auf
GCCs Sanitiser verwiesen, der zur Laufzeit in Debug-Builds Prüfungen
macht.
Dann kann und sollte man auch statische Codeanalyse verwenden. GCC mit
maximalem Warnungslevel muß ohne Warnungen compilieren, sonst sieht man
bei hunderten Warnungen die kritischen nicht mehr. Wenn ich Code kriege,
der mit Warnungen durchcompiliert, sehe ich das als einen Hinweis auf
schlampige Arbeit.
GCC ist mitunter gnadenlos, was undefinierten Code angeht, aber er läßt
einen durchaus nicht im Regen stehen.
CppCheck sollte man auch regelmäßig über seine Sourcen laufen lassen.
Für kommerziellen Einsatz gibt's auch teure Tools, die noch mehr finden
und die sich durchaus rechnen können.
> Der Unterschied zwischen "implementation-defined" und "undefined" ist,> dass man sich auf ersteres ebenfalls verlassen kann, wenn man denn will.> Das ist nicht zwingend ein Fehler.
Zustimmung. Wenn es explizit ausgewiesen ist, ja. Nur sollte man sich
gut überlegen, ob man das wirklich machen muß, und es vermeiden, wenn
sich gleichwertige Alternativen finden. Es fällt einem spätestens dann
auf die Füße, wenn man den Code migrieren muß.
Das Management sagt dann nämlich "gut, wir nehmen jetzt statt 8051 einen
Cortex-M, dann können wir die bestehende Applikation auch gut erweitern
und haben auf Jahre Ruhe vor Hardware-Redesigns. Ist ja alles in C, also
sollte das bis aufs Hardwarelayer ja ohne Änderungen laufen". Und dann
stehst Du da mit nem völlig anderen Compiler...
Erschwerend kommt dann hinzu, daß das einige Jahre später sein dürfte,
wenn die Entwickler des Ausgangscodes nicht mehr in der Firma sind.
Bzw., wenn das Opensource-Projekt verwaist ist.
Nop schrieb:> Praktisches Beispiel: Shift Overflow. Shift um mehr die die Wortbreite> ist undefiniert.
Neben der für x86 dargestellten Verhaltensweise darf der Compiler sogar
noch weiter gehen und z.B. bei Schiebeoperationen um mehr als die Breite
des Datentyps den Code auch komplett weglassen. Undefiniertes Verhalten
muss nicht einmal reproduzierbar sein, sondern darf z.B. bei
Folgeoperationen auf zufälligen Register- oder Speicherinhalten
basieren.
Einige Compiler erzeugen bei niedrigen Optimierungsstufen noch
"richtigen" Code, in dem auch zu große Schiebeoperationen noch wie vom
Entwickler gewünscht ausgeführt werden. Erst bei höheren
Optimierungsstufen wird dann rigiros zusammengestrichen. Daher finde ich
es reichlich absurd, wenn in manchen Projekten die gesamte Entwicklung
mit "-O0" (oder dem entsprechende Äquivalent) erfolgt und erst nach
erfolgten Tests das eigentliche Release mit -O2 oder -O3 kompiliert
wird.
> Dann kann und sollte man auch statische Codeanalyse verwenden. GCC mit> maximalem Warnungslevel muß ohne Warnungen compilieren, sonst sieht man> bei hunderten Warnungen die kritischen nicht mehr. Wenn ich Code kriege,> der mit Warnungen durchcompiliert, sehe ich das als einen Hinweis auf> schlampige Arbeit.
Ein früherer Kollege produzierte immer wieder äußerst fehlerhaften Code
und war dann tage- und wochenlang am Herumstochern. Gleichzeitig waren
seine Code-Anteile auch diejenigen mit den meisten Warnungen, was auch
alle anderen Entwickler in dem Projekt aus den o.a. Gründen nervte.
Dieser Kollege berief sich aber darauf, dass keine Warnungen mehr
aufträten, wenn man den Code zweimal kompiliere. Das war auch kein
Wunder, denn die Make-Regeln erkannten, dass der Quellcode nicht erneut
kompiliert werden musste...
Andreas S. schrieb:> Das war auch kein> Wunder, denn die Make-Regeln erkannten, dass der Quellcode nicht erneut> kompiliert werden musste...LOL Das ist der Oberhammer!
Nop schrieb:> Natürlich alles über defines gekapselt. Bei anderen Compilern kann man> diese Dinge dann erstmal zu "nicht da" definen, so daß der Code halt> etwas langsamer läuft, bis man die entsprechende Formulierung für den> anderen Compiler dort einbaut.
Du nimmst an, dass ich Code schreiben wollte, der compiler- und
möglicherweise architekturunabhängig ist. Wenn du oben schaust, dann
ging es mir aber gerade um ein compiler- und architekturspezifisches
Headerfile (und eventuell die dazu gehörigen Hardwaretreiber).
An der Stelle ist mir Portabilität nicht wirklich wichtig. Den Code muss
ich weder für hypothetische Compiler noch für hypothetische
Architekturen optimieren. Compiler-Erweiterungen usw. sind dann fair
game.
Für den portablen Teil (Anwendungslogik) gelten andere Regeln!
>> Im ersten Fall solltest du über einen Compiler mit dazu passender>> Standardbibliothek, die gezielt undefiniertes Verhalten finden und>> möglichst idiotisch ausnutzen, um möglichst viele Bugs zu triggern, doch>> froh sein?>> So einfach ist es nicht. Es geht vielmehr so herum, daß der Compiler> ANNIMMT, der Code sei definiert, und nur für diesen Fall seine Ausgabe> korrekt macht.
Genau das ist die Grundlage für viele große, braune Haufen. Weil eben
den Programmierern bei weitem nicht klar ist, was genau alles
undefiniert/implementation-defined ist - und weil es umgebungsspezifisch
ist.
Du argumentierst als Compiler-Hersteller. Selbstverständlich darf man
sich hinstellen und "das ist laut Standard erlaubt" schreiben, aber
effektiv sollte der Compiler dem Programmierer dienen, nicht umgekehrt.
> Wer bereit ist, Performance zu opfern und dabei mehr Zuverlässigkeit zu> gewinnen, für den sehe ich C einfach als eine schlechte Wahl an.
Performance ist nicht binär, das sind alles Kompromisse. Und jeder ist
grundsätzlich bereit, Performance für Zuverlässigkeit und Bequemlichkeit
zu opfern, denn sonst würde man gleich Assembler schreiben.
> Dann sollte man sich doch gleich eine dafü gedachte Sprache nehmen.> ADA wäre eine Möglichkeit, und für hochkritische Systeme wie> z.B. Triebwerksteuerung eines Flugzeugs sicherlich eine bessere> Grundentscheidung als ausgerechnet C.
Das hilft mir nicht, wenn weder ich noch mein Compiler Ada können.
Außerdem gibt es einen großen Bereich zwischen "Triebwerkssteuerung" und
"Web-Browser", wo C durchaus eine gute Wahl ist, dir aber solches
Verhalten den Tag versaut.
> Wenn ich Code kriege, der mit Warnungen durchcompiliert, sehe ich das> als einen Hinweis auf schlampige Arbeit.
Alter Produktivcode wirft eigentlich immer Warnungen auf modernen
Compilern, selbst wenn er zur Entwicklungszeit keine hatte. Das fällt
immer dann negativ auf, wenn "-Wall -Wextra -Werror" überall im
Sourcebaum verteilt ist und das Build dann wegen unbenutzten Variablen
bricht.
Andreas S. schrieb:> Undefiniertes Verhalten muss nicht einmal reproduzierbar sein, sondern> darf z.B. bei Folgeoperationen auf zufälligen Register- oder> Speicherinhalten basieren.
Laut Standard darf der Compiler das gesamte Programm zu einem NOP
kompilieren, weil "Shift größer Wortbreite" dazu führt, dass dein
Programm schon vor seiner Ausführung ungültig war. Damit sind alle
Seiteneffekte, die das Programm möglicherweise hatte, auch nichtig.
> Ein früherer Kollege produzierte immer wieder äußerst fehlerhaften Code> und war dann tage- und wochenlang am Herumstochern. Gleichzeitig waren> seine Code-Anteile auch diejenigen mit den meisten Warnungen, was auch> alle anderen Entwickler in dem Projekt aus den o.a. Gründen nervte.
Das ist dann ein Problem fürs Management. ;-)
S. R. schrieb:> An der Stelle ist mir Portabilität nicht wirklich wichtig. Den Code muss> ich weder für hypothetische Compiler noch für hypothetische> Architekturen optimieren. Compiler-Erweiterungen usw. sind dann fair> game.
Dann solltest Du jedenfalls in der GCC-Doku nachschlagen, ob sie die
Implementation von Bitfields denn klar definieren, so daß Du Dich
zumindest mit diesem Compiler und dieser Plattform darauf auch verlassen
kannst.
Da z.B. sowas wie Padding bei Bitfields nicht definiert ist, sind je
nach Optimierungsstufe interessante Effekte durchaus denkbar. Es sei
denn, es ist dokumentiert, was GCC da tut.
> Für den portablen Teil (Anwendungslogik) gelten andere Regeln!
Ah, ok.
> Genau das ist die Grundlage für viele große, braune Haufen.
Ja. Das ist bei C so. Performance ist der Schwerpunkt, das ist der Sinn
dieser Sprache. C ist ein portabler Makro-Assembler.
> Weil eben> den Programmierern bei weitem nicht klar ist, was genau alles> undefiniert/implementation-defined ist - und weil es umgebungsspezifisch> ist.
Undefiniertes Verhalten ist nicht umgebungsspezifisch, sondern steht im
C-Standard. Shift Overflow ist nicht nur auf x86 undefined, sondern
überall.
> Du argumentierst als Compiler-Hersteller.
Nein, auch als C-Nutzer. Ich will maximale Performance, ohne mich
deswegen flächendeckend auf Assembler einlassen zu müssen. Ich will
Portabilität zwischen Plattformen, zumindest in der Applikation. Für
Debug-Builds hat der GCC den Sanitiser - lahm, und das wäre ein
"sicheres" C dann nämlich genauso, aber für Testläufe ist das ja kein
Problem.
Wenn C-Programmierer sich erstens nicht mit C auskennen, zweitens die
Compiler-Warnungen ignorieren und drittens den eigens angebotenen
Sanitiser nicht nutzen, dann ist das IMO ein Layer-8-Problem.
> effektiv sollte der Compiler dem Programmierer dienen, nicht umgekehrt.
In C tut der Compiler, was man ihm sagt. Und nicht das, von dem er raten
mag, daß das wohl gemeint sein könnte.
> Das hilft mir nicht, wenn weder ich noch mein Compiler Ada können.
Ersteres kann man lernen, wenn man die Sicherheit benötigt. Zweiteres
kann GCC sehr wohl.
> Außerdem gibt es einen großen Bereich zwischen "Triebwerkssteuerung" und> "Web-Browser", wo C durchaus eine gute Wahl ist, dir aber solches> Verhalten den Tag versaut.
Dann nutz die Warnungen, nutz statische Codechecker, nutz den Sanitiser
für Debugbuilds. Letzterer geht übrigens nur unter Linux, leider nicht
unter Cygwin, aber mal eben ne Live-Distro booten ist auch als
Windowsnutzer kein Akt.
> Alter Produktivcode wirft eigentlich immer Warnungen auf modernen> Compilern, selbst wenn er zur Entwicklungszeit keine hatte.
Gutes Argument, weil die Compiler ja auch Fortschritte gemacht haben und
mehr komische Dinge erkennen. Das heißt aber auch nur, daß man den Code
dann mal geradeziehen muß. Wofür es vom Management nie Geld gibt, dafür
aber gerne die dreifache Summe, wenn irgendwas dann im Feld schiefgeht.
;-)
Nop schrieb:>> Genau das ist die Grundlage für viele große, braune Haufen.>> Ja. Das ist bei C so. Performance ist der Schwerpunkt, das ist der Sinn> dieser Sprache. C ist ein portabler Makro-Assembler.
Mit dem Unterschied, dass beim Makro-Assembler sämtliches Verhalten
definiert ist.
>> Weil eben den Programmierern bei weitem nicht klar ist, was genau>> alles undefiniert/implementation-defined ist - und weil es>> umgebungsspezifisch ist.>> Undefiniertes Verhalten ist nicht umgebungsspezifisch, sondern steht im> C-Standard. Shift Overflow ist nicht nur auf x86 undefined, sondern> überall.
Mag ja sein, dass der Tatbestand universell existiert. Ob er aber
auftritt, hängt von der Umgebung ab. Ob (i << 16) definiert ist oder
nicht, hängt von der Wortbreite und damit von der Umgebung ab.
Außerdem bleibe ich dabei, dass den meisten Programmierern nicht klar
ist, was genau undefiniert ist. Ich bin mir auch sicher, dass du nicht
alle Fälle kennst. Und das führt wieder zu braunen Haufen.
> In C tut der Compiler, was man ihm sagt. Und nicht das, von dem er raten> mag, daß das wohl gemeint sein könnte.
Eben nicht. Der Compiler tut das, was ich ihm sage, /wenn er der Meinung
ist, das sei so erlaubt/. Das ist etwas völlig anderes, was man zwar mit
dem Standard rechtfertigen kann, aber vielen ziemlich grundlos ins Bein
schießt.
S. R. schrieb:> Mit dem Unterschied, dass beim Makro-Assembler sämtliches Verhalten> definiert ist.
Nein. Ein Shift um mehr als die Wortbreite ist auch dort nicht
definiert. Weil, wie erwähnt, x86-32 nur 5 bit dafür HAT.
> Mag ja sein, dass der Tatbestand universell existiert. Ob er aber> auftritt, hängt von der Umgebung ab. Ob (i << 16) definiert ist oder> nicht, hängt von der Wortbreite und damit von der Umgebung ab.
Wenn man die standard-Datentypen wie uint32_t nutzt, hat man das Problem
nicht. Die wurden allerdings erst mit C99 eingeführt, und das ist erst
17 Jahre her, von daher hat sich das natürlich noch nicht überall
herumgesprochen.
> Ich bin mir auch sicher, dass du nicht> alle Fälle kennst. Und das führt wieder zu braunen Haufen.
Nunja, ich weiß zumindest, was die häufigen Fehler dabei sind. Das weiß
ich, weil ich auch entsprechende Blogs lese. Was daher kommt, daß ich
mich dafür eben interessiere und mein Werkzeug auch gut handhaben
möchte. Deswegen kenne und schätze ich auch das Sanitiser-Feature des
GCC gegen undefiniertes Verhalten.
Also wirklich, da servieren einem die GCC-Leute schon die Möglichkeit,
undefiniertes Verhalten sogar zur Laufzeit zu prüfen, auf dem
Silbertablett - und es ist immer noch nicht recht? Soll man C-Compiler
lahmen Code erzeugen lassen, weil Leute zu faul für einen Debug-Build
mit allen checks sind? Gefällt mir nicht.
Übrigens, für umfangreiche Anwendungen halte ich C schon für fragwürdig.
Dafür ist C nunmal nicht gedacht, sondern es ist eine
Systemprogrammiersprache. Ich hab GUI-Programmierung in purem C gemacht,
das war der Horror. Dafür gibt's geeignetere Werkzeuge.
> Das ist etwas völlig anderes, was man zwar mit> dem Standard rechtfertigen kann, aber vielen ziemlich grundlos ins Bein> schießt.
Erstens: nein, nicht grundlos. Es ist gerade der Spielraum des
undefined, der so aggressive Optimierungen überhaupt erst ermöglicht,
wie heutige C-Compiler sie beherrschen. Das sind zwei Seiten derselben
Medaille.
Mal ein Beispiel, WIE aggressiv GCC das kann: Neulich hatte ich das
Problem, in einer Schleife in einem Hotspot eine größere
switch-case-Struktur zu haben. Die wollte ich optimieren, weswegen ich
versuchshalber mal mit computed gotos (Sprungtabellen) rangegangen bin,
um die Branches zu eliminieren. Das ging, weil der Wertebereich der
cases nicht sehr groß war. Ich war sehr überrascht, daß GCC bei O2
minimal, aber meßbar schneller war ohne die computed gotos. Weil der
etwas Ähnliches nämlich auch von selber aus dem switch macht - nur
besser, wenn ich ihm nicht halb reinpfusche. Bemerkenswert.
Zweitens sollen die sich halt eine andere Sprache suchen, mit der sie
leichter zurechtkommen. Möglichst auch noch eine, wo sie sich nicht mit
dem Werkzeug beschäftigen müssen. Das wird dann allerdings schwierig.
Wenn man sein Werkzeug nicht kennt, kann man damit halt nicht arbeiten.
Nop schrieb:> S. R. schrieb:>> Mit dem Unterschied, dass beim Makro-Assembler sämtliches Verhalten>> definiert ist.>> Nein. Ein Shift um mehr als die Wortbreite ist auch dort nicht> definiert. Weil, wie erwähnt, x86-32 nur 5 bit dafür HAT.
Wenn es nur 5 Bit dafür gibt, kannst du keine 6 Bit Shiftweiter
ausdrücken (bzw. der Assembler sollte das eher als ungültigen Operanden
verwerfen).
>> Mag ja sein, dass der Tatbestand universell existiert. Ob er aber>> auftritt, hängt von der Umgebung ab. Ob (i << 16) definiert ist oder>> nicht, hängt von der Wortbreite und damit von der Umgebung ab.>> Wenn man die standard-Datentypen wie uint32_t nutzt, hat man das Problem> nicht.
Dennoch finden alle Berechnungen erstmal als "int" statt, nicht als
uint32_t oder was auch immer.
> Nunja, ich weiß zumindest, was die häufigen Fehler dabei sind. Das weiß> ich, weil ich auch entsprechende Blogs lese. Was daher kommt, daß ich> mich dafür eben interessiere und mein Werkzeug auch gut handhaben> möchte. Deswegen kenne und schätze ich auch das Sanitiser-Feature des> GCC gegen undefiniertes Verhalten.
Das ist löblich. Als Grundvoraussetzung für C ist das aber Unfug. Die
Mehrheit der Programmierer will C nicht ein Leben lang studieren,
sondern benutzen - und dafür wurde es mal entworfen.
>> Das ist etwas völlig anderes, was man zwar mit>> dem Standard rechtfertigen kann, aber vielen ziemlich grundlos ins Bein>> schießt.>> Erstens: nein, nicht grundlos. Es ist gerade der Spielraum des> undefined, der so aggressive Optimierungen überhaupt erst ermöglicht,> wie heutige C-Compiler sie beherrschen. Das sind zwei Seiten derselben> Medaille.
Nein. Ich müsste das Paper mal raussuchen, aber aggressiv mit
undefiniertem Verhalten optimieren bringt selten mehr als 5%
Leistungsvorteil. Ein nicht aufgefangener Cache Miss ist teurer.
Dafür riskierst du halt Sicherheitslücken und Bugs. Einen
Integer-Overflow-Check baust du sicherlich nicht ein, damit der
Optimizer den als "integer overflow ist undefined" rauswirft, sondern um
Randfälle gezielt abzufangen.
> Zweitens sollen die sich halt eine andere Sprache suchen, mit der sie> leichter zurechtkommen. Möglichst auch noch eine, wo sie sich nicht mit> dem Werkzeug beschäftigen müssen. Das wird dann allerdings schwierig.> Wenn man sein Werkzeug nicht kennt, kann man damit halt nicht arbeiten.
Mach das mal auf Systemen, wo du nur einen C-Compiler hast. Selbst, wenn
du einen GCC hast, heißt das noch lange nicht, dass du damit auch C++
oder Ada spielen kannst.
Eine Systemsprache, ähnlich wie C, mit weniger undefiniertem Verhalten
und ein paar historischen Unfällen weniger wäre schon schön. Es gibt ja
fehlervermeidende APIs, die taugen. Aber das will ja niemand, sonst gäbs
das ja...
S. R. schrieb:> Nop schrieb:>> S. R. schrieb:>>> Mit dem Unterschied, dass beim Makro-Assembler sämtliches Verhalten>>> definiert ist.>>>> Nein. Ein Shift um mehr als die Wortbreite ist auch dort nicht>> definiert. Weil, wie erwähnt, x86-32 nur 5 bit dafür HAT.>> Wenn es nur 5 Bit dafür gibt, kannst du keine 6 Bit Shiftweiter> ausdrücken (bzw. der Assembler sollte das eher als ungültigen Operanden> verwerfen).
Nein, ein Assembler sollte dann mit einem Fehler abbrechen.
>>> Mag ja sein, dass der Tatbestand universell existiert. Ob er aber>>> auftritt, hängt von der Umgebung ab. Ob (i << 16) definiert ist oder>>> nicht, hängt von der Wortbreite und damit von der Umgebung ab.>>>> Wenn man die standard-Datentypen wie uint32_t nutzt, hat man das Problem>> nicht.>> Dennoch finden alle Berechnungen erstmal als "int" statt, nicht als> uint32_t oder was auch immer.
Nein, nicht wenn z.B. int kleiner als uint32_t ist.
>> Nunja, ich weiß zumindest, was die häufigen Fehler dabei sind. Das weiß>> ich, weil ich auch entsprechende Blogs lese. Was daher kommt, daß ich>> mich dafür eben interessiere und mein Werkzeug auch gut handhaben>> möchte. Deswegen kenne und schätze ich auch das Sanitiser-Feature des>> GCC gegen undefiniertes Verhalten.>> Das ist löblich. Als Grundvoraussetzung für C ist das aber Unfug. Die> Mehrheit der Programmierer will C nicht ein Leben lang studieren,> sondern benutzen - und dafür wurde es mal entworfen.
C hat nur sehr wenige Konstrukte und Ausnahmen, die man sich merken
muss, besonders im vergleich zu anderen Programmiersprachen. Wenn man
sich einmal ernsthaft mit der Sprache auseinandersetzt, kann man alles
in weniger als einem Jahr lernen. Sorry, aber ein Informatiker der seine
Werkzeuge nicht kennt und es auch nicht versucht ist ein schlechter
Informatiker.
>>> Das ist etwas völlig anderes, was man zwar mit>>> dem Standard rechtfertigen kann, aber vielen ziemlich grundlos ins Bein>>> schießt.>>>> Erstens: nein, nicht grundlos. Es ist gerade der Spielraum des>> undefined, der so aggressive Optimierungen überhaupt erst ermöglicht,>> wie heutige C-Compiler sie beherrschen. Das sind zwei Seiten derselben>> Medaille.>> Nein. Ich müsste das Paper mal raussuchen, aber aggressiv mit> undefiniertem Verhalten optimieren bringt selten mehr als 5%> Leistungsvorteil. Ein nicht aufgefangener Cache Miss ist teurer.
Es kommt immer auf das wo an. z.B. bei der Manipulation von grossen
Datenmengen kann das einen grossen unterschied machen, ob der Kompiler
nun weiss, das der inhalt von buffer a nicht geändert wird, wenn in
buffer b geschrieben wird. Und was wilst du mit dem "Cache Miss" sagen?
Das kann keine Programmiersprache verhindern, aber viele Kompiler
beherschen es besser als die Assembler typen.
> Dafür riskierst du halt Sicherheitslücken und Bugs.
Das würde man, wenn man undefiniertes verhalten ausnutzt, und auf
sämtliche Kompilerunterstützung, die einen darauf hinweisen Ignoriert.
Was in undefinierten fällen sinvoll wäre, ist ausserdem ansichtssache.
Ich könnte dir sagen: "Teile den Kuchen für 0.0 Personen auf", und ich
würde unendlich viele stücke erwarten. Für andere gibt es immernoch nur
ein Stück, und beide sind falsch.
> Einen> Integer-Overflow-Check baust du sicherlich nicht ein, damit der> Optimizer den als "integer overflow ist undefined" rauswirft, sondern um> Randfälle gezielt abzufangen.
1) Wiso verwendest du keine unsigned integer!?! Signed integer sind
wirklich nur selten nützlich.
2) Wiso implementierst du die Prüfung falsch, du willst doch vorher
wissen, ob es einen geben würde?
3) Spätestens bei den Unittests fällt das auf, besonders wenn man mit
-ftrapv kompiliert.
PS: Ich mag es nicht, wenn C als Macroassembler bezeichnet wird. Für
mich ist es eine hochsprache. Echtes C ist Platformunabhängig, dafür
wurde es erfunden, deshalb wurde Unix in c neu implementiert.
S. R. schrieb:> Wenn es nur 5 Bit dafür gibt, kannst du keine 6 Bit Shiftweiter> ausdrücken (bzw. der Assembler sollte das eher als ungültigen Operanden> verwerfen).
Im Trivialfall, daß Du um einen konstanten Betrag shiftest, warnt Dich
der C-Compiler auch schon. Warnung ignoriert? Layer-8-Problem. Nein,
spannend wird es dich erst, wenn das Shiften um einen variablen Betrag
erfolgt, der erst zur Laufzeit ermittelt werden kann.
> Dennoch finden alle Berechnungen erstmal als "int" statt, nicht als> uint32_t oder was auch immer.
Auch auf einem 16bit-System ist ein 16-bit-Shift auf einen uint32_t
wohldefiniert. Das ist der Witz an den Standard-Datentypen. Nach 17
Jahren C99 immer noch mit raw int arbeiten, wenn die Wortbreite
entscheidend ist? Layer-8-Problem.
> Das ist löblich. Als Grundvoraussetzung für C ist das aber Unfug.
Kommt drauf an. Für Hobbyprogrammierer, die mal eben ein wenig mit einem
Microcontroller spielen wollen, gebe ich Dir recht. Aber an dieser
Klientel richtet man keine Tools wie C-Compiler aus. Bei Leuten, die
ernsthaft in dem Bereich tätig sind, erwarte ich, daß sie jedenfalls die
häufigsten undefined-Stellen kennen. Viele sind es nicht.
Genauso wie man sich auch im Klaren sein sollte, daß die
Operator-Prioritäten in C gelegentlich verwirrend sein können. Selbst
wenn man sie nicht alle kennt, dann klammert man halt. Mache ich bei
meinen Code sowieso, weil den auch andere Leute lesen können sollen.
> Die> Mehrheit der Programmierer will C nicht ein Leben lang studieren,> sondern benutzen - und dafür wurde es mal entworfen.
Für "einfach mal eben anwenden" gibt es diverse Scriptsprachen, die
einem auch gleich das Ressourcenmanagement abnehmen. Genaugenommen war
es nicht C, was so gedacht war, sondern Basic.
> Ein nicht aufgefangener Cache Miss ist teurer.
Aha. Undefined behahviour im Hinblick auf Pointer ALiasing adressiert
aber gerade die Cache Misses. Genau wie ich sagte, das dient der
Performance.
> Dafür riskierst du halt Sicherheitslücken und Bugs.
Dagegen gibt's Tools für die Debugphase. Wieso willst Du die eigentlich
nicht benutzen? Wirklich, der Sanitiser beißt nicht, und CppCheck hat
mir auch nur mal aus Reflex ein wenig in den Finger gebissen, weil es
sich angesichts unerwartet frühmorgendlicher Nutzung erschreckt hatte.
> Einen> Integer-Overflow-Check baust du sicherlich nicht ein, damit der> Optimizer den als "integer overflow ist undefined" rauswirft, sondern um> Randfälle gezielt abzufangen.
Erstens ist unsigned overflow sehr wohl definiert. Zweitens heißen die
beiden Zauberworte hier wiederum "debug-build" und "assert".
> Mach das mal auf Systemen, wo du nur einen C-Compiler hast.
Die sind auch nichts für Anfänger.
> Selbst, wenn> du einen GCC hast, heißt das noch lange nicht, dass du damit auch C++> oder Ada spielen kannst.
Warum nicht? Gibt Leute, die C++ auf Mikrocontrollern verwenden. Ich bin
kein Fan von C++, aber wenn man sich auf C plus Klassen beschränkt (im
Wesentlichen), dann soll das nicht mehr Speicher kosten als C. Dazu muß
man, Überraschung, allerdings C++ beherrschen und insbesondere wissen,
welche Features man lieber nicht nutzen sollte.
> Eine Systemsprache, ähnlich wie C, mit weniger undefiniertem Verhalten> und ein paar historischen Unfällen weniger wäre schon schön.
Mit einer Systemprogrammiersprache wird man IMMER einigen Unsinn machen
können. Allein schon wegen der Pointer und deren Casting.
> Es gibt ja fehlervermeidende APIs, die taugen.
Was haben APIs denn jetzt damit zu tun?
Daniel A. schrieb:> PS: Ich mag es nicht, wenn C als Macroassembler bezeichnet wird. Für> mich ist es eine hochsprache. Echtes C ist Platformunabhängig, dafür> wurde es erfunden, deshalb wurde Unix in c neu implementiert.
Deswegen bezeichne ich es ja auch als "portablen Macroassembler". :-)
Daniel A. schrieb:>> Die Mehrheit der Programmierer will C nicht ein Leben lang studieren,>> sondern benutzen - und dafür wurde es mal entworfen.>> C hat nur sehr wenige Konstrukte und Ausnahmen, die man sich merken> muss, besonders im vergleich zu anderen Programmiersprachen.
Es gibt ein Paper von der TU Wien: "What every compiler compiler writer
should know about programmers"
(http://www.complang.tuwien.ac.at/kps2015/proceedings/KPS_2015_submission_29.pdf).
Lies das mal.
Ich zitiere:
>>> There are 203 undefined behaviours listed in appendix J of the>>> C11 standard (up from 191 in C99).
Das ist nicht gerade "sehr wenig".
> Wenn man sich einmal ernsthaft mit der Sprache auseinandersetzt, kann> man alles in weniger als einem Jahr lernen.
Wenn es so einfach wäre, warum gibt es dann ständig Bugs und
Sicherheitsprobleme deswegen?
> Sorry, aber ein Informatiker der seine Werkzeuge nicht kennt und es> auch nicht versucht ist ein schlechter Informatiker.
Ach, da bin ich aber froh. Ich bin nämlich überhaupt kein Informatiker.
Davon abgesehen: Es gibt einen Unterschied zwischen "ich, der sich damit
ein kleines bisschen auskennt, finde das große Scheiße und eine
nachweislich nahezu nutzlose Grundlage für viele real existierende
Probleme" und "ich vermeide das in meinem Code, wenn möglich".
> Es kommt immer auf das wo an. z.B. bei der Manipulation von grossen> Datenmengen kann das einen grossen unterschied machen, ob der Kompiler> nun weiss, das der inhalt von buffer a nicht geändert wird, wenn in> buffer b geschrieben wird.
Strohmann. Wenn ich "const" ranschreibe, weiß der Compiler, dass da nix
geändert wird. Da braucht der nicht über undefiniertes Verhalten
inferieren.
> Und was wilst du mit dem "Cache Miss" sagen?
Dass ein Cache Miss mehr Performance kostet als eine nicht gemachte
Optimierung, die undefiniertes Verhalten gezielt ausnutzt.
>> Einen Integer-Overflow-Check baust du sicherlich nicht ein, damit>> der Optimizer den als "integer overflow ist undefined" rauswirft,>> sondern um Randfälle gezielt abzufangen.>> 1) Wiso verwendest du keine unsigned integer!?! Signed integer sind> wirklich nur selten nützlich.> 2) Wiso implementierst du die Prüfung falsch, du willst doch vorher> wissen, ob es einen geben würde?> 3) Spätestens bei den Unittests fällt das auf, besonders wenn man mit> -ftrapv kompiliert.
Ist das mein Code? Nein.
Ist das produktiver Code? Ja.
Wurde der Overflow-Check falsch implementiert? Weißt du nicht.
Kannst du in C einen "signed integer overflow check" effizient
implementieren? Nein, denn "signed integer" kann per Standard nie
auftreten. (Du könntest auf unsigned casten, aber da der Standard auch
nicht sagt, wie signed integer auf Bitebene aussehen, ist das auch
wieder undefiniert.)
Nop schrieb:>> Das ist löblich. Als Grundvoraussetzung für C ist das aber Unfug.>> Bei Leuten, die ernsthaft in dem Bereich tätig sind, erwarte ich,> daß sie jedenfalls die häufigsten undefined-Stellen kennen. Viele> sind es nicht.
Es gibt erstaunlich wenig Ingeneure, die sinnvoll programmieren
können...
>> Ein nicht aufgefangener Cache Miss ist teurer.>> Aha. Undefined behahviour im Hinblick auf Pointer ALiasing adressiert> aber gerade die Cache Misses. Genau wie ich sagte, das dient der> Performance.
Lies du bitte auch mal das oben angegebene Paper.
>> Dafür riskierst du halt Sicherheitslücken und Bugs.> Dagegen gibt's Tools für die Debugphase.
Ich weiß, du wiederholst das ja ständig, als ob das die universelle
Lösung für alles wäre.
> Wieso willst Du die eigentlich nicht benutzen?
Habe ich gesagt, dass ich das nicht tue?
Es gibt einen Unterschied zwischen "ich find sowas scheiße" und "ich
versuche, so weit wie möglich damit klarzukommen".
> Erstens ist unsigned overflow sehr wohl definiert. Zweitens heißen die> beiden Zauberworte hier wiederum "debug-build" und "assert".
Also dürfen Fehler durch ungültige Eingaben nur in der Debugphase
erkannt werden. Gut zu wissen. :-)
>> Mach das mal auf Systemen, wo du nur einen C-Compiler hast.>> Die sind auch nichts für Anfänger.
Und jeder, der kein Anfänger ist, muss alle Seltsamkeiten von C
beherrschen? Ziemlich arrogant, wenn du mich fragst.
>> Selbst, wenn du einen GCC hast, heißt das noch lange nicht, dass du>> damit auch C++ oder Ada spielen kannst.>> Warum nicht? Gibt Leute, die C++ auf Mikrocontrollern verwenden.
Das setzt voraus, dass die Runtime (Startup-Code, Linkerscript etc.) das
kann. Es gibt erstaunlich viel Vendor-Code, der das nicht ordentlich
implementiert und dann funktioniert es, bis es laut knallt.
> Mit einer Systemprogrammiersprache wird man IMMER einigen Unsinn machen> können. Allein schon wegen der Pointer und deren Casting.
Es gibt auch einen Unterschied zwischen "Unsinn machen können" und
"versehentlich Unsinn machen, obwohl es nicht nach Unsinn aussieht".
Das ist ja das Hauptproblem mit z.B. Perl, dass viele Konstrukte nicht
exakt das tun, was man erwarten würde, wenn man die Sprache nicht
vollständig kann. Sowas kann man im Design vermeiden.
>> Es gibt ja fehlervermeidende APIs, die taugen.>> Was haben APIs denn jetzt damit zu tun?
Weil solche Standard-APIs wie gets() oder scanf() einfach stinken.
Weil globale Zustände wie "errno" einfach stinken.
Und jetzt komm mir nicht damit, dass gets() seit C11 nicht mehr im
Standard steht, das weiß ich auch. Aber welcher Compiler unterstützt
schon C11 komplett? Selbst C99 wird von MSVC nicht vollständig
unterstützt.
S. R. schrieb:> Weil solche Standard-APIs wie gets() oder scanf() einfach stinken.> Weil globale Zustände wie "errno" einfach stinken.
Wenn ich gezwungen wäre, mit der Einstellung jeden Tag in C zu
programmieren, wäre es bei mir Zeit für einen neuen Job. Auch, wenn du
recht hast.
Oliver
S. R. schrieb:> Es gibt ein Paper von der TU Wien: "What every compiler compiler writer> should know about programmers">
(http://www.complang.tuwien.ac.at/kps2015/proceedings/KPS_2015_submission_29.pdf).
> Lies das mal.
Ich werde erstmal den Artikel kommentieren, den rest sehe ich mir später
an.
Wow, der verlinkte Typ kann Wörter verdrehen, mal sehen:
> C* A language (or family of languages) where language constructs> correspond directly to things that the hardware does.> E.g., * corresponds to what a hardware multiply instruction does.> In terms of the C standard, conforming programs are written in C*.
Ok, er führt eine neue Unterscheidung ein, aber er behautet auch, dass
Hardwarekonstrukte direkt zu den Eigenschaften der Hardwareinstruktionen
korrespondieren. Das steht so aber nirgends im Standard(1), im
Gegenteil, es steht:
> The semantic descriptions in this Standard describe the behavior of an> abstract machine in which issues of optimization are irrelevant.
...
> In the abstract machine, all expressions are evaluated as specified by> the semantics. An actual implementation need not evaluate part of an> expression if it can deduce that its value is not used and that no> needed side effects are produced (including any caused by calling a> function or accessing a volatile object).
In anderen Worten, die Operationen haben nichts mit der Endhardware zu
tun, sondern deren Verhalten wird im Rahmen einer Hypothetischen
Maschine beschrieben, und welche Operationen tatsächlich erfolgen, um
das Verhalten zu erreichen.
Der Denkfehler des Author ist, das er im gesamten Dokument nicht
zwischen Implementation defined und undefined behaviour, btw.
argumentiert dass eine Implementation etwas, dass Implementation defined
ist, auch als undefined spezifizieren kann. Mir sind beim GCC sind keine
Implementation defined fälle bekannt, die von diesem nicht definiert
werden, aber zu definieren sinnvoll wären, wobei ich meinen Code dennoch
nicht auf derartiges stütze. Als Beispiel Signed integer overflow ist
undefined, aus dem Standard:
> As with any other arithmetic overflow, if the result does not fit in the> space provided, the behavior is undefined.
und
> The integral promotions are performed on each of the operands. The type> of the result is that of the promoted left operand. If the value of the> right operand is negative or is greater than or equal to the width in> bits of the promoted left operand, the behavior is undefined.
Also definiert GCC das nicht. Aber viele Implementation defined
Binäroperationen werden von GCC definiert, wie z.B. (2):
> Bitwise operators act on the representation of the value including both > the
sign and value bits, where the sign bit is considered immediately
> above the highest-value value bit. Signed ‘>>’ acts on negative numbers> by sign extension.
Der Autor des Artikels stellt sich geschickt an, anderen glauben zu
machen, das dass nicht genau im sinne der Schreiber des Standard gewesen
währe, wenn undefined behaviour nicht definiert wird, und schreibt:
> Original idea was that C compilers implement C*, and that undefined> behaviour gives them wiggle room to choose an efficient hardware> instruction; e.g., for << use the hardware’s shift instruction, and> differences between different architectures for some parameters result> in not defining the behaviour in these cases.
Er stützt sich hierbei auf seine anfangs eingeführte Definition von dem
was er "C*" nennt. Er betrachtet soetwas vermutlich als "conforming
programs". Des halb schreibt er auch:
> The compiler maintainers try to deflect from their responsibility for> the situation by pointing at the C standard. But the C standard actually> takes a very different position from what the compiler maintainers want> to make us believe. In particular, the C99 rationale 22 [C03] states:>> C code can be non-portable. Although it strove to give program->> mers the opportunity to write truly portable programs, the C>> 89 Committee did not want to force programmers into writing portably,>> to preclude the use of C as a ”high-level assembler”: the ability to>> write machine-specific code is one of the strengths of C. It is this>> principle which largely motivates drawing the distinction between>> strictly conforming program>> and conforming program (§4).
Ich habe ja nur einen draft von c99 (3), aber dort konnte ich dieses
Statement nicht finden. Es steht zwar folgendes:
> A program that is correct in all other aspects, operating on correct> data, containing unspecified behavior shall be a correct program and> act in accordance with 5.1.2.3.
Aber es steht nicht, ob ein Programm mit undefined behavior oder
implementation defined behavior auch ein conforming program sei, und
unspecified ist im standard sowieso etwas anderes als undefined. Es
steht eigentlich nur, dass ein strictly conforming program auch ein
conforming program ist.
1) http://port70.net/~nsz/c/c89/c89-draft.html#2.1.2.3
2)
https://gcc.gnu.org/onlinedocs/gcc/Integers-implementation.html#Integers-implementation
3) http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf
S. R. schrieb:>> Wenn man sich einmal ernsthaft mit der Sprache auseinandersetzt, kann>> man alles in weniger als einem Jahr lernen.>> Wenn es so einfach wäre, warum gibt es dann ständig Bugs und> Sicherheitsprobleme deswegen?S. R. schrieb:>> Es kommt immer auf das wo an. z.B. bei der Manipulation von grossen>> Datenmengen kann das einen grossen unterschied machen, ob der Kompiler>> nun weiss, das der inhalt von buffer a nicht geändert wird, wenn in>> buffer b geschrieben wird.>> Strohmann. Wenn ich "const" ranschreibe, weiß der Compiler, dass da nix> geändert wird. Da braucht der nicht über undefiniertes Verhalten> inferieren.
Nein, das weiss er, wenn du zusätzlich noch restrict hinschreibst, btw.
dann darf er davon ausgehen.
Beispiel:
1
#include<stdio.h>
2
voidbla(char*a,constchar*b){
3
while(*a&&(*a++=*--b));
4
}
5
intmain(){
6
{// same memory, const changes in bla
7
chara[]="\0Hello World";
8
bla(a+1,a+sizeof(a)-1);
9
puts(a+1);
10
}
11
{// different memory, const doesn't changes in bla
12
chara[]="\0Hello World";
13
constcharb[]="\0Hello World";
14
bla(a+1,b+sizeof(b)-1);
15
puts(a+1);
16
}
17
}
Ausgabe:
1
dlroW World
2
dlroW olleH
Wenn ich im Beispiel oben nur die Aufrufvariante mit 2 unterschiedlichen
buffern zulassen will, muss ich "void bla( char*restrict a, const
char*restrict b )" schreiben. Const hilft mir da garnichts.
-----------------
OK, Ich geb ja zu C ist nicht perfekt, und dein Standpunkt hat durchaus
Hand und Fuss, aber es entspricht einfach nicht meinem Verständnis, wie
man die Sprache am besten Anwenden sollte. Ich werde nicht anfangen
nicht portablen Code zu schreiben, nur weil man das könnte.
Es gibt sicher viele, die schonmal über eine eigene Programmiersprache
nachgedacht haben, mit den verschiedensten Ansprüchen und Ideen. Wenn
man alle Ideen und das Wissen um bisherige Fehler sammeln könnte, mit
den Pros und Contras, etc. womöglich könnte man dann ein Sprachset
definieren, dass verschiedene Sprachen zur Verfügung stellt, welche die
jeweiligen Anforderungen exakt abdecken, und miteinander Harmonieren.
Dann wäre das Sprachproblem endlich gelöst.
S. R. schrieb:> Es gibt ein Paper von der TU Wien: "What every compiler compiler writer> should know about programmers"
Ja, kenne ich schon. Und nein, ich stimme dem nicht zu. Wenn er eine
"sichere Sprache"(tm) will, soll er welche erschaffen. Hat der gute Herr
Wirth ja mehrfach versucht, besonders praxisrelevant sind sie nicht
geworden.
Ich habe noch mit Pascal angefangen, was als Lehrsprache schon gut war.
Aber für praktische Anwendung insbesondere bei hardwarenaher
Programmierung völlig ungeeignet. Deswegen gab's dann ja auch z.B. bei
Turbo Pascal immer mehr Hacks, die die Begrenzungen der Sprache umgehen
sollten, zugleich aber auch immer mehr eine proprietäre Borland-Insel
erschufen.
Übrigens, mir ist das seinerzeit schon beim abstract negativ
aufgefallen: "In recent years C compiler writers have taken the attitude
that tested and working production (i.e., conforming according to the
C standard)"
Auf gut deutsch: "conforming" heißt für ihn, daß es compiliert und
läuft. Er beherrscht aber nichtmal grundlegende Logik, ein echtes
Trauerspiel. WENN der Code konform zum C-Standard und inhaltlich korrekt
ist, DANN läuft es. Der Umkehrschluß, wenn es inhaltlich korrekt ist und
läuft, muß es ja standardkonform sein, ist ein Logikfehler. Non
sequitur.
Wenn Leute, die schon Probleme mit grundlegender Logik haben, dann etwas
über Compiler schreiben, wird es absurd.
Nebenbei, wenn Entwickler so lernen, wie er das macht, mal ein
Einführungsbuch durchblättern und ansonsten rumprobieren, dann schreiben
sie schlechten Code. Die Lösung ist aber kein compiler-dumb-down
äquivalent zu "diese Kaffeetasse kann heiße Flüssigkeit enthalten"!
Bei Anwendern sehe ich es schon so, für die MUSS man seine Programme
freundlich machen, denn Nutzer sollen keine Experten sein müssen. Dafür
bezahlen sie die Entwickler ja. Aber wenn Entwickler für ihre
Entwicklung diese Einstellung haben, sind sie eine Fehlbesetzung.
http://www.rmbconsulting.us/a-c-test-the-0x10-best-questions-for-would-be-embedded-programmers
Sowas ist der IMO richtige Ansatz dafür.
>>>> There are 203 undefined behaviours listed in appendix J of the>>>> C11 standard (up from 191 in C99).>> Das ist nicht gerade "sehr wenig".
Eine Frage der Zählweise, weil so ziemlich dieselben Konstrukte ja
formal in verschiedenen Aspekten definiert werden müssen. Daß mehrfache
Seiteneffekte in einem Ausdruck ein Problem werden können, nunja. Keine
Überraschung.
> Wenn es so einfach wäre, warum gibt es dann ständig Bugs und> Sicherheitsprobleme deswegen?
Nicht wegen undefined behaviour, sondern weil man wie bei jeder
tatsächlichen Systemprogrammiersprache das gesamte Ressourcenmanagement
zu Fuß machen muß. Das bedeutet, man muß auf jeden Fehler (kein
Speicher, File nicht da, sonstwas) manuell reagieren. Man muß außerdem
selber tracken, wann man welche Ressource überhaupt hat (user after
free). Inputs muß man selber validieren, wie z.B. Längenangaben in
Paketen.
Und man muß soviel Speicher belegen, wie man denn wirklich braucht. Da
gibt's ja Spezis, die den sizeof-Operator nicht kennen und dann mit
magic numbers arbeiten. Das zerbricht natürlich bei der kleinsten
Änderung, und ganz besonders, wenn man von 32bit auf 64bit geht, wo die
Pointer auf einmal 8 Byte haben und nicht mehr 4. Das sind dann auch die
Leute, die mit "unsigned int" Pointerarithmetik machen, weil es ja unter
32bit (meistens) funktioniert. Statt einfach uintptr_t zu nutzen, das
genau dafür gedacht ist. Oder die Längenparameter als int übergeben
anstatt als size_t.
Ja, wenn man eine Systemprogrammierprache nutzen will und dann nicht
versteht, daß die Pointerlänge von der Plattform abhängt UND C dafür
aber Werkzeuge bereitstellt, mithin Pointer nicht verstanden hat, dann
ist der Ofen halt aus.
Das ist kein Problem von C, sondern C ist eben keine managed language.
Das hätte dann nämlich auch wieder andere Nachteile wie z.B. nicht
deterministische Ausführung, worauf man ja bei malloc schonmal einen
negativen Vorgeschmack bekommt (deswegen meidet man das ja embedded auch
tunlichst).
Eine Systemprogrammiersprache ist nunmal vor allem dafür da, das System
bereitzustellen, was den Applikationen ihr Ressourcenmanagement
ermöglicht.
Und es soll eben schnell sein. Deswegen hat C bei Array-Zugriffen keine
Boundary-Checks. Deswegen sind Pointer und Arrays sehr eng verwandt
(wenn auch nicht identisch). Deswegen kann man auf Datenbereiche mit
einem anderen Pointertyp zugreifen, beispielsweise um ein schnelles
memzero() zu schreiben. Allerdings muß man das dann auch richtig tun,
sonst's gibt's aliasing. Und/oder alignment faults. Aber wer solche
Routinen schreibt, weiß das auch.
Und deswegen reicht man in C Strukturen anders als in anderen Sprachen
halt nicht auf dem Stack an Funktionen, sondern übergibt nur den Zeiger.
Wer C ernsthaft vorwirft, für komplexe Anwendungen nicht sonderlich
geeignet zu sein, weil man alles selber machen muß, der setzt
schlichtweg die falsche Sprache für sein Problem ein.
> Strohmann. Wenn ich "const" ranschreibe, weiß der Compiler, dass da nix> geändert wird. Da braucht der nicht über undefiniertes Verhalten> inferieren.
Dann hast Du das Problem noch nicht genug betrachtet. Mit "const" kommt
man da jedenfalls nicht weiter. Obwohl man natürlich auch const überall
verwenden sollte, wo es Sinn ergibt, keine Frage. Allein schon der
Lesbarkeit wegen.
Es geht halt auch darum, daß bei Strukturen, die nicht const sind, der
Compiler wissen muß, daß nicht jeder beliebige anderweitige
Pointer-Speicherzugriff diese Struktur ändern kann, wenn das von den
Datentypen her gar nicht paßt.
Das ist ganz besonders für numerische Anwendungen ein Thema, und genau
für diese wird das dann seit C99 auch noch mit restrict weiter
unterstützt.
> Dass ein Cache Miss mehr Performance kostet als eine nicht gemachte> Optimierung, die undefiniertes Verhalten gezielt ausnutzt.
Eben, und Pointer Aliasing ist sogar noch schlimmer, weil der Compiler
einen load/store einbauen muß, statt die Werte im Register zu nutzen
(schneller als Register geht nicht). Das ist besonders lästig auf
Architekturen, die viele Register haben, wie z.B. ARM.
> Kannst du in C einen "signed integer overflow check" effizient> implementieren?
Ja, die Zauberworte sind mal wieder assert und die Debugbuilds. Und
Testen natürlich. Über den gesamten Eingabebereich. Unittests und so.
> Es gibt erstaunlich wenig Ingeneure, die sinnvoll programmieren> können...
Rate mal, wieso es MISRA-C gibt. Kurz gesagt programmiert man damit C
auf sehr pascalhafte Weise.
> Lies du bitte auch mal das oben angegebene Paper.
Ja, kenne ich. Ein typisches Uni-Paper. Mit der Einstellung wurde auch
Pascal gemacht, und es ist geflopt.
> Ich weiß, du wiederholst das ja ständig, als ob das die universelle> Lösung für alles wäre.
Sie helfen ja auch sehr viel und entkräften Deine Argumente
größtenteils.
> Also dürfen Fehler durch ungültige Eingaben nur in der Debugphase> erkannt werden. Gut zu wissen. :-)
Während der Debugphase (genauer eigentlich: Testphase) testest Du, ob Du
ungültige Eingaben korrekt abfängst. Aber Teststrategien sind nochmal
ein anderes Thema.
> Und jeder, der kein Anfänger ist, muss alle Seltsamkeiten von C> beherrschen? Ziemlich arrogant, wenn du mich fragst.
Wer mit einer Systemprogrammiersprache in einem entsprechenden Umfeld
programmieren will und sich auch nicht mit dem sehr leistungsfähigen
Werkzeug befassen mag, sondern eigentlich lieber eine Art Sandbox
möchte, der ist bei C definitiv verkehrt, ja.
Und nein, ich möchte nicht, daß man mir mein scharfes Küchenmesser
stumpf schleift, weil es Leute gibt, die eigentlich am liebsten einen
Löffel hätten, aber aus irgendwelchen Gründen sich lieber über das
Messer aufregen, statt einfach einen Löffel zu nehmen.
> Das setzt voraus, dass die Runtime (Startup-Code, Linkerscript etc.) das> kann.
Kann sie. Wenn nicht, ändert man sie eben. Obwohl das natürlich dann
wieder nichts für Leute ist, die sich nicht mit dem Werkzeug befassen
wollen. ;-)
> Weil solche Standard-APIs wie gets() oder scanf() einfach stinken.
Ah, ja das stimmt. Aber wie Du weißt, ist das auch in C nicht erst seit
gestern durch Besseres behoben worden.
> Und jetzt komm mir nicht damit, dass gets() seit C11 nicht mehr im> Standard steht, das weiß ich auch.
Zuvor konnte man es nicht "mal eben" rauswerfen, weil sonst jede Menge
standardkonformer, bestehender Produktivcode nicht mehr compiliert
hätte. Das wiederum hätte verhindert, daß ein neuerer Standard sich
überhaupt durchsetzen kann. Also gab's fgets() dazu.
> Selbst C99 wird von MSVC nicht vollständig unterstützt.
Daß Microsoft ein Saftladen ist, der lieber ein unübersichtliches
Ribbon-Interface in seine Programme reinschmiert, anstatt mal seinen
C-Compiler auf Vordermann zu bringen, kann man ja nun nicht C anlasten.
Zuguterletzt kann man jedenfalls beim GCC per Compiler-Option auch z.B.
angeben, daß er signed overflow bitteschön brav machen soll (mit
implementationsabhängigem Resultat logischerweise) und daß Pointer auch
aliasen dürfen.
Was ich damit mache: Ich schreibe sauberen Code, teste den mit dem
Sanitiser, korrigiere ggf. Stellen, die ich übersehen habe. Das sind
meiner Erfahrung nach keine direkten C-Fehler, sondern eigentliche
algorithmische Bugs, die in der Folge erst über undefined behaviour
stolpern. Die typischen one-off-bugs beispielsweise. Und dann noch
Reviews. Ich reviewe meinen Code mehrfach und finde dabei üblicherweise
mehr Fehler als durch Testen.
Dann wird der Release gebenchmarked - mit allem undefined behaviour und
mit den genannten Abschaltungen. Wenn ich feststelle, daß die
Abschaltungen keinen Nachteil bringen, mithin Optimierung unter
Ausnutzung von solchem undefined behaviour keinen Beitrag zur
Geschwindigkeit leistet, dann schalte ich es fürs Release tatsächlich
ab.
Aber für Debug und Test fordere ich GCC ganz gezielt heraus, meinen Code
zu zerbrechen, wenn er irgend kann.
Wo man übrigens echt aufpassen muß beim GCC, ist das total kranke
inlining. "inline" wird nur als Hinweis verstanden, GCC macht, was er
will. Ohne inline inlined er auch, wenn er will. Deswegen gehe ich da
über _atribute_ und inline, wo ich will, und verbiete es ihm, wo er
Mist bauen kann. Letzteres vor allem wegen der Stackproblematik, die
embedded immer ein Thema ist.
Daniel A. schrieb:> Nein, das weiss er, wenn du zusätzlich noch restrict hinschreibst, btw.> dann darf er davon ausgehen.
Restrict ist dafür da, um zu sagen, daß Pointer auf dieselben Datentypen
auf nicht-übereinanderlappende Bereiche Zeigen.
Bei Aliasing geht's darum, daß z.B. ein float-pointer keinen
Speicherplatz für einen integer verändern kann. Oh und btw, char-Pointer
dürfen immer aliasen, genau wie void.