Forum: Compiler & IDEs Linkerskript Memoryspaces


von M.M. (Gast)


Lesenswert?

Ich will für eine RISCV CPU ein passendes Linkerskript erstellen. Die 
CPU hat einen ROM für das Programm (bare metal, ohne OS) und einen RAM 
als Arbeitsspeicher. Nun habe ich folgendes Skript geschrieben:
1
OUTPUT_ARCH( "riscv" )
2
3
ENTRY(main)
4
5
MEMORY {
6
  rom (rx) : ORIGIN = 0x00, LENGTH = 1024    /* size of Instruction Memory (max. ISPMemory size = 1024 Byte */
7
  ram (!rx) : ORIGIN = 0x10000000, LENGTH = 5152  /* size of Data Memory and offset */
8
}
9
10
SECTIONS {
11
  .text : { *(.text) } > rom    /* executable data */
12
13
  .data : { *(.data) } > ram    /* global/static variables */
14
  .bss : { *(.bss) } > ram    /* uninitialized data */
15
}

Die Addressierung funktioniert soweit. Das Problem ist, dass die fertige 
Binary Datei 250MByte groß ist. Das liegt daran, weil der Linker die 
Speicherbereiche rom und ram nicht trennt. Wenn ich beispielsweiße den 
ORIGIN von ram klein genug mache, bringt der Linker einen overlapping 
error. Ich verstehe aber nicht warum, weil der Memorybereich rom hat 
doch nichts mit ram zu tun?!

Zusätzlich würde ich noch gerne wissen, wie der Compiler die Adresse vom 
Stackpointer festlegt. Muss der nicht auch normalerweise im Linkerskript 
angegeben werden, und wenn ja wie?

: Verschoben durch User
von Cyberpunk (Gast)


Lesenswert?

M.M. schrieb:
> Zusätzlich würde ich noch gerne wissen, wie der Compiler die Adresse vom
> Stackpointer festlegt. Muss der nicht auch normalerweise im Linkerskript
> angegeben werden, und wenn ja wie?

Damit hat der Compiler gar nichts zu tun. Das ist Aufgabe des 
Startup-Codes die Umgebung so zu initialisieren, dass der produzierte 
Maschinencode lauffähig ist. Es kann aber auch sein, dass die 
Architektur den Stackpointer selber aus einem bestimmten Speicherbereich 
lädt, wie z.B. beim ARM Cortex-M*, wo der Interruptvektor den initialen 
Stackpointer enthält.

von meckerziege (Gast)


Lesenswert?

So kann das nicht klappen, denn mit diesem Linker skript versucht der 
Linker die Sections gleich so zu platzieren, wie sie hernach im Speicher 
liegen.

Was ist dein Output Format? Binäry also .bin oder so?

Ich hätte bei .data kein >ram sondern eher ein >ram AT> rom erwartet. 
Denn der Startup Code muss .data erst mal aus dem Flash laden und dann 
ins RAM kopieren.
Dann fehlen dir aber noch mindestens Sachen wie LOADADDR(...) usw.

von Andreas M. (amesser)


Lesenswert?

Die section ".data" enthält Variablen, die mit Werten vorinitialisiert 
werden müssen, bevor die eigentliche Applikation anläuft. Bei 
Mikrocontrollern legt man diese üblicherweise mit ins ROM und kopiert 
diese mittels memcpy direkt im Reset Handler in die Data Section. Sieht 
dann in etwa so aus;

1
OUTPUT_ARCH( "riscv" )
2
3
ENTRY(main)
4
5
MEMORY {
6
  rom (rx) : ORIGIN = 0x00, LENGTH = 1024    /* size of Instruction Memory (max. ISPMemory size = 1024 Byte */
7
  ram (!rx) : ORIGIN = 0x10000000, LENGTH = 5152  /* size of Data Memory and offset */
8
}
9
10
SECTIONS {
11
  .text  :  { *(.text) }  > rom       /* executable data */
12
  .rodata : { *(.rodata) } > rom      /* reado only data */
13
  .data : { *(.data) } > ram AT>rom   /* initialized global/static variables */
14
  .bss (NOLOAD) : { *(.bss) } > ram    /* uninitialized data */
15
16
  PROVIDE(_data_addr         = ADDR(.data));
17
  PROVIDE(_data_loadaddr     = LOADADDR(.data));
18
  PROVIDE(_data_addr_end     = ADDR(.data) + SIZEOF(.data));
19
20
  PROVIDE(_bss_addr         = ADDR(.bss));
21
  PROVIDE(_bss_addr_end     = ADDR(.bss) + SIZEOF(.bss));
22
}

und dann im reset handler, oder eventuell auch in der main() ganz am 
anfang
1
extern unsigned char _data_addr;
2
extern unsigned char _data_loadaddr;
3
extern unsigned char _data_addr_end;
4
5
extern unsigned char _bss_addr;
6
extern unsigned char _bss_addr_end;
7
8
reset()
9
{
10
  memcpy(&_data_addr, &_data_loadaddr, &(_data_addr_end) - &(_data_addr);
11
  memset(&_bss_addr,  0x00, &(_bss_addr_end) - &(_bssa_addr);
12
}

Achtung,zum debuggen muss das so erstellte binary immer in die firmware 
geflasht werden. Es kann nicht mehr per debugger geladen werden.

von M.M. (Gast)


Lesenswert?

meckerziege schrieb:
> Was ist dein Output Format? Binäry also .bin oder so?
.bin, also Binary.

> Ich hätte bei .data kein >ram sondern eher ein >ram AT> rom erwartet.
> Denn der Startup Code muss .data erst mal aus dem Flash laden und dann
> ins RAM kopieren.

Ok, das habe ich verstanden. Es funktioniert nun auch mit >ram AT>rom.

@Andreas Messner:
Das mit dem kopieren vom rom in ram will ich eincodieren, wenn das 
andere geht. Warum macht das der Compiler eigentlich nicht automatisch?

In .rodata sind solche Sachen wie z.B. konstante Strings nehme ich an. 
Also wie z.B. bei printf("asdf").


Ich habe mein Skript nun folgendermaßen angepasst:
1
OUTPUT_ARCH( "riscv" )
2
3
/* initialize helper variables */
4
HIDDEN(RAMSIZE = 2048);
5
6
/* provide variables for startup code (startup.c) */
7
PROVIDE_HIDDEN (_stack = RAMSIZE);    /* start address of stack */
8
9
ENTRY (Startup)
10
11
MEMORY {
12
  rom (rx) : ORIGIN = 0x00, LENGTH = 1024    /* size of Instruction Memory (max. ISPMemory size = 1024 Byte */
13
  ram (!rx) : ORIGIN = 0x10000000, LENGTH = RAMSIZE  /* size of Data Memory and offset */
14
}
15
16
SECTIONS {
17
  .start : { *(.start*) } > rom    /* startup code */
18
  .text : { *(.text) } > rom    /* executable data */
19
  .rodata : { *(.rodata) } > rom  /* read only data */
20
21
  .data : { *(.data) } > ram AT>rom  /* global/static variables */
22
  .bss (NOLOAD) : { *(.bss) } > ram  /* uninitialized data */
23
  _end = .;        /* begin of heap */
24
}

Außerdem habe ich noch einen Startupcode eingefügt, der mir den 
Stackpointer auf das Ende initialisiert.

Dies ist mein C-code zum testen:
1
# include <stdint.h>
2
3
// functions
4
int testf(void){
5
  static int testvar = 12;
6
  ++testvar;
7
  return testvar;
8
}
9
10
// main loop
11
int main(){
12
  int temp;
13
  while(1){
14
    temp += 1;
15
    if(temp>100) temp=0;
16
  }
17
}

Stackpointer, Größe des .bin-Files und Adressen von .bss Variablen 
(testvar) stimmen.


Dies ist die main-Funktion in Assembler.
[c]
main():
  6c:  fe010113            addi  sp,sp,-32
  70:  00812e23            sw  s0,28(sp)
  74:  02010413            addi  s0,sp,32
  78:  fec42783            lw  a5,-20(s0)
  7c:  00178793            addi  a5,a5,1
  80:  fef42623            sw  a5,-20(s0)
  84:  fec42703            lw  a4,-20(s0)
  88:  06400793            li  a5,100
  8c:  fee7d6e3            ble  a4,a5,78 <main+0xc>
  90:  fe042623            sw  zero,-20(s0)
  94:  fe5ff06f            j  78 <main+0xc>
Dort wird die Addresse der Variable 'temp' benötigt, die in .bss liegen 
müsste. Die Adresse kommt hier aus dem Stack. Muss ich also den 
Framepointer auch initialisieren?

von M.M. (Gast)


Lesenswert?

M.M. schrieb:
> Dort wird die Addresse der Variable 'temp' benötigt, die in .bss liegen
> müsste. Die Adresse kommt hier aus dem Stack. Muss ich also den
> Framepointer auch initialisieren?

Das ist natürlich Schwachsinn, 'temp' liegt in dem Fall auf dem Stack. 
Somit müsste der Code eigentlich passen. Er funktioniert aber nicht, 
wahrscheinlich hab ich dann noch einen VHDL Fehler in meiner CPU.

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


Lesenswert?

M.M. schrieb:
> Das mit dem kopieren vom rom in ram will ich eincodieren, wenn das
> andere geht. Warum macht das der Compiler eigentlich nicht automatisch?

Weil das nicht seine Aufgabe ist.  Er ist nur für alles ab main()
zuständig.

Insbesondere ist diese Kopiererei ja nur für Mikrocontroller nötig;
normale Binaries innerhalb eines OSes werden auf völlig anderem Wege
geladen.  Bei heute üblichen Systemen mit Virtual Memory wird hierbei
in der Regel nur ein Mapping produziert zwischen der entsprechenden
Section im Abbild (also der ELF-Datei) und dem entsprechenden virtuellen
Adressbereich.  Wenn das OS dann die Ausführung startet, gibt's einen
page fault, das OS stellt nun physischen Speicher bereit und kopiert
on demand die Inhalte aus der ELF-Datei in den physischen Speicher.

Auf einem Mikrocontroller hat man sowas nicht, daher muss dort der
Startup-Code in Zusammenspiel mit dem Linkerscript dafür Sorge tragen,
dass die Initialisierungsdaten aus dem Flash in den RAM gelangen. Beide
müssen entsprechend aufeinander abgestimmt sein.

von M.M. (Gast)


Lesenswert?

Jörg W. schrieb:
> Bei heute üblichen Systemen mit Virtual Memory wird hierbei
> in der Regel nur ein Mapping produziert zwischen der entsprechenden
> Section im Abbild (also der ELF-Datei) und dem entsprechenden virtuellen
> Adressbereich.

Mit den pagefaults wird quasi die Cash-Funktionalität realisiert, oder?

Dieses memcpy und memset sind C-Funktionen der stdint.h? Die könnte ich 
auch selber in meinem Startup.c schreiben, nehme ich an.

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


Lesenswert?

M.M. schrieb:

> Mit den pagefaults wird quasi die Cash-Funktionalität realisiert, oder?

Cash gibt's dafür nicht. :-)

Wenn man so will, kann man es als eine Art Cache ansehen.  Ein Cache
speichert ja auch das, was am häufigsten zugegriffen wird.  Hier wird
gleich nur das überhaupt geladen, was einmal zugegriffen wird.  Was
nie zugegriffen wird, braucht auch nie physischen Speicher.

Aber das ist für dein Target eher off-topic.

> Dieses memcpy und memset sind C-Funktionen der stdint.h?

stdint.h ist das Headerfile, in dem sie deklariert sind.

Implementiert müssen sie irgendwo in der Standardbibliothek sein.
Im einfachsten Fall kann man sie natürlich so implementieren:
1
void *memcpy(void *dst, const void *src, size_t s)
2
{
3
  uint8_t *d = (uint8_t *)dst;
4
  uint8_t *s = (uint8_t *)src;
5
6
  while (--s != 0) *d++ = *s++;
7
8
  return dst;
9
}
10
11
void *memset(void *dst, int n, size_t s)
12
{
13
  uint8_t *d = (uint8_t *)dst;
14
15
  while (--s != 0) *d++ = (uint8_t)n;
16
17
  return dst;
18
}

Inwiefern das jedoch für deine Plattform eine effiziente
Implementierung darstellt, musst du selbst entscheiden.  Da diese
Funktionen häufig benötigt werden, ist es durchaus angebracht, hier
etwas Geist reinzustecken in eine effiziente Assemblerversion.

Auch kann es im Startup-Code passieren, dass der Compiler noch
Voraussetzungen benötigt (wie bspw. ein Zero-Register), die an der
entsprechenden Stelle so noch nicht gegeben sind.  Schließlich ist
es ja Startup, es muss also noch nicht alles erledigt sein, was man
als Umgebung dann innerhalb von main() voraussetzen kann.

von M.M. (Gast)


Lesenswert?

Ich will nun den gesamten Initialisierungscode in Assembler schreiben. 
Es funktioniert soweit alles. Jetzt hätte ich nur im 
Initialisierungscode (Startup.S) gerne Zugriff auf eine Variable im 
Linker Skript (stack_top).

Mit C ist es schon gegangen, leider kann ich den Stack so nicht 
initialisieren, da C diesen ja schon benutzt.
Im Linker Skript steht folgendes:
1
PROVIDE_HIDDEN(stack_top = 0x1000);
Und im .c-File kann ich so auf die Variable zugreifen:
1
extern int stack_top;

Das muss doch mit Assembler auch irgendwie gehen, nur wie?

von Markus F. (mfro)


Lesenswert?

M.M. schrieb:
> Das muss doch mit Assembler auch irgendwie gehen, nur wie?

genauso.

In deinen Assembler-Dateien kannst Du auf alle Symbole zugreifen, die in 
deinem Linker-Script definiert sind. Gnu as braucht dafür noch nicht 
einmal ein .extern (man kann's aber hinschreiben, wenn man will). Es 
behandelt alle nicht gefundenen Labels als externe Referenz.

PROVIDE_HIDDEN(...)

im Linker-Script ist übrigens für diesen Zweck (eigentlich) ungeeignet. 
Zumindest für ELF-Targets sollte das ein Script-lokales Symbol 
definieren.

IRGENDEIN_SYMBOL = .   /* zum Beispiel */

ist völlig ausreichend. Möglicherweise solltest Du dein Map-File 
inspizieren. Es kann sein, daß deine Toolchain ein _ vor den Symbolnamen 
packt.

von M.M. (Gast)


Lesenswert?

Irgendwas mach ich falsch.

Linker Skript:
1
stack_top = 0x123;

Map-File zur Kontrolle:
1
OUTPUT(build/firmware.elf elf32-littleriscv)
2
                0x0000000000001000                RAMSIZE = 0x1000
3
                0x0000000010000000                RAM_OFFSET = 0x10000000
4
                0x0000000010001000                stack_top = 0x123

Assembler Code:
1
.extern stack_top
2
li sp, stack_top

Dann bekomme ich die Fehlermeldung "illegal operands 'li sp, 
stack_top'".

von Nop (Gast)


Lesenswert?

Jörg W. schrieb:

>   while (--s != 0) *d++ = *s++;

Und wenn dann mal die Funktion mit Länge 0 aufgerufen wird, weil die 
Länge das Ergebnis irgendwelcher Berechnungen ist, hat man ein Problem.

von Nop (Gast)


Lesenswert?

Und bei allen anderen Längen wird ein Byte zuwenig kopiert...

von Kaj G. (Firma: RUB) (bloody)


Lesenswert?

Nop schrieb:
> Und wenn dann mal die Funktion mit Länge 0 aufgerufen wird, weil die
> Länge das Ergebnis irgendwelcher Berechnungen ist, hat man ein Problem.
Der Fehler muss schon vor dem Aufrufen der Funktion abgefangen werden 
und nicht in der Funktion. In der Funktion ist es schon zu spaet.

Es gilt nun mal:
Wenn du die Funktion mit Muell fuetterst musst du dich nich wundern wenn 
Muell passiert.

von Andreas M. (amesser)


Lesenswert?

M.M. schrieb:
> Jörg W. schrieb:
>> Bei heute üblichen Systemen mit Virtual Memory wird hierbei
>> in der Regel nur ein Mapping produziert zwischen der entsprechenden
>> Section im Abbild (also der ELF-Datei) und dem entsprechenden virtuellen
>> Adressbereich.
>
> Mit den pagefaults wird quasi die Cash-Funktionalität realisiert, oder?

Nee, den Cache sollte die CPU schon von alleine verwalten. Die 
Pagefaults werden dazu benutzt um den Peripheriezugriff erst dann zu 
machen, wenns wirklich nötig ist. D.h. Unter Linux würde, wenn eine DLL 
(.so) von einem Programm benötigt wird, diese DLL in den Virtuellen 
Speicher des Programms gemappt, dieser Bereich wird aber zu nächst als 
ungültig markiert. Sobald das Programm versucht Funktionen in der DLL 
aufzurufen, wird ein Page Fault ausgelöst. Dieser wird vom Linux Kernel 
abgefangen und der angeforderte Speicherbereich mit den entsprechenden 
Daten von der Datei gefüllt. Dadurch werden nur die Teile von der 
Festplatte geladen, die auch wirklich benötigt werden. Man kann dann 
sogar DLLs die von mehreren Programmen benötigt werden nur einmal in den 
Physikalischen RAM legen und per Virtuellen Speicher an Mehrere 
Programme verteilen...

M.M. schrieb:
> Linker Skript:stack_top = 0x123;

Also wenn das in c funktioniert, sollte das in assembler eigentlich auch 
funktionieren. Ich denke es ist eine Falsche Assemblerinstruktion, du 
solltes mal
1
la sp, stack_top
probieren, "li" ist für Konstanten, also Zahlen im Programmfluss.

von Nop (Gast)


Lesenswert?

Kaj G. schrieb:

> Der Fehler muss schon vor dem Aufrufen der Funktion abgefangen werden

Länge 0 ist kein Fehler, sondern eine gültige Anweisung für 
memcpy/memset.

von M.M. (Gast)


Lesenswert?

Andreas M. schrieb:
> la sp, stack_top

Ah, dankeschön, das wars.


Jetzt, da alles soweit funktioniert, will ich .data initialisieren. Dazu 
muss ich im startup.S einen Code schreiben, der die Daten vom ROM in den 
RAM kopiert. Hier habe ich das nächste Problem: Mit welchen 
Assemblerbefehlen bekomme ich Zugriff auf den ROM vom RISC-V. Es gibt ja 
keinen LPM Befehl, wie bei den AVRs.

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


Lesenswert?

Nop schrieb:
> Kaj G. schrieb:
>
>> Der Fehler muss schon vor dem Aufrufen der Funktion abgefangen werden
>
> Länge 0 ist kein Fehler, sondern eine gültige Anweisung für
> memcpy/memset.

Ich finde im Standard keine Aussage dazu.

Insofern ist es sicherlich sinnvoll, die Länge 0 abzufangen.  Das kann
man aber gleich beim Eintritt in die Funktion erledigen.

Nop schrieb:
> Und bei allen anderen Längen wird ein Byte zuwenig kopiert...

Stimmt, also "while (s-- != 0)".  Damit hat man den Fall der Länge 0
ohnehin gleich mit erledigt.

von M.M. (Gast)


Lesenswert?

M.M. schrieb:
> Es gibt ja
> keinen LPM Befehl, wie bei den AVRs.

Ich werde die Hardware so ändern, dass ich mit den CSR-Registern den ROM 
auslesen kann. Somit wäre der Thread abgehakt.

Nochmals vielen Dank für die Hilfe!

von Nop (Gast)


Lesenswert?

Jörg W. schrieb:

> Ich finde im Standard keine Aussage dazu.

From the C99 standard (7.21.1/2):

Where an argument declared as size_t n specifies the length of the array 
for a function, n can have the value zero on a call to that function. 
Unless explicitly stated otherwise in the description of a particular 
function in this subclause, pointer arguments on such a call shall still 
have valid values, as described in 7.1.4. On such a call, a function 
that locates a character finds no occurrence, a function that compares 
two character sequences returns zero, and a function that copies 
characters copies zero characters.

(Quelle: 
https://stackoverflow.com/questions/3751797/can-i-call-memcpy-and-memmove-with-number-of-bytes-set-to-zero 
)

Also, die Längenangabe darf 0 sein.

Das -- hinter das s zu setzen schlägt aber tatsächlich zwei Fliegen mit 
einer Klappe. :-)

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


Lesenswert?

Danke, so eine pauschale Stelle hatte ich natürlich nicht vermutet,
ich hatte nur in der Beschreibung der jeweiligen Funktion geschaut.

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.