C-Laufzeitumgebung (ARM) verstehen - wo soll ich anfangen?

8

Ich bin ein embedded Entwickler, der hauptsächlich mit ARM Cortex-M Geräten arbeitet. Vor kurzem habe ich zu Linux gewechselt und beschlossen, mehr über den Build / Assemble / Link-Prozess zu lernen, wie man Makefiles etc. schreibt, während ich IDEs (IAR, Keil, Eclipse etc.) benutzte, wo viele Dinge automatisiert sind und der Benutzer hat eigentlich keine Ahnung was im Hintergrund passiert. Das Ziel besteht darin, mehr von diesem Low-Level-Prozess zu verstehen, wie man die Tools richtig verwendet und nicht nur auf die IDE-Standardeinstellungen angewiesen ist.

Nachdem ich ein Makefile geschrieben habe, konnte ich meine Applikation erstellen. Ich entschied mich jedoch, den Linkprozess manuell durchzuführen, indem ich den Linker direkt (nicht über Compiler) aufruft und überraschenderweise Probleme auftraten! Nicht definierte Verweise auf libc, _init Funktion in __libc_init_array, _exit usw. waren die Probleme. Nach dem ganzen Tag der Untersuchungen konnte ich alle Objektdateien (crt0.o, crti.o) und Bibliotheken (libnosys.a) manuell einbinden. Scheinbar macht der Compiler das Zeug automatisch.

Nachdem ich diese Schwierigkeiten überwunden hatte, fand ich heraus, dass ich von diesen Interna absolut keine Ahnung habe. Warum brauche ich einige ctr0.o, crti.o usw. Wo diese herkommen usw. Ist es mit den Compiler / Linker oder C-Laufzeitbibliotheken oder dem System verbunden?

Ich würde gerne mehr über diese Interna erfahren, aber ich bin mir nicht sicher, wonach ich eigentlich suche. Ist es eine Bibliothek, System, Compiler alle zusammen?

Ich verstehe, dass das System (MCU) die Variablen im RAM und andere Sachen initialisieren muss, aber ich vermisse das komplette Bild. Kannst du mich zu einem guten Buch / Manuals, Lesungen über diese Dinge, bitte? Wonach suche ich eigentlich?

Bearbeiten:

Nach Gesprächen mit Ihnen habe ich wahrscheinlich herausgefunden, was ich brauche, also würde ich meine Frage folgendermaßen umformulieren:

1) Ich habe eine MCU erhalten (sagen wir STM32F4xx) und ich sollte ein blinkendes LED-Beispiel erstellen. All dies sollte von Grund auf neu gemacht werden, eigenen Startup-Code, keine Verwendung externer Bibliotheken etc ..

2) Im zweiten Schritt hat mir jemand gesagt, dass all das bereits von anderen gemacht wurde (GCC-Toolchain / Standard-Bibliotheken, MCU-Hersteller-Startup-Dateien usw.). Also muss ich meine Arbeit nur mit dem, was getan wurde, verstehen / verknüpfen und die Unterschiede vergleichen, warum sie es so machen, etc.

All dies wurde von @mbjoe beantwortet + Ich fand eine interessante Lektüre: Ссылка

Ich danke Ihnen allen für Ihre Hilfe und leite mich richtig!

    
ST Renegade 27.04.2017, 06:48
quelle

5 Antworten

2

Die Module, auf die Sie sich beziehen (ctr0.o, crti.o, _init, __libc_init_array, _exit) sind vorgefertigte Bibliotheken / Objektdateien / Funktionen von IAR und / oder Keil. Wie Sie sagen, werden sie benötigt, um die Umgebung initialisiert zu bekommen (globale Variableninitialisierung, Interruptvektortabelle, etc.), bevor Sie Ihre main () Funktion ausführen.

Irgendwann in diesen Bibliotheken / Objektdateien wird es eine Funktion in C oder Assembly geben:

%Vor%

Sie können diese Beispiele betrachten, die den Startcode von Grund auf neu erstellen:

Ссылка

Ссылка

    
pebkac 27.04.2017, 08:27
quelle
2
  

1) Ich habe eine MCU erhalten (sagen wir STM32F4xx) und ich sollte eine erstellen   blinkende LED Beispiel. All dies sollte von Grund auf neu gemacht werden   Startcode, keine Verwendung externer Bibliotheken etc.

Ich habe eine MCU sagen ein STM32F4xx und ich möchte die LED auf PA5 ohne Bibliotheken blinken, von Grund auf neu, nichts externes.

blinker01.c

%Vor%

flash.s

%Vor%

linker Skript flash.ld

%Vor%

Dies ist alles mit gcc / gnu-Tools

%Vor%

um sicherzustellen, dass es richtig booten wird und es richtig verlinkt ist, überprüfen Sie die Vektortabelle von der Listendatei

%Vor%

das sollten ungerade Zahlen sein, die Handleradresse orred mit einem

%Vor%

und beginnen bei 0x08000000 im Falle dieser STM32 Teile (einige Hersteller bauen Sie für Null) (beim Einschalten Null wird gespiegelt von 0x08000000, so dass der Vektor Sie an die richtige Stelle in Flash bringt).

Was die LED angeht, machen Sie den gpio Pin einen Push-Pull-Ausgang und schalten ihn aus und wieder ein. In diesem Fall brennen Sie einige CPU-Zyklen und ändern dann den Status. Durch die Verwendung einer Funktion, die nicht in der blinker01.c enthalten ist, zwingt es den Compiler, diese Zählungen durchzuführen (anstatt eine flüchtige Sache zu machen), einfacher Optimierungstrick. PUT32 / GET32 persönliche Präferenz, die Versicherung der richtigen Anweisung verwendet wird, verwenden Compiler nicht immer die richtige Anweisung und wenn die Hardware eine bestimmte Größe erfordert, könnten Sie in Schwierigkeiten geraten. Abstrahieren hat mehr Vorteile als Nachteile, IMO.

Ziemlich einfach zu konfigurieren und zu verwenden diese Teile. Gut, um es so zu lernen, genauso wie die Bibliotheken, professionell müssen Sie vielleicht mit beiden Extremen umgehen, vielleicht werden Sie derjenige, der die Bibliotheken für andere schreibt und beide gleichzeitig wissen muss.

Das Wissen über Ihre Werkzeuge ist das Wichtigste und ja, die meisten Leute wissen nicht, wie man das in diesem Geschäft macht, sie verlassen sich auf ein Werkzeug, arbeiten um die Warzen des Werkzeugs oder der Bibliothek, anstatt zu verstehen, was vor sich geht und / oder repariere es. der Punkt dieser Antwort ist 1) Sie gefragt und 2) zu zeigen, wie einfach es ist, die Werkzeuge zu verwenden.

hätte es noch einfacher machen können, wenn ich die Funktionen in der Assembly loswerden würde und nur die Assembly als eine sehr einfache Möglichkeit verwenden würde, die Vektortabelle zu erstellen. Der Cortex-m ist so, dass Sie alles in C außer der Vektortabelle (die Sie können, aber es ist hässlich) und dann etwas wie der gut getestete und funktionierende Assembler verwenden, um die Vektortabelle zu erstellen.

Beachten Sie Cortex-M9 vs die anderen

%Vor%

der Kortex-m0 und (m1, wenn Sie einen finden) sind armv6m basiert, wo der Rest armv7m ist, der wie 150 mehr thumb2 Erweiterungen zum Daumen Befehlssatz (früher undefined Anweisungen verwendet, um Anweisungen variabler Länge zu machen). alle Cortex-MS laufen Daumen, aber der Cortex-m0 unterstützt nicht die Armv7m spezifischen Erweiterungen, Sie können den Build ändern, um Cortex-M0 anstelle von M4 zu sagen, und es funktioniert gut auf der M4, nehmen Sie Code wie folgt (Patch up die Adressen nach Bedarf vielleicht der GPIO ist anders für Ihren spezifischen Teil vielleicht nicht) und bauen für m0 wird es auf m0 laufen ... Genau wie die Notwendigkeit, regelmäßig zu überprüfen, um zu sehen, die Vektortabelle wird richtig gebaut, können Sie die prüfen Demontage, um zu sehen, dass der richtige Geschmack von Anweisungen verwendet wird.

    
old_timer 27.04.2017 12:40
quelle
2

Ich habe diesen zweiteiligen Blogpost recht gut und interessant gelesen und erkläre genau die Details, nach denen Sie fragen:

Ссылка
Ссылка

Einige der zentralen Punkte:

  • main() ist nicht der Einstiegspunkt für Ihr Programm.

    Der Kernel / Loader ruft überhaupt keine Funktion auf, sondern richtet den virtuellen Adressraum ein, legt einige Daten auf dem Stack ab und startet dann den Prozess an einer Adresse, die von der ausführbaren Datei angegeben wird.

  • Ihr Programm kann nicht als Funktion zurückgeben .

    Dies ist eine direkte Konsequenz aus dem obigen Punkt: Es gibt einfach keine Rückkehradresse auf dem Stapel, zu der das Programm zurückkehren könnte. Stattdessen muss der Prozess einen Syscall machen, um den Kernel zu zu bitten, den Prozess zu zerstören. Dieser Syscall ist der exit_group() Syscall, um genau zu sein.

    Dies geschieht, indem ein Software-Interrupt erstellt wird, der die Ausführung eines Kernel-Modus-Interrupt-Handlers bewirkt. Dieser Interrupt-Handler wird dann die Kernel-Datenstrukturen manipulieren, um den Prozess zu zerstören und zu verwerfen und die Ressourcen, die er hielt, freizugeben. Während der Effekt dem eines Funktionsaufrufs ziemlich ähnlich ist (der nie zurückkehrt), sind die CPU-Mechanismen, die hier verwendet werden, ziemlich unterschiedlich.

    Beachten Sie, dass Sie keine Verbindung zu einer Bibliothek herstellen müssen, um einen syscall zu erstellen. Der syscall ist einfach eine Anleitung zum Laden der syscall-Argumente in CPU-Register, gefolgt von einer Interrupt-Anweisung. Die _exit() -Funktion, die Sie bei Ihren Verknüpfungsversuchen übersehen haben, ist nicht der Syscall, sondern nur ein Wrapper. C weiß nicht, was ein Syscall ist, die libc-Wrapper müssen Spracherweiterungen verwenden, um einen Syscall erstellen zu können. Deshalb verlinken Sie in der Regel gegen die libc und rufen ihre syscall wrappers auf, anstatt sie direkt zu machen: Sie isoliert Sie mit den in der Implementierung definierten Details eines Syscalls.

  • Der libc-Code, der vor und nach dem Aufruf von main() ausgeführt wird, sorgt im Allgemeinen dafür, dynamische Bibliotheken zu laden, statische Daten zu initialisieren (falls erforderlich), Funktionen aufzurufen, die mit __attribute__((constructor)) oder __attribute__((destructor)) gekennzeichnet sind die mit atexit() usw. registriert wurden.

cmaster 27.04.2017 13:19
quelle
1

Das ist eine ziemlich große Frage, aber ich werde versuchen, sie zu beantworten und Ihnen einen Überblick über alle Schritte zu geben, die erforderlich sind, um eine "Hallo Welt" in eine tatsächliche ausführbare Armdatei zu verwandeln. Ich werde mich auf die Befehle konzentrieren, um jeden Schritt zu zeigen, anstatt jedes einzelne Detail zu erklären.

%Vor%

Ich werde gcc auf Ubuntu 17.04 für dieses Beispiel verwenden. arm-none-eabi-gcc (15:5.4.1+svn241155-1) 5.4.1 20160919

1. Vorverarbeitung

Es kümmert sich grundsätzlich um jede Zeile, die mit einem # beginnt. Um die Ausgabe des Präprozessors anzuzeigen, verwenden Sie arm-none-eabi-gcc -E oder arm-none-eabi-cpp .

  

arm-none-eabi-gcc -E haupt.c

Die Ausgabe ist sehr lang wegen all der Dinge, die passieren, wenn Sie #include <stdio.h> und immer noch "unlesbare" Zeilen wie # 585 "/usr/include/newlib/stdio.h" 3

enthalten

Wenn Sie die Argumente -E -P -C verwenden, wird die Ausgabe viel klarer.

  

arm-none-eabi-gcc -E -P -c main.c -o main-preprocessed.c

Jetzt können Sie sehen, dass #include gerade alle Inhalte von stdio.h in Ihren Code kopiert hat.

2. Kompilieren

Dieser Schritt übersetzt die vorverarbeitete Datei in Assembleranweisungen, die noch für Menschen lesbar sind. Um den Maschinencode zu erhalten, verwenden Sie -S .

  

arm-keine-eabi-gcc -S main.c

Sie sollten mit einer Datei namens main.s enden, die Ihre Montageanweisungen enthält.

3. Montage

Jetzt beginnt es viel weniger lesbar zu werden. Übergeben Sie -c an gcc , um die Ausgabe anzuzeigen. Dieser Schritt ist auch der Grund, warum Inline-Montage möglich ist.

  

arm-keine-eabi-gcc -c main.c

Sie sollten mit einer main.o Datei enden, die mit hexdump oder xxd angezeigt werden kann. Ich würde xxd empfehlen, weil es Ihnen die ASCII-Darstellung neben den rohen hexadezimalen Zahlen zeigt.

  

xxd main.o

4. Verknüpfung

Die letzte Stufe, danach ist Ihr Programm bereit, vom Zielsystem ausgeführt zu werden. Der Linker fügt den "fehlenden" Code hinzu. Zum Beispiel gab es keine Anzeichen für die Funktion printf() oder irgendetwas von stdio.h .

  

arm-keine-eabi-gcc main.c --specs = nosys.specs -o main

Für die --specs=nosys.specs siehe hier: Ссылка

Dies ist nur ein ungefährer Überblick, aber Sie sollten in der Lage sein, hier auf stackoverflow viel mehr Informationen zu jedem Schritt zu finden. (Beispiel für den Linker: Was machen Linker? )

    
Skynet 27.04.2017 07:52
quelle
0

Es ist nicht so einfach, zumindest sollte es nicht so einfach gemacht werden.

Da Sie die Sprache C verwenden, müssen einige Voraussetzungen für den Startcode erfüllt sein.

  1. Sie müssen Stack und Heap in Betracht ziehen und rapperly einrichten.
  2. Mindestens eine minimalistische Vektortabelle
  3. C-Daten können initialisiert werden (zB können Sie int a = 5; deklarieren), so dass der Startup-Code die Kopierroutine zur Verfügung stellen sollte (aus dem Flash-Ram-Segment)
  4. C setzt voraus, dass die nicht initialisierten globalen Daten auf Null gesetzt sind - die Startup-Routine sollte diese Funktion ebenfalls haben
  5. Abhängig von Ihrer Anwendung ist möglicherweise zusätzlicher Code erforderlich.

Sie müssen also mindestens erstellen (um C ohne besondere Einschränkungen verwenden zu können) - eine Startroutine und das Linker-Skript, in dem Sie Speicherabschnitte, ihre Größen, Grenzen deklarieren, Anfangs- und Endadressen für Initialisierungsroutinen berechnen .

Meiner Meinung nach ist es sinnlos, den Grundstein zu legen - Sie können die mitgelieferten Skripte und Startup-Dateien jederzeit ändern. Für ARM uC CMSIS ist wahrscheinlich die beste Wahl, da es Ihnen absolute Freiheit gibt.

    
PeterJ_01 28.04.2017 12:27
quelle

Tags und Links