News   Magazin   Börse   Links   ArcArchie   Homepages
 Magazin  RISC OS & C-Programmierung 3: Übergabe von Parametern Home 
Hardware   Software   Praxis   Sonstiges  
RISC OS & C-Programmierung 3: Übergabe von Parametern  Alexander Ausserstorfer 1. 3. 2015

Der Raspberry Pi basiert auf einem Broadcom BCM2835 SoC (System-on-a-Chip). Dieser Chip verwendet die ARMv6 Architektur und verfügt über die 16 Register R0 bis R15. [Anm. cms: Auch die Architekturen ARMv2 bis ARMv7, also alle ARM Architekturen unter denen RISC OS läuft, haben 16 Register.] Register sind Speicherzellen des Prozessors, die Daten enthalten und manipulieren können.

Beim Aufruf von Betriebssystemroutinen werden Parameter beziehungsweise Daten übergeben. Auf der ARM funktioniert das so. Die zu übergebenden Parameter werden in die Register ab R0 geschrieben und danach die gewünschte Betriebssystemroutine angesprungen beziehungsweise der gewünschte SWI aktiviert. Beim Verlassen der Betriebssystemroutine können statt der übergebenen Parameter die Ergebnisse oder auch irgend etwas anderes in den Registern stehen. Dies ist jedoch vom verwendeten SWI abhängig. Jedes dieser Register R0 bis R15 ist ein Wort (englisch: Word) groß. Bei einen Wort handelt es sich um vier Byte oder 32 Bit. Ein Register kann also 32 Bit Information enthalten. Folgendes Assemblerprogramm schreibt das Asterik-Zeichen * auf den Schirm, indem es das Register R0 mit dem hexadezimalen Wert &2A (in C 0x2A geschrieben) füllt und anschließend den SWI OS_WriteC anspringt:

   MOV R0, #&2A
   SWI 0
						

In C würde zum Beispiel folgender Programmcode diese Funktion erfüllen:

   printf('*');
						

oder auch

   printf("%c", 0x2A);
						

Die Funktion printf() findet sich in der Standard-C-Library stdlib, welche hierzu mit der Anweisung #include <stdlib.h> vor dem eigentlichen Programmcode eingebunden werden muss. Werden für einen SWI mehrere Parameter benötigt, so müssen diese in mehrere Register geschrieben (Assembler)

   MOV R0, ...
   MOV R1, ...
   MOV R2, ...
						

beziehungsweise in C mit mehreren Kommas getrennt werden:

   funktion (..., ..., ..., ...);
						

In C kann eine Funktion grundsätzlich nur einen einzigen Wert zurückliefern:

   result = funktion (..., ..., ..., ...);
						

Nun ist diese Art der Parameterübergabe stark begrenzt. Was ist zum Beispiel, wenn man eine lange Zeichenkette, also einen Text ausgeben möchte, der aus mehreren hundert Buchstaben besteht? Die wenigen Register der ARM könnten diesen Text nicht aufnehmen. Man müsste nun jeden einzelnen Buchstaben zuerst ins Register R0 schreiben und anschließend mittels OS_WriteC ausgeben. Das wäre ziemlich umständlich. In diesem Fall übergibt man die benötigte Information nicht direkt über die Register, sondern hinterlegt in den Registern eine kleine Information, die besagt, wo die benötigte große Information zu finden ist.

Der Raspberry Pi verfügt über einen sehr großen, zusammenhängenden Speicherbereich (RAM), der über den Adress- und Datenbus angesprochen wird. In diesen großen Speicher können nahezu beliebig viele und große Daten hinterlegt, gelöscht oder manipuliert werden. Jede Speicherzelle hat eine ihr zugewiesene Adresse. Die Speicherzellen werden beginnend mit Null durchgezählt beziehungsweise adressiert (hexadezimal von &00000000 bis &FFFFFFFF). Sie wird über ihre Adresse angesprochen. Dabei werden jeweils 8 Bit zu einer Gruppe miteinander verknüpft und gleichzeitig angesprochen. Eine Adresse verweist damit immer auf eine Gruppe von 8 Bit.

Man kann sich den Hauptspeicher also als eine Art Liste mit Adresse und Inhalt vorstellen. Mit Hilfe des frei verfügbaren Editors StrongED, der bei der Raspberry Pi Distribution von RISC OS zum Lieferumfang gehört, kann man sich beliebige Bereiche des Arbeitsspeichers anschauen. Dazu öffnet man über das StrongED Symbol auf der Symbolleiste mit der mittleren Maustaste das Menü und fährt mit der Maus rechts über den Eintrag Create. In dem daraufhin erscheinenden Untermenü sucht man nach dem Eintrag Dump (von englisch dump für Müllhalde, Abladeplatz) und klickt darauf. Jetzt kann man den Speicherbereich auswählen, den man sich ansehen möchte.

Dump des RAMs Das Bild zeigt einen solchen Auszug aus dem RAM. Jede Zeile stellt dabei sechs Worte oder 24 Byte Information dar. Die erste schwarze Spalte gibt die Adresse wieder, mit welcher der Speicherauszug je Zeile anfängt. Die Adressen werden hier in hexadezimaler Schreibweise dargestellt. Die blauen Spalten stellen jeweils die Werte oder Inhalte der dazugehörigen Speicheradressen des RAMs dar. Adressen und Inhalte werden hier jeweils im hexadezimalen Zahlensystem dargestellt. Die letzte, rechte Spalte, die wieder schwarz ist, gibt die Inhalte des RAMs ASCII-codiert wieder.

Wenn die Register des Prozessors nicht ausreichen, übergibt man in solchen Fällen also nicht mehr die eigentlichen Daten, sondern Adressen, die besagen, wo die eigentlichen Daten zu finden sind. Jedes Register ist dabei so groß, dass es jede beliebige Adresse des Hauptspeichers aufnehmen kann. Da ein Register 32 Bit groß ist, können auch nur maximal

   232 = 4.294.967.296 Bytes

angesprochen werden. Das sind 4 GByte. Mehr Hauptspeicher könnte der ARM des Raspberry Pi und überhaupt jede 32 Bit CPU gar nicht direkt ansprechen. Indirekt über einen Umweg, wie man das beim 8-Bit Computer Commodore 64 macht der 8 Bit große Register hat, ginge rein theoretisch freilich mehr. [Anm. cms: Bei 8 Bit Registern sind es 28 = 256 Byte Speicher. Tatsächlich wurde beim C64 der Speicher über zwei Register, also mit 16 Bit, adressiert. Das sind dann maximal 216 = 65536 Bytes = 64 kByte RAM. Über einen Trick kann der C128 128 kByte RAM nutzen.] Somit kann in jedem Register des ARM-Prozessors jede Adresse des Hauptspeichers hinterlegt werden.

Der Speicher wird flexibel von RISC OS verwaltet. RISC OS weist einem Programm freien Speicher zu und reserviert diesen für das Programm. Erst dadurch wird es möglich, dass sich verschiedene Programme gleichzeitig dem Hauptspeicher bedienen, ohne sich gegenseitig in die Quere zu kommen. Diese flexible Speicherverwaltung ist Voraussetzung für ein multitaskingfähiges System, bei dem mehrere Programme gleichzeitig laufen. Denn bei solchen Systemen ist der Inhalt des Hauptspeichers davon abhängig, wann welche Programme gestartet wurden und wieviel Speicher sie gerade benötigen und so weiter. Man kommt daher mit absoluten Adressen kaum in Berührung. Sondern überlässt die Speicherverwaltung der Übersetzungssoftware, egal ob in C oder Assemblersprache, die sie an das Betriebssystem weiterreicht. Statt einer festen Adresse verwendet man in diesen Sprachen daher andere Konstrukte, die eigentlich nur als Platzhalter dienen. Der Platzhalter wird letzten Endes bei der Ausführung des Programms jedoch durch eine tatsächliche Adresse ersetzt. Und diese Adresse kann sich bei jeder Ausführung des Programms ändern.

Folgendes Beispiel in ARM-Assembler schreibt die Zeichenkette Hello World! auf den Bildschirm:

   ADR, R0, string1
   SWI "OS_Write0"

   .string1
   EQUS "Hello World!"
   EQUB 0
						

Der Befehl ADR, R0, string1 schreibt die Adresse string1 in das Register R0. string1 ist hier ein Platzhalter, der später vom Assembler und Betriebssystem durch eine tatsächliche Adresse ersetzt wird. SWI "OS_Write0" springt den SWI zur Zeichenausgabe an. Die beiden nachfolgenden Zeilen .string1 und EQUS "Hello World!" teilen dem Compiler mit, dass die Zeichenfolge Hello World! im Speicher hinterlegt werden soll. EQUS ist kein Maschinenbefehl. Die ARM CPU kennt diesen Befehl gar nicht. Es handelt sich tatsächlich um einen Pseudobefehl. Eine Anweisung, der dem Assembler etwas sagt, nicht aber der ARM CPU. EQUB 0 fügt der Zeichenkette Hello World! nun noch den Wert Null hinzu, damit der SWI "OS_Write0" das Ende der Zeichenkette erkennen und die Ausgabe abbrechen kann. Der SWI würde sonst die Werte unter den nachfolgenden Speicheradressen auch noch auslesen und auf den Bildschirm ausgeben und da könnte alles Mögliche stehen. [Anm. cms: Diese Null ist nicht das Zeichen "0", sondern der ASCII Wert Null, den man auch NUL nennt.]

Der einfachste Fall in C für diese sogenannte indirekte Adressierung ist ein Zeiger. Ein Zeiger kann eine Adresse aufnehmen und ist typisiert, damit beim Übersetzungs- oder Compilierungsvorgang die Datentypen überprüft werden können ob Funktionsparameter und übergebener Parameter in ihrer Art zusammenpassen.

Vorheriges Beispiel in C:

   char *buchstabenkette;
						

richtet einen Zeiger namens buchstabenkette vom Typ char ein. Dieser Zeiger kann eine Speicheradresse aufnehmen. Alles, was ab dieser Adresse abgelegt wird, wird als Typ char beziehungsweise als einzelnes ASCII-Zeichen gewertet.

   buchstabenkette="Hello World!";
						

weist diesem Zeiger namens buchstabenkette die Adresse zu, ab welcher die Zeichenfolge Hello World! im Speicher abgelegt wird.

   printf("%s", buchstabenkette);
						

übergibt die Adresse in buchstabenkette als Funktionsparameter an die Funktion printf(), welche nun über die in buchstabenkette hinterlegte Adresse die Zeichenfolgen Hello World! vom Hauptspeicher auslesen und diese auf den Bildschirm ausgeben kann. Damit die Funktion printf() weiß ab welchem Zeichen beziehungsweise ab welcher Adresse die Zeichenausgabe abgebrochen werden soll, fügt der Compiler der Buchstabenkette Hello World! automatisch ein Endzeichen hinzu, das im Speicher dem Ausrufezeichen folgt. Wie in Assembler ist dies der Wert Null. Sobald printf() auf dieses Endzeichen trifft, wird die Zeichenausgabe beendet.

Nun gibt es Betriebssystemroutinen, die noch wesentlich komplexere Parameter erwarten. Zum Beispiel der SWI Wimp_CreateWindow für die Erzeugung eines Fensters. Der Unterschied zum vorherigen Beispiel ist der, dass es sich bei den zu übergebenen Parametern um viele verschiedene Datentypen handelt. Also etwa um Zahlenwerte für Größe und Position der Fenster, um Zeichenketten für Titelleiste des Fensters und Symbole und um einzelne Bits, die als Merker fundieren und Zustände speichern zum Beispiel an und aus. Diese Vielzahl von unterschiedlichen Parametern, welche jeweils zusammengehören, werden im Speicher normalerweise unter aufeinanderfolgenden Adressen, also in zusammenhängenden Adressbereichen, abgelegt. Man spricht hier auch von Datenblöcken, die eine fest vorgeschriebene Struktur aufweisen müssen. Die Parameter müssen demnach in der richtigen Reihenfolge abgelegt sein. Der Aufbau dieser Datenblöcke wird ausführlich bei der Beschreibung jedes einzelnen SWIs in den Programmer's Reference Manuals (PRMs) behandelt. Nach Einrichtung des Datenblocks übergibt man dem SWI dann nur noch die Startadresse des Blocks.

In Assembler und BASIC kann so was sehr fehlerträchtig sein, weil grundsätzlich keine Überprüfung der Struktur stattfindet. Das Hinterlegte wird vom SWI einfach entsprechend interpretiert. Hat man aber die Struktur falsch aufgebaut, weil man sich verzählt hat, erhält man ein falsches, das heißt unerwartetes, Ergebnis.

Beispiel in BBC BASIC:

   DIM window% 255

   REM sichtbarer Bereich
   window%=200
   window%!4=200
   window%!8=500
   window%!12=600

   REM Scroll offsets
   window%!16=0
   window%!20=0

   REM Fensterebene und -zeiger
   window%!24=-1
   window%!28=&&FF000012

   REM Fensterfarben
   window%?32=7
   window%?33=2
   window%?34=7
   window%?35=1
   window%?36=3
   window%?37=1
   window%?38=12

   REM Arbeitsbereich
   window%!40=0
   window%!44=-700
   window%!48=700
   window%!52=0

   REM Titelbar und Arbeitsbereichzeiger
   window%!56=&19
   window%!60=&3000

   REM Spritearbeitsbereichzeiger und minimale Spritegröße
   window%!64=0
   window%!68=0

   REM Fenstertitel
   $(window%+72)="Hello World!"

   REM Anzahl der Symbole
   window%!84=0

   SYS "Wimp_CreateWindow",,window% TO handle%
						

Man sieht das Dilemma. Wenn man sich irgendwo verzählt oder vertippt hat, stimmt die vom SWI erwartete Struktur nicht mehr und man erhält falsche Ergebnisse.

In C arbeitet man hier deshalb mit Strukturen, welche vom Compiler überprüft werden können. Die Sprache C bietet jedoch nur Sprachelemente zum Aufbau solch komplexer Strukturen an. Die Struktur von Datenblöcken einzelner SWIs kennt es freilich nicht. Diese müssen dem Compiler aber irgendwie bekannt gemacht werden. Dies kann man selbst mit Hilfe der Sprachelemente von C oder unter anderem auch mit Hilfe von Libraries beziehungsweise Bibliotheken machen, welche uns geeignete Strukturen zur Verfügung stellen. Wie wir noch sehen werden, bietet die Library OSLib für RISC OS eine ganze Menge solcher Strukturen und Definitionen für viele SWIs an. Damit wird es möglich, den korrekten Aufbau eines komplexen Datenblockes beim Programmieren besser zu kontrollieren und zu überprüfen. Vorhergegangenes BBC BASIC Beispiel für die Definition eines Fensters übersetzt nach C unter Verwendung der uns von OSLib bereitgestellten Strukturen:

   wimp_window window;

   /* sichtbarer Bereich */
   window.visible.x0 = 200;
   window.visible.y0 = 200;
   window.visible.x1 = 500;
   window.visible.y1 = 600;

   /* Scroll offsets */
   window.xscroll = 0;
   window.yscroll = 0;

   /* Fensterebene und -zeiger */
   window.next = wimp_TOP;
   window.flags = 0xFF000012;

   /* Fensterfarben */
   window.title_fg = 7;
   window.title_bg = 2;
   window.work_fg = 7;
   window.work_bg = 1;
   window.scroll_outer = 3;
   window.scroll_inner = 1;
   window.highlight_bg = 12;

   /* Arbeitsbereich */
   window.extent.x0 = 0;
   window.extent.y0 = -700;
   window.extent.x1 = 700;
   window.extent.y1 = 0;

   /* Titelbar und Arbeitsbereichzeiger */
   window.title_flags = 0x19;
   window.work_flags =  0x3000;

   /* Spritearbeitsbereichzeiger und minimale Spritegröße */
   window.sprite_area = NULL;
   window.xmin = 0;

   /* Fenstertitel */
   strcpy(window.title_data.text, "Hello World!");

   /* Anzahl der Symbole */
   window.icon_count=0;

   block.open.w = wimp_create_window (&window);
						

Den Quellkode kann man herunterladen. Nach dem Aufruf einzelner C-Funktionen beziehungsweise SWIs können die im Speicher beziehungsweise in den Strukturen geschriebenen Werte von den C-Funktionen beziehungsweise SWIs natürlich geändert worden sein, um Informationen zurückzuliefern.

Eine Bemerkung am Rande sei noch erwähnt. Da der Prozessor keine einzelnen Speicherzellen (Bits), sondern immer nur acht zusammengefasste Speicherzellen (Bytes) gleichzeitig lesen oder schreiben kann, übergeben wir hier in beiden Fällen (BASIC und C) immer mehrere Bits gleichzeitig.

   block%!28=&&FF000012
						

beziehungsweise in C

   window.flags = 0xFF 00 00 12;
						

Diese Bits dienen als Merker für an, aus und so weiter und müssen daher in ihrer Bedeutung getrennt betrachtet werden. Da ein getrenntes oder einzelnes Lesen beziehungsweise Schreiben von Bits hardwaretechnisch aber nicht geht, müssen diese Werte zuvor von uns festgelegt und anschließend in einem Schlag geschrieben werden. Wir verwenden in voranstehenden Programmlistings jeweils die hexadezimale Darstellung der Bitmuster. Die einzelnen Bits können wir erst erkennen, wenn wir die hexadezimale Zahl 0xFF 00 00 12 ins binäre Format umwandeln beziehungsweise -rechnen. [Anm. cms: Dafür kann der Desktoprechner von RISC OS recht nützlich sein, da man dort einfach zwischen den Zahlenformaten umschalten kann.]

   
binär     hexadezimal
11111111 00000000 00000000 00010010     FF 00 00 12

Die hexadezimale Zahl 0xFF000012 umfasst damit 32 Bit, 4 Bytes oder ein Wort, entspricht also gerade der Größe oder Breite des adressierbaren Speichers oder eines Registers der ARM. Die Bits werden normalerweise von rechts nach links durchnummeriert. Man beginnt dabei mit der Null an zu zählen. Die Bedeutung des Bits ganz rechts außen entspricht damit der Null oder Eins im geläufigen 10er-System:

   
Bit 31
MSB
  30     29   ...   4     3     2     1   0
LSB
Wert 1 1 1 ... 1 0 0 1 0

StrongHelp Manual Auszug von OSLib Beim Bit Null (Nullbit) spricht man auch vom Least Significant Bit oder kurz LSB, dem Bit mit dem geringsten Stellenwert, bei der Position 31 vom Most Significant Bit oder kurz MSB, dem Bit mit dem größten Stellenwert. OSLib bietet nun für einzelne Bits auch noch Konstanten an. Für wimp_window_flags kann man einige im Bild aus dem StrongHelp-Manual sehen.

In OSLib erkennt man Konstanten an der Großschreibung. [Anm. cms: Dies ist in vielen Programmiersprachen so üblich.] Diese einzelnen Konstanten können dazu verwendet werden, um den Compiler den benötigten Wortwert, der in unserem Beispiel 0xFF000012 lautet, selbst berechnen zu lassen. Wie das geht, das wird jedoch erst in einer späteren Folge dieser Serie behandelt werden, nämlich nachdem wir OSLib eingerichtet und verwendet haben.

Literaturempfehlung:

 
Weiter geht es im vierten Teil mit der Verwendung von OSLib.

Einen Fehler im Artikel gefunden, etwas unklar, Ergänzung oder ein Tipp? Einfach eine Nachricht schicken!

Name: *

EMail:

Text: *
Hardware   Software   Praxis   Sonstiges  
ArcSite   News   Magazin   Börse   Links   ArcArchie   Homepages