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:
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?
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.
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.
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;
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 */
.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
inttestf(void){
5
staticinttestvar=12;
6
++testvar;
7
returntestvar;
8
}
9
10
// main loop
11
intmain(){
12
inttemp;
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?
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.
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.
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.
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,constvoid*src,size_ts)
2
{
3
uint8_t*d=(uint8_t*)dst;
4
uint8_t*s=(uint8_t*)src;
5
6
while(--s!=0)*d++=*s++;
7
8
returndst;
9
}
10
11
void*memset(void*dst,intn,size_ts)
12
{
13
uint8_t*d=(uint8_t*)dst;
14
15
while(--s!=0)*d++=(uint8_t)n;
16
17
returndst;
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.
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
externintstack_top;
Das muss doch mit Assembler auch irgendwie gehen, nur wie?
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.
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.
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.
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.
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.
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.
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.
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!
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. :-)