arm gcc, stack reuse

Fórumok

Sziasztok!

Egy erdekes problema jott szembe most. Van egy ARMv6 Cortex-M0 mikrokontrollerre szant program, ami kb ezt csinalja tobbek kozott:

main()
{
// ...
 while ( 1 )
  {  // ... 
     if ( whatever )
      {    call_something_that_prints_the_stack_pointer();
      }
     // ...
     if ( something_totally_unrelated )
      {    uint8_t array[256];
           do_something_with_array(array);
      }
     // ...
  };
}

Namost, azt tapasztaltam hogy a call_something_that_prints_the_stack_pointer() altal kiirt ertek, meg ugy altalaban a stack fogyasztasa (itt ebben a peldaban) hatarozottan fugg az array[] meretetol. Ha azt kisebbre veszem, pl 128-ra, akkor a stack pointer kiirt erteke 128-cal nagyobb lesz. Most gondolnank hogy "igen, ez logikus". Dehat ugye nem az. Kis utanajarassal eljutottam ide, es ezalapjan... hat, igen, ezalapjan is nagyon ugy nez ki hogy nem kene fuggnie mert az array[] az elegge local, es ezt a fordito szereti romma optimalizani (es aki dangling pointereket csinal, az nyilvan igy jart es jarjon is igy). 

Kiprobaltam mas reszleteiben is a programot, valahol azert jol mukodik ez, szoval abszolut nem altalanos. Node mivel mikrokontroller, ezert minden RAM szamit, plane igy, hogy egy ilyen nagyon local scope local scopejaban levo valami miert is fogyasszon barmit... Es hat ugy is tunt fel hogy 1-2 plusz featura hozzaadasa utan osszeert a statikus terulet (data, bss) a stack-kel, es akkor jott a karorakutya, stb :) Raadasul libc-heap nincs is, csak egyedi allokatorok. 

Latott barki mar barmi hasonlo ilyesmit? Nyilvan ez inkabb beagyazott tema, nem klasszik C, de akkoris erdekes... van-e valami mod pl az arm-none-eabi-gcc hasznalataban ahol ezt le tudom debuggolni? Nyilvan automatikus valtozoknak nincsen memoriaterkepe, max frame pointer-hez viszonyitott, de ARM-nel meg kulon frame pointer se nagyon kell mert sp-relativ cimzesu az utasitaskeszlet (LDR*, STR*). Ami meg meginkabb erdekesse teszi ezt a kerdest szerintem...

Thx, A.

Hozzászólások

Szerintem te arra a kérdésre keresed a választ, mekkora stack -re van szükséged.

Ha felveszel egy nagy lokális tömböt, megváltozhat a stack igénye a programnak. Nyilván a mérték attól is függ, hogy mekkora a stack -ed, és időben mikor használod a lokális változót. Pl. ha több függvényed (local scope -od)  van ahol ilyen tömböket veszel fel, és ezeket nem használod azonos hívási útvonalon, akkor a memória használat átlapolódik, így a teljes memória felhasználás nem nő.

Az esetek túlnyomó többségében a fordító nem tudja meghatározni a stack igényét a programnak, mert az futásidőben derül csak ki. (Pl. állapotgép egyes állapotait nem tudja a compiler hogyan fogod futásidőben bejárni). Így aztán a stack méret meghatározása és megfelelő beállítása a programozó feladata. Azaz a GCC nem foglal neked stack -et, annak mérete fixen bekerül (általában) a linker szkriptbe.

Szerintem a fordító egyszerűen lefoglalja a tömböt a függvénybe belépéskor. Az alternatíva az lenne, hogy minden iterációban, amikor a feltétel teljesül, akkor módosítja a stack pointert, de ezt meg nem szeretné. Futásidőre optimalizál, nem memóriára.

Hagyományosan a stack visszafelé növekszik, ARM-on (mármint az ARM utasításkészletben, Thumbban nem biztos) megvan a lehetősége az elölről növekedő stacknek is, de valamiért ez terjedt el.

Én régebben (13 évvel ezelőtt) OpenOCD-vel debuggoltam, kell hozzá egy JTAG adapter, és pici konfigurációval össze lehet kötni gdb-vel.

Szerintem a fordító egyszerűen lefoglalja a tömböt a függvénybe belépéskor. Az alternatíva az lenne, hogy minden iterációban, amikor a feltétel teljesül, akkor módosítja a stack pointert, de ezt meg nem szeretné. Futásidőre optimalizál, nem memóriára.

Ez konkrétan így van, és semmi köze az optimalizációhoz. C99 előtt a függvényeket mindig a függvény elején lehetett csak deklarálni, utána már a függvényen belül bárhol, de az egész blokkon belüli változódeklaráció csak syntax sugar, ill. így a fordító jobban segít, hogy ne használd a változót véletlenül a blokkon kívül (pl. inicializálás nélkül), de attól azok még ugyanúgy a függvényhez tartozó változók, és nem dinamikusan kerülnek foglalgatásra, tehát a bináris interfész és a stacklayout nem módosul ettől. A stackframe-et a fordító mindíg csak függvényhatáron számolja újra továbbra is, akár csak korábban (C99 előtt).

-=- Mire a programozó: "Na és szerintetek ki csinálta a káoszt?" -=-

Ez konkrétan így van, és semmi köze az optimalizációhoz. C99 előtt a függvényeket mindig a függvény elején lehetett csak deklarálni

őőőőő... sztem nem. az ansi/c89 is mar megengedte azt hogy ne csak a fuggveny legelejen hanem a scope-ok/blokkok legelejen is deklaralj automatikus valtozokat - de a fuggvenyen belul barhol lehetnek a blokkok. Nalam mindig az ansi/c89 az alap, ettol csak relative ritkan terek el, es megis szoktam blokkok/scope-ok elejen is deklaralni. Nem arra gondolsz miszerint "ISO C90 forbids mixed declarations and code"? Az biztos hogy C99+ featura. 

Ami viszont kozben eszembe otlott az egesz problematikaval hogy ami forditasi idoben es/vagy optimalizacioban bekavarhat a stack framing temakort erintoen az az alloca() es hasonlo nyalanksagok. Szoval oke, hogy egy ptr=alloca(something) is kb egy `mov/ldr r0, r{something}; sub sp, r0; mov r{ptr}, sp` jellegu utasitas-blokkra fordul, de itt mar akkor a lebontansnal hasznalni kell a frame pointert (azaz kb tok ugy mint az x86 ABI-nal). De a mivel az alloca() nem kulso fuggveny, hanem abszolut architekturafuggo inline cuccmany, ezert a fordito tudja hogy oke, akkor ottan valamelyik regisztert be kell aldozni frame pointernek (ami van is, fp=r11 neven, de az thumb1-ben nehezen elerheto a gyakorlatban). Es ezt persze le is lehet vermelni az alloca() utan hogy a scope vegen kiszedd.

Es ehhez lazan kapcsolodik hogy a topicnyitasban felmerult problema eseten alloca()-t sem hasznalok :] 

Szerkesztve: 2022. 07. 25., h – 15:17

Ez teljesen normális C stack működés. Ahogy fent is írtam, a block variable scope csak szintaktikai cukorka, a változó attól még a függvényhez tartozik, és nem a blokkhoz. A stackframe-et mindig csak a függvénybe belépéskor ill. kilépéskor számolja újra a fordító. Szóval ha nem akarod hogy az array-ed a mainben folyamatosan foglaljon, akkor a teljesen unrelated dolgot tedd ki függvénybe és hívd meg úgy. Azért van, arra találták ki az ilyesmit, hogy struktúrált programozás...

Ha Thumb-ot programozol, akkor ami miatt előfordulhat mégis hogy ez "jól működik" (szerinted) az teljesen véletlenszerű és igazából csak a processzor limitációinak mellékhatása. A Thumb utasításkészlet nagyon limitált, ha egy-egy függvény "túl nagyra" nő, vagy túl nagy lesz a lokális változók mérete, akkor a fordítók belül hajlamosak a függvényt láthatatlanul szétszedni több kisebb automatán generált függvényre belül, hogy egyáltalán le bírják fordítani Thumb-ra. Ekkor valóban többször számolódhat a stackframe, de ez csak a Thumb utasításkészlet korlátai miatt van így, nem azért mert a C nyelv szerint így kéne működnie.

-=- Mire a programozó: "Na és szerintetek ki csinálta a káoszt?" -=-

Hm, érdekes amit írsz a második bekezdésben. Én még nem találkoztam ilyen fordítói viselkedéssel thumb/thumb2-re fordított több 10 megás firmware-ek esetén sem. Inkább az ellenkezőjével, hogy a fordító inline-osítja a függvényeket, ha nincs erre semmilyen forráskód-szintű megszorítás. Sőt, obfuszkátorok control flow flattening-je egy óriás méretű függvénybe gyúr mindent és ott sem szokott problémát okozni a thumb mód stack frame kezelése. Inkább a taszkhoz rendelt stack méret szokott korlát lenni, multitaszk környezetben. Ettől persze még  viselkedhet így a fordító, ami azért elég különös.

No, ezt most amit irtatok, megneztem egy kicsit jobban: a thumb1 utasitaskeszlet az [sp]-hez kepest tud 8 bitnyit cimenzni, es csak is felfele es azt is csak word access-el. Azaz ez az ami egy 16 bites utasitasba belefer. Minden masra ott a ket v tobb utasitasos valtozat, viszont ott meg egy altalanos {r0, ... r7} regiszterhez kepest csak 6 bitnyi relativ cimzest lehet csinalni, azt is csak felfele (azaz byte access-nel 64 bytenyi, halfworld-nel 128 bytenyi es world accessnel meg 256bytenyi cimteret lathat be a proci egyszeruen). Ez tenyleg nem sok, de vegulis osszessegben igy 1kbyte-nyi cimter all rendelkezesre az automatikus valtozok szamara. Az meg annyira nem keves, foleg ha azt nezzuk hogy ezt a stack frame-t minden egyes blokkra (scope-ra) valojaban egyszeruen letre lehet hozni (sub sp, #...), le lehet bontani (add sp, #...), es tudjuk hasznalni (ldr r0, [sp, #...], str r1 [sp, #...]). 

Itt ezekben a peldakban es/vagy konkret esetekben meg messze nem ertem el az 1k-nyi memoriat, inkabb ilyen 500-600 byteok voltak maximum.

A masik dolog, meg az az hogy pont az arm-nel a fuggvenyhivas az nem is igenyel stack-muveletet, tehat oke hogy "fuggvenyeket hivogatunk", de az az lr/pc-n keresztul valosul meg. Igy egy scope-ba valo belepes es kilepes az a stack szemszogebol nezve nem kulonbozik, nem is kulonbozhet egy fuggvenyhivastol. 

Lehetne minden scope-nál stackframe kialakítási móka, de mégsem ezt csinálják a fordítók, tapsztalataim szerint. A hívó, a négynél több argumentumot (abi függő) már nem regiszterben, hanem saját veremkeretén adja át, majd a hívott a saját függvény-prológusában a verembe tolja a használt regisztereket (a fordító eldönti melyek lesznek azok), valamint ha lesz további link regisztert használó hívás, akkor lr-t is (ezért lehet arm-ra rop-os exploitokat készíteni, mert ha a stack-en tárolt lr-t felülírod pl. egy lokális változónak fenntartott hely túlírásával, akkor a rop gadget-edre fog "visszatérni" a hívás), majd hozzáférhetővé teszi az esetlegesen vermen átadott értékeket, végül a lokális változók össz-méretének megfelelően (align kellhet) lefoglalja a veremben a helyet, elkészítve a függvény saját veremkeretét és kész.

Inkább a taszkhoz rendelt stack méret szokott korlát lenni, multitaszk környezetben

Igen, pont ez az egyik fő kerdes most nekem is. Marmint konkretan az oke, hogy egy taszk plusz egy stack frame (MSP) eseten mi van, de tobb taszk eseten mindegyiknek kulon-kulon van egy relative kicsi frame-je, es akkor abba kell elferni. Raadasul a profilozast az MSP-PSP is megkavarja (de halistennek egy kicsit).

Ha ra lehetne venni a forditot arra hogy minden egyes fuggvenyhez hozzarendeljen egy db szamot miszerint mi a maximalis stack frame amit maganak megeszik (felteve tovabbra is hogy alloca()-t meg hasonlo nyalanksagokat nem hasznalunk), akkor az mar onmagaban is egy hasznos featura lenne.

Addig meg marad a jozan esz es/vagy a real-time hardver szintu szimulacio-emulacio...