hallo und schöne Ostern!
Mal eine ganz einfache Frage: Man baut hier gerne asm in C-Funktionen
ein. An einer Stelle kann man Konstanten gut per enum definieren. Wie
kann man diese Konstanten auch in asm benutzen? Oder umgekehrt?
Dieses funktioniert zwar, aber wie sieht das aus und wie lange geht das
gut?
1
// import-export.h -- Unterstützung für zweigeteilte Programme
Bauform B. schrieb:> Man baut hier gerne asm in C-Funktionen> ein.
So?
Wer tut das denn "hier gerne"?
Ich nehme mal an, daß das wieder mal eine Art Gedankenexperiment ohne
praktische Anwendung ist.
Normalerweise schafft man sich möglichst sauber definierte
Schnittstellen zwischen den verschiedenen Programmteilen bzw. Ebenen. Da
sind dann .h Dateien zu den zugrundeliegenden Assemblerteilen nötig,
wenn man die in Assembler geschriebenen Funktionen in C verfügbar machen
will. Aber gemeinsame Konstanten? Wohl eher nicht.
W.S.
W.S. schrieb:> Ich nehme mal an, daß das wieder mal eine Art Gedankenexperiment ohne> praktische Anwendung ist.
Die praktische Anwendung ist ein UART-Bootloader für den STM32L031C4.
Dem Chip fehlt es an allem (wenn man vom L4 verwöhnt ist). Zum Beispiel
kann der eingebaute Bootloader keine Programme ins RAM laden. Bei 8K RAM
ist es eben günstig, wenn das Programm im RAM Funktionen nutzen kann,
die im Bootloader im Flash sowieso vorhanden sind.
> Normalerweise schafft man sich möglichst sauber definierte> Schnittstellen zwischen den verschiedenen Programmteilen bzw. Ebenen.
Die Schnittstelle fand ich schon sauber, die Sache ist ja eigentlich
trivial. Es fehlte nur die eine Kleinigkeit. Inzwischen sieht es besser
aus, jetzt wird C nur noch benutzt um die diversen Attribute für die asm
section lesbarer zu machen. Die eigentliche Aufgabe war ja, die Offsets
in einer *.h zu haben, die von beiden Programmen included wird. Jetzt
ist das kein Problem mehr, weil alles Assembler ist. Ich finde es so
sogar übersichtlicher.
Bauform B. schrieb:> Die eigentliche Aufgabe war ja, die Offsets> in einer *.h zu haben, die von beiden Programmen included wird. Jetzt> ist das kein Problem mehr, weil alles Assembler ist. Ich finde es so> sogar übersichtlicher.
Ja, genau meine Erfahrung: macht man alles in Assembler, wird's am Ende
deutlich übersichtlicher. Jedenfalls so lange man "hardwarenah" agiert.
Und das ist schon immer dann der Fall, wenn man das strunzdumme und
primitive C-Speichermodell verlässt...
Also auf heutigen realen µC-Systemen praktisch immer.
Nur Ausweichen auf ein OS löst das Problem der Dummen und Inkompetenten.
Also das der reinen App-Bastler. Aber OK, wenn die Basis da ist, bin
auch ich nur noch App-Bastler. Genehmige mir dann aber typisch auch
einen großen Schluck aus der Pulle und quäle mich nicht mit C/C++ rum,
sondern benutze das sehr viel durchdachtere und komfortablere DotNet.
Nunja, es hat gewisse Performance-Nachteile gegenüber nativen Compilern.
Aber drauf geschissen, der Beschluss war ja: wir nehmen ein dickes
Eisen, um all unsere Probleme zu lösen. Also, wenn schon sinnlos
prassen, dann richtig.
Die Python-Typen werden mir hier absolut zustimmen... ;o)
Eine const-Variable, von der man eine Adresse bilden kann, kann über den
Linker auch von Asm aus genutzt werden.
Eine enum gehört nicht dazu.
Wenn ich sowas bräuchte, würde ich mir mit irgendeinem Praprozessor
entsprechende Definitionen für beide Seiten aus einer Datei generieren.
Oder man macht es mit einer Header-Datei mit #defines für beide
Sprachen, aber dann muss man halt mit #defines leben. Mag ich nicht ..
c-hater schrieb:> Die Python-Typen werden mir hier absolut zustimmen... ;o)
Naja, vielleicht hast du dich ja bemüht, etwas sinnvolles beizutragen?
Klaus W. schrieb:> Eine const-Variable, von der man eine Adresse bilden kann, kann über den> Linker auch von Asm aus genutzt werden.
Nett! Also so ungefähr? impex.h:
Es fehlt noch an zwei Stellen:
trotz _attribute_ ((section (".ABS."))) kommen die Konstanten so raus:
1
BUILD/boot.map: 0x20006208 idx_putchar
gewünscht hätte ich mir 0x08. Der Wert wird ja als immediate für movs
gebraucht. Und da gibt es das zweite Problem: der as merkt, dass das
keine absoluten Konstanten sind.
Bauform B. schrieb:> Mal eine ganz einfache Frage: Man baut hier gerne asm in C-Funktionen> ein.
Ich halte das generell für keine gute Idee. Schon aus Gründen der
Portabilität würde ich Assembler-Spielchen meiden. Wenn es denn an der
"Performance" kranken sollte, würde ich auf einen leitungsfähigeren
Prozessor setzen.
> An einer Stelle kann man Konstanten gut per enum definieren. Wie> kann man diese Konstanten auch in asm benutzen? Oder umgekehrt?
Wenn ich denn sowas machen müsste und wollte (!), würde ich mich einzig
auf den Preprozessor (1) verlassen, sprich auf Directives wie "#defines"
und "#if"...
-Armin
(1) Und dann gibt es ja noch M4 und Konsorten ...
Klaus W. schrieb:> Eine const-Variable, von der man eine Adresse bilden kann, kann über den> Linker auch von Asm aus genutzt werden.
Gröhl...
Also, was du meinst, ist eine im Code lokalisierte Variable. Also damit
(im Pascal Slang) eine typisierte Konstante.
Etwa so:
const
ottokar : integer = 4711;
Das ist eine Konstante, die eine Adresse in ihrem Speichersegment hat,
was für gewöhnlich der Flash in einem Mikrocontroller ist.
Allerdings ist das etwas anderes, als der TO beabsichtgte. Der will
lediglich im C-Teil sowas haben:
#define ottokar 4711
und im Assemblerteil sowas:
ottokar: EQU 4711
OK, wie das im jeweiligen Assemblerformat konkret aussehen muß, ist
unteschiedlich je nach Plattform und Toolchain.
W.S.
Armin schrieb:> Schon aus Gründen der Portabilität würde ich Assembler-Spielchen> meiden.
Die Alternative in C wäre hier vielleicht ein longjmp() im RAM-Programm
mit dem setjmp() im Flash-Programm. In Fortran gab es ein computed goto,
das wäre ziemlich übersichtlich. Zwecks der Gaudi sollte ich sowas
direkt mal probieren :)
Wahrscheinlich kann man auch einen Buffer Overflow nutzen. Dabei würde
man den Maschinencode in ein C-array schreiben müssen, es wäre also
nichts gewonnen und noch dazu wäre es von der Compiler-Version abhängig.
Also, manchmal scheint mir Assembler viel übersichtlicher zu sein.
> Wenn ich denn sowas machen müsste und wollte (!), würde ich mich einzig> auf den Preprozessor (1) verlassen, sprich auf Directives wie "#defines"> und "#if"...
Das war eigentlich die meine Frage, ich hab' einiges erfolglos mit
Macros probiert. M4 wäre allerdings eine echte Alternative, oder, für so
simple Sachen wie hier, einfach ein Script. Das hat mir schon oft
geholfen, aber solange es rein mit "Bordmitteln" geht...
W.S. schrieb:> Allerdings ist das etwas anderes, als der TO beabsichtgte. Der will> lediglich im C-Teil sowas haben:> #define ottokar 4711> und im Assemblerteil sowas:> ottokar: EQU 4711
Vor allem darf die 4711 nur an einer Stelle auftauchen.
Bauform B. schrieb:> In Fortran gab es ein computed goto, das wäre ziemlich übersichtlich.> Zwecks der Gaudi sollte ich sowas direkt mal probieren :)
gcc kennt das auch als Erweiterung für C.
https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html
Das Sprungziel muss aber in der selben Funktion liegen wie das goto.
compiliert mit cc -S main.c -> wirft main.s aus. Dieses wird umbenannt
in main.sx, also ein Assembler-File, das noch durch den Preprozessor
(cpp) muss.
main.s:
1
.file"main.c"
2
.text
3
.section.rodata
4
.LC0:
5
.string"hello world\n"
6
[usw...]
Dort habe ich ergänzt/geändert:
main.sx:
1
#define T_TEXT "Hello World\n"
2
3
.file"main.c"
4
.text
5
.section.rodata
6
.LC0:
7
.stringT_TEXT
8
[usw...]
Vorgeplänkel Ende ---
cc main.sx wirft dann das Executable a.out aus.
In der Praxis würde man natürlich #define T_TEXT "Hello World\n" in ein
Headerfile auslagern und dieses #includen - im .c oder .ax file.
(#include wird auch vom cpp prozesst...)
Alternativ:
Assemblercode als Inline Code im C-file formulieren.
Rolf M. schrieb:> gcc kennt das auch als Erweiterung für C.> https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html
Unglaublich, dankeschön. Das kann ich bestimmt missbrauchen ;)
> Das Sprungziel muss aber in der selben Funktion liegen wie das goto.
Schwach. Hier liegt das Ziel in einem anderen Programm. Immerhin nicht
auf einem anderen Rechner. Dabei fällt mir ein, ein goto reicht ja nie,
ich will ja z.B. printf() ganz normal aufrufen.
Armin schrieb:> main.sx, also ein Assembler-File, das noch durch den Preprozessor> (cpp) muss.
Danke! Das ist wohl die natürlichste Lösung. Und eine gute Gelegenheit,
mein import.c komplett in *.sx-Assembler zu schreiben. Inline-Assembler
will man doch nur im Notfall. Den Zwischenschritt "cc -S main.c -> wirft
main.s aus" würde ich (nur) nutzen, um die richtigen sections und
-Attribute zu finden.
Nur das #define statt enum stört ein wenig; das Programm könnte sonst
100% #define-frei sein. Aber es ist ja für einen guten Zweck ;)
c-hater schrieb:> sondern benutze das sehr viel durchdachtere und komfortablere DotNet.
DotNet? Oh mir schauderts. Bin froh das ich das nunmehr links liegen
lassen darf. Aber ich muß Dir dennoch recht geben, gegenüber C/C++ ist
es deutlich durchdachter und auch komfortabler. Leider wurden nicht alle
guten Ansätze konsequent bis zum (bitteren) Ende durchgezogen.
Bauform B. schrieb:> Armin schrieb:>> main.sx, also ein Assembler-File, das noch durch den Preprozessor>> (cpp) muss.>> Danke! Das ist wohl die natürlichste Lösung. Und eine gute Gelegenheit,> mein import.c komplett in *.sx-Assembler zu schreiben.
Naja, das "natürlich" nehme ich zurück. So ein Assembler-Quelltext ohne
C-Hülle drum herum ist nichts für schwache Nerven. Alleine 9 Zeilen in
der Art ".eabi_attribute 30, 4" oder "code 16" kombiniert mit mehreren
thumb-irgendwas -- das mag ich dann doch nicht lernen. Also brauche ich
doch den Umweg .c -> .sx -> o.
Dafür sieht ein #include in der .c jetzt lustig aus:
1
__asm__("#include \"import-export.h\"");
Ein normales include wird ja vom C-preprocessor platt gemacht.
Bauform B. schrieb:> Dafür sieht ein #include in der .c jetzt lustig aus:__asm__ ("#include> \"import-export.h\"");> Ein normales include wird ja vom C-preprocessor platt gemacht.
Kuck dir hierzu mal
https://stackoverflow.com/questions/52525630/define-in-inline-assembly-in-gcc
an.
Bauform B. schrieb:> Naja, das "natürlich" nehme ich zurück. So ein Assembler-Quelltext ohne> C-Hülle drum herum ist nichts für schwache Nerven. Alleine 9 Zeilen in> der Art ".eabi_attribute 30, 4" oder "code 16" kombiniert mit mehreren> thumb-irgendwas
Nimm mal eines deiner bestehende .s-files, benenne es um in .sx - und
versuch dich an #define. (Mein Vorgeplänkel brauchst du ja nicht.)
https://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html könnte
dich auch interessieren. (Kap. 7 bzw Kap 7.5)
Falls es GCC (GNU Compiler Collection) nicht tut, muss eh MAKE ran. Dann
bist nicht mehr an CPP gebunden.
Armin schrieb:> Nimm mal eines deiner bestehende .s-files, benenne es um in .sx - und> versuch dich an #define. (Mein Vorgeplänkel brauchst du ja nicht.)
Ich nicht, aber die Maschine braucht ganz viel davon, viel mehr als dein
Vorgeplänkel.
> Falls es GCC (GNU Compiler Collection) nicht tut, muss eh MAKE ran. Dann> bist nicht mehr an CPP gebunden.
Ich glaube die tut durchaus. Inzwischen erzeuge ich mit ganz normalen
Macros inline Assembler. Dabei kommt genau das raus, was ich mir
vorgestellt hatte. Vor allem sorgen die Macros dafür, dass der Import
automatisch zum Export passt. Na gut, die Macros sehen aus wie, nun ja,
muss man nicht mögen...
Natürlich funktioniert das nur so lange, wie beide Programme die gleiche
Version des Headers verwenden. Der Bootloader kann das jetzt prüfen,
weil die mtime der Header-Datei mit eingebaut wird. Dafür brauche ich
noch eine Zeile im Makefile, obwohl der GCC ein Macro namens __
TIMESTAMP __ anbietet. Leider liefert das wohl die mtime der c-Datei und
nicht die des Headers.
Mehrere Programme (Programmteile) unterhalten sich über ein festgelegtes
Protokoll - welches sich leider ab und zu ändert...
Wenn (nicht nur) ich sowas implementiere, gebe ich den Protokoll einen
Versions-Kenner mit - sinnvollerweise gleich als erstes Zeichen. Das
mache ich sogar, wenn ich im Leben nicht dran denke, daran je mal wieder
was zu ändern.
Diese Geitzkrägen bei der Internet Engineering Task Force (IETF) haben
dafür nur 4bit vorgesehen. Ich nehme immer 1 Byte - und bilde mir ein,
egal wie großzügig ich mit den Versionen um mich werfe, dass ich damit
auskommen werde.
Vielleicht wäre also ein
#define PROTOCOL_IC 1
in deiner import-export.h eine brauchbare Option?
Wenn es dir um "Sprungadressen" geht, könnte eine Adressleiste wie bei
den CP/M'schen BDOS-Systemcalls eine Option sein, um die Leiste über
längere Zeit konstant zu halten?
Die BDOS-Calls verwendeten eine Systemcall-Kennung im 8080-C-Register,
welche dann über eine Sprungleiste die richtige Funktion aufgerufen
hatte.
Der DOS-Interrupt 21h funktionierte so ähnlich ...
Armin schrieb:> Wenn (nicht nur) ich sowas implementiere, gebe ich den Protokoll einen> Versions-Kenner mit - sinnvollerweise gleich als erstes Zeichen.
Genau so. Bei mir ist es ein 32-Bit Wort, weil da die Unix-Zeit 1:1 rein
passt. Wenn ich jede Sekunde eine neue Version baue, reicht das
mindestens bis 2038, praktisch bis Februar 2106 ;) Der entscheidende
Vorteil: bei jeder Änderung wird zwangsweise eine neue Versionsnummer
vergeben.
> Wenn es dir um "Sprungadressen" geht, könnte eine Adressleiste wie bei> den CP/M'schen BDOS-Systemcalls eine Option sein, um die Leiste über> längere Zeit konstant zu halten?
Genau so wird's gemacht. Die Adressleiste selbst bleibt solange
konstant, bis ein neuer "Syscall" dazu kommt. Und weil man den am Ende
anhängt, merkt ein altes Programm immer noch keinen Unterschied. Der
Inhalt der Leiste darf sich beliebig oft ändern.
Armin schrieb:> Wenn es dir um "Sprungadressen" geht, könnte eine Adressleiste wie bei> den CP/M'schen BDOS-Systemcalls eine Option sein, um die Leiste über> längere Zeit konstant zu halten?
Ich schätze mal, daß du zu jung bist, um noch CP/M kennengelernt zu
haben.
Also: die BDOS-Systemcalls wurden per CALL 5 erreicht und die gewünschte
BDOS-Funktion wurde im C Register übergeben.
Genau SO war das damals.
W.S.
W.S. schrieb:> Ich schätze mal, daß du zu jung bist, um noch CP/M kennengelernt zu> haben.> [...]> Genau SO war das damals.
Und wurde GENAU auf SO WAS ausgeliefert...
-A
Armin schrieb:> Wenn es dir um "Sprungadressen" geht, könnte eine Adressleiste wie bei> den CP/M'schen BDOS-Systemcalls eine Option sein, um die Leiste über> längere Zeit konstant zu halten?>> Die BDOS-Calls verwendeten eine Systemcall-Kennung im 8080-C-Register,> welche dann über eine Sprungleiste die richtige Funktion aufgerufen> hatte.
@Bauform B:
Um das etwas genauer zu erklären, habe ich mal die CP/M 2.2 BDOS-Sourcen
ausgegbraben - und hier abgekürzt wiedergegeben:
Heute schreibt man das in "drei" Zeilen ...
Wozu nun der ganze Aufwand?
Solange keine neuen Funktionen dazu kommen (oder alte sich ändern),
bleibt auf diese Weise die BDOS-Schnittstelle gleich. Jedenfalls bei
CP/M (und später bei DOS) hatte sich das Einfrieren der Schnittstelle
bewährt.
Armin schrieb:> Wozu nun der ganze Aufwand?
Da sind wir wieder beim Ausgangsthema: Nicht das gemeinsame Benutzen von
irgendwelchen Konstanten ist die Lösung für Verbindungen zwischen
verschiedenen Ebenen (wie hier Funktionalität in Assembler und dito in
C), sondern das saubere Ausarbeiten von sinnvollen Schnittstellen und
eine sinnvolle Planung der Ebenen-Struktur in der zu schreibenden
Firmware.
Und Sprungleisten (also Arrays von Maschinencode-Stücken) sind ein
Notbehelf und keine wirkliche Lösung.
Nochwas: bei der damaligen Lernbetty hatten wir das mittels SVC (also
dem Supervisor-Call bei ARM-Controllern) gelöst. Das ist quasi ein
Interrupt, bloß der GCC kann das nicht.
W.S.
Armin schrieb:> Solange keine neuen Funktionen dazu kommen (oder alte sich ändern),> bleibt auf diese Weise die BDOS-Schnittstelle gleich. Jedenfalls bei> CP/M (und später bei DOS) hatte sich das Einfrieren der Schnittstelle> bewährt.
Das ist immer ein guter Plan. Schau dir die Schnittstelle
Linux-Kernel/Userland an, da ist "kompatibel bleiben" das 1. Gebot seit
20 Jahren.
W.S. schrieb:> Und Sprungleisten (also Arrays von Maschinencode-Stücken) sind ein> Notbehelf und keine wirkliche Lösung.>> bei der damaligen Lernbetty hatten wir das mittels SVC gelöst.
Ist das ein großer Unterschied? Innendrin hast du wieder eine Liste, die
beiden Partnern bekannt sein muss und die sich nicht ändern sollte. Die
SVC-Nummern entsprechen 1:1 meinen Problem-Konstanten. Ob ich die
Konstanten jetzt für ein movs oder für ein svc Immediate brauche, macht
doch wirklich keinen Unterschied. Der Verwaltungsaufwand ist gleich,
aber was Laufzeit und Stack-Verbrauch angeht ist meine Lösung klar im
Vorteil.
> Das [SVC] ist quasi ein Interrupt, bloß der GCC kann das nicht.
Ich hab' ihm erklärt, ein wenig asm tut garnicht weh, schau mal, so geht
das. Und er hat es brav ausgeführt ;)
W.S. schrieb:> #define ottokar 4711> und im Assemblerteil sowas:> ottokar: EQU 4711
nun ja das hängt sehr von der Toolchain ab
bei Keil Compilern/Assembler geht ein #define ottokar 4711 auch in
Assembler.
Ich benutze das um c Header auch in ASM zu verwenden. Das ist aber auf
wenige c macros beschränkt und ganz sicher nicht portabel.
Thomas Z. schrieb:> nun ja das hängt sehr von der Toolchain ab> bei Keil Compilern/Assembler geht ein #define ottokar 4711 auch in> Assembler.
Das weiß ich.
Aber hier geht es um Prinzipien und nicht um eine spezielle Toolchain.
Deshalb hab ich beides (ASM und C) jeweils so etwa in der Form
geschrieben, wie es so einigermaßen typisch ist und deshalb wohl von den
meisten Lesern verstanden wird.
W.S.
Bauform B. schrieb:> Das erklärt alles, danke!
O ja, bittesehr.
Für etwaige Mitleser: Der SVC, also der Supervisor-Call ist ein
besonderer Maschinenbefehl bei ARM-Befehlssätzen. Er erzeugt quasi einen
Interrupt und braucht deshalb keinerlei Adress-Angabe. Der
Maschinenbefehl enthält ein Bitfeld, wo man eine Botschaft oder
Funktionsnummer oder so etwas ähnliches unterbringen kann. Bei diesem
Quasi-Interrupt wird auch der Stack umgeschaltet (vom User-Stack auf den
Supervisor-Stack). Die zugehörige ISR kann das Bitfeld auswerten und
entsprechende Aktionen veranlassen oder eine Fehlerbehandlung starten.
Sowas ist vergleichbar mit der DOS-Schnittstelle "INT 21h", allerdings
mit dem Unterschied, daß beim SVC die CPU vom Usermodus in den
Supervisormodus wechselt und auch der Stack gewechselt wird. Näheres
kann man bei Arm nachlesen.
W.S.
Nachtrag, da ich Verständnis-Probleme wittere:
Bauform B. schrieb:> Innendrin hast du wieder eine Liste, die> beiden Partnern bekannt sein muss
Nein, das ist so nicht, sondern: das aufgerufene Programm hat eine Art
API, das veröffentlicht ist und einer zu schreibenden Anwendung bekannt
gemacht werden muß, damit die Anwendung das API benutzen kann.
Ob da "innendrin" ein kleiner grüner Buchhalter vom Mars werkelt oder ob
das anders gemacht ist, ist völlig unerheblich. Deshalb sind auch die
Innereien hinter der Pforte, die hier SVC heißt und woanders Int21h,
kein Teil der API-Dokumentation. Ob da nun anhand einer Liste von
Funktionen irgendwohin verzweigt wird oder es eben anders gemacht wird
(s.o.), ist schnurz.
Ein API ist eine Schnittstelle und als solche dient es dazu, daß zwei
völlig unterschiedliche Programme miteinander kommunizieren können, und
das sogar ohne wissen zu müssen, wo wer lokalisiert ist - und ohne
gemeinsam gelinkt worden zu sein.
Nochwas: Eine Sprungleiste sieht im Prinzip etwa so aus:
Sprungleiste:
{ goto ErsteFunktion;
goto ZweiteFunktion;
goto DritteFunktion;
...usw.
}
Bei sowas muß man sowohl die Adresse der Sprungleiste kennen, als auch
den Umfang der Elemente, damit man anhand eines Index den Anfang des
gewünschten Elements berechnen kann.
W.S.
Na, meinetwegen verstecke ich die "Benutzer"-Seite in einer library.
Dann merkt man nicht mehr, ob innen drin ein Sprung oder ein SVC
werkelt. Der Benutzer sieht nur sowas wie "int putchar (int)", das wäre
zwar ohne library genauso, aber mit ist es nicht so fremdartig.
Nur, innen drin muss immer noch die korrekte Zuordnung passieren, egal
ob SVC oder Sprung, library oder nicht.
Bernd schrieb:> Sprungleiste...> Die ganze Zeit musste ich grübeln, was er damit wohl meint.> Bei mir hieß das Ding immer Sprungtablle oder jump table.
Da kommt mir ein zorniger Leserbrief in der Funkschau der 70er Jahre in
den Sinn: Ein Kunde wollte sich ein neues Radio kaufen und fragte den
Verkäufer nach den technischen Daten, ob es sich z.B. bei der Kurzwelle
um einen Doppelsuper handelt. Antwort des Verkäufers: "Mein Herr, so
etwas wie Super gab es mal früher, das ist veraltet. Wir verkaufen nur
noch RECEIVER!"
Fachleute eben...
W.S.