Forum: Mikrocontroller und Digitale Elektronik bss, data, Fragen zu den Segmenten


von Alexander M. (a_lexander)


Angehängte Dateien:

Lesenswert?

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

von c-hater (Gast)


Lesenswert?

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.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

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
static int foo;
2
// oder
3
static int bar = 0;

schreibst: beide landen im .bss. Im Unterschied dazu landet
1
static int mumble = 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.

von Alexander M. (a_lexander)


Lesenswert?

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

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

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).

von Alexander M. (a_lexander)


Lesenswert?

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

von Sebastian W. (wangnick)


Lesenswert?

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

von Sebastian W. (wangnick)


Lesenswert?

Hier mal der C-Quellcode eines AVR-Bootloaders, der ohne statisch 
initialisierte Variablen arbeitet:
1
void init (void) __attribute__ ((naked)) __attribute__ ((section (".vectors")));
2
void init (void) {asm volatile ( "rjmp start" );}
3
4
void __do_copy_data (void) __attribute__ ((naked)) __attribute__ ((section (".text9")));
5
void __do_copy_data (void) {}
6
void __do_clear_bss (void) __attribute__ ((naked)) __attribute__ ((section (".text9")));
7
void __do_clear_bss (void) {}
8
void start (void) __attribute__ ((naked)) __attribute__ ((section (".init0")));
9
10
void start (void) {
11
  asm volatile ( "ldi  16, %0" :: "i" (RAMEND >> 8) );
12
  asm volatile ( "out %0,16" :: "i" (AVR_STACK_POINTER_HI_ADDR) );
13
  asm volatile ( "ldi  16, %0" :: "i" (RAMEND & 0x0ff) );
14
  asm volatile ( "out %0,16" :: "i" (AVR_STACK_POINTER_LO_ADDR) );
15
  asm volatile ( "clr __zero_reg__" );                  // GCC depends on register r1 set to 0
16
  asm volatile ( "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

von Planloser (Gast)


Lesenswert?

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.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

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.

von Sebastian W. (wangnick)


Lesenswert?

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

: Bearbeitet durch User
von Alexander M. (a_lexander)


Lesenswert?

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

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

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.

von Planloser (Gast)


Lesenswert?

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.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

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.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.