PDA

Archiv verlassen und diese Seite im Standarddesign anzeigen : NOP in "C"



Siro
06.12.2012, 07:56
Hallo zusammen,
ich wollte in meinen "C"-Code einen Assembler Befehl NOP einbauen.

MPLAB C-Compiler 8.87
------------------------------------
Das der C-Compiler mit dem Befehl NOP nichts anfangen kann leuchtet mir ein

NOP;

deshalb kam auch die Fehlermeldung:
410.1 undefined identifier "NOP"
------------------------------------
also habe ich es entsprechend declariert

#define NOP asm("NOP");

NOP;

Doch nun bekam ich folgende Meldung:

405.23 redefining preprocessor macro "NOP"

--------------------------------------------
Nach einigem Suchen habe ich dann die "Definierte Declaration" ;) von NOP gefunden.
Da hat man doch tatsächlich den NOP wie folgt declariert:

#pragma intrinsic(_nop)
extern void _nop(void);
#define NOP() _nop()
Warum macht man denn so etwas ? Was sollen da die Klammern, das ist doch keine Funktion, das sieht für mich aus wie ein CALL .....

Wenn ich also in meinem "C" Programm schreibe:
NOP();
dann funktioniert es nun auch. Aber wo liegt da der Sinn ?

mfg. Siro

radbruch
06.12.2012, 08:16
Hallo

In der RP6-Library wird nop() so definiert:

// Assembly and system macros:
#define nop(); {__asm__ volatile("nop\n\t");}
(Aus RP6M256.h)

Was das aber bedeutet weiß ich auch nicht. Vermutlich wird so in den Programmablauf eine Zeile Assembler eingefügt bevor die Hex-Datei erzeugt wird.

Gruß

mic

Siro
06.12.2012, 09:38
Hallo mic, also dort auch mit den Klammern, so muss man also in "C" nun auch
nop();
schreiben, was aussieht wie ein Unterprogramm Aufruf ohne Parameter.

man hätte doch auch:
#define nop; {__asm__ volatile("nop\n\t");}
declarieren können, dann würde man in "C" nur
nop;
schreiben müssen, was meiner Meinung nach wesentlich sinnvoller bzw. übersichtlicher ist, da ja hier direkt, wie Du schon richtig sagtest, eine Assemblerzeile eingefügt wird und nix aufgerufen wird.
Das ist für eine Vortäuschung falscher Tatsachen, weil ich würde im Assemblelisting einen "CALL" suchen und durch die Schreibweise eine zusätzliche Stackbelastung vermuten.
Ist ja kein Weltuntergang, aber ich find das eher verwirrend und der Sinn ist mir bisher verborgen geblieben.

Siro

radbruch
06.12.2012, 10:10
Hallo

cli() und sei() sind auch keine Funktionen.

Das sind die Folgen der großen Freiheiten die man bei C hat.

mic

Klebwax
06.12.2012, 19:07
Wie man sieht, kommt das mit dem nop() öfter vor. Bei den PICs kenne ich das von HI-TECH und vom GCC. Beim RP6 schein das ja so zu sein. Eine wirklich Erklärung habe ich nicht, ich vermute, das man den Namespace der Variablen nicht mit prozessorspezifischen Namen füllen will.


Das ist für eine Vortäuschung falscher Tatsachen, weil ich würde im Assemblelisting einen "CALL" suchen und durch die Schreibweise eine zusätzliche Stackbelastung vermuten.

Auch wenn ab und an das Gegenteil behauptet wird, C ist eine Hochsprache. Eine C-Funktion ist einen Funktion in C und gehorcht den Regeln von C. Ob daraus ein CALL in Assembler wird, ist nicht gesagt und wird auch in der C-Definition nicht gefordert. Diese zu erwarten ist schlichtweg Unsinn. Der Compiler kann jede Funktion inlinen und es gibt keinen CALL. Er kann umgekehrt bei einem Ausdruck wie float x * float y eine interne "Assemblerfunktion" aufrufen. Gleiches gilt natürlich auch für komplexere Konstrukte wie switches.

Die meissten C-Programmierer haben noch nie in ihrem Leben ein Assemblerlisting ihres C Codes gesehen. Ob mit ihrem Code ein ARM, MIPS, SPARC PowerPC oder x86 läuft, ist und soll ihnen auch egal sein. Und bei der heutigen Leistungsfähigkeit der µC kann man die dort auch einreihen. So ein klassischer APPLE ][ hatte einen 1 MHz Prozessor und 32 bis 64 kB Speicher für Code und Daten. Auf diesem wurde unter anderem das erste Spreadsheet entwickelt, mit Sicherheit nicht, mit dem Assemblerlisting in der Hand.

Seitdem ich die PIC16 in C programmiere, hab ich mir die Listings immer seltener angeschaut. Jetzt benutze ich die PIC24, ich hab erst einmal ins Listing geschaut, ich kenne den Assembler garnicht. Den x86 Assembler kenne ich auch nur im 16 Bit Mode, welche Register und welche Befehle er im protected oder 64 Bit mode hat weiß ich nicht. Trotzdem programmiere ich da in jeder Sprache, die mein Problem löst, C eingeschlossen. Und die Vorstellung eines Assemblerlistings eines einige 10 MB großen Programms ...

Wer in C (C++, C#, Java, PASCAL, PERL, PHP ...) programmiert, muß auch in dieser Sprache sein Problem formulieren, analysieren und lösen. Und ob der Rechner daraus Assemblercode macht oder sich Hilfe aus der Cloud holt, spielt keine Rolle, solange er die Sprachdefinition erfüllt.

MfG Klebwax

Siro
07.12.2012, 11:59
Ich denke einfach generell in Assembler und schaue mir selbst bei Kleinigkeiten an, was der Compiler draus macht.
Aber Du hast natürlich recht, der Compiler darf anscheinend Unterprogamme als Inline codieren.
Da war ich ebenso erstaunt, dass der Compiler mal eben meinen Ablaufplan selbst geändert hat. Denn der Code entsprach oftmals nicht mehr meinem Flussdiagramm.
Das Resultat blieb jedoch erhalten.
Für mich ist eine Funktion generell ein Stückchen Programmcode welcher mittels call aufgerufen wird. Weil sonst ist das ja keine Funktion mehr.

Bei cli() und sti() finde ich die "Funktionsklammern" (nennt man die nicht so ?) schon mehr als unnütz.
Aber das ist sicher Geschmackssache. Ich habe jetzt meine eigenen Assemblerbefehle declariert, das erlaubt ja die Freiheit von "C". ;)

Klebwax
08.12.2012, 15:42
Ich denke einfach generell in Assembler und schaue mir selbst bei Kleinigkeiten an, was der Compiler draus macht.
Dann solltest du konsequenterweise auch in Assembler programmieren.

Der Compiler macht genau das, was du ihm sagst. Wie er das macht, mußt du ihm überlassen. Wenn du in den Flieger von Frankfurt nach New York steigst, wird er dich (meist) auch dahin bringen. Welchen genauen Kurs er nimt, wieviel Gas er gibt etc. liegt beim Piloten, nicht bei dir. Und wenn du pünktlich in New York bist, hat er seine Aufgabe zur vollsten Zufriedenheit ausgeführt.

Wenn du in C scheibst:

a = b + c;

ist die Aufgabe, in die Variable a die Summe von b und c zu speichern, und nicht den Prozessor eine Addition ausführen zu lassen.

Findet der Compiler heraus, daß b und c Konstante sind, dann erfüllt er die Aufgabe auch (und sogar effektiver) wenn er Summe gleich selbst berechnet und sie in a speichert. Änliches gilt auch, wenn in a schon die Summe steht.

Und in C ist es nun einmal so, daß am Anfang einer Anweisung entweder ein reseviertes Wort wie if, while, etc., oder ein Symbol steht. Ist diese Symbol eine Varible, dann muß darauf eine direkte oder indirekte Zuweisung, also ein = oder += oder ein ++ folgen. Folgt das nicht, ist es eine Funktion und da sind Klammern erforderlich.

Sieht das mal nicht so aus, verbirgt sich dahinter Preprozessor Magic, die je nachdem den Code leichter lesbar macht, oder den Anwender so in die Irre führt, daß er ihn garnicht mehr versteht. Der Compiler selbst sieht das aber nicht.

MfG Klebwax

Siro
08.12.2012, 17:09
Dann solltest du konsequenterweise auch in Assembler programmieren.

Auch wenn ich eingefleischter Assemblerfreak bin, so manches sieht in einer Hochsprache "geringfügig" ;) eleganter aus:

if (a > b)

oder:


;------------------------------------------------------
; if A > B ; 32 Bit Signed Compare
IF_INT32_G_VAR MACRO VarA,VarB,ElseAddress
Local not_equal
BANKSEL VarB
movf VarB+3,W ;
BANKSEL VarA
subwf VarA+3,W ;
btfss STATUS,Z ; scip if zero, is equal
BRA not_equal ; else goto not equal
BANKSEL VarB
movf VarB+2,W ; W = Low Byte VarB
BANKSEL VarA
subwf VarA+2,W ; W = VarA-VarB
btfss STATUS,Z ; scip if zero, is equal
BRA not_equal ; else goto not equal
BANKSEL VarB
movf VarB+1,W
BANKSEL VarA
subwf VarA+1,W
btfss STATUS,Z ; scip if zero, is equal
BRA not_equal ; else goto not equal
BANKSEL VarB
movf VarB+0,W
BANKSEL VarA
subwf VarA+0,W ; low byte variable A
btfsc STATUS,Z ; scip if not equal
goto ElseAddress ; else is equal
not_equal:
rrcf STATUS,W ; signed carry correction
xorwf VarA+3,W
BANKSEL VarB
xorwf VarB+3,W
addlw 0x80
btfss STATUS,C ; scip if greater
goto ElseAddress ; else is below
endm
;------------------------------------------------------


.... Spass muss sein....

Siro

oberallgeier
02.10.2013, 14:08
... so muss man also in "C" nun auch
nop();
schreiben ...Funktioniert bei Euch dieses

nop();in C? Bei mir wird ein Fehler ausgegeben - egal ob Groß- (wie SEI oder CLI) oder Kleinschreibung. Nur das Format
__asm__ volatile("nop\n\t");funktioniert; bei mir aktuell ohne define, da ich es nur einmal benötig(t)e.

Anmerkung: A.VR Studio 4, Version 4.18, Build 700, mit GCC/WinAVR-20100110; WinXPpro 2002 SP3

Siro
02.10.2013, 14:49
ich vermute, daß in deinem Programmcode

#include <intrinsics.h>

fehlt, könnte das sein ?

Siro

wenn nicht: welche Fehlermeldung bekommst du genau ?

Peter(TOO)
02.10.2013, 16:06
Hallo Siro,

1. Wie du erkannt hast darf ein Compiler einen Funktion auch inline generieren. Es gibt dafür sogar das Schlüsselwort inline in Manchen C-Compilern und in C++.

2. In ANSI-C wurde definiert, dass compiler- und prozessorspezifische Erweiterungen, welche nicht Bestandteil von ANSI-C sind, mit einem, oder zwei, _ anfangen. Dies gilt auch für entsprechende Variablen und Konstanten.
Damit ist recht schnell ersichtlich, wenn solche Erweiterungen in einem Programm verwendet werden.
Das wird wichtig, wenn man Code portieren will, bzw. wenn der selbe Code auf unterschiedlichen Prozessoren laufen soll (kommt bei mir öfters vor, besonders bei Übertragungsprotokollen. Wenn man nur ein Code-Modul zu pflegen hat, wird das Leben einfacher).

3. Grundsätzlich ist nop(); eine Funktion ohne Parameter. NOP ist eine Spezialfall, da es der einzige Maschinenbefehl ist, welcher ausser dem Verbrauch von CPU-Takten keinerlei Wirkung hat. Alle anderen Maschinenbefehle haben irgendwelche Effekte und benötigen teilweise auch Parameter. Für nop(); hat man jetzt nicht extra einen syntaktischen Sonderfall einführen wollen. Es wäre auch eine CPU denkbar, welche als Parameter von nop(); die Anzahl CPU-Takte übernimmt, welche "verbraten" werden sollen.

4. Dass dein Ablauf und der vom Compiler erzeugte nicht ganz übereinstimmen ist normal, für optimierende Compiler!
Der stellt dir auch mal Schleifen um oder rollt sie auf. Die Programmlogik bleibt dabei aber erhalten. Bei den meisten CPUs versucht der Compiler auch Speicher- und IO-Zugriffe zu optimieren, d.h. nur einmal darauf zuzugreifen und dann mit einer Kopie im Register zu arbeiten, was meist schneller ist.
Das kann natürlich besonders bei IO-Ports in die Hose gehen

while (IO_bit)
;

Wenn hier der der Compiler entdeckt, dass IO-bit in der Schleife nicht verändert wird, arbeitet er mit eine Kopie :-(
Deshalb gibt es das Schlüsselwort volatile, dann weiss der Compiler, dass er bei jeder Verwendung von IO_bit auch auf das entsprechende Port zugreifen muss.
Man kann aber die Optimierung auch ganz abschalten, dann entspricht der Code mehr deinen Erwartungen, wird aber grösser und langsamer.

MfG Peter(TOO)

oberallgeier
02.10.2013, 17:23
ich vermute, daß in deinem Programmcode ... fehlt ...Ohhh Mannomann wie Recht Du nur hast. Sorry und vor Allem: Danke!

Siro
02.10.2013, 21:02
Ich danke Dir erstmal für deine ausführliche Beschreibung Peter.

zu 1.
Ich dachte "inline" bedeutet dass jetzt ein Assembler-Code folgt.
Upps, da bin ich anscheinend noch auf dem Holzwege.

zu 2.
Das war mir auch neu mit den Unterstrichen und gibt natürlich Sinn. Beim Portieren ist leicht zu ersehen wo man evtl. Prozessorspezifisch Hand anlegen muss.
Dafür ist ja dann (meist) die Datei "intrinsics.h zuständig, welche oberallgeier vergessen hatte.

zu 3.
Mit der nop Implementierung ist natürlich Prozessorspezifisch. Mit der Übergabe von Parametern für die Zyklenzeit wäre tatsächlich sinnvoll, habe ich noch garnicht dran gedacht.
Dann gäben die Klammern auch wieder Sinn.

zu 4.
Die Optimierung der Compiler hat mich damals und manchmal auch heute noch, völlig aus der Bahn geworfen, ganz besonders beim Debuggen.
Oftmals sieht der generierte Assemblercode völlig anders aus als erwartet.
Der Code wurde aber bisher immer richtig ausgeführt, auch wenns merkwürdig aussah.
Da muss man plötzlich völlig umdenken als Assemblerprogrammierer.

Schön, daß ich helfen konnte und hab auch gleich noch was gelernt ;)

Siro

Peter(TOO)
02.10.2013, 22:55
Hallo Siro,

ANSI-C = Standard-C gibt es erst seit 1990, davor gab es nur die Beschreibung von K&R, welche aber vieles nicht definierte.
Im Prinzip konnte jeder Compilerhersteller machen was er wollte und das ganze C nennen.
Erst mit C90 entstand dann die erste verbindliche Norm.


zu 1.
Ich dachte "inline" bedeutet dass jetzt ein Assembler-Code folgt.
Upps, da bin ich anscheinend noch auf dem Holzwege.


Manche Compiler haben es ursprünglich so verwendet, in den Standard wurde es erst mit C99 aufgenommen. Wobei es von C++ übernommen wurde.

Die ursprünglichen C-Compiler unter Unix bestanden aus mehreren eigenständigen Programmen, wobei die Ausgab eines Programms die Eingabe für das nächste war:

1. Präprozessor.
Das war eine Stufe, welche eigentlich nur Text finden und ersetzen konnte und keine Ahnung von der C-Syntax hatte. Damit der PP "seine Befehle" finden kann, fangen diese mit # an.
#include <stdio.h>
bewirkt eigentlich nur, dass die Datei "stdio.h" an dieser Stelle in die Ausgabe kopiert wird.
Der PP funktioniert auch mit anderen Sprachen oder man konnte damit auch Bücher erstellen: einfach alle Kapitel mit #include zusammenlinken und dazwischen die Kapitelüberschriften einfügen :-)

2. C-Compiler.
Diese Stufe hat dann aus dem C-Source einen Assembler-Source gemacht.
Normalerweise besteht der CC aus zwei Teilen, dem Frontend, welches aus C eine interne Baumstruktur erstellt und dem Backend, welches aus dem Baum dann den prozessorspezifischen Assembler-Source erzeugt. Das hat den Vorteil, dass für eine neue CPU-Architektur nur das Backend neu geschrieben werden muss, alles davor muss nicht geändert werden.
Das "alte" inline war da recht einfach zu implementieren. Alles was zwischen den Anführungszeichen steht, wird direkt in den Assembler-Source kopiert

Die einfache Registeroptimierung findet im Backend statt, das Aufrollen und umstellen von Schleifen macht man an der Internet Baumstruktur.

3. Optimierer.
Diese Stufe war ursprünglich nur optional und optimierte den Assembler-Source.

4. Assembler.
Schlussendlich wurde dann alles vom Assembler in Maschinencode übersetzt. Dieser Assembler wurde auch alleine für die Assemblerpogrammierung verwendet.

5. Lint.
Das war ein optionaler Pass, zwischen PP und Compiler, welcher auch die falsche Verwendung von Variablen entdeckt hat, also z.B. das Zuweisen eines int an einen char und generelle Unsauberkeiten, welche heute meistens als Warning ausgegeben werden..

Heute ist dies oft in einem einzigen Programm zusammengefasst, aber auch moderne Compiler können noch die Zwischendateien, nach dem PP und den Assembler-Source ausgeben.


zu 2.
Das war mir auch neu mit den Unterstrichen und gibt natürlich Sinn. Beim Portieren ist leicht zu ersehen wo man evtl. Prozessorspezifisch Hand anlegen muss.
Dafür ist ja dann (meist) die Datei "intrinsics.h zuständig, welche oberallgeier vergessen hatte.

"intrinsics.h" ist nur für prozessorspezifische Erweiterungen zuständig.
Es gibt aber auch Bibliotheksspezifische Erweiterungen, welche dann Compilerabhängig sind.
Diese werden dann, sofern sie sich in den definierten Standard-Bibliotheken befinden, auch mit dem _ "markiert".


zu 4.
Die Optimierung der Compiler hat mich damals und manchmal auch heute noch, völlig aus der Bahn geworfen, ganz besonders beim Debuggen.
Oftmals sieht der generierte Assemblercode völlig anders aus als erwartet.
Der Code wurde aber bisher immer richtig ausgeführt, auch wenns merkwürdig aussah.
Da muss man plötzlich völlig umdenken als Assemblerprogrammierer.

Oft hat der Sourcelevel-Debugger dann auch seine Probleme, den Source-Code dem Assembler-Code zuzuordnen.
Deshalb produziert man meist auch zwei Versionen, Debug und Retail.
Debug optimiert dann weniger und oft wird auch noch zusätzliche Code eingefügt für z.B. ein Laufzeitüberwachung des Stacks. Oft gibt es dazu auch noch separate Bibliotheken.
Der Code ist natürlich grösser und langsamer und manche Fehler treten genau dann gar nicht auf :-( Zusätzlich enthält dann die Datei der Debugversion auch noch die ganzen Symbole für den Debugger.
Lustig wird's auch, wenn die Optimierung Fehler erzeugt :-( Da kann es dann sehr hilfreich sein, den Assembler-Source ansehen zu können, zumal der meist auch mit dem C-Source kommentiert wird.

MfG Peter(TOO)

Siro
03.10.2013, 09:42
Nun muss ich mich mal für die detailierte Ausführung von Peter bedanken.

{ ein bischen vom Thema abschweifend, ich bitte um Vergebung, aber es wird ja grad interessant und gehört auch irgendwie dazu ;)}

Lint hab ich noch nie gehört, natürlich gleich mal gegoogelt.
Hab ich aber verstanden: Typprüfungen, statische Codeanalyse....
Die Funktionsweise von Lint wurde also in die heutigen Compiler integriert.

Mit dem Frontend und Backend erklärt jetzt auch so einiges. Da ich grad mühsamst versuche einen Crosscompiler für die ARM Struktur zum Laufen zu bringen. Der C-Compiler (Frontend) erzeugt also NICHT den prozessorspezifischen Code und ist damit noch "portabel" während der Backend den Prozessor spezifischen Assemblercode erzeugt.
Ist dann im GCC der Front und der Backend enthalten ? Ich vermute mal ja, denn "DEN" GCC gibt es meiner Meinung nach nicht, sondern "Der" GCC wird prozessorspezifisch gebaut und deshalb heist er dann z.B arm-none-eabi-gcc.exe
Dieser beinhaltet dann den allgemein gültigen Frontend und den ARM spezifischen Backend.


Das führt uns zum eigentlichen Thema NOP zurück:

Der NOP wird dann also vom Backend prozessorspezifisch implementiert, wobei sich der Compiler am "intrinsics.h" Code bedient.

Ich hoffe, ich habe es so richtig verstanden, ansonsten korrigiert mich bitte.

Siro

Peter(TOO)
03.10.2013, 13:48
Hallo Siro,

Im Prinzip hast du alles richtig verstanden.

Nur ist in der Praxis die Trennung zwischen Front- und Backend nicht ganz so sauber wie in der Theorie.
Es gibt Prozessorabhängige Schlüsselwörter, diese sind im wesentlichen in intrinsics.h abgelegt. (Intrinsic = spezifisch). Das Frontend muss also wissen für welches Backend es arbeitet, bzw. sind diese Teile im Backend abgelegt.
Das Frontend macht also nur die lexikalische Überprüfung nach Standard-C-Norm, wird das Schlüsselwort nicht erkannt, wird dann eine entsprechende Erweiterung im Backend aufgerufen. Erkennt diese dann auch kein gültiges Wort, wird eine Fehlermeldung erzeugt.
So ähnlich geht es dann auch bei der syntaktische Analyse weiter, das Schlüsselwort wurde bereits als intrinsic erkannt und markiert, also muss die gültige Syntax auch aus dem Backen kommen.

Jetzt wird dir vielleicht auch klar, wieso nop(); als Funktion geschrieben wird. Das Backend kann die Standardfunktionen für die syntaktische Analyse im Frontend verwenden und wird wesentlich einfacher. Das Backend muss nämlich alle syntaktischen Analysen, welche vom Standard abweichen, selber ausführen.

Die Methode C mit intrinsic zu erweitern ist relativ schlank und einfach.

Versucht man das Ganze, bei einem modernen Compiler, über Inline-Assembler zu machen, wird es recht schnell hässlich. Zuerst einmal muss man im Backend einen kompletten Assembler einbauen mit der ganzen lexikalischen und syntaktischen Analyse. Dann ist er interne Zwischencode nicht mehr Prozessorabhängig, was wiederum zu jeder Menge Ausnahmebehandlungen führt, besonders an den Schnittstellen zwischen unabhängigen Code-Teilen und den Assembler-Teilen. Der Assembler-Teil braucht definierte Übergabekonventionen für z.B. lokale Variablen. Dadurch geht dann aber auch das Registermodell der CPU in den Zwischencode ein, um nur einige Probleme zu nennen. ;-(

MfG Peter(TOO)