logo elektroda
logo elektroda
X
logo elektroda
Adblock/uBlockOrigin/AdGuard mogą powodować znikanie niektórych postów z powodu nowej reguły.

AVR GCC wycieki pamięci, praca z dużymi tablicami char/ stringi

Karol966 10 Mar 2023 10:06 657 13
  • #1 20478989
    Karol966
    Poziom 31  
    Cześć! Poszukuję wsparci gdyż powstał mętlik w głowie. Mam program, który komunikuję się z modułem GSM, używa wielu globalnych tablic, funkcji operujących na strumieniach typu strcpy, strncpy, strstr, strtok, poniżej wklejam większość deklaracji dużych zmiennych:
    Kod: C / C++
    Zaloguj się, aby zobaczyć kod


    a to kawałek programu:
    Kod: C / C++
    Zaloguj się, aby zobaczyć kod


    Bardzo proszę o wskazanie najważniejszych problemów/ potencjalnych źródeł błędów i wycieku pamięci. Dodam, że procesor usypia się regularnie a po każdym obudzeniu celowo resetuję go za pomocą WDT (aby wyczyścić RAM). Niestety najwyraźniej błędy trafiające się nawet po wielu dniach pracy powodują że dochodzi do wycieku przed uśpieniem po czym program już nie wstaje (lub wstaje tylko za pomocą jednego z dwóch różnych przerwań zewnętrznych).

    Z tego co zdążyłem doczytać:
    1) punkt 3.2 https://ww1.microchip.com/downloads/en/Appnotes/doc8453.pdf aby nie używać zmiennych globalnych (program przerabiałem wiele razy - były również największe tablice lokalne), jeżeli tablica jest globalna to od razu jest zajęty ram na jej alokację, jeżeli jest lokalna to ram początkowo nie jest zajęty a na stos odkładana jest ta tablica w momencie wywołania funkcji po czym po powrocie jest zdejmowana (zwalniany stos) więc wydaje się to faktycznie lepszym pomysłem.

    2) Jeżeli używam strncpy a w src na pozycji 'n' nie ma znaku '\0' to znak ten nie zostanie dołączony do dst, jeżeli dalej wykona się kolejne operacje na stringach to mogą one nie działać prawidłowo.

    3) Próbowałem sprawdzić zajętość RAM'u ale ciągle dostaje wynik 0, użyłem funkcji z pliku stackmon.c z https://www.elektroda.pl/rtvforum/topic1197789.html
    Kod: C / C++
    Zaloguj się, aby zobaczyć kod

    A użycie:
    Kod: C / C++
    Zaloguj się, aby zobaczyć kod

    Sprawdzałem w symulatorze i w obszarze data IRAM widzę znak canary czyli błąd musi leżeć w funkcji liczenia zajętości, tak?[/b][/b]

    Dodano po 1 [godziny] 6 [minuty]:

    Przeniosłem deklarację StackPoint z .int1 do .int3 i program zaczął wyświetlać 'jakiś' wynik
    void StackPaint(void) __attribute__ ((naked)) __attribute__ ((section (".init3")));
    Program idąc spać wyświetla: STACK=1061 (czasami STACK=1059), wynik kompilacji to: Data Memory Usage : 898 bytes 43,8 % Full
    text data bss dec hex
    13494 339 570 14403 3843
  • #2 20479266
    gps79
    Poziom 35  
    Jako, że w zademonstrowanym programie nie ma dynamicznej alokacji pamięci, to nie możemy mówić o wyciekach pamięci.

    1. Funkcja strtok2() nie robi tego, co ma w opisie, więc nie mogę stwierdzić, czy działa poprawnie, a raczej stwierdzam, że nie działa poprawnie. Dość dużo miesza tutaj "static src". Napraw albo funkcję, albo opis.

    2. funkcja strtok2() może zwrócić null, więc instrukcja
    if(ptr[0] != '\0') strncpy(IMEI, ptr+2,15);
    wygeneruje wtedy wyjątek

    3. Instrukcja strtok2(RESPONSE_BUFFER, "OK") prawdopodobnie (patrz 1.) wyszuka w RESPONSE_BUFFER litery 'O' lub 'K', bo tak działa "strpbrk"
    4. Zakładasz, że "ptr+2 bo na początku są "\r\n"". Że tak zapytam: a skąd wiesz, że tak będzie? Przecież nigdzie tego nie sprawdziłeś i prawdopodobnie czytasz nieswoją pamięć.
    5. Te wywołania strcat() są beztroskie.

    W embedded zawsze sprawdzaj argumenty i wynik operacji. Projektujesz oprogramowanie, które ma działać bezobsługowo.
  • #3 20479632
    Karol966
    Poziom 31  
    Dzięki za odpowiedź,
    Jeżeli nie o wycieki pamięci chodzi to o nadpisanie niedozwolonych obszarów (np poza zadeklarowaną wielkość tablicy, sprawdziłem celowo do char tab[10] wpisałem tab[11] = coś i program od razu się wywala - ciągłe resety także jest to typowo undefined behavior).

    1) Funkcja nie jest mojego autorstwa a po usunięciu "static src" przestaje działać.

    gps79 napisał:
    2. funkcja strtok2() może zwrócić null, więc instrukcja
    if(ptr[0] != '\0') strncpy(IMEI, ptr+2,15);
    wygeneruje wtedy wyjątek

    Wygeneruje wtedy wyjątek - na pewno? Cel był taki, że jeżeli jest coś pod adresem ptr to ma kopiować a inaczej nic nie robić. Bardziej boję się zachowań gdy np z niewiadomych mi przyczyn moduł GSM nie prześle całej informacji (będzie krótsza) to wtedy będę kopiował jakieś śmieci aczkolwiek nigdy jeszcze odczytana wartość IMEI nie była błędna.

    gps79 napisał:
    3. Instrukcja strtok2(RESPONSE_BUFFER, "OK") prawdopodobnie (patrz 1.) wyszuka w RESPONSE_BUFFER litery 'O' lub 'K',
    To prawda, wystarczy zamiast "OK" napisać samo "O", w tym konkretnym przypadku mogę również wykorzystać klasyczną strtok zamiast modyfikowanej strtok2.

    gps79 napisał:
    4. Zakładasz, że "ptr+2 bo na początku są "\r\n"". Że tak zapytam: a skąd wiesz, że tak będzie?
    Z dokumentacji mogułu GSM ale faktycznie różne rzeczy mogą się wydarzyć i ramka mogło by przyjść przekłamana/ niekompletna/ nie wiem. Sugerujesz abym sprawdził czy na pewno jest tam ciąg "\r\n"? Pamięć czytam w tym przypadku właściwą bo zawsze poprawnie IMEI odczytuję i na serwerze również widzę, że jest poprawnie wysłany.

    gps79 napisał:
    5. Te wywołania strcat() są beztroskie.
    Bardzo Cię proszę, rozwiń, zaoponuj coś lepszego. Jednocześnie wolał bym uniknąć tego typu zapisów:
    sprintf_P(txframe, PSTR("&id=%s&lat=%s&lon=%s&alt=%s&speed=%s&course=%s&utc=%s&vbat=%d&vchg=%d"), IMEI, latitude, longitude, altitude, speedOTG, course, UTCdatetime, v_bat, v_chg/10);
  • #4 20480111
    mpier
    Poziom 29  
    Witam
    to strtok2 też nie wiem jak ma działać. Jeśli masz dostępne możesz użyć strsep zamiast strtok_r (bez zmiennych statycznych, zamiast strtok).

    W avr-libc są też strlcat i strlcpy, i snprintf.
  • Pomocny post
    #5 20480277
    gps79
    Poziom 35  
    Hej,
    Karol966 napisał:
    Funkcja nie jest mojego autorstwa
    Czy możesz napisać, skąd ją wziąłeś? Może uda się przeanalizować tok myślenia autora.

    Karol966 napisał:
    Wygeneruje wtedy wyjątek - na pewno?
    Sprawdziłem i dla AVR niestety nie będzie wyjątku. Tym gorzej dla programisty.

    Operacje na stringach mogą być niebezpieczne, szczególnie, jeśli dostajesz stringi z zewnątrz.
    https://duckduckgo.com/?q=strcat+unsafe
    Powstały bezpieczniejsze wersje tych funkcji, które jako dodatkowy argument przyjmują maksymalną ilość danych do skopiowania.
    strcpy_P => strncpy_P
    strcat_P => strlcat_P
    strcat => strlcat

    Rozmiar, na który możesz sobie pozwolić, to sizeof(txframe).
    czyli:
    Kod: C / C++
    Zaloguj się, aby zobaczyć kod

    O wspomnianych tutaj funkcjach możesz poczytać tutaj:
    https://www.nongnu.org/avr-libc/user-manual/group__avr__pgmspace.html

    Do wyszukiwania jednego stringu w drugim użyj funkcji strstr().
  • #6 20481908
    Karol966
    Poziom 31  
    gps79 napisał:
    Operacje na stringach mogą być niebezpieczne, szczególnie, jeśli dostajesz stringi z zewnątrz

    Poczytałem, faktycznie śliski temat. Rozumiem ryzyko.
    Ciekawe rozwiązanie problemu jest poniżej:
    Kod: C / C++
    Zaloguj się, aby zobaczyć kod


    gps79 napisał:
    5. Te wywołania strcat() są beztroskie.

    Czy chodzi Ci o ryzyko jakie z tego płynie? Zauważ, że np "strcat(txframe,IMEI);" tctframe ma 230 bajtów a IMEI ma 15 jednak czy to, że "IMEI" może nie posiadać znaku końca stringu przypadkiem nie wywali programu?

    Sporo funkcji do komunikacji z modułem pochodzi z tej strony: https://www.electronicwings.com/avr-atmega/http-client-using-sim900a-gprs-and-atmega16

    Co do strtok, nie pamiętam gdzie znalazłem ani jaki był pierwotny problem ale czuję po kościach, że chodziło właśnie o to, że funkcja biblioteczna modyfikuje/ traci źródło.
  • #7 20482183
    tmf
    VIP Zasłużony dla elektroda
    Karol966 napisał:
    Czy chodzi Ci o ryzyko jakie z tego płynie? Zauważ, że np "strcat(txframe,IMEI);" tctframe ma 230 bajtów a IMEI ma 15 jednak czy to, że "IMEI" może nie posiadać znaku końca stringu przypadkiem nie wywali programu?

    Oczywiście, że wywali. Każdy string musi się kończyć znakiem NUL. Stąd też używaj funkcji typu strncat itd. One mają możliwość podania maksymalnej długości ciągu. Więc przynajmniej od tej strony są bezpieczne. Oczywiście nie gwarantuje to, że otrzymany ciąg ma sens.
    Najlepiej debugować takie problemy używając sprzętoweg debugera i pułapek na pamięć lub w przypadku AVR masz symulator, na którym można przetestować różne scenariusze błędów.
    BTW, IMHO pakowanie wszystkiego na siłę jako tablic lokalnych niekoniecznie ma sens - nie masz kontroli nad tym ile to zajmie pamięci. Zmienne globalne przynajmniej są liczone przez avr-size.
  • #8 20482839
    gps79
    Poziom 35  
    "strcpy_safe1" i "strcat_safe1", które wkleiłeś działają tak samo, jak funkcje biblioteczne, zaproponowane przeze mnie i @mpier 'a wyżej, więc nie trzeba "odkrywać koła na nowo".

    Zauważ, że wielokrotne wywołanie funkcji strcat(txframe,<cośtam>); (a po naprawie kodu będzie to "strlcat") dokleja kolejno jakiś ciąg znaków do zmiennej "txframe". Bezpieczna funkcja "strlcat" nie pozwoli na przepełnienie bufora. Jeśli string w zmiennej "txframe" wypełnia cały bufor, to najprawdopodobniej nastąpiło ucięcie stringu i jest on dla Ciebie bezużyteczny. Tu musisz sam zdecydować, co z tym fantem zrobić.
    AVR GCC wycieki pamięci, praca z dużymi tablicami char/ stringi
    Twój kod mógłby wyglądać tak:
    Kod: C / C++
    Zaloguj się, aby zobaczyć kod


    "Appnotes/doc8453.pdf" odnosi się do optymalizacji kodu i nie pomoże rozwiązać Twojego problemu z kodem.

    Czy możesz napisać, skąd wziąłeś funkcję "strtok2()"? Może uda się przeanalizować tok myślenia autora.
  • #9 20482913
    Karol966
    Poziom 31  
    gps79 napisał:
    Zauważ, że wielokrotne wywołanie funkcji strcat(txframe,<cośtam>)
    Kontroluję długość doklejanych elementów.
    Wydaje mi się, że w tym przypadku nie mogę sobie pozwolić na użycie strlncat gdyż skopiuje ona nie to co potrzebuję, np UTC ma 18 znaków ale nie potrzebne mi ostatnie cyfry więc kopiuję tylko 14 znaków.
    Przerobiłem trochę tą funkcję (nie jest powiedziane, że odczyt GNSS jest miejscem, w którym mój program się wysypuje ale faktycznie jest do poprawy), obecnie mam tak:
    Kod: C / C++
    Zaloguj się, aby zobaczyć kod

    txframe jest sporo większe niż do tej pory najdłuższa ramka którą moduł wysłał na serwer. Do funkcji get_value mogę dodać sumator/ licznik obecnej długości powstałego stringu a dalej dodać zabezpieczenie przed jego przepełnieniem. Tylko czy użyć globalnej zmiennej? Chciałbym w pisać poprawne programy a wszelkie czytane źródła piszą o tym, że globalne są brzydkie. Lokalny licznik, static też nie pozwoli na rozwiązanie problemu bo muszę go też zerować. Ostatecznie w funkcji get_value mogę dodać warunek np if(dst == NULL) {curr_str_length = 0; return;} i w ten sposób zerować licznik przy nowej transmisji. Dobrze?

    Znalazłem źródło strtok2: https://stackoverflow.com/questions/30294129/i-need-a-mix-of-strtok-and-strtok-single
  • #10 20482950
    tmf
    VIP Zasłużony dla elektroda
    Masz np. strncpy, który kopiuje nie więcej niż n znaków. Hak wygląda definicja funkcji get_value?
    Ponadto masz w powyższej funkcji użyte txtframe, ale definicja najwyraźniej jest gdzie indziej. Ja osobiście nie lubię jeśli funkcja odwołuje się do zmiennych zdefiniowanych poza nią - łatwo wtedy o błędy.
    Karol966 napisał:
    Do funkcji get_value mogę dodać sumator/ licznik obecnej długości powstałego stringu a dalej dodać zabezpieczenie przed jego przepełnieniem. Tylko czy użyć globalnej zmiennej?

    W c++ można się tak bawić, w c nie za bardzo. W praktyce za każdym razem trzeba sprawdzić, gdzie jest koniec, bo jak coś ci w międzyczasie zmieni ten licznik to będzie kłopot. Ale mam wrażenie, że rozwiązujesz problem, który sam stworzyłeś.
    Karol966 napisał:
    Chciałbym w pisać poprawne programy a wszelkie czytane źródła piszą o tym, że globalne są brzydkie. L

    Bo są. Ale w embedded czasami zasady się zmieniają. Zmienne globlane mają tą zaletę, że miejsce pod nie jest alokowane na etapie kompilacji i ta alokacja się nie zmienia przez cały czas życia programu. Stąd też masz na etapie kompilacji dokładną informację o ilości zajętego miejsca i odpadają problemy np. z ewentualnym przepełnieniem stosu. Tak w ogóle to jakiego procka używasz?
    Dla testów można np. wszystkie łańcuchy wydłużyć o 1 bajt, nadpisać go jakąś charakterystyczną wartością. Jeśli coś nadpisuje łańcuch to szybko to zauważysz, bo nadpisze ci ten marker. Oczywiście to dobrze zadziała jeśli masz sprzętowy debugger.
  • #11 20482966
    Karol966
    Poziom 31  
    Procesor to poczciwa ATmega328PB,
    tmf napisał:
    Jak wygląda definicja funkcji get_value?
    Jest wyżej, aczkolwiek zacząłem wprowadzać modyfikację i jednocześnie doszedłem do stwierdzenia, że muszę globalnie zadeklarować mój licznik (by w innych funkcjach kopiujących/ sklejajacych również sprawdzać pozostałe miejsce. Lub inna opcja akurat do głowy wpadłą, za każdym razem sprawdzać, czy to co chcę dokleić + aktualna długość jest większy niż rozmiar bufora.
    Kod: C / C++
    Zaloguj się, aby zobaczyć kod
  • Pomocny post
    #12 20483064
    tmf
    VIP Zasłużony dla elektroda
    Pamiętaj, że zmienne statyczne są tworzone raz, więc jeśli zmienisz dst, to i tak licznik będzie jeden.
    Poza tym, w czym twoja funkcja jest lepsza od strncat? Równie dobrze możesz sprawdzić czy strlen(dst) + strlen(txt) jest mniejsze niż sizeof(txtbuf) przed kopiowaniem. Tu tez potencjalnie masz błąd:
    if(str_curr_length+length >= TXFRAME_SIZE)
    Pamiętaj, że strlen nie zwraca terminatora NUL jako dodatkowego znaku. Stad tez możesz mieć sytuację, że sklejasz dwa teksty, ale nie ma już w buforze miejsca na NUL. W efekcie masz nieprawidłowo zakończony łańcuch w c, czyli większość funkcji ze string.h się wysypuje.
  • Pomocny post
    #13 20483455
    gps79
    Poziom 35  
    Przyjrzałem się funkcji "strtok2()" i wniosek mam taki, że nie nadaje się ona do Twojego programu.
    W funkcji "odczyt_IMEI" potrzebujesz odebrać IMEI, które jest zawarte przed napisem "OK". Możesz to zrealizować w taki sposób (przenieś do siebie tylko funkcję "odczyt_IMEI"):
    Kod: C / C++
    Zaloguj się, aby zobaczyć kod
  • #14 20486011
    Karol966
    Poziom 31  
    gps79 napisał:
    Przyjrzałem się funkcji "strtok2()" i wniosek mam taki,

    Bardzo Ci dziękuję za zaangażowanie w moim temacie. Oczywiście jetem wdzięczny każdemu za każdą odpowiedź.

    Wczoraj do późnej godziny pracowałem nad tym programem, napisałem bardzo dużo kodu od początku, nie patrząc wcale wstecz na to co i jak pierwotnie wykonałem. Oto część kodu, który tworzę. Jak widać funkcję do odczytu IMEI również napisałem i jest mega krótka w porównaniu do Twojej (przede wszystkim dlatego, że funkcja "send_at_command" sama usuwa wszystkie znaki cr\lf z bufora odbiornika USART. Jak również widać, przestałem korzystać z strtok2 na rzecz bibliotecznej strtok oraz przyjąłem taki sam mechanizm przeszukiwania stringów - korzystam z tego, że strstr zwraca wskaźnik na początek znalezionego ciągu. Nie wiem na ile to niebezpieczne, na ile możliwe by producent zmienił długość niektórych odpowiedzi na komendy AT gdyż zakładam sztywne ich długości.
    Od nowa napisałem również wysyłanie komend AT i odbiór odpowiedzi. Nie dodałem jeszcze ewentualnej funkcji powtarzania n razy/ prób.
    Kod: C / C++
    Zaloguj się, aby zobaczyć kod

Podsumowanie tematu

W dyskusji poruszono problem wycieków pamięci w programie komunikującym się z modułem GSM, który wykorzystuje wiele globalnych tablic i funkcji operujących na łańcuchach znaków, takich jak strcpy, strncat, strtok. Uczestnicy zwrócili uwagę, że brak dynamicznej alokacji pamięci w kodzie eliminuje ryzyko wycieków pamięci, jednak istnieje ryzyko nadpisania pamięci i undefined behavior przy niewłaściwym użyciu funkcji. Zasugerowano użycie bezpieczniejszych wersji funkcji do operacji na łańcuchach, takich jak strlcat i strlcpy, oraz konieczność sprawdzania długości łańcuchów przed ich kopiowaniem. Uczestnicy podkreślili znaczenie debugowania i testowania kodu w kontekście embedded, a także omówili modyfikacje w kodzie, które poprawiają jego bezpieczeństwo i stabilność.
Podsumowanie wygenerowane przez model językowy.
REKLAMA