Hallo Zusammen,
Ich bin grad dabei, mal hinter die Kulissen der ESP32 APIs zu schauen
und bin da u. A. auf folgende Funktion im bootloader gestoßen: (Trotzdem
sind das natürlich sehr allgemeine Fragen, die ich da habe)
1
#ifndef NDEBUG
2
{
3
assert(&_bss_start<=&_bss_end);
4
assert(&_data_start<=&_data_end);
5
int*sp=get_sp();
6
assert(sp<&_bss_start);
7
assert(sp<&_data_start);
8
}
9
#endif
10
// clear bss section
11
bootloader_clear_bss_section();
--> Die BSS section wird auf 0 gesetzt
(BSS = block starting symbol, https://en.wikipedia.org/wiki/.bss)
In dieser Section werden ja alle statischen Variablen hinterlegt, die
nicht auf 0 initialisiert wurden.
1. Frage: Stimmt das?
2. Frage: Warum werden diese Variablen eigentlich zu Beginn nicht auf 0
initialisiert, was hat das für Vorteile / Nachteile? Meiner Meinung nach
könnte das nur einen Geschwindigkeitsvorteil haben?
(Zum Bild) Hier sieht man ja die Aufteilung der verschiedenen Segmente.
3. Frage: Diese Segmente können ja teilweise "geschützt" werden, damit
man dort nicht mehr herum pfuschen kann und Variablen zum Beispiel
verändern kann (z. B. einen String, der so initialisiert wird: char *s
="ABC";). Stimmt das?
Wenn ja: Wie können dann diese Segmente geschützt werden. Müssen diese
dann in einem anderen Adressraum gespeichert werden wie z. B. der Stack
und der Heap, weil diese müssen ja ständig verändert werden (z. B. Stack
im Adressraum 0x4000000-0x5000000 und BSS im Adressraum
0x1000000-0x2000000...
Dann wären nämlich klare Anweisungen möglich: 0x4000000-0x5000000 nicht
geschützt, 0x1000000-0x2000000 geschützt, ist das so?
Fragen über Fragen...
Vielleicht will mir jemand die ein oder andere davon beantworten ;)
Grüße
Alexander M. schrieb:> In dieser Section werden ja alle statischen Variablen hinterlegt, die> nicht auf 0 initialisiert wurden.
Nein. Da wird im Gegenteil all das hinterlegt, was auf Null
initialisiert wird. Also all der Scheiß, der nicht explizit auf etwas
von Null verschiedenes initialisiert wird.
> 1. Frage: Stimmt das?
Nein.
> 2. Frage: Warum werden diese Variablen eigentlich zu Beginn nicht auf 0> initialisiert
Werden sie ja.
> was hat das für Vorteile
Die Vorteile, alle auf Null zu initialisierenden Variablen zu einem
Block zusammenzufassen sind zwei:
1) Es wird dafür im Executable nur sehr wenig Platz benötigt. Da steht
nur drin, wo der Block beginnen soll und wie groß er sein muss.
2) Ein großer Block lässt sich zur Laufzeit viel schneller mit Nullen
füllen, als viele kleine Vorkommen an weit verstreuten Locations.
> Nachteile?
Was bei einfachen Architekturen nach einer guten Idee aussieht, kann bei
Architekturen mit Datencaches böse nach hinten losgehen. Der Vorteil
wirkt nämlich nur einmal beim Startup der Anwendung, der Nachteil durch
die bezüglich der tatsächlichen Nutzung durch die Anwendung über den
Speicher "verstreuten" Daten aber ständig. Das kann unsäglich teure
dauernde Cache-Reloads auslösen.
Alexander M. schrieb:> (BSS = block starting symbol, https://en.wikipedia.org/wiki/.bss)>> In dieser Section werden ja alle statischen Variablen hinterlegt, die> nicht auf 0 initialisiert wurden.
Anders herum: es werden alle statischen (storage type, unabhängig von
der Sichtbarkeit – "static" hat ja zwei Bedeutungen) Variablen abgelegt,
die (explizit oder implizit) auf 0 initialisiert werden müssen.
Es ist dabei völlig wurscht, ob du
1
staticintfoo;
2
// oder
3
staticintbar=0;
schreibst: beide landen im .bss. Im Unterschied dazu landet
1
staticintmumble=42;
in .data; dafür muss dann nicht nur RAM, sonder im Falle eines
Conntrollers auch Flash reserviert werden mit der Zahl 42, die beim
Programmstart von da in den RAM kopiert wird.
> 1. Frage: Stimmt das?
Nein
> 2. Frage: Warum werden diese Variablen eigentlich zu Beginn nicht auf 0> initialisiert, was hat das für Vorteile / Nachteile?
Sie werden doch zu Beginn auf 0 initialisiert. Oder was dachtest du, was
bootloader_clear_bss_section() sonst tut?
Wer sie da initialisiert, ist nebensächlich, üblicherweise der Code, der
direkt vor main() läuft.
> (Zum Bild) Hier sieht man ja die Aufteilung der verschiedenen Segmente.> 3. Frage: Diese Segmente können ja teilweise "geschützt" werden, damit> man dort nicht mehr herum pfuschen kann und Variablen zum Beispiel> verändern kann (z. B. einen String, der so initialisiert wird: char *s> ="ABC";). Stimmt das?
Wenn das alles RAM ist und es keine MMU gibt (ich kenne den darunter
liegenden Prozessor nicht gut genug), dann gibt es keinen Schutz.
Wenn ein Teil davon Flash ist, sind die darin enthaltenen Daten
natürlich ziemlich automatisch read-only.
> Wenn ja: Wie können dann diese Segmente geschützt werden. Müssen diese> dann in einem anderen Adressraum gespeichert werden wie z. B. der Stack> und der Heap, weil diese müssen ja ständig verändert werden (z. B. Stack> im Adressraum 0x4000000-0x5000000 und BSS im Adressraum> 0x1000000-0x2000000...
Wie geschrieben, ich kennen den Prozessor zu wenig um zu wissen, ob er
eine MMU hat. Wenn ja, dann kann diese u.U. bestimmte Bereiche (in einer
von der MMU festgelegten Granularität, das muss nicht zwingend byteweise
sein) gegen bestimmte Aktionen (lesen, schreiben, ausführen) schützen.
Sorry für die verspätete Antwort...
Danke für die Antworten ;)
Dann ist etwa "auf 0 setzen per memset" das Gleiche wie "auf 0
initialisieren"? Den Begriff "Initialisieren" hab ich bisher nur dafür
verwendet, wenn etwas direkt vor dem Programmablauf auf einen Wert
gesetzt wird...
Und da stellt sich mir die Frage: Was bedeutet eigentlich "vor dem
Programmablauf", gibt es das überhaupt? Speziell ist die Frage: Wann im
Programmablauf / wo wird z. B. bei einer globalen Variablen: int i = 2
das i initialisiert?
Mich wundert es halt, dass z. B. bei int i = 2 die Variable
"automatisch" mit 2 erzeugt wird, und beim bss extra noch der memset
Befehl erfolgen muss... Ist das so auch üblich bei anderen Controllern?
Eigentlich könnten ja dann auch die beiden Sectionen zusammengefasst
werden, oder? Weil große Unterschiede gibt es da ja nicht...
Zudem die Frage: Wann würde es denn Sinn machen, die beiden Sectionen an
unterschiedliche physikalische Adressräume zu packen? Was gibt das für
Vorteile / Nachteile?
(Beim ESP32 gibt's eine MMU, im externen Flash wird der Code
gespeichert. Über Prozessorbefehle können Sectionen geschützt werden. So
habe ich das bisher verstanden)
Grüße
Alexander M. schrieb:> Dann ist etwa "auf 0 setzen per memset" das Gleiche wie "auf 0> initialisieren"?
Wenn das memset() vor dem main() abläuft, ja. Etwas anderes ist es
letztlich nicht, was da passiert.
> Den Begriff "Initialisieren" hab ich bisher nur dafür> verwendet, wenn etwas direkt vor dem Programmablauf auf einen Wert> gesetzt wird...
Genau das passiert ja.
> Und da stellt sich mir die Frage: Was bedeutet eigentlich "vor dem> Programmablauf", gibt es das überhaupt?
Irgendwo ist natürlich immer der Anfang, an dem der CPU-Gott die Welt
erschafft …
> Speziell ist die Frage: Wann im> Programmablauf / wo wird z. B. bei einer globalen Variablen: int i = 2> das i initialisiert?
Irgendwann vor dem Aufruf von main(). Wie der Startup-Code das
organisiert, ist seine Sache.
(Bei Betriebssystemen kann das u. U. komplett entfallen, nämlich dann,
wenn das Betriebssystem anderweitig garantiert, dass aller initial dem
Prozess zugewiesener RAM mit 0 gefüllt ist. Sowas macht man um
sicherzustellen, dass sich nicht etwa sensitive Daten anderer Prozesse
darin befinden.)
> Mich wundert es halt, dass z. B. bei int i = 2 die Variable> "automatisch" mit 2 erzeugt wird, und beim bss extra noch der memset> Befehl erfolgen muss...
Wo hast du denn "extra" noch einen "memset"-Aufruf?
> Eigentlich könnten ja dann auch die beiden Sectionen zusammengefasst> werden, oder? Weil große Unterschiede gibt es da ja nicht...
Ja, dann pumpst du aber sinnlos ein Speichermedium (Flash beim
Controller, Harddisk beim PC) mit Nullen voll, nur um diese beim
Programmstart dann in den RAM als Initialwerte zu kopieren. Da die
Initialisierung mit 0 wohl der häufigste Spezial-Fall ist, hat es sich
eingebürgert, diese getrennt von den anderen initialisierten Daten zu
behandeln. Bei allen anderen musst du dir die jeweiligen Initialwerte
explizit irgendwo hin schreiben, bei .bss musst nur merken "4 weitere
Bytes hier", und beim Start werden in einer Schleife (die natürlich
letztlich das gleiche ist wie memset(… 0 …)) dann alle diese Daten
ausgenullt.
> Zudem die Frage: Wann würde es denn Sinn machen, die beiden Sectionen an> unterschiedliche physikalische Adressräume zu packen? Was gibt das für> Vorteile / Nachteile?
Sinn hat es nicht (sind ja alles Daten), aber wenn du unterschiedliche
Adressräume hast, kannst du das machen.
Mehr Sinn hat es, read-only-Daten abzutrennen, um sie per MMU schützen
zu können, oder den Stack in einen anderen Adressraum zu legen, um ihn
gegen die Ausführung von Code zu schützen (das erschwert Exploits bei
buffer overflows).
Vielen Dank für die klasse Antworten!
Das mit der Initialisierung habe ich bisher noch nie so hinterfragt, da
habe ich jetzt komplett neue Erkenntnisse :P
Um ganz sicher zu gehen, meine Vermutung, wie der Programmablauf (im
Allgemeinen) abläuft:
1. Bei einem Reset springt das Programm zuerst in den Reset Handler. Von
da aus wird dann der Startup-Code abgearbeitet.
2. Zuerst werden dann die Variablen angelegt / Speicher geschützt usw.
Dieser Code wird dann meistens nicht in der IDE angezeigt, sondern
erfolgt (wahrscheinlich?) im Inneren des Compilers?
3. Danach geht's dann mit dem Code weiter, der in der IDE in der
Reset-Funktion geschrieben wurde.
4. Von da aus geht's dann in die main
Wie sieht's denn da beim ATMega328 mit der AVR Architektur aus. Gibt's
dazu irgendwo genauere Informationen, wie der Startup-Code erzeugt wird?
Grüße
Alexander M. schrieb:> Wie sieht's denn da beim ATMega328 mit der AVR Architektur aus. Gibt's> dazu irgendwo genauere Informationen, wie der Startup-Code erzeugt wird?
Such mal nach crt0 in deiner toolchain (oder auch in Wikipedia).
LG, Sebastian
asmvolatile("clr __zero_reg__");// GCC depends on register r1 set to 0
16
asmvolatile("out %0, __zero_reg__"::"I"(_SFR_IO_ADDR(SREG)));// set SREG to 0
17
18
mcusr=MCUSR;
19
MCUSR=0;
20
wdt_reset();
21
wdt_disable();
22
...
Im Microchip Studio kann dann der Linker mit der Option -nostartfiles
die crt0 weglassen.
Versuch auch mal ein Listing-file zu erstellen, um die einzelnen
Sektionen des Executables zu identifizieren.
LG, Sebastian
Alexander M. schrieb:> (Beim ESP32 gibt's eine MMU, im externen Flash wird der Code> gespeichert. Über Prozessorbefehle können Sectionen geschützt werden. So> habe ich das bisher verstanden)
Im ESP32 gibt es mehrere MMUs, die aber nur für einen Teil des RAMs
(jeweils eine für die oberen 128KB von SRAM0 und SRAM2) zuständig sind
sowie eine Cache MMU für den externen Speicher (Flash und SPI/PS RAM).
Die drei MMUs unterscheiden sich jeweils hardwaremäßig.
"Sektionen" werden (wie beim ARM Cortex M0+/M3/M4/M7) über eine MPU
geschützt.
Sebastian W. schrieb:> Hier mal der C-Quellcode eines AVR-Bootloaders, der ohne statisch> initialisierte Variablen arbeitet:
Das ist ja nun weniger sinnvoll, hier zu posten, denn hier geht es um
den Standardfall.
Ich halte so einen Bootloader übrigens für hochgradig problematisch, da
es eben zu den Regeln der Sprache gehört, dass der Benutzer sich auf
bestimmte Dinge verlassen kann. Genauso, wie man sich drauf verlassen
kann, dass 1 + 1 = 2 ist, soll man sich auf die Zusicherung des
Standards verlassen können, dass globale Objekte stets initialisiert
werden.
Alexander M. schrieb:> Dieser Code wird dann meistens nicht in der IDE angezeigt, sondern> erfolgt (wahrscheinlich?) im Inneren des Compilers?
„Im Inneren des Compilers“ ist nur die Generierung von Assemblercode.
;-) Allerdings in der Tat, IDEs zeigen diese Periode typischerweise
nicht an, sondern setzen den ersten Breakpoint auf main(), sodass alles
davor bereits abgearbeitet worden ist. Neben dem Initialisieren von
Stackpointer und statischen Variablen gehört dazu u. U. auch sowas wie
globale Konstruktoren bei C++.
Aus Sicht des C-Standards fällt all dies (und die Standardbibliothek)
unter “the implementation”, d.h. wo genau die Trennlinie zwischen
Compiler und Bibliothek liegt, wird nicht vorgegeben.
Der Startcode ist sehr eng verflochten mit dem so genannten
Linkerscript, denn solche Informationen, wo die initialierten Variablen
anfangen oder aufhören und wo die Initialisierungswerte liegen, kennt
erst am Ende des Compilierens der Linker. Über den Linkerscript gelangt
diese Information dann in Form vordefinierter globaler Symbole in den
Startcode.
Für die avr-libc findest du den Startcode hier:
http://svn.savannah.gnu.org/viewvc/avr-libc/trunk/avr-libc/crt1/gcrt1.S?view=markup
Sieht ein bisschen spagghettimäßig aus :), weil dieser Code letztlich
für alle möglichen AVR-Controller übersetzungsfähig gehalten ist, diese
aber halt sehr unterschiedlich aufgebaut sein können. (Beispiel: manche
davon kennen JMP und CALL, andere nur RJMP und RCALL.)
Für jeden unterstützten Controller ist davon eine übersetzte Kopie in
der Systembibliothek hinterlegt, die vom Compiler passend gesucht und
über den Linker eingebunden wird.
Jörg W. schrieb:> Ich halte so einen Bootloader übrigens für hochgradig problematisch,
Ich hatte damals einen Grund dafür ... kann mich aber nicht mehr genau
erinnern. Ich glaube, es ging darum die Vektortabelle wegzulassen (weil
der Bootloader ja eh mit -Wl,-section-start=.text=0x7000 beim 1284P an
das Ende des Flash reloziert wird und nicht an Adresse 0).
In jedem Fall war das Schreiben eines AVR-Bootloaders für mich sehr
lehrreich, gerade was das Prozedere der Toolchain "hinter den Kulissen"
betrifft ...
LG, Sebastian
Danke für die Posts!
Muss ich mir alles genauer anschauen, dann wird da vielleicht noch die
ein oder andere Frage kommen...
Eure Posts haben mir da auf jeden Fall ziemlich weiter geholfen, jetzt
weiß ich zumindest wonach ich suchen muss :)
Dieses Thema hab ich wohl bisher immer ziemlich verdrängt / nie auf dem
Schirm gehabt...
Grüße
Alexander M. schrieb:> Dieses Thema hab ich wohl bisher immer ziemlich verdrängt / nie auf dem> Schirm gehabt...
Das hat man normalerweise auch erst dann im Blickfeld, wenn man entweder
an der Toolchain mitarbeitet oder aber im Embedded-Bereich so „weit
unten“, dass man genau diese Stellen verstanden haben muss, weil man sie
ggf. auch anpassen können muss. Gerade bei ARM kocht (anders als bei
AVR) hinsichtlich Startup-Code und Linkerscripts so ziemlich jeder
(Hersteller, IDEs, Anwender) sein eigenes Süppchen, nicht selten mit
alten Bugs, neu aufgewärmt. :-/
(Ich hatte in beiden genannten Bereichen zu tun.)
Der ESP32 ist da ja wahrscheinlich schon wieder „monokulturig“ genug,
dass es da nicht x verschiedene Varianten von alldem gibt – vermute ich
mal.
Jörg W. schrieb:> Der ESP32 ist da ja wahrscheinlich schon wieder „monokulturig“ genug,> dass es da nicht x verschiedene Varianten von alldem gibt – vermute ich> mal.
Die Zeiten dürften vorbei sein:
Mit Single-/Dualcore, LX6, LX7, RISC-V hat sich die ESP32-Familie
mittlerweile stark aufgefächert.
Planloser schrieb:> Mit Single-/Dualcore, LX6, LX7, RISC-V hat sich die ESP32-Familie> mittlerweile stark aufgefächert.
Ist aber trotzdem erstmal einer, der das macht.
Bei ARM sind es halt Dutzende, und niemand von ihnen hat sich den Hut
aufgesetzt, diese Dinge in der Toolchain einheitlich anzugehen. Dass
jeder Hersteller seine IO-Headerfiles selbst liefern muss, ist logisch,
aber sowas wie Startup und Linkerscripte hätte man schon auch
einheitlich handhaben können. Braucht aber halt eine zentrale Instanz,
die das tut – also am ehesten ARM selbst. Die haben sich zwar um den
Compiler gekümmert (bei GCC mit geholfen, soweit ich das verstanden
habe), aber eben das „Rundrum“ den einzelnen Herstellern überlassen. Die
wurschteln das dann jeder für sich in ihrer IDE zurecht oder
dergleichen.