Lågnivå, hur läggs ett objekt på stacken?

Permalänk
Medlem

Lågnivå, hur läggs ett objekt på stacken?

Hej, jag har börjat fräscha upp mina kunskaper i c++ och såg att objekt kan läggas direkt på stacken. Har försökt föreställa mig hur detta går till på lågnivå och frågan grämer mig.

Efter att ha läst runt lite:

void foo() { MyObj *myObj = new MyObj(); //heap MyObj myObj; //stack }

Jag kan se hur när något läggs på heapen så kan man enkelt pusha en pekare till den datan på stacken men hur lägger man ett helt objekt som inte får plats i ett register direkt på stacken? Det känns som att det inte borde fungera bra alls. Jag har försökt fundera ut sätt som det skulle kunna fungera på men nu undrar jag om någon här har koll på hur det faktiskt fungerar och kan förklara.

Visa signatur

| Ryzen 5800x | Asus prime x470 pro | Asus rtx 3080 tuf oc | Gskill 32gb 3,6ghz | aw3225qf |

Permalänk

Stacken används traditionellt för att skicka parametrar mellan funktioner, hålla reda på returadressen och att allokera utrymme för lokala variabler. Se wikipedia för mera detaljer.

För att reservera utrymme till lokala variabler lägger kompilatorn helt enkelt ut en instruktion som flyttar på stackpekaren X antal bytes. Innan man lämnar funktionen "släpps" stackutrymmet genom att stackpekaren flyttas X bytes åt andra hållet.

Permalänk
Medlem

ok, men för att förenkla frågan något.

MyObj myObj; här skapar jag en instans av ett object. Säg att det är 500kb stort, allokeras minne för hela det objektet direkt på stacken? Genereas isåfall kod som får hålla koll på hur stort objektet är så att stacktop-pekaren får flyttas ifall data måste hämtas som ligger högre upp i stacken?

Visa signatur

| Ryzen 5800x | Asus prime x470 pro | Asus rtx 3080 tuf oc | Gskill 32gb 3,6ghz | aw3225qf |

Permalänk

@Ragin Pig: Jag vet inte hur mycket detaljer du vill ha så vi håller det lite översiktligt. Det hela beror på vilken kompilator du använder men en vanlig variant är att man i funktionen använder en pekare som frame-pointer och låter den peka ut "början" på funktionens lokala variabler:

| Inkommande | | parametrar | |--------------------| | returadress | FP -> | lokala variabler | | | | | |--------------------| SP -> | | | |

Eftersom man använder sig av frame-pointern för att adressera lokalvariabler kan man med den här modellen kan man pusha och poppa saker från stacken vid funktionsanrop utan att behöva ta hänsyn till det när man accessar lokala variabler. Allting går via FP och man har samma offset under hela funktionen.

Till GCC kan du ge switchen -fomit-frame-pointer som betyder att man inte skall använda FP. Då får man ett extra ledigt register, men då får kompilatorn istället gå via SP när man vill komma åt lokala variabler och justera variablernas offset från SP när det pushas och poppas värden.

Om MyObj är 500k stort kan det vara svårt att nå hela arean för lokala variabler direkt via SP eller FP om "register indirect with offset"-adresseringsmoderna inte kan ta tillräckligt stora offset. Då får kompilatorn använda ett extra register för adressberäkningen. (Det går att justera SP eller FP också men det går normalt åt fler instruktioner eftersom man måste sätta tillbaka den efter accessen.)

Svarade detta på din fråga?

Permalänk
Medlem

@Ingetledigtnamn:

Edit: Nevermind, nu är jag nog med. Det enda frågetecknet som jag har kvär är ifall objektet ligger i eller utanför stackframen som du ritade ut (fp). Tack för att du tagit dig tid och förklarat

Tanketrassel
Visa signatur

| Ryzen 5800x | Asus prime x470 pro | Asus rtx 3080 tuf oc | Gskill 32gb 3,6ghz | aw3225qf |

Permalänk
Datavetare
Skrivet av Ragin Pig:

@Ingetledigtnamn:

Edit: Nevermind, nu är jag nog med. Det enda frågetecknet som jag har kvär är ifall objektet ligger i eller utanför stackframen som du ritade ut (fp). Tack för att du tagit dig tid och förklarat

När du anropar en funktion där saker ligger på stacken utförs rent logiskt denna prolog (rent logiskt, i praktiken finns det en rad optimeringar som gör att man kanske inte alls explicit behöver allokera stack-utrymme)

move basePtr, stackPtr ; detta är inte ett krav, görs i ; praktiken bara om du behöver debugga ; och därmed kompilerat utan ; optimeringar sub stackPtr, #frameSz ; där 'frameSz' är storleken på alla ; lokala variabler

Alla objekt/variabler som är lokala för funktionen ligger då mellan stackPtr och basePtr, d.v.s svaret på din fråga är: objektet ligger inuti stack-ramen.

Notera att stacken växer mot lägre adresser i pseudokoden ovan. Detta är inte ett hårt krav men nästan alla CPU-arkitekturer (eller rent formellt, alla ABIs är definerade så) gör på detta sätt (inklusive t.ex. x86 och ARM).

Huruvida man direkt kan adressera allt som ligger på stacken beror på CPU-arkitektur, så tyvärr är svaret här: det beror på. x86 kan i praktiken alltid adressera allt oavsett storlek på det man har på stacken. ARM har en gräns på 4 kB, sedan måste man involvera temporära register. Andra RISC CPUer lär nästan alltid få göra lite beräkningar för att nå fält.

I slutet av funktionen finns sedan epilogen

add stackPtr, #frameSz ; lämna tillbaka stackutrymmet ret

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Medlem

Tack för väldigt bra svar @Ingetledigtnamn och @Yoshman nu är jag med på noterna.

Visa signatur

| Ryzen 5800x | Asus prime x470 pro | Asus rtx 3080 tuf oc | Gskill 32gb 3,6ghz | aw3225qf |

Permalänk
Skrivet av Ragin Pig:

@Ingetledigtnamn:

Edit: Nevermind, nu är jag nog med. Det enda frågetecknet som jag har kvär är ifall objektet ligger i eller utanför stackframen som du ritade ut (fp). Tack för att du tagit dig tid och förklarat

Det beror på hur kompilatorn gör. Enklast är att allokera plats för alla objekt på en gång och då gör man precis som Yoshmans prolog- och epilog-kod. FP = SP; SP -= sizeof(alla lokala variabler som skall ligga på stacken);

Kompilatorn kan även vänta med allokeringen och allokera/deallokera plats för variabeln först när den behövs. Exempelvis:

void foo() { if (...) ... else { char buffer[10000]; ... } }

I det här fallet kan man tänka sig att det inte allokeras plats för buffer i prologen utan man väntar tills exekveringen kommer in i else-klausulen. Då måste man givetvis deallokera innan man lämnar else-statementet.

Oavsett hur kompilatorn gör så ligger objektet i framen (mellan FP och SP). "Ovanför" FP ligger returadress och inkommande parametrar och "under" SP kan vi inte heller lägga objektet för där kan vi behöva pusha parametrar till de funktioner som vi skall anropa.

Permalänk
99:e percentilen
Skrivet av Yoshman:

x86 kan i praktiken alltid adressera allt oavsett storlek på det man har på stacken. ARM har en gräns på 4 kB, sedan måste man involvera temporära register.

Har det någon betydelse i praktiken? Om ja, vilken, när och för vem?

Visa signatur

Skrivet med hjälp av Better SweClockers

Permalänk

@Alling: Det har ingen betydelse i den bemärkelsen att man kan eller inte kan göra något. Om man inte kan nå det man vill läsa eller skriva via FP/SP får man räkna ut adressen och kostar typiskt en extra instruktion och ett extra register. Det innebär att något som skulle ha kunnat ligga i det registret måste placeras på stacken och just accesserna till den variabeln tar aningen längre tid. Programmet kommer gå långsammare. Om man hur otur i registerallokeringen och en variabel som används i en het loop hamnar på stacken kan det ge mätbara effekter och programmet kanske går ett antal procent långsammare.

Permalänk
Datavetare
Skrivet av Alling:

Har det någon betydelse i praktiken? Om ja, vilken, när och för vem?

De enda jag kan tänka mig att detta spelar roll för är de som utvecklar kompilatorer. Sedan är 4 kB knappast en relevant begränsning i praktiken, det lär täcka väldigt nära 100 % av fallen.

Det lite roliga är att de beskrivningar som gjorts ovan må vara en korrekt beskrivning av vad som sker rent konceptuellt. Skulle någon däremot börja titta på strukturen hos koden som kompilatorer faktiskt lägger ut när man bygger med optimeringar och liknande skulle man bl.a. se detta

  • på 64-bitars x86/ARM Linux/OSX/BSD/UNIX kommer funktioner som inte anropar andra funktioner eller bara anropar sådant som går att "inline:a" överhuvudtaget inte skapa ett stackutrymme om utrymmet för lokala variabler understiger 128 bytes. I stället läggs dessa i register samt ett implicit utrymme kallat "red zone" som alltid finns ovanför toppen av stacken

  • på 64-bitars Windows skapas ingen stackram i funktioner som inte anropar andra funktioner om storleken på lokala variabler som inte kan hållas i register understiger 32 bytes. Till skillnad från *NIX så lagras detta 8-40 bytes under toppen av stacken (de första 8 bytes på toppen är initialt returadressen)

  • ABI som används på 64-bitars x86 av *NIX är explicit designat så att debuggers kan fungera utan att sätta upp en base-pointer, gcc/llvm skippar därför att sätta upp en sådan i normalfallet (går att tvinga dessa att generera kod med BP uppsatt)

Så precis som vanligt: finns ingen regel utan massor med undantag

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk

Fint att du vill delge oss dina kunskaper @Yoshman. De må vara korrekta, men med tanke på den ursprungliga frågan tror jag att ditt senaste inlägg hamnar i kategorin "too much information".

Ja, jag får väl be om ursäkt för att jag har förenklat en del, men jag trodde inte TS skulle vara betjänt av en detaljerad beskrivning av, exempelvis, ARM-kompilatorernas fixed-frame-modell (som redan i prologen allokerar plats så stacken rymmer det största antal parametrar som något anrop i funktionen kommer behöva och att kompilatorn "pushar" parametrar genom att skriva på lagom offset ovanför stackpekaren. Åh, javisst, ja, glöm inte att de fyra första orden går alltid i register, även om bara en del av objektet ryms). Jag trodde inte det skulle öka förståelsen, utan valde en enklare modell. Se det som att jag gav dig en chans att briljera med dina kunskaper

Permalänk
Datavetare

@Ingetledigtnamn: undvek explicit att nämna något om detta fram till att TS svarade att den ursprungliga frågan var ur världen.

Tänkte sedan att om TS eller någon annan faktiskt väljer att titta på genererad assember så kommer svaren ovan se ut att vara felaktiga i majoriteten av fallen. Du får gärna förklara varför det är så hemsk att ge en kort beskrivning kring vad man faktiskt kommer få se och varför det på en logisk nivå ändå ger samma resultat som den beskrivning som gavs innan.

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Medlem
Skrivet av Ingetledigtnamn:

Fint att du vill delge oss dina kunskaper @Yoshman. De må vara korrekta, men med tanke på den ursprungliga frågan tror jag att ditt senaste inlägg hamnar i kategorin "too much information".

Ja, jag får väl be om ursäkt för att jag har förenklat en del, men jag trodde inte TS skulle vara betjänt av en detaljerad beskrivning av, exempelvis, ARM-kompilatorernas fixed-frame-modell (som redan i prologen allokerar plats så stacken rymmer det största antal parametrar som något anrop i funktionen kommer behöva och att kompilatorn "pushar" parametrar genom att skriva på lagom offset ovanför stackpekaren. Åh, javisst, ja, glöm inte att de fyra första orden går alltid i register, även om bara en del av objektet ryms). Jag trodde inte det skulle öka förståelsen, utan valde en enklare modell. Se det som att jag gav dig en chans att briljera med dina kunskaper

Min fråga var inte direkt välformulerad och förtjänade väl inte någon överdetaljerad förklaring, mycket pga att jag själv inte riktigt kunde sätta fingret på exakt vad det var som jag inte förstod.

Jag pysslade lite med reverse engineering för skojs skull i somras och stirrade på en hel del assembler och dekompilerad kod så har sett mycket av det som tagits upp men inte hunnit bilda mig någon särskilt djup förståelse. Har t.ex. sätt hur pekare till objekt och strukturerad data har pushats på stacken vid funktions anrop.

Tanken på att lägga större data direkt på stacken gav dock känslan att det på något sätt skulle ligga i vägen och inte gå att nå ordentligt (såg 1000 pop instruktioner framför mig). Det var sen efter ditt inlägg som det klickade och jag förstod att det faktiskt inte spelar någon roll. Objektet kan nås genom att hålla koll på vilken address i stacken det ligger och ska man deallokera så flyttar man bara på stackpekaren. Har jag fel så får ni rätta mig

För mig gör det sen inget att det kommit mer detaljerad information om stackhantering, jag tycker bara det är intressant

Kul att det finns så mycket expertis på forumet!

Visa signatur

| Ryzen 5800x | Asus prime x470 pro | Asus rtx 3080 tuf oc | Gskill 32gb 3,6ghz | aw3225qf |

Permalänk

@Yoshman: Sorry, jag uppfattade inte att ditt förra inlägg var tänkt som överkurs till ett avslutat ämne utan tyckte att det var mer info än vad TS behövde.