Assembler programmering

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 systemanropet

Motsvarande 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 systemanropet

Fö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 _syscall

Ett 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.