Introduktion till Assembly - Del 6: Procedurer och funktioner
Förord
Med MASM kan du skapa procedurer och funktioner, och det är det vi nu ska gå igenom. Jag kommer förklara hur procedurer och funktioner fungerar, hur man anropar de, hur de skapas, osv, men också gå in lite på djupet för de som är intresserade. Jag kommer också ta upp lokala variabler enär de relaterar till huvudämnena.Innehåll
»»
»
Relaterade artiklar
» Introduktion till Assembly» Introduktion till Assembly - Del 2: Instruktioner
» Introduktion till Assembly - Del 3: Hello World!
» Introduktion till Assembly - Del 4: Stacken
» Introduktion till Assembly - Del 5: Globala variabler
Förord
Innan vi börjar tänker jag snabbt gå igenom vad en procedur resp. funktion är för de som inte känner till begreppen. En procedur, i data-termer, är ett stycke kod som gör någonting och sedan returnerar till programplatsen man var på innan proceduren anropades. Så du befinner dig på platsen D i programmet och anropar funktionen X som sedan gör någonting och när den är klar hoppar den tillbaka till D och fortsätter programflödet som vanligt. En funktion är nästan detsamma som en procedur, den väsentliga skillnaden är att den kan returnera ett värde som anroparen sedan kan läsa, något som procedurer inte gör. Om du programmerar i C eller VB så kommer du känna igen procedurer i assembly då de motsvarar nyckelorden "Sub" i VB och "void" i C.
Procedurer och funktioner
Vi börjar med att titta på syntaxen för hur man skapar procedurer och funktioner i MASM.; kroppen för funktionen eller proceduren
min_funktion_eller_procedur endp
Så du skriver alltså namnet på funktionen eller proceduren först och sedan nyckelordet "proc", kort för procedure, därefter koden och sist namnet igen tillsammans med nyckelordet "endp", kort för end procedure, men du kan givetvis skriva de i vilken ordning du vill när du väl har blivit bekant med syntaxen. Till skillnad från diverse högnivåspråk så finns det inget speciellt nyckelord för att deklarera funktioner, utan man går tillväga på samma sätt som procedurer och returnerar ett värde i registret EAX om man nu skriver en funktion och ett returvärde önskas. Nedan följer två exempel.
test_procedur proc
nop
ret
test_procedur endp
NOP, kort för no operation, är en instruktion som precis som namnet antyder; inte gör någonting. RET, kort för return, är en instruktion som returnerar till det tidigare programflödet innan funktionen eller proceduren anropades.
test_funktion proc
mov eax,1 ; flytta värdet 1 till EAX
ret
test_funktion endp
Regel: EAX är registret som används för att returnera värden i när man skriver funktioner, både i assembly och högnivåspråk.
Nu vet du hur man skapar funktioner och procedurer i assembly! Det finns två viktiga saker att komma ihåg som nybörjare: Du måste specifiera namnet på funktionen eller proceduren när du avslutar den, dvs "min_funktion_eller_procedur endp" alltså inte bara "endp", annars kommer du få ett assemblerfel som kan vara jobbiga att hitta om man har många assemblerfel, och du måste ALLTID specifiera instruktionen RET innan du avslutar funktionen eller proceduren med "endp", annars kommer programflödet fortsätta på fel plats och du kommer få tokiga fel eller en programkrasch
Säg det inte, jag vet vad du tänker! Ja, hur specifierar man parametrar i procedurer eller funktioner? Enkelt. Du specifierar parametrarna efter nyckelordet "proc". Nedan följer ett exempel.
test_funktion_parametrar proc parameter1:DWORD,parameter2:DWORD
mov eax,parameter1
mov ecx,parameter2
add eax,ecx
ret
test_funktion_parametrar endp
Koden ovan tar värdet specifierat i parameter1 och adderar det med värdet specifierat i parameter2, returvärdet finns i EAX.
Du har oftast bara behov av parametrar av datatyperna BYTE, WORD och DWORD, men andra datatyper finns. Se "masm32.hlp" i masm32-projektet för en komplett lista.
Nedan följer en användbar funktion jag har skrivit för att demonstrera hur parametrar kan användas i assembly.
pellesoft_strlen PROTO :DWORD ; en prototyp för vår funktion
.data
gszText db "testar att skapa en funktion och anropa den i assembly",0
.code
invoke pellesoft_strlen,addr gszText ; anropa pellesoft_strlen() och specifiera adressen till gszText som parameter
; EAX innehåller nu längden av strängen gszText
; ...
; vår egna strlen()/lstrlen() funktion
pellesoft_strlen proc lpString:DWORD
mov eax,lpString ; spara pekaren i EAX
dec eax
; en s k "unrolled-loop", kollar 4 bytes åt gången
@check_bytes:
inc eax ; öka och kolla nästa byte
cmp byte ptr [eax],0
jz @end
inc eax ; öka och kolla nästa byte
cmp byte ptr [eax],0
jz @end
inc eax ; öka och kolla nästa byte
cmp byte ptr [eax],0
jz @end
inc eax ; öka och kolla nästa byte
cmp byte ptr [eax],0
jnz @check_bytes
@end:
sub eax,lpString ; substrahera minnesadresserna för att få stränglängden
ret
pellesoft_strlen endp
Notera att du gärna får använda pellesoft_strlen() i dina egna assembly- eller högnivåspråksprojekt om du så önskar, men det vore uppskattande om du inkluderade en referens till min artikelserie eller mitt namn.;)
Jag tänker lite snabbt gå igenom procedurer och funktioner "under ytan", så att säga, men om du inte har hängt med i de tidigare artiklarna eller riktigt förstått assembly ännu så skulle jag rekommendera att du gick vidare till lokala variabler och kom tillbaka någongång senare när du har förkovrat dig lite.
Om du har en s k "disassembler" till hands, dvs ett program som översätter en binär programfil till maskinkod, så kan du kolla hur procedurer och funktioner egentligen ser ut. Funktioner och procedurer översätts egentligen bara till en "label" i assembler, som befinner sig på en konstant adress. Nedan ser du ett exempel på hur "disassemblad" (ursäkta svengelskan) kod ser ut.
00401244 55 push ebp
00401245 8BEC mov ebp,esp
00401247 6880000000 push 80h
0040124C FF7508 push dword ptr [ebp+8]
0040124F 6A00 push 0
00401251 E81C000000 call fn_00401272
De första siffrorna till vänster kallas för en relativa adress, eller RVA, och är egentligen bara en "offset" som är relativ med vart i minnet programfilen laddades av operativsystemet, och i mitten har du de hextalen som representerar instruktionerna till höger ("push ebp" blir alltså översatt till 0x55 av assemblern). De intressanta instruktionerna i koden ovan är de två första som befinner sig på 00401244 och 00401245. Vad dessa gör kallas för en "stack frame", och är egentligen bara ett enkelt och standardiserat sätt att initiera procedurer och funktioner på. Först reserveras EBP och sedan flyttas ESP (stack pekaren) till EBP och efter det kan du börja manipulera stacken, dvs allokera data osv. Vid funktionens slut kan du sedan se hur man återställer stacken och avslutar återvänder till returadressen...
00401269 C20400 ret 4
LEAVE är en instruktion som återställer stacken genom att flytta ESP (stack pekaren) som vid funktionen- eller procedurens start reserverades till EBP och POP:ar sedan EBP. Därefter hoppar vi tillbaka till den ursprungliga platsen genom RET.
Även trots en s k "stack frame" är ett standardiserat sätt att initiera procedurer och funktioner med, så behövs den egentligen inte. Det finns ofta en inställning i optimeringssektionen av ex. C-kompilerare som inaktiverar skapandet av s k "stack frames", och det är även möjligt i assembler. Du sparar ett par klockor om du väljer att inte skapa stack-frames, därför räknas det som en mindre optimering. Nedan har vi pellesoft_strlen() fast utan en "stack-frame".
OPTION PROLOGUE:NONE ; inaktivera skapandet av
OPTION EPILOGUE:NONE ; en stack-frame
pellesoft_strlen proc lpString:DWORD
mov eax,[esp+1*4] ; lpString (esp+4)
sub eax,1
; en s k "unrolled-loop" som kollar 4 bytes åt gången
@check_bytes:
add eax,1 ; öka och kolla nästa byte
cmp byte ptr [eax],0
jz @end
add eax,1 ; öka och kolla nästa byte
cmp byte ptr [eax],0
jz @end
add eax,1 ; öka och kolla nästa byte
cmp byte ptr [eax],0
jz @end
add eax,1 ; öka och kolla nästa byte
cmp byte ptr [eax],0
jnz @check_bytes
@end:
sub eax,[esp+1*4] ; substrahera minnesadresserna för att få stränglängden
ret 4
pellesoft_strlen endp
OPTION PROLOGUE:PROLOGUEDEF ; återställ inställningen för
OPTION EPILOGUE:EPILOGUEDEF ; stack-frame
Att inaktivera "stack-frames" är bra i synnerhet för funktioner där du inte skapar lokala variabler eller på något sätt allokerar utrymme i stacken. Notera att om du inaktiverar skapandet av "stack-frames" i assembler så måste du allokera utrymme för dina lokala variabler själv och alltid hålla ett öga på stack-pekaren vilket kan vara jobbigt och svårt för nybörjare.
Lokala variabler
Att lära sig att deklarera lokala variabler är väldigt enkelt, även om du inte har hängt med i de tidigare artiklarna. De är egentligen bara utrymme allokerad på stacken, både i assembly och högnivåspråk så som C. I MASM deklarerar du en lokal variabel med nyckelordet LOCAL, och följer syntaxen nedan.LOCAL variabel_namn:data_typ
Nedan följer ett par exempel på hur man deklarerar lokala variabler av olika datatyper.
test_procedur proc
LOCAL byte_id:BYTE ; kan hålla en BYTE
LOCAL buf[255]:BYTE ; kan hålla 255 bytes
LOCAL num0:WORD ; kan hålla ett 16-bitars värde
LOCAL num1:DWORD ; kan hålla ett 32-bitars värde
mov byte_id,'A'
mov buf[128],'B'
mov num0,16000
mov eax,3240000
add eax,dword ptr [num0]
mov num1,eax
; ...
ret
test_procedur endp
Nedan finner du ett fullständigt exempel där vi använder oss av pellesoft_strlen() tillsammans med konsolen.
; Exempel: konsolapplikation och demonstration av pellesoft_strlen()
include \masm32\include\masm32rt.inc
pellesoft_strlen PROTO :DWORD
.code
start:
call main
inkey
exit
; vår egna strlen()/lstrlen() funktion, med mindre optimeringar tillämpade
OPTION PROLOGUE:NONE ; inaktivera skapandet av
OPTION EPILOGUE:NONE ; en stack-frame
pellesoft_strlen proc lpString:DWORD
mov eax,[esp+1*4] ; lpString (esp+4)
sub eax,1
; en s k "unrolled-loop" som kollar 4 bytes åt gången
@check_bytes:
add eax,1 ; öka och kolla nästa byte
cmp byte ptr [eax],0
jz @end
add eax,1 ; öka och kolla nästa byte
cmp byte ptr [eax],0
jz @end
add eax,1 ; öka och kolla nästa byte
cmp byte ptr [eax],0
jz @end
add eax,1 ; öka och kolla nästa byte
cmp byte ptr [eax],0
jnz @check_bytes
@end:
sub eax,[esp+1*4] ; substrahera minnesadresserna för att få stränglängden
ret 4
pellesoft_strlen endp
OPTION PROLOGUE:PROLOGUEDEF ; återställ inställningen för
OPTION EPILOGUE:EPILOGUEDEF ; stack-frame
main proc
LOCAL lpData:DWORD
mov lpData,input("Mata in ditt namn: ") ; lpData håller pekaren till texten vid retur av input()
invoke pellesoft_strlen,lpData
push eax ; reservera EAX eftersom den förstörs av "print"-makron nedan
print "Ditt namn är "
pop eax ; återhämta EAX
print ustr$(eax)," bokstäver långt",0dh,0ah ; CRLF ; skriv ut längden av namnet
ret
main endp
end start
Du finner det fullständiga programmet ovan med tillhörande källkod här.
Så enkelt är det!;) Och det var det för denna gången! Jag hoppas du har förstått det mesta av vad som har dryftats i artikeln, i annat fall, kommentera gärna och ställ frågor som jag möjligen kan lägga till i artikeln eller svara på i kommentar-sektionen. Ha en trevlig dag!
0 Kommentarer