C - łańcuchy tekstowe definiowane w parametrach

Mam pytanie do osób w większym stopniu niż ja ogarniających zachowanie kompilatorów języka C. Właściwie chcę się upewnić, że moje rozumowanie jest słuszne.

Pisze obecnie pewną bibliotekę, której głównym elementem będzie maszyna stanów. Jednym z jej głównych zadań będzie przetwarzanie i przesyłanie dalej wiadomości wrzucanych do bufora pierścieniowego.

Od strony użytkownika będzie to wyglądało w ten sposób, że w dowolnym momencie w programie będzie musiał wywołać funkcję, która będzie wyglądała mniej więcej tak:

send(const char* str);

Wskaźnik str zostanie zapisany w buforze cykliczny, gdzie będzie czekał do momentu, aż maszyna stanów będzie gotowa go odczytać i przesłać dalej. W przypadku danych zapisywanych w RAM-ie siłą rzeczy trzeba będzie więc zadbać, żeby żyły odpowiednio długo i np. nie znikły ze stosu. Konieczne będzie więc używanie tablic globalnych albo lokalnych statycznych.

A co w sytuacji, kiedy będę chciał wysłać po prostu wartość podaną wprost w argumencie funkcji, np.?

send("przykladowy tekst");

Co w takiej sytuacji zrobi kompilator?

  1. Zapisze tekst bezpośrednio we flashu i przekaże funkcji wskaźnik na początek łańcucha zapisanego w pamięci nieulotnej.
  2. Przed wywołaniem skopiuje wartość z flasha na stos i przekaże wskaźnik do miejsca w pamięci RAM - po zwinięciu się stosu zawartość może zostać nadpisana.
  3. Zachowanie nie jest jasno zdefiniowane i zależy od innych czynników.

Oczywiście mówimy o w miarę współczesnym, 32bitowym mikrokontrolerze, z jedną przestrzenią adresową dla flasha/RAM-u.

Najbardziej logiczna wydaje mi się opcja pierwsza, ale jak mówię - wolę się upewnić.

Reply to
Atlantis
Loading thread data ...

piątek, 26 sierpnia 2022 o 15:43:15 UTC+2 Atlantis napisał(a):

Zawartość będzie zapisana we flashu, a sama zmienna (tablica znaków) w RAMie. Podczas initu nastąpi kopiowanie z flashu do RAMu.

send("przykładowy tekst"); zaś dostanie za argument stałą - adres w RAMie. Najprawdopodobniej każde wywołanie send("ten sam tekst"); zajmie taką samą porcję flasha i RAMu - ale tu pewności już nie ma...

W AVR - i pewnie w innych harvardach - jest możliwość zrobienia tak, że nie będzie używany RAM - send(PSTR("tekscik z ROMu")); a jako argument leci wskaźnik - ale do flasha. Ale wtedy w buforze musi być zapisana również informacja, że ten akurat wskaźnik jest do flasha.

Myślę, że ogólnie bardzo utrudniasz sobie życie.

Reply to
Dawid Rutkowski

Tak to było robione na części ośmiobitowych mikrokontrolerów, takich jak PIC16/PIC18 albo właśnie AVR (i w związku z tym również większość płytek Arduino). Żeby uniknąć kopiowania do RAM-u, trzeba było deklarować łańcuchy tekstowe za pomocą specjalnych makrodefinicji. Istniały też specjalne wersje funkcji do operacji na łańcuch, przygotowane z myślą o nich.

W przypadku nowoczesnych układów 32bitowych (STM32, PIC32, ESP8266/ESP36) nie ma już takiej potrzeby, bo zarówno flash jak i RAM stanowią część tej samej przestrzeni adresowej i można się do nich odwoływać za pomocą tych samych wskaźników, a łańcuchy zdefiniowane jako const char* trafiają do flasha. Oczywiście trzeba uważać na to co się robi, bo np. próba zapisu pod adres we flashu spowoduje rzucenie wyjątku.

Moje pytanie dotyczyło czegoś innego - chciałem się upewnić, czy faktycznie łańcuch zdeklarowany jako argument funkcji (a nie jawnie, jako globalna stała z kwalifikatorem const) zawsze będzie zapisany we flashu. Wyobraźmy sobie np. hipotetyczną sytuację:

Send("Lights on"); Send("Lightf off");

Czy nie istnieje np. ryzyko, że kompilator spróbuje to niejawnie zoptymalizować i zdefiniuje sobie we flashy łańcuchu "Lights ", "on" oraz "off", a potem będzie tworzył ich kombinacje na stosie, przed przekazaniem w argumencie funkcji?

Reply to
Atlantis

W dniu 2022-08-26 o 20:25, Atlantis pisze:

No to skompiluj taki program i podejrzyj co wyprodukował kompilator, ja tak zawsze robię jak nie jestem pewien działania wyrażenia czy funkcji w avr-ach, plik lss. (32-bitowcami się nie zajmuję).

Reply to
Janusz

Ale wymyśliłeś... Ogólnie należy przypomnieć sobie C - i niezależnie, co zrobi kompilator, taka zmienna będąca argumentem funkcji ma gwarantowany czas życia tylko do wyjścia z tego konkretnego wywołania tej funkcji (a jeszcze może chciałbyś "reentrant"?). Tak jest w C i tyle. Więc albo sobie w tej funkcji gdzieś kopiujesz ten string albo jako argumentów używasz globali.

Reply to
Dawid Rutkowski

Atlantis snipped-for-privacy@wp.pl napisał(a):

Dlaczego miałby tak robić? Skąd taki pomysł? Poza tym możesz sam sprawdzić, zajęłoby Ci to mniej czasu niż pisanie posta. Nie mówiąc o czekaniu na odpowiedzi. . Potwierdzam, że GCC na cortex-m4 umieszcza łańcuch we Flashu i przekazuje do niego wskaźnik.

Reply to
Grzegorz Niemirowski

On 26.08.2022 20:25, Atlantis wrote: [...]

Skompiluj sobie do assemblera z parametrem -fverbose-asm np. taki programik:

===================== CIĄĆ TUTAJ========================= void send(const char* str);

const int x = 5; const int a;

int main(int argc, char *argv[]) { const int y = 7; static const int z = 8; const char *s = "dupa"; const int t[] = {1, 2, 3, 4, 5};

send("Lights on"); send("Lights off");

return 0; } ===================== CIĄĆ TUTAJ=========================

Np. w przypadku ARM-a wychodzi coś takiego:

===================== CIĄĆ TUTAJ========================= .cpu arm7tdmi .arch armv4t .fpu softvfp .eabi_attribute 20, 1 @ Tag_ABI_FP_denormal .eabi_attribute 21, 1 @ Tag_ABI_FP_exceptions .eabi_attribute 23, 3 @ Tag_ABI_FP_number_model .eabi_attribute 24, 1 @ Tag_ABI_align8_needed .eabi_attribute 25, 1 @ Tag_ABI_align8_preserved .eabi_attribute 26, 1 @ Tag_ABI_enum_size .eabi_attribute 30, 6 @ Tag_ABI_optimization_goals .eabi_attribute 34, 0 @ Tag_CPU_unaligned_access .eabi_attribute 18, 4 @ Tag_ABI_PCS_wchar_t .file "testargt.c" @ GNU C17 (GNU Toolchain for the Arm Architecture 11.2-2022.02 (arm-11.16)) version 11.2.1 20220111 (arm-none-eabi) @ compiled by GNU C version 7.3-win32 20180312, GMP version 6.2.1, MPFR version 3.1.6, MPC version 1.0.3, isl version isl-0.15-1-g835ea3a-GMP

@ GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072 @ options passed: -mcpu=arm7tdmi -mfloat-abi=soft -marm -march=armv4t .text .global x .section .rodata .align 2 .type x, %object .size x, 4 x: .word 5 .global a .align 2 .type a, %object .size a, 4 a: .space 4 .align 2 LC0: .ascii "dupa\000" .align 2 LC1: .ascii "Lights on\000" .align 2 LC2: .ascii "Lights off\000" .text .align 2 .global main .syntax unified .arm .type main, %function main: @ Function supports interworking. @ args = 0, pretend = 0, frame = 16 @ frame_needed = 1, uses_anonymous_args = 0 push {fp, lr} @ add fp, sp, #4 @,, sub sp, sp, #16 @,, str r0, [fp, #-16] @ argc, argc str r1, [fp, #-20] @ argv, argv @ testargt.c:8: const int y = 7; mov r3, #7 @ tmp115, str r3, [fp, #-8] @ tmp115, y @ testargt.c:10: const char *s = "dupa"; ldr r3, .L3 @ tmp116, str r3, [fp, #-12] @ tmp116, s @ testargt.c:13: send("Lights on"); ldr r0, .L3+4 @, bl send @ @ testargt.c:14: send("Lights off"); ldr r0, .L3+8 @, bl send @ @ testargt.c:16: return 0; mov r3, #0 @ _6, @ testargt.c:17: } mov r0, r3 @, <retval>

sub sp, fp, #4 @,, @ sp needed @ pop {fp, lr} @ bx lr @ L4: .align 2 L3: .word .LC0 .word .LC1 .word .LC2 .size main, .-main .section .rodata .align 2 .type t.0, %object .size t.0, 20 t.0: .word 1 .word 2 .word 3 .word 4 .word 5 .align 2 .type z.1, %object .size z.1, 4 z.1: .word 8 .ident "GCC: (GNU Toolchain for the Arm Architecture 11.2-2022.02 (arm-11.16)) 11.2.1 20220111" ===================== CIĄĆ TUTAJ=========================

Jak widać, stringi lądują w sekcji .rodata (czasami nazywanej też rdata), która w przypadku systemów embedded zazwyczaj jest umieszczana we Flashu (aczkolwiek ktoś na starcie systemu może chcieć skopiować ją do SRAM, bo odczyty z SRAM są szybsze). I tak jest „wszędzie”. Natomiast const-y „typów prostych” zazwyczaj również lądują w .rodata, ale nie zawsze – na ARM (jak widać) czy na x86-64 tak, ale na MIPS już nie (tutaj w sekcji .sdata – „small data”).

Powyższe dotyczy oczywiście jedynie słusznego kompilatora, czyli gcc.

Reply to
JDX

Zbyt przekombinowane, C takie nie jest :-) Szczegolnie, ze ma przekazac adres tekstu.

Ale .. jak juz mowa o "lepszych prockach" ... generujesz do niego wlasny program, czy masz tam jakis "boot loader", ktory ten Twoj program w jakis sposob startuje? Przepisywanie programu z flash do RAM tez bylo modne, bo flash za wolny ...

J.

Reply to
J.F

Jeśli już proponujesz przypomnienie sobie C, to należałoby sobie przypomnieć czym jest const char* (albo char*) w tym języku. Tutaj nie mamy do czynienia z czymś takim jak String C++ albo innych językach wysokiego poziomu. To nie jest obiekt albo kontener na dane, podczas tworzenia którego zachodziłaby alokacja pamięci. To jest po prostu zwykły wskaźnik, który przechowuje jedną, jedyną informację - adres początku łańcucha znaków w pamięci.

Żeby pokazać różnicę, wyobraźmy sobie następną sytuację w C:

const char* globalny_wskaznik = NULL;

void foo(const char* str) { globalny_wskaznik=str; }

Z następującą sytuacją w C++:

std::String globalny_string;

void bar(std::String str) { globalny_string = str; }

Co się stanie po wywołaniu pierwszej funkcji? Otrzyma ona wskaźnik z adresem na jakiś obszar w pamięci. Adres ten zostanie przekopiowany do globalnego wskaźnika, a sam str zostanie zdjęty ze stosu. To co się będzie działo z samymi danymi na które wskazywał nie jest w żaden sposób określone - wszystko zależy od tego o jakim typie pamięci mówimy. Mogą rezydować wiecznie we flashu, mogą być cały czas dostępne jako zmienna globalna w RAM-ie, ale mogą też zniknąć w wyniku zdjęcia ze stosu albo dealokacji ze sterty w innej części programu.

Co się natomiast dzieje w drugim przykłaadzie? Wywołanie funkcji powoduje utworzenie obiektu klasy std::String, który zostaje zainicjowany konkretnym tekstem i utworzy swoją instancję w pamięci. W skutek użycia operacji przypisania zostanie wywołany konstruktor kopiujący, który utworzy osobną kopię zawartości str w obiekcie globalny_string. Po wyjściu z funkcji bar zostanie wywołany destruktor obiektu str, jednak jego globalna kopia będzie nadal istniała.

Reply to
Atlantis

Tutaj już czas na przypomnienie sobie "Alicji w krainie czarów" - rozdziału z piosenką bodajże Białego Rycerza (czyli skoczka): "Nazwa tej piosenki nazywa się >>rybie oczy<<" itd. Czyli różnica między nazwą wskaźnika, zawartością wskaźnika, nazwą tablicy i zawartością tablicy.

Przy wywołaniu send("tekscik"), gdy send(const char *aArg), "tworzony" jest zarówno wskaźnik aArg jak i anonimowa tablica z zawartością "tekscik". I obie te zmienne mają czas życia do zakończenia wywołania tej funkcji.

Jakaś sekta twierdzii, że "dynamicznie" można tworzyć tylko obiekty? Różnica jest tylko taka, że przy tworzeniu obiektu wywoływany jest konstruktor (w C++ cholera wie, który).

A co do send(const char *aArg) to nigdy nie potrafiłem zapamiętać, czy zabronione jest zmienianie wartośvi aArg czy też wartości wskazywanej... Czyli czy nie wolno: aArg=b; czy aArg[3]=c; Bo była chyba jeszcze konstrukcja, która nie pozwalała na to inne podstawienie.

Ogólnie to są bzdury do męczenia studentów. C K&R rulez na wieki ;>

Reply to
Dawid Rutkowski

Tak z ciekawości pytam, czemu opisy stanów jako parametry robocze mają być stringami? To ma działać na styku z białkiem?

Reply to
Marek

On 27.08.2022 10:28, Dawid Rutkowski wrote: [...]

Wolno.

Nie wolno.

Nadal jest: send(const char * const aArg)

Zwłaszcza trigraphy. :-D

Reply to
JDX

Tutaj nie masz racji, a przynajmniej opisywane przez Ciebie zachowanie nie jest żadną uniwersalną regułą. Nie ma żadnej zasady która mówiłaby, że anonimowa tablica jest tworzona na stosie tuż przed utworzeniem wskaźnika, który by na nią wskazywał. Takie ciągi znaków zwykle są zbierane podczas kompilacji i umieszczone w rodata (czyli w przypadku mikrokontrolerów najczęściej we flashu) i potem po prostu wskaźnik będzie inicjowany wartością wskazującą na początek odpowiedniego ciągu znaków. Widać to chociażby w HEX-ach wsadów do starszych systemów mikroprocesorowych (Z80, 8051) gdzie wszystkie teksty są zebrane w jednym miejscu. Problemy pojawiają się w przypadku niektórych architektur, gdzie kompilator (o ile nie poinformujemy go, by robił inaczej) będzie przy starcie programu kopiował dane do RAM-u, bo wskaźnik na dane w RAM-ie i we flashu to nie to samo. Nawet wtedy ciągle jednak będą one istniały cały czas w formie zmiennych globalnych, a nie zostanę ulokowane na stosie.

Moje pytanie odnosiło się do czegoś innego - chciałem się upewnić, czy przypadkiem w pewnych sytuacjach program w sposób niejawny nie zrobi mi niespodzianki i np. zamiast użyć wskaźnika wprost do ciągu w rodata w sposób niejawny nie wygeneruje tymczasowego tekstu na stosie, złożonego z fragmentów innych tekstów, w ramach optymalizacji.

w przypadku const char* masz do czynienia ze wskaźnikiem na const char. Czyli nie wolno ci modyfikować wartości na którą wskazuje wskaźnik, ale już sam wskaźnik możesz modyfikować. Zabronione jest *aArg='a', ale już jak najbardziej wykonasz aArg++. Bez tego nie byłoby przecież możliwe iterowanie po łańcuchach tekstowych w formie const char*. W C istnieją też wskaźniki char* const - w tym przypadku możesz modyfikować zawartość, ale już nie wolno zmienić adresu na który wskazuje wskaźnik. Dostępna jest też najbardziej restrykcyjna kombinacja: const char* const

- ani nie zmodyfikujesz wartości, ani adresu na który wskazuje wskaźnik.

Reply to
Atlantis

Piszesz w C. Nie w asemblerze, który z tego C zrobi kompilator. Zrobi jak zrobi i może nawet działać. Ale nie ma pewności. Inny kompilator, a nawet ten sam z innym -Ox, może zrobić inaczej i działać już nie będzie. Jak jest send("tekscik") to po zakończeniu tej funkcji "tekscik" w tym samym miejscu w pamięci może dalej być, ale nie musi. Dlatego należy go skopiować - albo dawać wskaźnik na globala. Tyle z punktu widzenia C. Wskaźniki nie są do zabawy - bardzo łatwo utrudnić sobie życie.

Cóż, nikt do C nie zmusza - można pusać w assemblerze, możnasobie zmodyfikować język i kompilator - tylko wtedy już nie będzie to C.

Reply to
Dawid Rutkowski

sobota, 27 sierpnia 2022 o 11:53:58 UTC+2 JDX napisał(a):

O, czego to się człowiek nie dowie na starość... Ale czy to na pewno K&R C? Nie pamiętam tego z książki (a książka na poziomie "Diuny" ;), no i nie jest to coś, co jest potrzebne samo z siebie, a dopiero na komputerach w dzikich krajach, co nie mają ASCII, a jakiś np. ISO646 (no dobra EDBIC też miał kłopot, ale na IBM360 pusze się w FORTRANie ;)

Ale jest fajne. Poubarwiam tak niektóre programy ;>

Reply to
Dawid Rutkowski

Albo send(char * const aArg) :-)

Obie o tyle bezsensowne, ze funkcja i tak dostanie kopie adresu, nie ma jak zmienic argumentu. W innym miejscu ma taka deklaracja sens.

J.

Reply to
J.F

Po co sie martwisz? Kompilator ma to zrobic tak zeby lancuch znakow zachowywal sie jak stala. W szczegolnosci ma istniec po tym jak zakonczy sie wykonanie funkcji. Teoretycznie mozna sobie wyobrazic jakis skomplikowany system ktory przydziela dynamicznie pamiec a zwalnia ja gdy nie jest potrzebna. Z tego co wiem zaden kompilator nie robi czegosc takiego. Robia co innego, jak masz np.

"turn lighths on"

i

"lights on"

kompliator moze jako drugi lancuch uzyc koncowa czesc pierwszego. Kompilator moze zastapic _uzycie_ lancucha znakow przez sztuczki w podobnym stylu do tego co sobie wyobrazales. Ale te sztuczki dzialaja gdy w miejcu uzycia lancuch jest znany.

Reply to
antispam

Prawie na pewno tak.

Ano. Chyba tylko raz w życiu użyłem trigraph-ów. Było to wtedy, gdy pisałem jakiś „heloł łorld” w C na S/390. :-D

To zobacz sobie ten niedoceniany język programowania:

formatting link
:-D

Reply to
JDX

Wiki twierdzi, że jednak dopiero ANSI C.

I nic dalej?

Przemyśliwałem. Ale moim ulubionym ezoterycznym językiem jest hq9+ (choć nie jest Turing-complete).

Reply to
Dawid Rutkowski

On 27.08.2022 21:43, Dawid Rutkowski wrote: [...]

A gdzie konkretnie tak twierdzi? Bo jestem pewien, ze o trigraphach czytałem kiedyś w książce autorstwa K&R.

W jakim sensie nic dalej?

Reply to
JDX

ElectronDepot website is not affiliated with any of the manufacturers or service providers discussed here. All logos and trade names are the property of their respective owners.