Stworzyłem stąd inspirowany kod startowy od podstaw dla kory mózgowej M3. Jednak napotykam następujący problem: załóżmy, że deklaruję niezainicjowaną zmienną globalną, powiedzmy typu unsigned char w main.c
#include ...
unsigned char var;
...
int main()
{
...
}
powoduje to, że region .bss w STM32 f103 zaczyna się od _BSS_START = 0x20000000, a kończy na _BSS_END = 0x20000001. Teraz kod startowy
unsigned int * bss_start_p = &_BSS_START;
unsigned int * bss_end_p = &_BSS_END;
while(bss_start_p != bss_end_p)
{
*bss_start_p = 0;
bss_start_p++;
}
próbuje zainicjować, aby wyzerować cały region .bss. Jednak wewnątrz tej pętli while wskaźnik zwiększa się o 4 bajty, dlatego po jednym kroku bss_start_p = 0x20000004 zawsze będzie różny od bss_end_p, co prowadzi do nieskończonej pętli itp.
Czy istnieje jakieś standardowe rozwiązanie tego problemu? Czy mam w jakiś sposób „wymusić” wymiar regionu .bss wielokrotnością liczby 4? A może powinienem użyć wskaźnika do niepodpisanego znaku, aby przejść przez region .bss? Być może coś takiego:
unsigned char * bss_start_p = (unsigned char *)(&_BSS_START);
unsigned char * bss_end_p = (unsigned char *)(&_BSS_END);
while(bss_start_p != bss_end_p)
{
*bss_start_p = 0;
bss_start_p++;
}
```
Odpowiedzi:
Jak podejrzewasz, dzieje się tak, ponieważ typ danych unsigned int ma rozmiar 4 bajtów. Każda
*bss_start_p = 0;
instrukcja faktycznie usuwa cztery bajty obszaru bss.Zakres pamięci bss musi być poprawnie wyrównany. Możesz po prostu zdefiniować _BSS_START i _BSS_END, aby całkowity rozmiar był wielokrotnością czterech, ale zwykle jest to obsługiwane przez umożliwienie skryptowi linkerowi zdefiniowania lokalizacji początkowej i końcowej.
Na przykład tutaj jest sekcja linkera w jednym z moich projektów:
Te
ALIGN(4)
wypowiedzi dbać o rzeczy.Możesz także chcieć się zmienić
while(bss_start_p != bss_end_p)
do
while(bss_start_p < bss_end_p)
.To nie zapobiegnie problemowi (ponieważ możesz wyczyścić 1-3 bajty więcej niż chcesz), ale może to zminimalizować wpływ :)
źródło
while(bss_start_p < bss_end_p - 1)
a następnie bajtowe usunięcie pozostałego zakresu pamięci wyeliminowałoby ostatnią obawę.Standardowe rozwiązanie to
memset()
:Jeśli nie możesz użyć standardowej biblioteki, musisz zdecydować, czy w twoim przypadku jest ok zaokrąglenie wielkości obszaru pamięci do 4 bajtów i kontynuowanie używania
unsigned int *
; lub jeśli musisz być wobec tego surowy, w takim przypadku musisz użyćunsigned char *
.Jeśli zaokrąglisz rozmiar, tak jak w pierwszej pętli,
bss_start_p
może rzeczywiście okazać się większy niż,bss_end_p
ale łatwo jest poradzić sobie z porównaniem mniejszym niż<
zamiast testu nierówności.Oczywiście można również wypełnić większość obszaru pamięci transferami 32-bitowymi, a tylko kilka ostatnich bajtów transferami 8-bitowymi, ale to więcej pracy dla małego zysku, szczególnie tutaj, gdy jest to tylko fragment kodu startowego.
źródło
memset()
. Ale wyrównanie do 4 bajtów jest mniej więcej koniecznością. Dlaczego więc tego nie zrobić?memset()
, i wydaje się, że C to programuje. Prosta implementacjamemset()
jest w zasadzie tylko tą pętlą, to nie tak, że zależy od wielu innych. Ponieważ jest to mikrokontroler, zakładam również, że nie ma dynamicznego linkowania lub takiego działania (a patrząc na link, nie ma, jest to tylko wywołaniemain()
po tej pętli zerowania), więc kompilator powinien być w staniememset()
tam wejść wraz z innymi funkcjami (lub w celu implementacji).Po prostu zmień
!=
na<
. To i tak zwykle jest lepsze, ponieważ dotyczy takich problemów.źródło
Istnieje niezliczona ilość innych stron i przykładów. Wiele tysięcy, jeśli nie dziesiątki tysięcy. Istnieją dobrze znane biblioteki c ze skryptami linkera i kodem boostrap, szczególnie newlib, glibc, ale są też inne, które można znaleźć. Bootstraping C z C nie ma sensu.
Odpowiedzi na twoje pytanie próbujesz dokonać dokładnego porównania rzeczy, które mogą nie być dokładne, mogą nie zaczynać się na znanej granicy lub kończyć na znanej granicy. Więc możesz zrobić mniej niż coś, ale jeśli kod nie działał z dokładnym porównaniem, oznacza to, że zerujesz przeszłość .bss do następnej sekcji, co może, ale nie musi, powodować złe rzeczy, więc po prostu zamień na mniej niż isnt rozwiązanie.
Więc oto idzie TL; DR jest w porządku. Nie uruchamiasz języka z tym językiem, możesz na pewno uciec, ale bawisz się ogniem, kiedy to robisz. Jeśli dopiero się uczysz, jak to zrobić, musisz zachować ostrożność, a nie głupie szczęście lub fakty, których jeszcze nie odkryłeś.
Skrypt linkera i kod bootstrap mają bardzo intymny związek, są małżeństwem, łączą się w biodrze, nie rozwijasz jednego bez drugiego, co prowadzi do ogromnej awarii. I niestety skrypt linkera jest zdefiniowany przez linker i język asemblera zdefiniowany przez asembler, więc przy zmianie łańcuchów narzędzi należy spodziewać się ponownego napisania obu. Dlaczego język asemblera? Nie wymaga bootstrapu, zwykle są to języki kompilowane. C robi, jeśli nie chcesz ograniczać używania języka, zacznę od czegoś bardzo prostego, który ma minimalne wymagania specyficzne dla łańcucha narzędzi, nie zakładasz, że zmienne .bss są zerowe (sprawia, że kod jest mniej czytelny, jeśli zmienna nigdy nie jest inicjowana w tym języku , staraj się tego unikać, nie jest to prawdą w przypadku zmiennych lokalnych, więc musisz być na bieżąco, kiedy z niego korzystasz. ludzie i tak unikają globalizacji, więc dlaczego mówimy o .bss i .data ??? (globale są dobre do pracy na tym poziomie, ale to inny temat)) drugą zasadą dla prostego rozwiązania nie jest inicjowanie zmiennych w deklaracji, zrób to w kodzie. tak, pali więcej flasha, na ogół masz dużo, nie wszystkie zmienne są inicjalizowane stałymi i tak, które ostatecznie zużywają instrukcje.
Z projektu cortex-m można wywnioskować, że mogli myśleć, że w ogóle nie ma kodu ładującego, więc nie obsługuje .data ani .bss. Większość ludzi, którzy używają globałów, nie może żyć bez, więc oto:
Mógłbym to uczynić bardziej minimalnym, ale minimalnym funkcjonalnym przykładem dla wszystkich kory za pomocą gnu toolchain, nie pamiętam, które wersje można zacząć od wersji 5.xx lub wyższej w obecnych wersjach 9.xx Przełączałem skrypty linkera gdzieś w okolicach 3. xx lub 4.xx, gdy dowiedziałem się więcej i gdy gnu zmieniło coś, co złamało moje pierwsze.
bootstrap:
punkt wejścia do kodu C:
skrypt linkera.
Wszystkie te mogą być mniejsze i nadal działać, dodałem tutaj dodatkowe rzeczy, aby zobaczyć, jak działają.
zoptymalizowana kompilacja i link.
dla niektórych dostawców chcesz użyć 0x08000000 lub 0x01000000 lub innych podobnych adresów, ponieważ flash jest tam mapowany i dublowany do 0x00000000 w niektórych trybach rozruchu. niektóre mają tylko tyle pamięci flash dublowanej na 0x00000000, więc chcesz, aby tablica wektorów wskazywała na przestrzeń flash aplikacji nie na zero. ponieważ jest oparty na tabeli wektorów, wszystko działa.
po pierwsze zauważ, że cortex-ms to maszyny tylko dla kciuka iz jakiegokolwiek powodu wymusiły adres funkcji kciuka, co oznacza, że lsbit jest nieparzysty. Znając swoje narzędzia, dyrektywy .thumb_func informują asembler gnu, że następna etykieta to adres funkcji kciuka. zrobienie rzeczy +1 w tabeli doprowadzi do niepowodzenia, nie daj się skusić, aby to zrobić, zrób to dobrze. istnieją inne sposoby asemblera GNU, aby zadeklarować funkcję, że jest to podejście minimalne.
nie uruchomi się, jeśli nie uzyskasz prawidłowej tabeli wektorów.
prawdopodobnie wystarczy wektor wskaźnika stosu (można w nim umieścić cokolwiek, jeśli sam chcesz ustawić wskaźnik stosu w kodzie) i wektor resetu. Umieściłem tutaj cztery bez konkretnego powodu. Zazwyczaj podaje 16, ale chciał skrócić ten przykład.
Więc co musi zrobić minimum bootstrapu C? 1. ustaw wskaźnik stosu 2. zero .bss 3. skopiuj. Data 4. rozgałęzienie lub wywołanie punktu wejścia C.
punkt wejścia C jest zwykle nazywany main (). ale niektóre łańcuchy narzędzi wyświetlają main () i dodają dodatkowe śmieci do kodu. Celowo używam innej nazwy. YMMV.
kopia .data nie jest potrzebna, jeśli wszystko oparte jest na pamięci RAM. będąc mikrokontrolerem z korą-m jest to technicznie możliwe, ale mało prawdopodobne, więc kopia .data jest potrzebna ..... jeśli istnieją .data.
Mój pierwszy przykład i styl kodowania to nie poleganie na .data ani .bss, jak w tym przykładzie. Arm zajął się wskaźnikiem stosu, więc pozostaje tylko zadzwonić do punktu wejścia. Lubię to mieć, aby punkt wejścia mógł wrócić, wielu ludzi twierdzi, że nigdy nie powinieneś tego robić. możesz po prostu to zrobić:
i nie wraca z centry () i nie ma zresetowanego kodu procedury obsługi.
linker umieścił rzeczy tam, gdzie prosiliśmy. Ogólnie rzecz biorąc mamy w pełni funkcjonalny program.
Więc najpierw popracuj nad skryptem linkera:
podkreślając, że nazwy rom i ram nie mają znaczenia, że łączą tylko kropki łącznika między sekcjami.
dodaj kilka elementów, abyśmy mogli zobaczyć, co zrobiły narzędzia
dodaj kilka elementów do umieszczenia w tych sekcjach. i dostać
oto rzeczy, których szukamy w tym eksperymencie (nie zauważaj żadnego powodu, aby w rzeczywistości ładować lub uruchamiać kod ... poznaj swoje narzędzia, naucz się ich)
dowiedzieliśmy się tutaj, że pozycja zmiennych jest bardzo wrażliwa w skryptach gnu linkera. zwróć uwagę na pozycję data_rom_start vs data_start, ale dlaczego data_end działa? Pozwolę ci to rozgryźć. Już rozumiem, dlaczego można nie chcieć bawić się skryptami linkera i po prostu przejść do prostego programowania ...
więc kolejną rzeczą, której się tutaj nauczyliśmy, jest to, że dla nas linker data_rom_start dla nas nie potrzebował ALIGN (4). Czy należy zakładać, że to zawsze zadziała?
Zauważ też, że padał on w drodze do, mamy 5 bajtów .data, ale wypełnił go do 8. Bez żadnych ALIGN (s) możemy już wykonać kopię za pomocą słów. Czy w oparciu o to, co widzimy dzisiaj dzięki temu zestawowi narzędzi na moim komputerze, może to dotyczyć przeszłości i przyszłości? Kto wie, nawet przy ALIGNach trzeba okresowo sprawdzać, czy niektóre nowe wersje nie psują rzeczy, robią to od czasu do czasu.
z tego eksperymentu przejdźmy do tego, żeby być bezpiecznym.
przesuwając końce do środka, aby były zgodne z tym, co robią inni ludzie. I to nie zmieniło:
jeszcze jeden szybki test:
dający
nie trzeba wstawiać między odbiciem a wyrównaniem
Och, racja, pamiętam teraz, dlaczego nie włożyłem _end__ do środka. ponieważ NIE DZIAŁA.
jakiś prosty, ale bardzo przenośny kod do połączenia z tym skryptem linkera
dający
możemy się tam zatrzymać lub iść dalej. Jeśli inicjalizujemy w tej samej kolejności co skrypt linkera, to dobrze, jeśli przejdziemy do następnej rzeczy, ponieważ jeszcze tam nie dotarliśmy. i stm / ldm są wymagane / pożądane tylko do używania adresów wyrównanych do słów, więc jeśli zmienisz na:
z bss najpierw w skrypcie linkera, i tak, nie chcesz ble.
te pętle będą szybsze. teraz nie wiem, czy magistrale ahb mogą mieć szerokość 64 bitów, czy nie, ale dla pełnego rozmiaru ramienia chciałbyś wyrównać te rzeczy na 64-bitowych granicach. cztery rejestry ldm / stm na 32-bitowej granicy, ale nie na 64-bitowej granicy, stają się trzema oddzielnymi transakcjami magistrali, przy czym wyrównanie na 64-bitowej granicy jest pojedynczą transakcją oszczędzającą kilka zegarów na instrukcję.
ponieważ robimy baremetal i jesteśmy w pełni odpowiedzialni za wszystko, co możemy umieścić, powiedzmy najpierw bss, a następnie dane, a następnie, jeśli mamy stos, to rośnie stos od góry do dołu, więc jeśli wyzerujemy bss i rozlejemy niektóre, dopóki zaczniemy od właściwe miejsce, które jest w porządku, jeszcze nie używamy tej pamięci. następnie kopiujemy .data i możemy przelać do stosu, który jest w porządku, sterty lub nie, jest dużo miejsca na stosie, więc nie nadepniemy na nikogo / nic (dopóki upewnimy się, że w skrypcie linkera to robimy. jeśli istnieje obawa, powiększ ALIGN (), abyśmy zawsze znajdowali się w naszej przestrzeni dla tych wypełnień.
więc moje proste rozwiązanie, weź to lub zostaw. witam, aby naprawić wszelkie błędy, nie uruchomiłem tego na sprzęcie ani na moim symulatorze ...
złóż wszystko razem, a otrzymasz:
zauważ, że działa to z arm-none-eabi- i arm-linux-gnueabi i innymi wariantami, ponieważ nie użyto żadnych świstów ghee.
Kiedy się rozejrzysz, przekonasz się, że ludzie oszaleją na punkcie skryptu ghee w swoich skryptach linkerów, ogromnych potwornych zlewozmywaków kuchennych. Lepiej po prostu wiedzieć, jak to zrobić (lub lepiej opanować narzędzia, aby kontrolować, co się dzieje), zamiast polegać na innych rzeczach i nie wiedzieć, gdzie to się zepsuje, ponieważ nie rozumiesz i / lub chcesz badać to.
jako ogólna zasada, nie ładuj języka z tym samym językiem (w tym sensie bootstrap oznacza, że kod nie kompiluje kompilatora z tym samym kompilatorem), chcesz używać prostszego języka z mniejszym ładowaniem. Dlatego C jest wykonywane w asemblerze, nie ma żadnych wymagań dotyczących ładowania początkowego, które zaczynasz od pierwszej instrukcji po zresetowaniu. JAVA, na pewno możesz napisać Jvm w C i załadować C z asm, a następnie załadować JAVA, jeśli chcesz z C, ale także wykonać JAVA w C.
Ponieważ kontrolujemy założenia dotyczące tych pętli kopiowania, są one z definicji ciaśniejsze i czystsze niż ręcznie dostrojone memcpy / memset.
Zwróć uwagę, że Twoim drugim problemem było:
jeśli są to lokalne kary, nie ma problemu, jeśli są globalne, najpierw trzeba zainicjować dane .data, aby działały, a jeśli spróbujesz wykonać tę sztuczkę, to nie powiedzie się. Zmienne lokalne, dobrze, które będą działać. jeśli z jakiegoś powodu zdecydowałeś się stworzyć statycznych miejscowych (lokalne globale lubię je nazywać), to znów jesteś w kłopotach. Za każdym razem, gdy wykonujesz zadanie w deklaracji, powinieneś się nad tym zastanowić, w jaki sposób jest ono realizowane i czy jest bezpieczne / rozsądne. Za każdym razem, gdy zakładasz, że zmienna ma wartość zero, gdy nie jest zadeklarowana, ta sama umowa, jeśli zmienna lokalna nie jest przyjmowana jako zero, jeśli jest globalna, to jest. jeśli nigdy nie zakładasz, że są zerowe, nigdy nie musisz się martwić.
źródło