Eingebettet: memcpy/memset wird von den meisten CRT-Startcodes nicht verwendet – warum?

Lesezeit: 9 Minuten

Kontext:

Ich arbeite an einem ARM-Target, genauer gesagt einem Cortex-M4F-Mikrocontroller von ST. Bei der Arbeit auf solchen Plattformen (Mikrocontroller im Allgemeinen) gibt es offensichtlich kein Betriebssystem; Um eine funktionierende C/C++-“Umgebung” zu erhalten (außerdem um standardkonform in Bezug auf die Initialisierung von Variablen zu sein), muss beim Zurücksetzen eine Art Startcode ausgeführt werden, der die erforderlichen Mindesteinstellungen vor dem expliziten Aufruf vornimmt main. Ein solcher Startcode muss, wie ich angedeutet habe, initialisierte globale und statische Variablen (wie z int foo = 42;im globalen Bereich) und die anderen globalen Werte auf Null setzen (z int bar; auf globaler Ebene). Dann werden ggf. globale „Ctors“ aufgerufen.

Auf einem Mikrocontroller bedeutet das einfach, dass der Startcode für jedes initialisierte Global (alles im Abschnitt „.data“) Daten vom Flash in den RAM kopieren und die anderen löschen muss (alles in „.bss“). Da ich GCC verwende, muss ich einen solchen Startcode bereitstellen, und ich habe gerne mehrere Startcodes (und das zugehörige Linker-Skript!) Analysiert, gebündelt mit zahlreichen Beispielen, die ich im Internet gefunden habe, die alle dasselbe Demoboard verwenden, auf dem ich entwickle .

Frage:

Wie bereits erwähnt, habe ich zahlreiche Startcodes gesehen, und sie initialisieren Globals auf unterschiedliche Weise, einige effizienter in Bezug auf Raum und Zeit als andere. Aber sie alle haben etwas Seltsames gemeinsam: Sie nicht verwenden memset Noch memcpy, greifen stattdessen auf handgeschriebene Schleifen zurück, um die Arbeit zu erledigen. Da es mir selbstverständlich erscheint, wenn möglich Standardfunktionen zu verwenden (einfaches “DRY-Prinzip”), habe ich anstelle der anfänglichen handgeschriebenen Schleifen Folgendes versucht:

/* Initialize .data section */
ldr r0, DATA_LOAD
ldr r1, DATA_START
ldr r2, DATA_SIZE
bl  memcpy       /* memcpy(DATA_LOAD, DATA_START, DATA_SIZE); */

/* Initialize .bss section */
ldr r0, BSS_START
mov r1, #0
ldr r2, BSS_SIZE
bl  memset       /* memset(BSS_START, 0, BSS_SIZE); */

… und es hat perfekt funktioniert. Die Platzersparnis ist vernachlässigbar, aber es ist jetzt eindeutig kinderleicht.

Also habe ich darüber nachgedacht und sehe keinen Grund, in diesem Fall handgeschriebene Schleifen zu machen:

  • memcpy und memset sind sehr wahrscheinlich sowieso in der ausführbaren Datei verlinkt, da der Programmierer sie direkt oder indirekt über eine andere Bibliothek verwenden würde;
  • Es ist kleiner;
  • Geschwindigkeit ist kein sehr wichtiger Faktor für Startcode, aber dennoch ist es wahrscheinlich schneller;
  • Es ist fast unmöglich, es falsch zu machen.

Irgendeine Idee, warum man sich nicht darauf verlassen würde memcpy und memset für Startcode?

  • Vielleicht ist für den Startcode “sehr wahrscheinlich verlinkt” nicht gut genug?

    – David Gelhar

    17. März 2013 um 3:13 Uhr

  • dwelch: erkläre dich. Sie haben keine Erklärung dafür geliefert, warum alle meine Annahmen falsch sind. Vielleicht könnten Sie noch ein paar aussagekräftigere Erkenntnisse hinzufügen?

    – Jarhmander

    17. März 2013 um 15:11 Uhr

  • @dwelch: Entschuldigung für die Flagge. Und ich weiß nicht, warum du deine Beiträge gelöscht hast. Wenn ich Sie kontaktieren könnte, würde ich mich erklären. Vielleicht ist meine Frage doch nicht sehr gut.

    – Jarhmander

    17. März 2013 um 16:07 Uhr


  • Die Antwort auf Ihre Frage ist sehr einfach. Es ist ein Henne-Ei-Problem, Sie versuchen, C-Code zu verwenden, um C-Code zu booten. Sie können nicht davon ausgehen, dass der von Ihnen verwendete C-Code keinen Bootstrap erfordert. Annahmen sind keine Tatsachen, sondern nur Meinungen. Wenn Sie den C-Code nicht selbst geschrieben oder persönlich validiert haben (oder noch besser, nur die paar Zeilen von asm schreiben und sich überhaupt keine Sorgen machen), können Sie KEINE FAKT-Aussagen darüber machen. Der FAKT ist der einzige Weg, um sicherzustellen, dass Sie das Henne-Ei-Problem gelöst haben, indem Sie den Code selbst schreiben, der asm ist so trivial und klein, verwenden Sie einfach den asm

    – Oldtimer

    17. März 2013 um 17:04 Uhr

  • Dies ist eine sehr gute Frage, und die Antwort ist sicherlich nicht so offensichtlich, wie einige Befragte behaupten. Während die Schlussfolgerung lautet, dass die Verwendung von memset und memcpy in diesem Zusammenhang wahrscheinlich keine gute Idee ist, denke ich andererseits nicht, dass diese Schleifen in Assembler geschrieben werden müssen. SystemInit() ist normalerweise in C geschrieben, und der generierte Code von ST ruft es oft auf nach Initialisierung von .bss. und .data. Bei einigen neueren ST-MCUs müssen jedoch einige Uhren aktiviert werden, bevor einige Teile des internen RAM überhaupt verfügbar sind, und die Aktivierung dieser Uhren würde normalerweise in C in SystemInit() erfolgen!

    – Nilo

    22. Mai 2020 um 18:07 Uhr


Ich vermute, der Startcode will keine Annahmen über die Implementierung treffen memcpy und so in libc. Zum Beispiel die Umsetzung von memcpy könnte eine globale Variable verwenden, die durch den libc-Initialisierungscode gesetzt wird, um zu melden, welche CPU-Erweiterungen verfügbar sind, um ein optimiertes SIMD-Kopieren auf Maschinen bereitzustellen, die solche Operationen unterstützen. An dem Punkt, an dem der frühe „crt“-Startcode ausgeführt wird, ist der Speicher für ein solches Global möglicherweise vollständig nicht initialisiert (enthält zufälligen Müll), in diesem Fall wäre es gefährlich, ihn aufzurufen memcpy. Auch wenn Sie telefonieren arbeitet für Sie, es ist eine Folge der Implementierung (oder vielleicht sogar der unvorhersehbaren Ergebnisse von UB …), damit es funktioniert; Dies ist wahrscheinlich nicht etwas, von dem der Crt-Code abhängig sein möchte.

  • Jawohl! Alternative, memcpy oder memset könnte SIMD/FPU-Anweisungen verwenden, die faul aktiviert sind. Sind die Handler zu Beginn des Starts vorhanden, um den Fehler zu beheben, der bei der ersten Verwendung einer solchen Anweisung auftritt? Sind die kontextsicheren Bereiche, die der Fault-Handler zum Einrichten schreiben muss?

    – Stefan Kanon

    17. März 2013 um 15:40 Uhr


  • Es gibt auch andere mögliche Probleme – die MMU oder CPU ist möglicherweise noch nicht so konfiguriert, dass sie falsch ausgerichteten Zugriff oder die von der verwendeten Übertragungsbreiten unterstützt libc Routinen usw. Es gibt viele sehr gute Gründe, nur Code zu verwenden, den Sie früh beim Start direkt steuern.

    – Stefan Kanon

    17. März 2013 um 15:47 Uhr


  • Interessanter Punkt. Natürlich ist in einem so frühen Stadium noch gar nichts eingerichtet, und wenn es fehlschlägt, wird es wahrscheinlich in einem Hardfault-Handler hängen bleiben.

    – Jarhmander

    17. März 2013 um 15:51 Uhr

  • Tatsächlich haben Sie auf einem Cortex-M Ihre Vektoren und Ihren Stack direkt auf Anhieb …

    – LThode

    4. Februar 2015 um 19:46 Uhr

Benutzer-Avatar
Clifford

Ob die Standardbibliothek überhaupt eingebunden wird, entscheidet der Anwendungsentwickler (--nostdlib kann verwendet werden), aber der Startcode ist erforderlich, kann also keine Annahmen treffen.

Ferner besteht der Zweck des Startcodes darin, eine Umgebung einzurichten, in der C-Code ausgeführt werden kann; Bevor dies abgeschlossen ist, ist es keineswegs selbstverständlich, dass irgendein Bibliothekscode, der vernünftigerweise davon ausgehen könnte, dass eine vollständige Laufzeitumgebung korrekt ausgeführt wird. Für die betreffenden Funktionen ist das vielleicht in vielen Fällen kein Thema, aber das kann man ja nicht wissen.

Der Startcode muss mindestens einen Stack aufbauen und statische Daten initialisieren, in C++ ruft er zusätzlich die Konstruktoren globaler statischer Objekte auf. Die Standardbibliothek könnte vernünftigerweise davon ausgehen, dass diese eingerichtet sind, also die Standardbibliothek verwenden Vor kann dann möglicherweise zu fehlerhaftem Verhalten führen.

Schließlich sollten Sie sich darüber im Klaren sein, dass die C-Sprache und die C-Standardbibliothek unterschiedliche Einheiten sind. Die Sprache muss notwendigerweise für sich alleine stehen können.

  • Saubere, prägnante Antwort. Das ist definitiv ein Teil der Antwort.

    – Jarhmander

    17. März 2013 um 15:17 Uhr


  • Ich habe weitere Punkte hinzugefügt – vielleicht zu Lasten der Prägnanz und Klarheit.

    – Clifford

    17. März 2013 um 19:41 Uhr

  • Überhaupt nicht, es ist sogar noch besser. Vielen Dank 🙂

    – Jarhmander

    18. März 2013 um 0:37 Uhr

Ich glaube nicht, dass dies wahrscheinlich etwas mit “Annahmen über den internen Zustand von Memcy/Memset” zu tun hat, es ist unwahrscheinlich, dass sie globale Ressourcen verwenden (obwohl ich annehme, dass es einige seltsame Fälle gibt, in denen sie dies tun).

Der gesamte Startcode auf Mikrocontrollern wird normalerweise auf diese Weise als “Inline-Assembler” geschrieben, einfach weil er in einem frühen Stadium des Codes ausgeführt wird, in dem möglicherweise noch kein Stapel vorhanden ist und das MMU-Setup möglicherweise noch nicht ausgeführt wurde. Init-Code möchte daher nicht riskieren, etwas auf den Stack zu legen, so einfach ist das. Funktionsaufrufe legen Dinge auf den Stack.

Während dies also zufällig der Initialisierungscode des Static-Storage-Copydowns war, werden Sie wahrscheinlich denselben Inline-Assembler auch in einem anderen solchen Init-Code finden. Zum Beispiel werden Sie wahrscheinlich irgendwo vor dem Herunterkopieren einen grundlegenden Register-Setup-Code finden, der in Assembler geschrieben ist, und Sie werden auch das MMU-Setup in Assembler irgendwo dort finden.

  • Stimmt über den Stack; Eine nette Sache bei der Cortex M-Serie ist jedoch, dass der Stack beim Zurücksetzen initialisiert wird (der Stack-Zeiger wird mit dem ersten Wort des Interrupt-Vektors geladen), so dass es sicher ist, den Stack direkt beim Programmstart zu verwenden. Aber ich habe verstanden, dass man im Allgemeinen beim Start keine C-Bibliothek aufrufen würde, weil es irgendwie auf die Initialisierung angewiesen sein könnte. Fürs Protokoll, ich habe mir die Implementierung von memcpy/memset mit objdump angesehen und sie scheinen effektiv keine Globals wie erwartet zu verwenden. Aber das bedeutet natürlich nicht, dass das keine andere Implementierung tut.

    – Jarhmander

    18. März 2013 um 13:55 Uhr

  • +1 In Bezug auf die MMU. Das memset() und memcpy() müssen möglicherweise verschoben werden. Dies ist im Assembler einfach zu gewährleisten. Außerdem bevorzugen Benutzer ein schnelles Hochfahren gegenüber sauberem Code. Insbesondere ist die Hardware zu diesem Zeitpunkt möglicherweise nicht vollständig initialisiert, und es ist in vielen Fällen wichtig, schnell zu diesem Code zu gelangen.

    – ungekünstelter Lärm

    18. März 2013 um 18:59 Uhr

  • Downvoting, weil Cortex-Ms einen in C geschriebenen Bootstrap haben können (Sie verwenden einfach naive Copy-and-Set-Schleifen, um mit dem Einrichten fertig zu werden .data und .bss)

    – LThode

    4. Februar 2015 um 19:47 Uhr

  • @LThode Das liegt nur daran, dass Cortex M den MSP aus dem festlegt, was beim Start unter Adresse 0 gespeichert ist. Die meisten MCUs haben eine explizite Assembler-Anweisung zum Setzen des Stapelzeigers, was es buchstäblich unmöglich macht, den gesamten Startcode in C zu schreiben. Fortgeschrittenere Architekturen erfordern, dass Sie verschiedene Speichereinstellungen festlegen, bevor Sie überhaupt wissen, wo der Stapel platziert werden kann – in diesen Fällen Sie müssen den Code in Inline-ASM schreiben, nur um zu verhindern, dass der C-Compiler Anweisungen generiert, die den Stack verwenden.

    – Ludin

    5. Februar 2015 um 7:33 Uhr

1176720cookie-checkEingebettet: memcpy/memset wird von den meisten CRT-Startcodes nicht verwendet – warum?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy