Assembler Programmering (del 2)

I del 1 såg vi hur man kan göra ett systemanrop i FreeBSD genom assembler. I del 2 ser vi närmare på processorn och registren i den samt på ett lite mer invecklat program.

Register

I assembler lagrar man så långt som det är möjligt data i processorns register eftersom registren är snabba jämfört med RAM-minnet. En optimeringsmetod som somliga kompilatorer använder sig av är att lagra variabler som används ofta i register i stället för i minnet.

I 8086 processorn fanns det 4 st 16 bits register för att lagra data i. Dom hette AX, BX, CX och DX. Alla dessa register kunde i sin tur spjälkas upp i två andra register. AX kan t ex spjälkas upp i AH och AL, BX i BH och BL. AH innehöll då de 8 första bitarna av AX medan AL de följande 8 bitana.

Registren i 8086 såg ut på det här sättet:

 Register         Bitar Använding
 ===============================================
 AX (AH och AL)   16    Lagra data
 BX (BH och BL)   16    Lagra data
 CX (CH och CL)   16    Lagra data
 DX (DH och DL)   16    Lagra data
 SI, DI           16    Används som pekare och för data
 BP, SP           16    Pekare till stacken
 
 CS               16    Code Segment
 DS               16    Data Segment
 SS               16    Stack Segment
 ES               16    Extra Segment
 
 IP               16    Instruction Pointer

Dessutom finns det ett FLAGS register som lagrar information om resultatet av den föregående instruktionen.

CS, DS, SS, ES innehåller information om hur programmet är lagrat i minnet. Adressen på följande instruktion räknar datorn ut genom att använda CS och IP.

80386 processorn och senare processorer har utökade register. AX registret har t ex utökats till 32 bitar. För att program ska vara bakåtkompatibla så är AX fortfande 16 bitar och EAX är det nya 32 bits registret. AX är då de 16 lägsta bitarna i EAX och AL de 8 lägsta bitarna i EAX. De andra registren fungerar på samma sätt om man bortser från segmentregisren som fortfarande är 16 bitar. Vi har t ex ESP (som vi såg i del 1) i 80386. Det har dessutom kommit två segmentregister till: FS och GS.

Ett lite mer avancerat progam

För att visa att assembler kan faktiskt användas för att göra cgi-bin program till web servrar så har jag gjort ett program som tar emot ett filnamn som argument och skriver ut filen. Detta är ett trevligt litet program att lägga in på dåligt skyddade servrar där web servern körs som root. Om vårt program heter fv kan vi genom att skriva http://server.ss/cgi-bin/fv?/etc/master.passwd få fram lösenordsfilen på skärmen.

Eftersom jag kommenterar programmet längre ner så har jag inte så mycket kommentarer i själva programkoden.

 ;För att programmet ska köras snabbare så använder
 ;vi en 8Kb buffer.
 %define BUFSIZE 8192
 
 section .data
     ;Alla cgi-bin program måste börja med en rad som
     ;berättar vilken typ innehållet är av följt av
     ;två radbyten.
     header db "Content-type: text/plain", 0xA, 0xA
     hlen   equ $ - header
 
 section .bss
     ;Vi skapar en buffert med namnet buffer som har
     ;storleken definierad i BUFSIZE
     buffer  resb BUFSIZE
 
 section .text
     global _start
 
 ;Vår systemanrops procedur
 _syscall:
     int    0x80
     ret
 
 _start:
     ;Först skriver vi ut det obligatoriska huvudet
     ;som alla cgi-bin program förväntas skriva ut
     push   dword hlen
     push   dword header
     push   dword 1
     mov    eax, 4
     call   _syscall
     add    esp, 12
 
     ;Första värdet på stacken anger hur många
     ;argument som getts åt programmet. Vi vill
     ;ha exakt ett argument. I annat fall avbryter
     ;vi programmet
     pop    ebx
     cmp    ebx,2
     jne    _exit
 
     ;Tar bort det första argumentet som är själva
     ;programnamnet från stacken
     pop    ebx
 
     ;Det andra argumentet (det första riktiga) läser
     ;vi in till ebx. I vårt fall är det filnamnet
     pop    ebx
 
     ;Vi gör ett systemanrop som öppnar filen för
     ;läsning.
     push   dword 0
     push   dword ebx
     mov    eax, 5
     call   _syscall
     jc     _exit
     add    esp, 8
 
     ;Här sätts parametrarna för read in på stacken
     push   dword BUFSIZE
     push   dword buffer
     push   dword eax
 
 _loop:
     mov    eax, 3
     call   _syscall
 
     ;Systemanropet för read har utförts. I aex
     ;Registret har vi antalet byte read läste in
     ;Om inget lästes in avslutar vi programmet
     cmp    eax,0
     jle    _exit
 
     ;Vi skriver ut det som lästes in i bufferten
     push   dword eax
     push   dword buffer
     push   dword 1
     mov    eax, 4
     call   _syscall
     add    esp,12
 
     ;Efter det hoppar vi tillbaka och läser in
     ;nästa 8Kb
     jmp    _loop
 
 _exit:
     ;Här avslutas programmet
     push   dword 0
     mov    eax, 1
     call   _syscall

I det här programmet finns en hel del nytt som vi inte såg i vårt enkla "Hello World"-program. Jag ska nu gå igenom steg för steg vad som sker i programmet.

 %define BUFSIZE 8192

På den här raden definieras ett macro. Det betyder att på alla ställen som det står BUFSIZE så ersätter assemblern BUFSIZE med 8192. För att göra koden lättare att läsa kan man definiera en massa macron så att t ex då man vill skriva ut något så skriver man endast sys.write som sen ersätts med den riktiga koden. Jag har ännu vid det här skedet valt att inte använda macron för att förenkla koden.

 header db "Content-type: text/plain", 0xA, 0xA
 hlen   equ $ - header

Inget nytt i de här raderna. Se del 1 för mer information.

 section .bss

Sektionen .bss används för att lagra variabler. Eftersom vi inte behövde sådana i vårt förra program så hade vi ingen .bss sektion där.

 buffer  resb BUFSIZE

Vi skapar en variabel med namnet buffer. Direktivet resb anger att vi vill använda enheten byte för det område som vi reserverar. Raden säger alltså att vi reservar BUFSIZE antal byte under namnet buffer.

 push   dword hlen
 push   dword header
 push   dword 1
 mov    eax, 4
 call   _syscall
 add    esp, 12

Den första koden efter _start ser bekant ut från föregående programmet.

 pop    ebx
 cmp    ebx,2
 jne    _exit

Varje program har en stack. Antalet argument, programnamnet och argumenten på stacken. Programnamnet anses också vara ett argument. T ex './fv /etc/master.passwd' gör att stacken ser ut på följande sätt:

 2
 fv
 /etc/master.passwd

Först poppar vi antalet argument från stacken till ebx registret. Eftersom vårt program ska ha exakt ett riktigt argument så måste vi kontrollera att detta värde är 2 (programnamnet+argumentet). Detta gör vi med cmp kommandot. Resultatet från cmp-kommandot använder vi genom att använda jne. Kommandot jne hoppar om det som jämfördes inte är lika. Alltså ett hopp till _exit utförs ifall värdet i ebx inte är 2.

 pop    ebx

Vi poppar bort programnamnet från stacken till ebx.

 pop    ebx

Här först poppas vårt argument (filnamnet) från stacken. Det sätter vi ebx varvid programnamnet skrivs över i ebx med filnamnet.

 push   dword 0
 push   dword ebx
 mov    eax, 5
 call   _syscall

För att kunna läsa in en fil behöver vi öppna den. Det gör vi med kernelfunktion nummer 5. Mer information om funktionen kan vi få genom att skriva 'man 2 open' i ett shell. Vi ser då att open tar emot två argument. Vi sätter först på stacken är att vi vill öppna filen som Read Only. Koden för detta är 0. Därefter sätter vi filnamnet på stacken. Efter det utför vi systemanropet. Om allt lyckades så finns vår File Descriptor i EAX.

 jc     _exit
 add    esp, 8

Vi kan testa om det gick att öppna filen genom att använda jc (Jump if Carry). Det ska inte finnas någon Carry ifall det lyckades att öppna filen. Efter det kan vi städa bort det vi har satt på stacken.

 push   dword BUFSIZE
 push   dword buffer
 push   dword eax
 
 _loop:
  mov    eax, 3
  call   _syscall

Funktionen för att läsa in från en fil är också ny. Den har numret 3. Vi sätter storleken på bufferten, själva bufferten och File Descriptorn som finns i EAX på stacken och gör därefter systemanropet.

 cmp    eax,0
 jle    _exit

Ifall att det finns något inläst så innehåller eax nu antalet inlästa byte. Vi jämför eax med 0. Kommandot jle hoppar ifall det vänsta argumentet är mindre eller lika med det högra. Ifall inget lästes in kan vi avsluta programmet.

 push   dword eax
 push   dword buffer
 push   dword 1
 mov    eax, 4
 call   _syscall
 add    esp,12

Här är inget nytt. Vi skriver ut det vi just läste in. EAX innehåller ju antalet inlästa byte.

 jmp    _loop

Kommandot jmp gör ett ovilkorligt hopp. Här hoppar vi tillbaka till _loop. Jag lämnade med flit kvar argumenten till read på stacken för att inte behöva sätta dom tillbaka.

 push   dword 0
 mov    eax, 1
 call   _syscall

Inget nytt här heller. Programmet avslutas.