Embedded Software Timing: Methodik, Analyse und Praxistipps am Beispiel Automotive
Von Peter Gliwa
()
Über dieses E-Book
Ähnlich wie Embedded Software Timing
Ähnliche E-Books
Modellbasierte Softwareentwicklung für eingebettete Systeme verstehen und anwenden Bewertung: 0 von 5 Sternen0 BewertungenLeitfaden Safety of the Intended Functionality: Verfeinerung der Sicherheit der Sollfunktion auf dem Weg zum autonomen Fahren Bewertung: 0 von 5 Sternen0 BewertungenSicherheit von Webanwendungen in der Praxis: Wie sich Unternehmen schützen können – Hintergründe, Maßnahmen, Prüfverfahren und Prozesse Bewertung: 0 von 5 Sternen0 BewertungenLeitfaden Automotive Cybersecurity Engineering: Absicherung vernetzter Fahrzeuge auf dem Weg zum autonomen Fahren Bewertung: 0 von 5 Sternen0 BewertungenModerne Realzeitsysteme kompakt: Eine Einführung mit Embedded Linux Bewertung: 0 von 5 Sternen0 BewertungenBasiswissen Sicherheitstests: Aus- und Weiterbildung zum ISTQB® Advanced Level Specialist – Certified Security Tester Bewertung: 0 von 5 Sternen0 BewertungenLockout-Tagout: Verriegelung von Stellgliedern zur umfassenden Wartungssicherung von Maschinen Bewertung: 0 von 5 Sternen0 BewertungenModellbasiertes Requirements Engineering: Von der Anforderung zum ausführbaren Testfall Bewertung: 0 von 5 Sternen0 BewertungenFunktionale Sicherheit in der Praxis: Anwendung von DIN EN 61508 und ISO/DIS 26262 bei der Entwicklung von Serienprodukten Bewertung: 0 von 5 Sternen0 BewertungenUsable Security und Privacy by Design Bewertung: 0 von 5 Sternen0 BewertungenVor- und Nachteile von Neubau oder Sanierung im Bestand: Schnelleinstieg für Architekten und Bauingenieure Bewertung: 0 von 5 Sternen0 BewertungenSoftwarelizenzmanagement kompakt: Einsatz und Management des immateriellen Wirtschaftsgutes Software und hybrider Leistungsbündel (Public Cloud Services) Bewertung: 0 von 5 Sternen0 BewertungenPrüfungswissen Fachkraft für Schutz und Sicherheit Band 1: Konzepte für Schutz und Sicherheit - Sicherheitsorientiertes Kundengespräch Bewertung: 0 von 5 Sternen0 BewertungenEvaluierung des kollaborativen Lifecycle-Managements mit der IBM Jazz Plattform Bewertung: 0 von 5 Sternen0 BewertungenFehlerbaumanalyse in Theorie und Praxis: Grundlagen und Anwendung der Methode Bewertung: 0 von 5 Sternen0 BewertungenLanglebige Software-Architekturen: Technische Schulden analysieren, begrenzen und abbauen Bewertung: 0 von 5 Sternen0 BewertungenElektromagnetische Verträglichkeit in der Praxis: Design-Analyse - Interpretation der Normen - Bewertung der Prüfergebnisse Bewertung: 0 von 5 Sternen0 BewertungenAufwandsschätzungen in der Software- und Systementwicklung kompakt Bewertung: 3 von 5 Sternen3/5Kostenoptimierte Anwendungsentwicklung: Reduzierung der Entwicklungskosten durch Trennung der Datenverwaltungs- und Fachfunktionen Bewertung: 0 von 5 Sternen0 BewertungenExoskelette in der Intralogistik: Erfolgreich implementieren und Prozesse optimieren Bewertung: 0 von 5 Sternen0 BewertungenArchitekturen für BI & Analytics: Konzepte, Technologien und Anwendung Bewertung: 0 von 5 Sternen0 BewertungenSoftwareentwicklungsprozess: Von der ersten Idee bis zur Installation Bewertung: 0 von 5 Sternen0 BewertungenSoftware-Test für Embedded Systems: Ein Praxishandbuch für Entwickler, Tester und technische Projektleiter Bewertung: 0 von 5 Sternen0 BewertungenProduktdatenmanagement – Anforderungen und Lösungen: Konzeption, Auswahl, Installation und Administration von PDM-Systemen Bewertung: 0 von 5 Sternen0 BewertungenFunktionale Sicherheit nach ISO 26262: Ein Praxisleitfaden zur Umsetzung Bewertung: 0 von 5 Sternen0 BewertungenBuchreihe: Produktivitätssteigerung in der Softwareentwicklung, Teil 2: Managementmodell, Aufwandsermittlung und KPI-basierte Verbesserung Bewertung: 0 von 5 Sternen0 BewertungenIT-Unternehmensarchitektur: Von der Geschäftsstrategie zur optimalen IT-Unterstützung Bewertung: 0 von 5 Sternen0 BewertungenITIL konformes Incident Management im Bereich der Software-Entwicklung: Chancen im Einsatz von Open Source Software Bewertung: 0 von 5 Sternen0 BewertungenProzessgesteuerte Anwendungen entwickeln und ausführen mit BPMN: Wie flexible Anwendungsarchitekturen wirklich erreicht werden können Bewertung: 0 von 5 Sternen0 Bewertungen
Softwareentwicklung & -technik für Sie
Sketchnotes in der IT: Abstrakte Themen mit Leichtigkeit visualisieren Bewertung: 0 von 5 Sternen0 BewertungenProgrammieren lernen mit Python 3: Schnelleinstieg für Beginner Bewertung: 0 von 5 Sternen0 BewertungenProjektmanagement für Anfänger: Grundlagen, -begriffe und Tools Bewertung: 0 von 5 Sternen0 BewertungenAgiles Projektmanagement: Scrum für Einsteiger Bewertung: 0 von 5 Sternen0 BewertungenProjekt Unicorn: Der Roman. Über Entwickler, Digital Disruption und das Überleben im Datenzeitalter Bewertung: 0 von 5 Sternen0 BewertungenLean Management für Einsteiger: Grundlagen des Lean Managements für Kleine und Mittelständische Unternehmen – mit Vielen Praxisbeispielen Bewertung: 0 von 5 Sternen0 BewertungenDas große Python3 Workbook: Mit vielen Beispielen und Übungen - Programmieren leicht gemacht! Bewertung: 4 von 5 Sternen4/53D-Drucken für Einsteiger: Ohne Frust 3D-Drucker selbst nutzen Bewertung: 0 von 5 Sternen0 BewertungenDigital Painting Workbook Bewertung: 0 von 5 Sternen0 Bewertungen50 Arten, Nein zu sagen: Effektives Stakeholder-Management für Product Owner Bewertung: 0 von 5 Sternen0 BewertungenDesign Thinking für Anfänger: Innovation als Faktor für unternehmerischen Erfolg Bewertung: 0 von 5 Sternen0 BewertungenLean Production - Grundlagen: Das Prinzip der schlanken Produktion verstehen und in der Praxis anwenden. Schlank zur Wertschöpfung! Bewertung: 0 von 5 Sternen0 BewertungenKanban für Anfänger: Grundlegendes über den Einsatz von Kanban in der Industrie und der Softwareentwicklung Bewertung: 0 von 5 Sternen0 BewertungenSoftwareentwicklungsprozess: Von der ersten Idee bis zur Installation Bewertung: 0 von 5 Sternen0 BewertungenEinfach Java: Gleich richtig programmieren lernen Bewertung: 0 von 5 Sternen0 BewertungenKOMA-Script: Eine Sammlung von Klassen und Paketen für LaTeX 2e Bewertung: 0 von 5 Sternen0 BewertungenEinstieg in Reguläre Ausdrücke Bewertung: 0 von 5 Sternen0 BewertungenEinfach Python: Gleich richtig programmieren lernen Bewertung: 0 von 5 Sternen0 BewertungenKompaktes Managementwissen: Die Grunstruktur agiler Prozesse Bewertung: 0 von 5 Sternen0 BewertungenAgiles Requirements Engineering und Testen Bewertung: 0 von 5 Sternen0 BewertungenScrum: Agiles Projektmanagement erfolgreich einsetzen Bewertung: 4 von 5 Sternen4/5Agile Spiele – kurz & gut: Für Agile Coaches und Scrum Master Bewertung: 0 von 5 Sternen0 BewertungenPrinzipien des Softwaredesigns: Entwurfsstrategien für komplexe Systeme Bewertung: 0 von 5 Sternen0 BewertungenAgiles Coaching als Erfolgsfaktor: Grundlagen des Coachings, um Agile Teams erfolgreich zu managen Bewertung: 0 von 5 Sternen0 BewertungenAutomatisiertes Testen: Testautomatisierung mit Geb und ScalaTest Bewertung: 0 von 5 Sternen0 BewertungenBessere Softwareentwicklung mit DevOps Bewertung: 0 von 5 Sternen0 BewertungenLean Management für Einsteiger: Erfolgsfaktoren für Lean Management – Lean Leadership & Co. als langfristige Erfolgsgaranten Bewertung: 0 von 5 Sternen0 BewertungenBaukunst für Softwarearchitekten: Was Software mit Architektur zu tun hat Bewertung: 0 von 5 Sternen0 BewertungenChange Management für Anfänger: Veränderungsprozesse Verstehen und Aktiv Gestalten Bewertung: 1 von 5 Sternen1/5Grundlagen und Methoden der Wirtschaftsinformatik: Eine anwendungsorientierte Einführung Bewertung: 0 von 5 Sternen0 Bewertungen
Rezensionen für Embedded Software Timing
0 Bewertungen0 Rezensionen
Buchvorschau
Embedded Software Timing - Peter Gliwa
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2021
P. GliwaEmbedded Software Timinghttps://doi.org/10.1007/978-3-658-26480-2_1
1. Allgemeine Grundlagen
Peter Gliwa¹
(1)
Gliwa GmbH, Weilheim, Bayern, Deutschland
Peter Gliwa
Email: peter.gliwa@gliwa.com
Elektronisches Zusatzmaterial
Die elektronische Version dieses Kapitels enthält Zusatzmaterial, das berechtigten Benutzern zur Verfügung steht https://doi.org/10.1007/978-3-658-26480-2_1.
Grundlegendes Wissen in den Bereichen Softwareentwicklung und Betriebssysteme sind Voraussetzung für Analyse und Optimierung des Timings von Embedded Software. Das Kapitel „Allgemeine Grundlagen verfolgt zwei Ziele. Zum einen sollen wichtige Grundlagen vermittelt bzw. zusammengefasst werden. Zum anderen wird schon an dieser Stelle bei den einzelnen Themen ein Bezug zum Timing hergestellt. Daher richtet sich das Kapitel nicht ausschließlich an diejenigen, die Grundlagen erlernen oder auffrischen möchten. Auch erfahrenen Softwareentwicklern hilft die neue Facette „Timing
, die sich bei Altbekanntem zeigt.
1.1 Echtzeit
Fragt man Entwickler von Desktopsoftware oder Webanwendungen, was sie unter Echtzeit verstehen, bekommt man mitunter die Antwort, dass Echtzeit im Sinne von „ganz schnell oder mit „mit ganz wenig Verzögerung
zu verstehen ist.
Auch wenn das für die meisten Echtzeitsysteme sicherlich nicht falsch ist, trifft es doch nicht den Kern der Sache. Echtzeit im Umfeld von Embedded Software sollte man eher im Sinne von „rechtzeitig" verstehen. Es existieren zeitliche Anforderungen, sogenannte Timinganforderungen, die eingehalten werden müssen. Bei harter Echtzeit muss die Einhaltung der Timinganforderungen unter allen Umständen gewährleistet sein, bei weicher Echtzeit reicht es aus, wenn die Timinganforderungen nicht zu häufig verletzt werden, wenn sie also statistisch garantiert werden können. Welcher statistischer Parameter zur Erfüllung der weichen Echtzeit herangezogen wird, ist nicht allgemeingültig definiert. Bei Bedarf muss für ein gegebenes Projekt, für eine gegebene Situation eine eigene Definition gefunden werden.
1.2 Phasengetriebenes Prozessmodell: das V-Modell
Das V-Modell beschreibt ein Konzept zur Vorgehensweise bei der Softwareentwicklung. Es kommt seit Jahrzehnten im Automobilbereich zum Einsatz und ist meist auch dann – zumindest im Hintergrund – vorhanden, wenn nach neueren Konzepten wie Scrum entwickelt wird. Seinen Ursprung hat es, wie so viele technische Entwicklungen, im militärischen Sektor. Später wurde es auf den zivilen Bereich übertragen und in den Ausprägungen V-Modell 97 und V-Modell XT an die neueren Anforderungen bei der Entwicklung angepasst [1].
../images/478274_1_De_1_Chapter/478274_1_De_1_Fig1_HTML.pngAbb. 1.1
V-Modell der Softwareentwicklung
Das „V des V-Modells stellt den idealisierten Verlauf der Entwicklung in einem Koordinatensystem mit zwei Achsen dar. Die horizontale Achse ist eine Zeitachse beginnend links mit dem Start des Projektes. Die vertikale Achse markiert die Abstraktion: von „detailliert
unten bis „abstrahiert" oben. Siehe auch Abb. 1.1. Ein Projekt sollte auf einer hohen Abstraktionsebene mit dem Einsammeln der Nutzer- oder Kundenanforderungen (Englisch: „Requirements") an das Produkt starten. Es folgt auf Systemebene das grundsätzliche Design des Produktes. Im weiteren Verlauf des Projektes wird das Design heruntergebrochen, verfeinert, detailliert. Eventuell ergeben sich auch weitere, detailliertere Anforderungen an das Produkt. Ist die Designphase abgeschlossen, startet die Implementierung, also die Umsetzung. Bezogen auf ein Softwareprojekt entspricht dies der Codierung. Dem Zusammenfügen der einzelnen Komponenten, der Integration, folgt die Absicherung, die Verifikation auf den verschiedenen Abstraktionsebenen. Dabei werden die zuvor formulierten Anforderungen der jeweiligen Ebene überprüft. Die letzte Überprüfung erfolgt auf der obersten Abstraktionsebene, indem sichergestellt wird, dass die Nutzer- oder Kundenanforderungen erfüllt werden.
Wird eine Anforderung nicht erfüllt, muss die Ursache der Abweichung beseitigt werden. Die Ursache liegt zwangsläufig irgendwo auf dem V zwischen der Anforderung und deren Überprüfung. In der Folge müssen alle abhängigen nachfolgenden Schritte ebenfalls korrigiert, angepasst oder zumindest wiederholt werden.
Es liegt auf der Hand, dass der von Fehlern verursachte Aufwand und auch die Kosten umso größer werden, je später die Fehler entdeckt werden. Das liest sich wie eine Binsenweisheit, doch ist es erstaunlich, wie viele Projekte das Embedded Software Timing stiefmütterlich behandeln. Viel zu oft werden in einer späten Projektphase mit viel Hektik, hohen Kosten und und großem Risiko Laufzeitprobleme untersucht und notdürftig behoben oder abgemildert.
1.2.1 Das V-Modell im Zusammenhang mit Timing
Praktisch jeder Softwareentwickler im Automobilbereich kennt das V-Modell wie in Abschn. 1.2 dargestellt. Bei der Verwendung des V-Modells stehen meist die funktionalen Aspekte im Mittelpunkt. Wie sieht es nun aus, wenn das Thema Timing ins Spiel kommt? Im Prinzip ändert sich nichts. Die dem Modell zugrunde liegende Idee kann auch auf das Timing angewendet werden. Abb. 1.2 zeigt eine Konkretisierung der Idee und gibt aufs Timing bezogene Beispiele für die unterschiedlichen Phasen des V-Modells.
../images/478274_1_De_1_Chapter/478274_1_De_1_Fig2_HTML.pngAbb. 1.2
V-Modell angewandt auf Timing-bezogene Aktivitäten
Kap. 9 beschäftigt sich eingehend damit, wie Timinganalyse systematisch in den Entwicklungsprozess integriert werden kann.
1.3 Buildprozess: vom Modell zum Executable
Zwischen dem linken Ast des V-Modells und dem Prozess , der aus dem Quellcode ausführbaren Maschinencode werden lässt, dem Buildprozess, gibt es Analogien. Gestartet wird auf einer vergleichsweisen hohen Abstraktionsebene und im Verlauf der Zeit nähert man sich immer mehr der ausführenden Hardware, dem Prozessor an.
Dieser Abschnitt beschreibt, wie aus dem Quellcode ausführbarer Maschinencode – also ein Executable – wird und welche Dateien, Werkzeuge und Übersetzungsschritte dabei relevant sind. Die in dem Abschnitt behandelten Grundlagen haben „nur" einen indirekten Bezug zum Thema Timing. Doch ohne das Verständnis, wie zum Beispiel ein Compiler grundsätzlich funktioniert, ist Codeoptimierung mit dem Ziel der Laufzeitminimierung nur schwer möglich.
../images/478274_1_De_1_Chapter/478274_1_De_1_Fig3_HTML.pngAbb. 1.3
Der Buildprozess: Werkzeuge und Dateien auf dem Weg zum Executable
1.3.1 Modellbasierte Softwareentwicklung, Codegenerierung
Mittlerweile ist wohl der größere Anteil der Software, die in einem Auto läuft, modellbasiert. Das heißt, dass der Quellcode nicht von Hand geschrieben ist, sondern von Code generierenden Werkzeugen wie Embedded Coder, Targetlink oder ASCET erzeugt wird. Zuvor wurde die Funktionalität – meist Regelungstechnik, digitale Filter oder Zustandsautomaten – mit grafischen Modellierungswerkzeugen wie MATLAB/Simulink oder ASCET formuliert und als „Modell" gespeichert.
1.3.2 C Präprozessor
Listing 1.1 zeigt ein einfaches – in diesem Fall von Hand codiertes – Programm. Anhand dieses Programms soll im Folgenden der Weg vom Quellcode zum ausführbaren Programm, zum Executable, veranschaulicht werden. Der Code des eingebundenen Headers myTypes.h ist in Listing 1.2 zu sehen.
../images/478274_1_De_1_Chapter/478274_1_De_1_Figa_HTML.png../images/478274_1_De_1_Chapter/478274_1_De_1_Figb_HTML.pngDas in Zeile 12 Listing 1.1 verwendete Schlüsselwort ../images/478274_1_De_1_Chapter/478274_1_De_1_IEq1_HTML.gif (das englische Wort „volatile bedeutet „flüchtig
) veranlasst den Compiler dazu, jeden Zugriff auf die betroffene Variable explizit im Speicher vorzunehmen und nicht etwa in einem Register zwischenzuspeichern. Das ist beispielsweise dann erforderlich, wenn der Inhalt der betroffenen Speicherstelle von der Hardwareperipherie beschrieben werden kann. Das Timerregister eines Hardwaretimers ist ein Beispiel dafür. In Listing 1.1 wird ../images/478274_1_De_1_Chapter/478274_1_De_1_IEq2_HTML.gif verwendet, um zu verhindern, dass der Compiler den Code „wegoptimiert, also feststellt, dass die Variable ../images/478274_1_De_1_Chapter/478274_1_De_1_IEq3_HTML.gif nie „sinnvoll
verwendet wird und daher sämtliche Zugriffe darauf entfernt.
Abb. 1.3 zeigt, welche Schritte auf dem Weg vom Quellcode zum Executable durchlaufen werden, welche Zwischenformate anfallen und welche zusätzlichen Dateien involviert sind. Optionale Datenflüsse, Dateien und Werkzeuge sind blass dargestellt.
Im ersten Schritt analysiert der Präprozessor des Compilers den Code und löst alle Makros („ ../images/478274_1_De_1_Chapter/478274_1_De_1_IEq4_HTML.gif ) auf, liest alle eingebundenen Dateien ein („ ../images/478274_1_De_1_Chapter/478274_1_De_1_IEq5_HTML.gif
), entfernt inaktiven Code bedingter Kompilierung („ ../images/478274_1_De_1_Chapter/478274_1_De_1_IEq6_HTML.gif (...) ../images/478274_1_De_1_Chapter/478274_1_De_1_IEq7_HTML.gif ) und berechnet alle Werte, die zu diesem Zeitpunkt bereits berechenbar sind („ ../images/478274_1_De_1_Chapter/478274_1_De_1_IEq8_HTML.gif
$$\rightarrow $$ „ ../images/478274_1_De_1_Chapter/478274_1_De_1_IEq10_HTML.gif ). Alle Anweisungen, die mit einem „ ../images/478274_1_De_1_Chapter/478274_1_De_1_IEq11_HTML.gif
beginnen, sind Präprozessoranweisungen. Tatsächlich erledigt der Präprozessor noch eine ganze Reihe weiterer Aufgaben, doch sollen die genannten Beispiele an dieser Stelle reichen, um das Prinzip zu verdeutlichen.
Tipp
Die meisten Compiler unterstützen die Kommandozeilenoption ../images/478274_1_De_1_Chapter/478274_1_De_1_IEq12_HTML.gif , die den Compiler dazu veranlasst, nach der Präprozessorstufe abzubrechen und den „präprozessierten" Code auf ../images/478274_1_De_1_Chapter/478274_1_De_1_IEq13_HTML.gif auszugeben. Das kann sehr hilfreich beim Debuggen von Problemen im Zusammenhang mit dem Präprozessor sein.
Außerdem eignet sich diese Ausgabe auch sehr gut, um Compilerprobleme an den Compilerhersteller zu melden. Wird die Ausgabe in eine Datei umgeleitet (hier hat sich die Dateiendung ../images/478274_1_De_1_Chapter/478274_1_De_1_IEq14_HTML.gif eingebürgert), kann diese Datei ohne weitere Dateien – wie beispielsweise inkludierten Headern – dem Compiler zum kompilieren übergeben werden. Der Compilerhersteller kann dann das Problem nachvollziehen, ohne dass er dafür Zugriff auf alle eingebundenen Header benötigt.
Listing 1.3 zeigt die in eine Datei main.i umgeleitete Präprozessorausgabe für die Quelldatei main.c.
../images/478274_1_De_1_Chapter/478274_1_De_1_Figc_HTML.pngDie #line (...) Angaben erlauben es dem Compiler, später jede Zeile der Datei ihrer ursprünglichen Position in ihrer ursprünglichen C Quelldatei zuzuordnen. Das ist zum Beispiel dann relevant, wenn der Compiler Fehler oder Warnungen meldet. Die angezeigte Zeilennummer eines Fehlers oder einer Warnung gibt immer die entsprechende Zeile in der ursprünglichen Quelldatei wieder.
1.3.3 C Compiler
Die Ausgabe des Präprozessors wandert in den Compiler, der daraus Prozessor-spezifischen Maschinencode, also eine dem C-Code entsprechende Datei mit Maschinenbefehlen erzeugt. Die Speicheradressen der Funktionen, Variablen, Sprungadressen etc. werden zu diesem Zeitpunkt noch nicht festgelegt, sondern symbolisch festgehalten.
Die Ausgabe des Compilers – in diesem Fall des TASKING Compilers für den Infineon AURIX Prozessor – ist auszugsweise in Listing 1.4 zu sehen. Da dieser Code als Eingabe für die nachfolgende Stufe, den Assembler, fungiert, wird er auch Assemblercode genannt.
../images/478274_1_De_1_Chapter/478274_1_De_1_Figd_HTML.png1.3.4 Codeoptimierung durch den Compiler
Beim Übersetzen von Quellcode in Maschinencode kann ein Compiler eine Vielzahl von Optimierungen vornehmen. Viele dieser Optimierungen verkleinern den Speicherbedarf und führen gleichzeitig zu schnellerem Code. Bei einigen Optimierungen geht eine Verbesserung des einen Aspektes aber nur auf Kosten des anderen. Hier muss der Entwickler entscheiden, was wichtiger ist.
Der tatsächliche Nutzen einer Optimierung lässt sich oft nur schwer im Vorfeld abschätzen. Wenn es darauf ankommt, muss auf jeden Fall das Ergebnis kontrolliert werden. Dies geschieht am besten indem für die unterschiedlichen Compilereinstellungen a) der generierte Maschinencode verglichen wird und b) vergleichende Messungen durchgeführt werden. Selbst Experten erleben hier immer wieder Überraschungen. Abschn. 8.3 „Laufzeitoptimierung auf der Codeebene" beschäftigt sich mit dem Thema im Detail.
1.3.5 Assembler
Der Assembler übersetzt die textuellen Maschinenbefehle des Assemblercodes in deren binäre Entsprechung. Somit ist die Ausgabe des Assemblers nicht mehr einfach von Menschen lesbar und wird hier nicht weiter dargestellt.
Aus der Assemblerdatei (meist mit Dateiendung .src oder .s) wird eine Objektdatei. Diese wird oft schlicht Objekt genannt. Wie zuvor im Assemblercode sind auch im Objektcode die Speicheradressen der Funktionen, Variablen, Sprungadressen etc. noch nicht festgelegt sondern liegen weiterhin ausschließlich symbolisch vor.
1.3.6 Linker
Der Linker baut alle ihm übergebenen Objekte zu einem fast fertigen Programm zusammen; es fehlen lediglich noch die konkreten Adressen. In unserem Beispiel wird ein einzelnes Objekt, nämlich main.o übergeben. Es kommen implizit ein paar weitere Objekte hinzu, zum Beispiel cstart.o für die grundlegende Initialisierung, die vor der Ausführung der main() Funktion erforderlich ist. Dazu gehört die Initialisierung des Speicherinterfaces, das Setzen des Stackpointers auf den Stackanfang und die Initialisierung von Variablen.
Darüber hinaus können dem Linker Funktionsbibliotheken übergeben werden, die typischer Weise an der Dateiendung .a oder .lib zu erkennen sind. Funktionsbibliotheken sind praktisch nichts anderes als Sammlungen von Objekten. In der Abb. 1.3 ist der Archiver zu erkennen, der ausgewählte Objekte in Archive packt – ganz ähnlich einem Kompressionsprogramm („ZIP") oder einem Tarball Erzeuger.
Eine weitere Aufgabe des Linkers ist es, alle referenzierten Symbole aufzulösen. Angenommen die main Funktion aus dem Beispiel würde eine weitere Funktion SomeOtherFunction aufrufen, die zuvor mittels einer extern Deklaration bekannt gemacht worden wäre. Diese im Englischen Forward-declaration genannte Bekanntmachung könnte zum Beispiel so aussehen: int SomeOtherFunction(int someParam);.
Wird diese Funktion nun nicht in main.c implementiert, merkt der Linker sich das Symbol SomeOtherFunction als eines, das referenziert aber noch nicht definiert, also aufgelöst, wurde. In allen weiteren dem Linker übergebenen Objekten sucht der Linker nun nach dem Symbol SomeOtherFunction. Findet er eine Definition, also eine Implementierung der Funktion, ist die Referenz auf das Symbol aufgelöst. Nachdem alle Objekte für die Auflösung von Referenzen durchsucht wurden, werden die beim Aufruf des Linkers übergebenen Funktionsbibliotheken herangezogen, um die verbleibenden Referenzen aufzulösen.
Bleibt die Suche nach einem Symbol auch hier erfolglos, meldet der Linker einen Fehler, typischerweise „unresolved external
$$<Symbolname>$$".
Wird ein Symbol in mehr als einem Objekt definiert, meldet der Linker ebenfalls einen Fehler, in diesem Fall „redefinition of symbol
$$<Symbolname>$$".
Wird ein Symbol in einem Objekt und in einer oder in mehreren Funktionsbibliotheken definiert, ignoriert der Linker die Definitionen des Symbols in den Funktionsbibliotheken und meldet weder eine Warnung noch einen Fehler.
Die Reihenfolge, in der Funktionsbibliotheken dem Linker übergeben werden, bestimmt die Suchreihenfolge. Wird ein Symbol aufgelöst, werden alle nachfolgenden Definitionen ignoriert und nicht „gelinkt".
1.3.7 Locator
Die allermeisten Werkzeughersteller fassen Linker und Locator in einem Werkzeug, das dann als Linker bezeichnet wird, zusammen. Die Rolle des Locators ergibt sich aus seinem Namen: er „lokatiert" (verortet) alle Symbole in den verfügbaren Speichern. Damit erfolgt die Festlegung der Speicheradressen für jedes einzelne Symbol.
Die Ausgabe des Locators ist schließlich das Executable in einem Format mit oder ohne Symbolinformationen. Diese sind unter anderem für das bequeme Debuggen der Software erforderlich. So erlauben die Symbolinformationen zum Beispiel beim Anzeigen des Inhalts von Variablen einfach den Namen der gewünschten Variable anzugeben. Das unhandliche Hantieren mit Speicheradressen ist nicht erforderlich.
Typische Ausgabeformate für das Executable ohne Symbolinformationen sind Intel HEX Dateien (*.hex) oder Motorola S-Records (*.s19). Am weitesten verbreitet für die Ausgabe des Executables mit Symbolinformationen ist das ELF Format (*.elf). ELF steht für „Executable and Linking Format".
Neben dem Executable kann eineLinker-Map, auch Mapdatei oderMapfile genannt, erstellt werden. Unter anderem beinhaltet diese Datei eine Liste aller Symbole nebst deren Speicheradresse.
1.3.8 Linkerskript
Eine sehr wichtige Rolle fällt dem Linkerskript (auch Linker Control File) zu. Genau genommen müsste es „Locatorskript oder „Locator Control File
heißen, doch fassen – wie bereits erwähnt – die meisten Hersteller Locator und Linker als „Linker" zusammen.
Listing 1.5 zeigt auszugsweise das Linkerskript für einen einfachen 8 Bit Mikrocomputer, den Microchip AVR ATmega32 mit 32 KByte Flash, 2 KByte RAM und 1 KByte EEPROM.
Das Linkerskript teilt dem Locator mit, wie die Symbole auf die verschiedenen Speicher des Mikroprozessors zu verteilen sind. In der Regel läuft dies wie folgt ab. Zunächst werden im C- oder Assemblerquellcode alle Symbole bestimmten Sections – genauer gesagt: Inputsections – zugewiesen. Diese Zuweisung erfolgt auch dann, wenn der Programmierer sie nicht explizit vornimmt, sie erfolgt dann implizit. Dabei haben sich die folgenden Sectionnamen eingebürgert, welche die Defaultsections benennen.
.text
Programmcode
Beispiel: int GiveMe42(void){return 42;}
.rodata
nur lesbare (read-only) Daten
Beispiel: const int a = 5;
.bss
les- und schreibbare (read-write) Daten, mit 0 initialisiert
Beispiel: int a;
Entsprechend dem C Standard müssen nicht initialisierte globale Variablen durch den Startupcode mit 0 initialisiert werden. Nicht alle Embedded Softwareprojekte implementieren den Startupcode in dieser Art, sodass man sich nicht darauf verlassen sollte, dass alle bei der Definition nicht initialisierten Variablen beim Startup tatsächlich auf 0 gesetzt werden.
.data
les- und schreibbare (read-write) Daten, initialisiert mit einem bestimmten Wert
Beispiel: int a = 5;
.noinit
les- und schreibbare (read-write) Daten, nicht initialisiert
Beispiel: int a;
Das Beispiel ist identisch mit dem bei .bss. Über Compilerschalter lässt sich meist steuern, ob im Code nicht initialisierte Variablen mit 0 oder gar nicht initialisiert werden sollen.
.debug
Debugsections beinhalten weder Code noch Daten des Programms sondern Zusatzinformationen, die das Debuggen der Software ermöglichen beziehungsweise vereinfachen. Abschn. 1.3.9 befasst sich mit diesem Thema und geht näher auf Debugsections ein.
Es hat sich etabliert, dass Sectionnamen mit einem Punkt beginnen.
Die Anweisungen im Linkerskript weisen im nächsten Schritt alle Inputsections Outputsections zu, die ihrerseits schließlich auf die verfügbaren Speicher abgebildet werden. Zum besseren Verständnis finden sich für die .text Sections im Listing 1.5 entsprechende Kommentare.
In einem klassischen Linkerskript finden sich wie in Listing 1.5 am Anfang die Definitionen der verfügbaren Speicher. Dann folgen die Definitionen der Outputsections und mit jeder dieser Definitionen die Verknüpfung mit den Inputsections sowie die Zuweisung zu einem Speicher.
Eine sehr gute Beschreibung der Syntax dieses Linkerskripts sowie die zugrunde liegenden Konzepte finden sich im Handbuch des GNU Linkers [2]. Die meisten anderen Werkzeughersteller haben für ihre Linker zumindest die Konzepte des GNU Linkers („ld") übernommen, oft sogar die Syntax des Linkerskripts kopiert.
../images/478274_1_De_1_Chapter/478274_1_De_1_Fige_HTML.pngEs mag verwirrend sein, dass Inputsections, Outputsections und sogar die Speicher die gleichen Namen haben dürfen, siehe eeprom am Ende von Listing 1.5, dennoch ist dies möglich und sogar üblich.
Was hat nun das Linkerskript mit dem Thema Timing zu tun? Wie wir in den Abschn. 2.4 „Wait-states, Burstzugriffe und 2.3 „(Speicher-) Adressierung, Adressierungsart
sehen werden, haben Speicherort und Zugriffsart einen wesentlichen Einfluss auf die Zugriffsdauer und damit auf die Laufzeit des zugreifenden Codes. Die Festlegung von Speicherort und Zugriffsart erfolgt über das Linkerskript und somit ist die Kenntnis dessen Syntax und Funktionsweise essentiell für die Laufzeitoptimierung. Abschn. 8.2 „Laufzeitoptimierte Speichernutzung" geht im Detail auf dieses Thema ein.
1.3.9 Debugger
In der Softwareentwicklung hat sich der englische Begriff „bug (Insekt; Ungeziefer) für Softwarefehler etabliert. Demnach ist der „Debugger
das Werkzeug, welches den Entwickler dabei unterstützt, Softwarefehler zu eliminieren. Darüber hinaus erfüllt er weitere wichtige Aufgaben wie zum Beispiel das Flashen des Executables in den Programmspeicher während der Entwicklungsphase.
Der Debugger gehört zwar nicht zu den Werkzeugen, die zum bauen des Executables erforderlich ist, doch rundet er den Abschnitt „Buildprozess" sehr schön ab. Im Verlauf des Abschnitts wurde der Weg vom Modell oder vom Quellcode zum Executable skizziert. Modell oder Quellcode befinden sich auf einer hohen Abstraktionsebene, das eigentliche Executable auf einer niedrigen – es besteht ja nur noch aus Einsen und Nullen im Programmspeicher. Die verschiedenen Abstraktionsebenen sind auch in Abb. 1.3 ersichtlich: der Buildprozess verläuft von oben nach unten.
Der Debugger geht gewissermaßen den umgekehrten Weg: er verbindet sich mit dem Prozessor und in der Regel liest er auch das Executable – meist im ELF Format – ein. Wenn verfügbar, kann auch der Quellcode geladen werden, was das Debuggen erheblich vereinfacht. Schritt für Schritt beziehungsweise Befehl für Befehl kann der Debugger den Prozessor Code abarbeiten lassen und zeigt für jeden Schritt die aktuelle Position im Programmcode an. Darüber hinaus wird sich der Entwickler auch den Inhalt bestimmter Speicherbereiche, Variablen oder aller Register darstellen lassen. Abb. 1.4 zeigt eine solche Debugsituation unter Verwendung des Codebeispiels, das schon in den Abschnitten zum Compiler, Assembler etc. herangezogen wurde.
Um jeder gültigen Adresse im Programmspeicher die passende Zeile im Quellcode zuordnen zu können, greift der Debugger auf die Debuginformationen zu, die in den bereits erwähnten Debugsections abgelegt sind. Ein Großteil der Debuginformationen liegen im DWARF Format innerhalb der ELF Datei vor. Ursprünglich ist DWARF keine Abkürzung sondern eine spielerische Anlehnung an ELF: Englisch „elf ist im Deutschen die Elfe und Englisch „dwarf
ist im Deutschen der Zwerg.
Weniger märchenhaft aber sehr praktisch sind die Möglichkeiten, welche die Debuginformationen eröffnen. Sie stellen gewissermaßen die Brücke vom Executable zu den Quellen dar – von einer niedrigen Abstraktionsebene zu einer hohen. Der Prozessor arbeitet mit Einsen und Nullen und dennoch kann der Debugger den dazugehörigen Quellcode darstellen. Die DWARF Informationen der ELF Datei geben dem Debugger zu jeder Speicheradresse die dazugehörige Quelldatei und Zeilennummer an.
Weitere Beispiele für DWARF Informationen sind Typinformationen für Variablen oder Details zum Aufbau von structs.
Das Konzept, dass Debuginformationen Aufschluss darüber geben, zu welcher Quelle auf einer höheren Ebene ein Befehl auf einer unteren Ebene gehört, ist allgemein sinnvoll. Es muss nicht auf der Ebene des C Quellcodes enden. Wenn ein Codegenerator den erzeugten C Code mit Debuginformationen anreichert, kann danach prinzipiell das Debuggen auch auf der Modellebene erfolgen. Der Entwickler kann dann beispielsweise schrittweise Blöcke ausführen und bekommt die Werte von Eingangsgrößen direkt an den entsprechenden Eingängen in der richtigen physikalischen Skalierung mit entsprechender Einheit angezeigt.
../images/478274_1_De_1_Chapter/478274_1_De_1_Fig4_HTML.pngAbb. 1.4
Screenshot des TRACE32 Debuggers mit Maschinencode, Assemblercode und C-Code
1.3.9.1 Erläuterung der Debuggeransicht
Wie bereits erwähnt zeigt Abb. 1.4 das Programm aus Listing 1.1. Im großen Fenster ist in blauer Schrift der C Quellcode und in grüner Schrift der disassemblierte Maschinencode inklusive der Sprungmarken, auch Labels genannt, zu sehen. Labels beginnen meist mit einem Punkt, hier zum Beispiel .L35 oder .L36. Der disassemblierte Maschinencode entspricht dem Assemblercode, siehe Listing 1.4 und wird in der Spalte „mnemonic dargestellt. Unter „Mnemonic
– dem englischen Begriff für Gedächtnishilfe – versteht man den sich einfach zu merkenden Namen für einen Maschinenbefehl, beispielsweise ../images/478274_1_De_1_Chapter/478274_1_De_1_IEq17_HTML.gif für die Addition. Die zusätzliche „16" an manchen Befehlen zeigt an, dass es sich hier um einen 16 Bit Befehl handelt, wie ihn die Infineon TriCore Architektur unterstützt.
Im grau hinterlegten Bereich links steht vor jeder Zeile mit Maschinencode die jeweilige Programmspeicheradresse und vor jeder Zeile mit Quellcode die jeweilige Zeilennummer der Quellcodedatei. In grauer Schrift jeder Programmspeicheradresse steht der Inhalt, der Opcode. Sehr schön erkennbar ist, dass die 16 Bit Befehle tatsächlich 16 Bits veranschlagen (zum Beispiel 0x129A für add16 d15,d2,#0x1) und 32 Bits für alle restlichen (zum Beispiel 0x0000FF59 für st.w [a15]0x0,d15).
Unten links im Screenshot ist die Registeransicht zu sehen. Der PC (Englisch „program counter) zeigt auf die Programmspeicheradresse des als nächstes zu bearbeitenden Befehls, hier 0x80000468. Diese Adresse ist in der Codeansicht mit einem grauen Balken hinterlegt, sodass man direkt sieht, „wo der Prozessor gerade steht
.
In dem „Watch-Fenster" in der Mitte rechts werden aktuell zwei Ausdrücke beobachtet: zum einen der Wert der Variablen a zum anderen deren Speicheradresse &a. Es lassen sich hier tatsächlich sogar komplexe C Ausdrücke angeben, was vielen Entwicklern nicht bewusst ist, was aber sehr hilfreich sein kann.
Schließlich zeigt die Speicheransicht unten rechts einen Ausschnitt des RAM Speichers an. Der angezeigte Bereich beginnt bei Adresse 0x70003FF8, der Adresse der Variablen a. Und tatsächlich: der Inhalt ist 2A hexadezimal, was 42 dezimal entspricht. Offenbar wurde die Funktion GetSomeValue() genau einmal aufgerufen (oder vielleicht doch
$$n \cdot 2^{32} + 1$$mal?). Nach der nächsten Ausführung dieser Funktion und dem Speichern des Ergebnisses in der Variablen a mittels des Maschinenbefehls st16.w [a10]0x0,d2 an der Programmspeicheradresse 0x8000046C (Label .L41) wird die Variable den Wert 43 haben.
1.4 Zusammenfassung
Im Kapitel Allgemeine Grundlagen wurde empfohlen, bestehende Entwicklungsprozesse um Aspekte des Embedded Software Timings zu erweitern. Wie dies konkret aussehen kann, wurde anhand des V-Modells gezeigt. Eine weitere Detaillierung der Entwicklungsschritte mit Timingbezug wird im weiteren Verlauf des Buches erfolgen.
Weiterhin wurden die einzelnen Stufen und Werkzeuge im Buildprozess näher beleuchtet. Ein besonderes Augenmerk wurde dabei auf das Linkerskript gerichtet, da es einerseits bei der Laufzeitoptimierung eine wichtige Rolle spielt und es andererseits erfahrungsgemäß stiefmütterlich behandelt wird und dann Probleme bei der Entwicklung verursacht.
Literatur
1.
Wikipedia Artikel „V-Modell (Entwicklungsstandard)", 2020https://de.wikipedia.org/wiki/V-Modell_(Entwicklungsstandard)
2.
Steve Chamberlain, Ian Lance Taylor The GNU linker, Abschnitt 3.1 „Basic Linker Script Concepts", 2010https://www.eecs.umich.edu/courses/eecs373/readings/Linker.pdf
© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2021
P. GliwaEmbedded Software Timinghttps://doi.org/10.1007/978-3-658-26480-2_2
2. Mikroprozessortechnik Grundlagen
Peter Gliwa¹
(1)
Gliwa GmbH, Weilheim, Bayern, Deutschland
Peter Gliwa
Email: peter.gliwa@gliwa.com
Elektronisches Zusatzmaterial
Die elektronische Version dieses Kapitels enthält Zusatzmaterial, das berechtigten Benutzern zur Verfügung steht https://doi.org/10.1007/978-3-658-26480-2_2.
Der Mikroprozessor – oft wird auch im Deutschen der englische Begriff „Microcontroller oder nur „Controller
verwendet – ist die Hardwareeinheit, auf der Embedded Software zur Ausführung kommt. Ohne das Verständnis des prinzipiellen Aufbaus von Mikroprozessoren ist eine Optimierung des Timings von Embedded Software kaum möglich.
Dieses Kapitel vermittelt die Grundlagen der Mikroprozessortechnik mit Blick auf die für das Timing relevanten Aspekte. Zusammen mit dem Datenblatt des jeweils verwendeten Mikroprozessors bildet es die notwendige Grundlage für die Entwicklung effizienter Embedded Software. An vielen Stellen werden allgemeine Grundlagen durch konkrete Beispiele verdeutlicht. Die für die Beispiele verwendete Spanne an Mikroprozessoren reicht dabei von kleinen 8 Bit Controllern bis 32 Bit Prozessoren, von Single- bis Multicore.
Nachfolgende Kapitel, insbesondere Kap. 7 „Timing bei Multicore, Manycore, Multi-ECU und Kap. 8 „Laufzeitoptimierung
, setzen die Grundlagen der Mikroprozessortechnik voraus.
2.1 Aufbau von Mikroprozessoren
Abb. 2.1 zeigt den schematischen Aufbau eines Mikroprozessors, hier am Beispiel eines Dualcoreprozessors, also eines Mikroprozessors mit zwei Kernen.
Dargestellt sind die beiden Rechenkerne, verschiedene Speicher, Peripherieeinheiten sowie zwei verschiedene Busse. Nicht alle Prozessoren folgen diesem Aufbau. Die Allgemeingültigkeit steht bei dieser Darstellung weniger im Mittelpunkt als eine einfache Verständlichkeit. Für den Begriff „Rechenkern wird im Folgenden meist nur „Kern
oder CPU (Central Processing Unit, zentrale Recheneinheit) verwendet.
Abb. 2.1
Blockdiagram eines Dualcoreprozessors
2.1.1 CISC vs. RISC
CISC steht für Complex Instruction Set Computer und beschreibt Prozessoren, die komplexe Befehle mit vergleichsweise viel Funktionalität bieten. Für die Umsetzung wird eine entsprechend komplexe und somit auch kostspielige Hardware benötigt. Weiterhin haben CISC die Eigenart, dass die Befehle unterschiedlich lange für die Ausführung brauchen.
RISC steht für Reduced Instruction Set Computer. Die Maschinenbefehle solcher Prozessoren sind einfach, benötigen wenige Transistoren und veranschlagen für die Ausführung meist die gleiche Zeit.
2.1.2 Register
Jeder Prozessor hat in seiner Ausführeinheit (Englisch „execution unit") einen Satz spezieller Speicherzellen, die sogenannten Register. Im Folgenden sollen einige der Register genauer beschrieben werden.
Befehlszeiger
Der Befehlszeiger (Englisch „program counter, im Folgenden PC) wird oft auch „instruction pointer
(IP) genannt. Jeder Befehl im Programmspeicher hat eine bestimmte (Speicher-) Adresse. Der PC enthält die Adresse des Befehls, der gerade abgearbeitet wird. Weitere Details der Befehlsausführung werden im folgenden Abschnitt „(Speicher-) Adressierung, Adressierungsart" behandelt.
Datenregister
Die Datenregister werden für logische Operationen, zum Rechnen und für Lese- und Schreiboperationen aus den Speichern beziehungsweise in die Speicher herangezogen. In dem Codebeispiel, das in Abb. 1.4 ersichtlich ist, addiert der Prozessor 1 zum Wert in Datenregister d2 und speichert das Ergebnis in Datenregister d15. Siehe den Code bei Label .L35:
add16 d15,d2,#0x1
Nach der Addition wird im nächsten Befehl der Inhalt von d15 in den Speicher geschrieben.
Akkumulator
Insbesondere RISC Prozessoren haben ein besonderes Datenregister, den Akkumulator, der für die meisten Logik- und Rechenoperationen herangezogen wird.
Adressregister
Adressregister werden verwendet, um Daten von Speichern zu lesen, Daten in Speicher zu schreiben, indirekte Sprünge auszuführen oder Funktionen indirekt aufzurufen. Im folgenden Abschnitt „(Speicher-) Adressierung, Adressierungsart" werden Sprünge und Funktionsaufrufe näher behandelt.
Nicht alle Prozessoren unterscheiden zwischen Adressregistern und Datenregistern.
Statusregister
Das Statusregister wird auch als „program status word (PSW), „condition code register
(CCR) oder „flag register bezeichnet. Es ist eine Ansammlung von Bits, die jeweils einen bestimmten Zustand anzeigen. Jedes Bit fungiert wie eine Flagge (Englisch „flag
) und wird meist mit einem oder zwei Buchstaben abgekürzt. Welche Zustände das im einzelnen sind, ist abhängig vom verwendeten Prozessor, wobei die folgenden Flags bei den meisten Architekturen anzutreffen sind.
IE, Interrupt Enable Flag
zeigt an, ob Interrupts generell freigegeben ( $$IE=1$$ ) oder generell gesperrt sind ( $$IE=0$$ ). Damit Interrupts zum Zuge kommen, müssen weitere Voraussetzungen erfüllt sein. Abschn. 2.7 geht näher auf Interrupts ein.
IP, Interrupt Pending Flag
zeigt an, ob ein Interrupt zur Bearbeitung ansteht ( $$IP=1$$ ) oder ob kein Interrupt „pending" ist ( $$IP=0$$ ).
Z, Zero Flag
zeigt an, ob das Ergebnis der zuletzt ausgeführten logischen oder arithmetischen Funktion Null war ( $$Z=1$$ ) oder nicht ( $$Z=0$$ ).
C, Carry Flag
wird für Überläufe beziehungsweise Überträge bei Rechenoperationen sowie bei logischen Operationen herangezogen. Werden beispielsweise bei einem 8 Bit Prozessor die beiden Zahlen 0xFF und 0xFF addiert, repräsentiert das Carry Flag das neunte Bit. Die führende „1", das MSB (most significant bit) des Ergebnisses 0x1FE steht im Carry Flag während die restlichen acht Bits 0xFE im Ergebnisregister landen.
Bei einer Addition mit Carry dagegen wird das Carry Flag wie ein Übertrag aus einer vorherigen Addition verwendet. Ist das Carry Flag gesetzt und wird eine solche Addition der beiden Zahlen 3 und 4 vorgenommen, ist das Ergebnis somit 8.
Aktuelle CPU Priorität
Dieser Wert (im Englischen die „current CPU priority") findet sich bei einigen Prozessoren als eigenes Register wieder, bei anderen ist er Teil des Staturegisters. Die Priorität des aktuell ausgeführten Codes entscheidet darüber, ob ein Interrupt angenommen wird oder nicht. Nur wenn der Interrupt eine höhere Priorität als die des aktuell ausgeführten Codes aufweist, unterbricht er den aktuell ausgeführten Code – vorausgesetzt, Interrupts werden generell zugelassen ( $$IE=1$$ ).
Stackpointer
Der Stackpointer beinhaltet eine Adresse, welche die aktuelle Grenze des bisher benutzten Stacks markiert.
2.2 Codeabarbeitung
Abschn. 1.3 Buildprozess: vom Modell zum Executable
hat gezeigt, wie der ausführbare Maschinencode erzeugt wird und dass es sich bei diesem Code um eine Ansammlung von Maschinenbefehlen handelt. Der Rechenkern eines Mikroprozessors arbeitet fortwährend Maschinenbefehle ab. Dazu werden diese Befehle sequentiell vom Programmspeicher (Englisch „program-memory oder „code-memory
) in die Ausführungseinheit geladen, dort dekodiert und dann ausgeführt.
Vom Befehlszeiger (PC) war bereits die Rede, er „zeigt" quasi auf den aktuellen Befehl im Programmspeicher. Solange keine Sprungbefehle oder Befehle für einen (Unter-) Funktionsaufruf vorliegen, wird mit jeder Abarbeitung eines Befehls der PC um eine Speicherstelle erhöht. Dadurch zeigt der PC auf den nächsten Befehl, der wiederum in die Ausführungseinheit geladen, dort dekodiert und ausgeführt wird. Der Programmspeicher ist in erster Linie eine Aneinanderreihung von Maschinenbefehlen.
An der Stelle sei schon einmal erwähnt, dass eine Serie von Maschinenbefehlen ohne Sprung Basisblock (Englisch „basic block") genannt wird. Genauer: ein Basisblock ist eine Serie von Maschinenbefehlen, in die nicht hineingesprungen wird und aus der nicht heraus gesprungen wird. Die Befehle eines Basisblocks werden also ausnahmslos alle sequentiell beginnend mit dem ersten Befehl abgearbeitet. Basisblöcke spielen unter anderem bei der statischen Codeanalyse eine wichtige Rolle, daher wird das Thema später noch einmal aufgegriffen werden.
Die Befehle, die ein Prozessor bietet, sind im Handbuch des Befehlssatzes (Englisch „Instruction Set Reference") des Prozessors beschrieben. Für eine weitgehende Optimierung von Software auf der Codeebene ist die Kenntnis des Befehlssatzes des jeweiligen Prozessors unerlässlich. Abschn. 8.3 „Laufzeitoptimierung auf der Codeebene" wird im Detail darauf eingehen.
../images/478274_1_De_2_Chapter/478274_1_De_2_Fig2_HTML.pngAbb. 2.2
Auszug aus der Befehlsreferenz des AVR ATmega Prozessors [1]
Am Beispiel eines Addierbefehls beim 8 Bit Microchip AVR Prozessor soll die Codierung eines Befehls und der Umgang mit der Dokumentation des Befehlssatzes veranschaulicht werden. Microchip AVR Prozessoren haben 32 Daten-/Adressregister. Abhängig vom Befehl dienen sie mal als Datenregister, mal als Adressregister. Abb. 2.2 zeigt einen Auszug (genau eine Seite) aus der Befehlsreferenz des Microchip AVR ATmega Prozessors [1], nämlich den Abschnitt, der den Addierbefehl mit Carry Flag beschreibt. Der Beschreibung in textueller und formaler Form (
$$Rd \leftarrow Rd + Rr + C$$) folgt die Definition der Syntax. Dies ist der Befehl in genau der Schreibweise, wie sie sich auch im Assemblercode findet. Ein solcher „Assemblercodebefehl" wird auch Mnemonic genannt (Englisch für „Gedächtnisstütze"). Die Tabelle darunter lässt den Opcode des Befehls erkennen, also durch welchen Wert im Speicher der Befehl repräsentiert wird. In diesem Fall sind die sechs höchstwertigen Bits fest (binär 000111) und die restlichen zehn Bits definieren, welche Register miteinander addiert werden sollen: die mit „d" bezeichneten Bitpositionen für Register Rd, die mit „r" bezeichneten für Register Rr. Beispiel: sollen die Register R3 und R22 addiert werden und soll das Ergebnis in R3 gespeichert werden, sieht der Opcode wie in Listing 2.1 dargestellt aus. Wann immer im Assemblercode adc r3,r22 zu finden ist, wird an der entsprechenden Stelle im Programmspeicher eine 0x1D63 stehen.
../images/478274_1_De_2_Chapter/478274_1_De_2_Figa_HTML.pngTipp
Die im Listing 2.1 dargestellten Kommentare zur Erläuterung der Bitcodierung haben sich bei der Programmierung sehr gut bewährt. Wann immer Informationen binär codiert sind und der Erklärung bedürfen, sind Kommentare dieser Art äußerst hilfreich. Die Bitposition wird über zwei Kommentarzeilen kenntlich gemacht: eine mit den Zehnern und eine mit den Einern. Die Bitposition wird nun einfach von oben nach unten gelesen, zum Beispiel