Assembler anses av många vara det mest svåra språket att programmera i. Detta är en myt som borde ha dött för länge sen. Den har antagligen kommit till pga av att om man omvandlar ett färdigt program till assembler så är koden svårläst. Välskriven och kommenterad assembler kod är lika lätt att läsa som normal kod. Om man vill ha små snabba program så är assembler den enda möjligheten. Kunskaper i assembler är också nödvändigt ifall man vill skriva en crack till något program.
Så här långt verkar det som om det fanns bara fördelar med assembler. Det finns en stor nackdel. Den assembler kod man skrivit för ett system fungerar inte i ett annat. Ett program skrivet i assembler för ett Linux system fungerar t ex inte i FreeBSD. Detta kan verka som ett stor minus, men faktum är att program skrivna i högnivåspråk går inte heller alltid att kompilera på ett annat system. Man kan faktiskt skriva assemblerprogram så att det är lätt att porta om dem till ett annat system.
I denna artikel kommer jag att använda mig av NASM, som är en gratis assembler, i FreeBSD.
Hello World
Ingen programmerings guide kan börja på något annat sätt än med ett "Hello World"-program. Det är därför också mitt första exempel. Man kan göra koden nedan lättare att läsa genom att använda lämpliga macron. Jag har valt att inte använda macron eftersom man då bättre ser hur ett assemblerprogram verkligen är uppbyggt.
% more bsd.asm
section .data ;i .data-sektionen kan man
;definiera konstanter
msg db "Hello World!", 0xA ;Vårt meddelande
len equ $ - msg ;Längden på meddelandet
section .text ;själva programkoden kommer i .text
global _start ;det här behövs för länkaren
_syscall: ;Systemanropet måste vara i en egen
;procedur i FreeBSD
int 0x80 ;Systemanrop görs genom att avbrott 0x80
ret ;Hoppar tillbaka från proceduren
_start: ;Här startar programmet
push dword len ;Sätter längden av meddelandet på stacken
push dword msg ;Sätter adressen till meddelandet på stacken
push dword 1 ;Sätter numret för standard out på stacken
mov eax, 0x4 ;Sätter koden för write i eax-registret
call _syscall ;Utför systemanropet
add esp,12 ;Tar bort argumenten från stacken
push dword 0 ;Sätter talet 0 på stacken
mov eax, 0x1 ;Sätter koden för sys_exit på stacken
call _syscall ;Gör systemanropetMotsvarande program i Linux skulle se ut så här:
% more linux.asm
section .data ;i .data-sektionen kan man
;definiera konstanter
msg db "Hello World!", 0xA ;Vårt meddelande
len equ $ - msg ;Längden på meddelandet
section .text ;själva programkoden kommer i .text
global _start ;det här behövs för länkaren
_start: ;Här startar programmet
mov edx, len ;Sätter längden av meddelandet i edx-registret
mov ecx, msg ;Sätter adressen till meddelandet i ecx
mov ebx, 1 ;Sätter numret för standard out i ebx
mov eax, 0x4 ;Sätter koden för write i eax-registret
int 0x80 ;Utför systemanropet
mov eax, 0x1 ;Sätter koden för sys_exit i eax
int 0x80 ;Gör systemanropetFör att göra en körbar fil måste man köra följande kommandon:
nasm -f elf hello.asm ld -s -o hello hello.o
Då ska det finnas en körbar fil som heter hello.
Koden ovan kan se ganska så svår ut ifall man aldrig har läst assembler förut. Ett program som gör absolut ingenting i FreeBSD skulle se ut så här:
section .data
section .text
global _start
_syscall:
int 0x80
ret
_start:
mov eax, 0x1
call _syscallEtt assembler program delar man in i olika sektioner. I .data-sektionen kan man definiera konstanter. I vårt "Hello World"-program definierar vi först en konstant som heter msg. Observera att fast jag kallar msg för en konstant så är msg egentligen bara en minnesadress (en pekare). Direktivet db betyder att vi ser på meddelandet som en följd av byte (tecken). OxA get ett radbyte efter meddelandet. Den andra konstanten len är längden på vårt meddelande. $ är minnesadressen för len. Eftersom len kommer direkt efter msg så måste längden på meddelandet vara $-msg.
Följande sektion är .text. I denna sektion kommer själva koden. Raden global _start behövs för länkaren. Själva huvudproceduren heter _start.
Det första vi vill göra i vårt program är att skriva ut "Hello World!". Kärnan har en funktion för att skriva ut meddelande. Vi får mer information om den genom att skriva i ett shell
man 2 write
Vi ser då att write ser ut på följande sätt: write(int d, const void *buf, size_t nbytes); Den första argumentet som write tar emot är en File Descriptor. Följande argument är bufferten vi vill skriva ut och det sista argumentet är längden på meddelandet.
För att få ut meddelandet på skärmen så måste vi ge åt kerneln dessa argument. Det gör vi genom att lägga dem på stacken. Observera att man alltid sätter argumenten i omvänd ordning på stacken. En stack fungerar ju enligt principen sist in, först ut.
push dword len
Sätter längden på meddelandet på stacken
push dword msg
Sätter en pekare till meddelandet på stacken
push dword 1
Sätter vår File Descriptor på stacken. 1 är i UNIX den File Descriptor som får meddelanden att skickas till Standard Out. Standard Out är ju normalt definierad så att meddelandet kommer upp på skärmen.
Alla kernelfunktioner har ett eget nummer. Numret för write är 4. Ifall man kör FreeBSD kan man kolla upp numren genom att läsa /usr/src/sys/kern/syscalls.master. Numret ska alltid sättas i eax-registret i processorn. Detta gör vi genom att skriva kommadot
mov eax, 0x4
Efter att vi har satt alla argument för write på stacken och funktionsnumret i eax är det bara att göra ett systemanropet. FreeBSD förväntar sig att det första argumentet i ett systemanrop skall vara 4 byte in på stacken. För att åstadkomma detta på ett lätt sätt placerar vi själva systemanropet i en procedur med namnet _syscall. Själva systemandropet görs genom att kalla på avbrott 0x80:
int 0x80
Efter att systemanropet har gjorts hoppar vi tillbaka genom att ge kommandot ret.
Nu behövs inte innehållet på stacken mer så vi kan ta bort det. I processorn finns det ett register som heter esp. Värdet i esp minskas med 4 varje gång man lägger ett dword på stacken. Eftersom vi satte 3 argument på stacken så måste vi lägga 3*4=12 till esp för att återställa stacken:
add esp,12
I detta fall kunde man tänka sig att lämna bort denna rad, men det är bra att ta det som en vana att alltid återställa stacken. I annat fall blir stacken för eller senare full ifall vi har ett större och mer invecklat program.
Efter att vi har skrivit ut vårt meddelande återstår det bara att avsluta programmet. Först sätter vi exit-koden 0 på stacken:
push dword 0
Därefter numret för sys_exit i eax:
mov eax, 0x1
Och tillsist utförs systemanropet varvid programmet avslutas.
Författare
Även denna artikel skrevs av en underbar programmerare och UNIX-användare som jag kände under Swehack-tiden och kallades Lasse.