Introduktion till Assembly - Del 4: Stacken
Förord
I denna artikeln ska jag så gott jag kan beskriva stacken, presentera flertalet exempel, och ta ett par ord om "address calculation", loopar osv. Har du läst de förra artiklarna så rekommenderas du starkt att läsa denna!
Stacken
Stacken kan beskrivas som ett minne där du temporärt sparar data. Data som sparas på stacken MÅSTE återhämtas igen, vare sig du har använding av den eller inte, eftersom stacken behöver balanseras, eller så riskerar programmet att krascha.Tänk på stacken som en verklig stack (läs: hög) av ex. serietidningar. Du har strikta order från din mor eller fru att återhämta de serietidningar du har skapat en hög (stack) av efter förbrukning, annars riskerar högen att bli för stor och då falla. Det enda som skiljer serietidningshögen från stacken i assembly är att du måste återhämta den SISTA tidningen du placerade på högen FÖRST och sedan gå den vägen, så att säga (alltså du börjar med den sista till den första). Det kan med enkla ord beskrivas i engelskan, "last on, first off", alltså om du placerar en Kalle Anka tidning från 2005 på högen först och sist en Kalle Anka tidning från 2006 och sedan vill återhämta de, så måste du börja med tidningen från 2006 om du vill spara värdens i dess ursprungliga platser (vilket man i 99% av fallen vill).
I assembly finns det två standard instruktioner att manipulera stacken med: PUSH och POP. PUSH används för att spara data på stacken, POP används för att återhämta den. När du sparar data på stacken så förminskas ESP-registret (Stack Pointer) och datan skrivs på toppen av stacken, och när du återhämtar data från stacken så ökar ESP-registret.
Du kan enbart spara PUSH:a 1 DWORD (dvs 4 bytes) av data på stacken, så om du exempelvis vill spara strukturer eller data som överskrider 4 bytes i storlek så bör du spara adressen till den i stället. Det är ett typiskt programmeringsfel, t o m för de erfarna, att POP:a ett värde i fel ordning, och programmet fortlöper sedan i tron att ex. värde X finns i EAX och värde Y finns i ECX, när det egentligen är tvärtom, och det dröjer då oftast inte länge förrän programmet kraschar. Vidare måste du också vara noga med att återhämta rätt storlek från stacken, dvs om du sparar ett 32-bitars värde (låt oss säga ett viktigt nummer) på stacken och sedan återhämtar den som ett 16-bitars värde så har du ännu inte balanserat stacken - den vill bli av med ytterliggare 16-bitar. Dock kan du spara ex. ett 32-bitars värde och sedan återhämta det som två 16-bitars värden, det fungerar fint, stacken bryr sig bara om att du sparar i rätt storlek (dvs. 1 DWORD) och att du balanserar den korrekt, och du själv bör även bry dig om att spara och återhämta i rätt ordning. Jag ska nedan gå igenom ett par exempel på hur stacken kan användas.
mov eax,500h ; eax=0x500 (1280)
mov ecx,10 ; ecx=10
div ecx ; eax/=10
; EAX innehåller nu värdet 128, ECX innehåller 10
; jag vill på ett snabbt och effektivt sätt spara de för senare användning...
push eax ; spara till stacken (ESP minskar med 4)
push ecx ; spara till stacken (ESP minskar med 4 igen)
; gör någonting annat med EAX och ECX eller anropa en API-funktion
pop ecx ; återhämta ECX, som fortfarande innehåller värdet 128, ESP ökar med 4
pop eax ; återhämta EAX, som innehåller värdet 10, ESP ökar med 4 igen
mul ecx ; eax*=ecx
; EAX innehåller nu 500h igen!
Nedan ser du ett exempel där vi använder stacken utan standardinstruktionerna PUSH/POP.
mov eax,500h ; eax=0x500 (1280)
mov ecx,10 ; ecx=10
div ecx ; eax/=10
; EAX innehåller nu värdet 128, ECX innehåller 10
; jag vill på ett snabbt och effektivt sätt spara de för senare användning...
sub esp,8 ; allokera utrymme för EAX och ECX (2 32-bitars register = 2 DWORDs = 8 bytes)
mov [esp],eax ; spara till stacken
mov [esp-4],ecx ; spara till stacken
; gör någonting annat med EAX och ECX eller anropa en API-funktion
mov ecx,[esp-4] ; återhämta ECX, som fortfarande innehåller värdet 128
mov eax,[esp] ; återhämta EAX, som innehåller värdet 10
add esp,8 ; balansera stacken
mul ecx ; eax*=ecx
; EAX innehåller nu 500h igen!
Nedan demonstrerar jag två misstag man ofta begår som nybörjare, men det händer för de erfarna assembly-programmerarna med!
mov eax,500h ; eax=0x500 (1280)
mov ecx,10 ; ecx=10
div ecx ; eax/=10
; EAX innehåller nu värdet 128, ECX innehåller 10
; jag vill på ett snabbt och effektivt sätt spara de för senare användning...
push eax ; spara till stacken (ESP minskar med 4)
push ecx ; spara till stacken (ESP minskar med 4 igen)
; gör någonting annat med EAX och ECX eller anropa en API-funktion
pop eax ; FEL! nu återhämtar vi ECX och placerar det i EAX
pop ecx ; FEL! nu återhämtar vi EAX och placerar det i ECX
mul ecx ; eax*=ecx
; nedan är också ett praktexempel på hur du sparar ett DWORD-värde på stacken och sedan bara återhämtar 16-bitar av den
push eax ; spara EAX
; ... gör någoting med EAX
pop ax ; ogiltigt om du inte POP:ar ytterliggare 16-bitar igen
;pop cx ; avkommentera detta, så har du balanserat stacken
Följande exempel demonstrerar hur du kopierar text på ett flertal olika optimerade sätt, och har du läst de tidigare artiklarna (främst del 2) så kommer du känna igen koden.
; placera följande i data-sektionen
srctxt db "Vi testar att hoppa lite med assembly",0
dsttxt db 40 dup(0) ; en buffert på 40 bytes, initierad med nollor
COUNT equ 24/4 ; vi delar antalet bytes vi vill kopiera med 4 för att få fram hur många DWORDS det blir (vi bearbetar en DWORD åt gången nu, istället för en BYTE åt gången som i exemplet i artikel 2)
.code
start:
; placera följande i kod-sektionen
; bevara tills vidare
push esi
push edi
mov esi,offset srctxt ; spara adressen för srctxt i ESI
mov edi,offset dsttxt ; spara adressen för dsttxt i EDI
xor eax,eax ; detsamma som MOV EAX,0, fast snabbare
xor ecx,ecx
@loop:
mov eax,[esi+ecx*4+0] ; hämta 1 DWORD från srctxt (notera att vi använder 32-bitars registret EAX)
mov [edi+ecx*4+0],eax ; spara 1 DWORD till dsttxt
inc ecx ; öka countern med 1
cmp ecx,COUNT ; om vi INTE har nått COUNT (24/4)...
jb @loop ; ...så hoppar vi tillbaka till @loop och kopierar nästa DWORD
; eftersom bufferten (dsttxt) redan är initierad med nollor behöver vi inte självmant
; lagra strängavslutaren (som är 0)
; när vi har kopierat COUNT (24/4) DWORDS, dvs ECX==COUNT..så
; återställ vi registerna - och dsttxt innehåller 24 bytes från srctxt
pop edi
pop esi
Koden ovan kan givetvis optimeras ytterliggare utan att göra alltför stora förändringar... hur, undrar du nu. En vanlig metod för att optimera främst loopar i assembly är ”loop unrolling”. Denna optimeringsmetod kan man också finna i ex. Intels optimerande C-kompilerare (finns en inställning där du kan ange ett nummer som bestämmer hur många gånger kompileraren ska ”unrolla” en loop). ”Loop unrolling” betyder att du förkortar antalet gånger en loop ska behöva köras genom att bearbeta mer data under varje loop. Så i stället för att bearbeta ex. en BYTE varje loop kan du bearbeta 2 och förkortar då antalet gånger loopen ska behöva köras (och därmed antalet gånger programmet villkorligt ska hoppa i koden och avbryta programflödet). Låt oss ta exemplet ovan och optimera det med ”loop unrolling”.
; placera följande i data-sektionen
srctxt db "Vi testar att hoppa lite med assembly",0
dsttxt db 40 dup(0) ; en buffert på 40 bytes, initierad med nollor
COUNT equ 24/4 ; vi delar antalet bytes vi vill kopiera med 4 för att få fram hur många DWORDS det blir (vi bearbetar en DWORD åt gången nu, istället för en BYTE åt gången som i exemplet i artikel 2)
.code
start:
; placera följande i kod-sektionen
; bevara tills vidare
push esi
push edi
mov esi,offset srctxt ; spara adressen för srctxt i ESI
mov edi,offset dsttxt ; spara adressen för dsttxt i EDI
xor eax,eax ; detsamma som MOV EAX,0, dock snabbare
xor ecx,ecx
@loop:
mov eax,[esi+ecx*4+0] ; hämta 1 DWORD från srctxt (notera att vi använder 32-bitars registret EAX)
mov edx,[esi+ecx*4+4] ; hämta ytterliggare 1 DWORD från srctxt (vi använder EDX för den generellt inte behöver reserveras före användning)
;mov ebx,[esi+ecx*4+8] ; vi kan unrolla loopen fler gånger om vi så vill, men för denna gången kör vi på 2
mov [edi+ecx*4+0],eax ; spara 1 DWORD till dsttxt
mov [edi+ecx*4+4],edx ; spara 1 DWORD till dsttxt
;mov [edi+ecx*4+8],ebx ; spara 1 DWORD till dsttxt
add ecx,2 ; öka countern med 2 eftersom vi bearbetade 2 DWORDS
cmp ecx,COUNT ; om vi INTE har nått COUNT (24/4)...
jb @loop ; ...så hoppar vi tillbaka till @loop och kopierar två DWORDS till
; eftersom bufferten (dsttxt) redan är initierad med nollor behöver vi inte självmant
; lagra strängavslutaren (som är 0)
; när vi har kopierat COUNT (24/4) DWORDS, dvs ECX==COUNT..så
; återställ vi registerna - och dsttxt innehåller 24 bytes från srctxt
pop edi
pop esi
MOV-instruktionerna i loopen kan uppfattas som förvirrande, det gjorde de i alla fall för mig när jag för första gången stirrade på assembly-kod. De kallas för ”addressing calculation” och är en av de mest kraftfulla egenskaperna i 32-bitars assembly programmering. ESI är källregistret, den håller minnesadressen till texten vi vill kopiera. EDI är destinationsregistret, den håller minnesadressen till bufferten vi vill spara den kopierade texten till. ECX är ”indexen” eller ”countern” (smaksak) och håller ett värde som specifierar hur många gånger vi bearbetat två DWORDS (alltså hur många gånger loopen genomförts). Vi multiplicerar det med 4, som kallas ”scaling”, för att det är en DWORD vi vill läsa och den behövs för att nå rätt minnesadress. Det sista värdet kallas ”displacement” och specifierar att N (i detta fallet 4) bytes ska adderas på minnesadressen så att vi når nästa DWORD i stället för samma som vi nyss kopierade. Notera att ”scaling”-värdet bara kan vara 1, 2, 4 eller 8 och ”displacement” oftast är ett jämnt tal. När någonting specifieras inom [] så betyder det att instruktionen derefererar det och därmed når VÄRDET i stället för adressen. ”Address calculation” tillhör en mer avancerad nivå av assembly, så om du inte förstår det nu så är det inget att bli upprörd över. Nedan följer ett litet exempel på vad jag menar.
/* C-kod... */
BYTE *pBuf=0x10101010;
BYTE bByte=*pBuf;
/* ... */
mov eax,10101010h ; vi låtsas att EAX är pBuf
mov bl,byte ptr [eax] ; kopiera 1 BYTE från minnesadressen 0x10101010
; XXX PTR är en form av ”casting” i assembly, dvs, du talar om för assembly
; att den ska behandla värdet den läser som en speciell datatyp – du kan specifiera
; BYTE, WORD, DWORD, QWORD osv... om inget anges vid en dereferering
; så använder assembly som standard DWORD PTR
Stacken kan också användas för att kopiera data. Nedan följer ett exempel på hur du använder stacken för att kopiera data, i samma anda som de tidigare ”kopiera lite text”-exemplena.
; placera följande i data-sektionen
srctxt db "Vi testar att hoppa lite med assembly",0
dsttxt db 40 dup(0) ; en buffert på 40 bytes, initierad med nollor
COUNT equ 24/4 ; vi delar antalet bytes vi vill kopiera med 4 för att få fram hur många DWORDS det blir (vi bearbetar en DWORD åt gången nu, istället för en BYTE åt gången som i exemplet i artikel 2)
.code
start:
; placera följande i kod-sektionen
; bevara tills vidare
push esi
push edi
mov esi,offset srctxt ; spara adressen för srctxt i ESI
mov edi,offset dsttxt ; spara adressen för dsttxt i EDI
xor eax,eax ; detsamma som MOV EAX,0, fast snabbare
xor ecx,ecx
@loop:
push [esi+ecx*4+0] ; spara 1 DWORD från srctxt på stacken
pop [edi+ecx*4+0] ; återhämta den och placera den i dsttxt
inc ecx ; öka på countern
cmp ecx,COUNT ; om vi INTE har nått COUNT (24/4)...
jb @loop ; ...så hoppar vi tillbaka till @loop och kopierar två DWORDS till
; eftersom bufferten (dsttxt) redan är initierad med nollor behöver vi inte självmant
; lagra strängavslutaren (som är 0)
; när vi har kopierat COUNT (24/4) DWORDS, dvs ECX==COUNT..så
; återställ vi registerna - och dsttxt innehåller 24 bytes från srctxt
pop edi
pop esi
Stacken användas ofta för att kopiera enskilda värden mellan olika minnesplatser. Du tror säkert att koden nedan skulle fungera, men så är inte fallet...
; placera följande i data-sektionen
.data
var0 dd 0
var1 dd 10
; placera följande i kod-sektionen
.code
start:
mov var0,var1 ; FEL! assembly kan inte kopiera data direkt mellan två minnesplatser
;mov eax,var1 ; detta...
;mov var0,eax ; ...fungerar dock
; men hur gör vi om vi inte vill slösa ett register för att flytta en variabel?
; vi använder stacken som mellanhand och sparar därmed ett register för annan användning
push var1 ; spara var1 på stacken
pop var0 ; återhämta variabeln och spara den i var0
; stacken bör dock inte användas för att kopiera stora mängder data
; mellan olika minnesplatser, utan främst små variabler osv och då
; man inte vill slösa ett register på två MOV-instruktioner
Jag tänker ta upp ett till exempel på hur man kopierar data i assembly, du vet, så du har ett urval att välja på när du skriver dina egna program och funktioner som bearbetar data.;) I assembly finns det en instruktion som använder en intern loop för att kopiera data. Instruktionen heter REP som är kort för REPEAT, men används tillsammans med en annan instruktion som specifierar vad för typ av data som ska kopieras. Det finns flertalet instruktioner den kan användas tillsammans med, men instruktionen vi kommer använda heter MOVSD. Instruktionen blir då alltså REP MOVSD och hur den fungerar ser du nedan.
; placera följande i data-sektionen
srctxt db "Vi testar att hoppa lite med assembly",0
dsttxt db 40 dup(0) ; en buffert på 40 bytes, initierad med nollor
COUNT equ 24/4 ; vi delar antalet bytes vi vill kopiera med 4 för att få fram hur många DWORDS det blir (vi bearbetar en DWORD åt gången nu, istället för en BYTE åt gången som i exemplet i artikel 2)
.code
start:
; placera följande i kod-sektionen
; bevara tills vidare
push esi
push edi
mov esi,offset srctxt ; spara adressen för srctxt i ESI
mov edi,offset dsttxt ; spara adressen för dsttxt i EDI
mov ecx,COUNT ; du MÅSTE specifiera hur många DWORDS du vill kopiera i registret ECX, då ECX är countern REP använder sig av internt
cld ; nollställer en s k ”direction flag” i CPU:n som specifierar hur EDI/ESI ska manipuleras när REP används
rep movsd ; REP MOVSD kopierar en DWORD åt gången, minskar ECX med 1 och avslutar loopen när ECX når 0
; REP MOVSD är alltså lite enklare att implementera, speciellt för nybörjare...det enda du behöver vara noga med är att initiera ESI, EDI och ECX samt köra CLD instruktionen innan du kör REP MOVSD
; när vi har kopierat COUNT (24/4) DWORDS, dvs ECX==COUNT..så
; återställ vi registerna - och dsttxt innehåller 24 bytes från srctxt
pop edi
pop esi
Du finner de kompletta källkoderna för tre ovanstående exemplen här.
Det var allt för denna gången! I nästa artikel kommer vi kasta lite mer ljus på globala variabler i assembly. Vi ses då!
0 Kommentarer