02-10-2013, 10:17
Assembler er det sprog der kommer nærmest maskinkode, uden at være det. Og jeg vil lige starte med en advarsel om,
man er nød til at læse lidt, før man kan begynde at skrive kode.
Det er sjældent man bruger assembler til at lave et helt program med, med mindre det har meget begrænset funktionalitet.
F.eks. bruges det til bootloaderen som initieres af BIOS, da man kun har 512 bytes tilgængelig.
Oftest vil man lave små moduler i assembler, hvor hastigheden er meget vigtig, og så kalde disse moduler fra et 3. generationssprog.
Det er mest set i grafik drivere og sikkert spil engines osv.
Selve assembler koden, som egentlig bare er tekst, vil først blive compilet til OP koder, eller rigtige maskinkode instruktioner.
Her efter linkes det sammen med eventuelle andre compilede assembler filer, som f.eks. libraries. Når det linkes, vil de faktiske adresser blive fastlagt.
Når forskellige stykker assembler kode bliver compilet seperat, ved compileren ikke, hvilke adresser, funktioner, variabler osv. vil få,
før det hele er linket sammen, til een fil.
Denne lille teaser vil ikke være dybdegående, men forhåbentlig give nogen af jer lidt blod på tanden, og komme i gang.
Der er ikke andre måder at lære assembler på, end at læse og kode.
Tro det eller ej, men i dagens danmark, og resten af verden, er assembler faktisk blevet en del nemmere.
Man skal ikke længere holde øje med, hvilket segment man er i, med mindre du vil kode dit eget operativsystem. Og man kan slippe afsted med at lave systemkald.
Altså kald til funktioner i kernen. Det er også den primære årsag til, der er forskel på assembler til Linux og Windows.
Det vil jeg dog ikke gå i detaljer med.
Der er primært to assembler stilarter til CPUer, som understøtter IA32 (Intel Architecture) og amd64, og disse er Intel-style og AT&T style.
Instruktionerne er ikke alle ens og AT&T-style flytter fra venstre til højre og Intel-style, flytter fra højre til venstre.
For mig, som har kodet en del forskellige sprog, er det underligt at "flytte mod højre", så jeg bruger altid Intel-style,
og det er det, vi vil bruge i dette eksempel.
Uden jeg vil forklare selve koden, er her et eksempel på forskellen:
Jeg kan ikke forene mig selv med tanken om at skulle flytte mod højre, så denne guide dækker kun Intel-style assembler.
Inde i CPUen er der nogle registre, som vi ofte fylder værdier i, når vi skal regne på dem. Man bruger ikke helt så meget variabler,
som man kender det fra andre sprog. Disse registre har nogle faste formål, men kan i dag, også bruges mere generelt.
Et af disse registre er det nye 64 bit RAX, som så selvfølgelig indeholder 64 bits. Det er ikke unormalt at tænke på disse som binære registre.
Et tomt RAX register vil se således ud:
RAX: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
Hvis man flytter et tal til RAX, gøres det med "mov(e)" kommandoen, som naturligt nok flytter data.
mov eax, 8
Her efter vil RAX se således ud:
RAX: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00001000
Selv om der med 64 bit CPUen, blev introduceret nye "General Purpose" registre, er det ikke fordi, der er så mange,
at man kan fråse med dem.
Man kan derfor arbejde i de 32 bits til højre i RAX, ved at bruge EAX. Så EAX og RAX er det samme register, og hvis du flytter noget til det ene,
vil det overskrive indholdet i det andet.
Her er et overblik:
Man kan ligeledes referere til de 16 bits til højre i EAX, ved at bruge AX.
AX registret kan deles op i to, og man kan flytte data til de 8 bits til højre og de 8 bits til venstre, seperat.
dvs:
Det samme gælder for registre som RBX, RCX, RDX osv.
Dette kommer sig af, at man i gamle dage, kun havde de små registre, og som tingene udviklede sig, blev registrene større og større.
Men for at holde bagud kompabiliteten, har man udvidet de eksisterende registre, men stadig beholdt de gamle referencer.
Andre Interessante registre som i gamle dage havde mere bestemte formål, men nu også kan bruges til andre ting:
EAX 32 GP register (General Purpose/Accumulator register)
EBX 32 GP register (General Purpose/Base register)
ECX 32 GP register (General Purpose/Counter register)
EDX 32 GP register (General Purpose/Data)
ESP 32 Stack Pointer (Pejer på det element i stacken, som vil blive fjernet næste gang der fjernes. Toppen af stacken)
EBP 32 Base Pointer (Bruges til at bygge en stack frame med)
EIP 32 Instruction Pointer (Offset)
ESI 32 Source Index
EDI 32 Destination Index
Ovenstående er de mest interessante 32 bit registre, og de findes også med forskellige bit længder. Her under deres 64 bit versioner som f.eks. RAX, RBX osv.
Før vi ser på et par instruktioner, vil jeg præsentere et lille skelet jeg bruger, når jeg starter på noget nyt:
Det mest interessante i skellettet, er de tre SECTION dele.
.data bruger vi til vores initialiserede data.
Tilsvarende pseudo kode kunne være:
string besked = "Hello World!"
Altså variabler som får en værdi fra starten.
Modsat har vi .bss sektionen, hvor vi har vores data som ikke er initialiseret.
F.eks. en buffer til at læse data ind i.
.text sektionen er den del som indeholder vores kode.
Dette kommer også fra de gamle dage, da man havde meget begrænset ram og bagudkompabilitet.
F.eks. kunne man i de gamle 80386 CPUer ikke have et segment som var større end 64K. Hvis man kunne presse både data,
stack og kode, ind i eet segment, havde man et .com program.
Ofte var det ikke nok, så stacken og heapen havde deres egne segmenter, og man havde måske flere kode segmenter.
Og man havde så en .exe fil.
Det slipper vi heldigvis for i dag, og man slipper for at holde øje med hvilket segment man er i, og hvor man skal hen.
Bemærk at heapen er ram, allokeret af operativ systemet, og ikke en del af de tre sektioner.
Som i kan se, bruger man ; til kommentarer.
De tre kode linier i skabelonen gør faktisk ikke andet, end at fortælle kernen at programmet er færdig med at køre.
At eksekveringen ikke længere skal fortsætte. Forunderligt nok, kræver det tre instruktioner, bare at afslutte.
Jeg starter med at forklare den sidste instruktion først, for så giver de andre to god mening.
int 0x80
Int(errupt) kommandoen prikker til CPUen og siger, den har noget, kernen skal udføre. Eller, det gjorde den i gamle dage, i realiteten, er det bare en form for hop (jump) ind i kernel space.
Under 0x80 findes en stak system kald og dette bruges af alt software der kører i userspace.
For at kernen ved hvad, INT instruktionen skal udføre, kigger den i eax registrret. Det er i rax registret i 64 bit systemer
mov eax,1
Vi har med mov(e) kommandoen, lagt et 1-tal over i EAX og det betyder vi ønsker at udføre en exit().
Jeg kunne sådan set godt have udført en mov al,1 kommando. Det ville have givet samme resultat,
men det er god skik at sørge for, hele registret er tomt, og der ikke er nogen gamle data, højere oppe i EAX.
Eller længere til venstre om man vil.
Hvis EAX ser således ud inden mov kommandoen:
00010100 00001000 01010010 00001000
vil mov al,1 ikke rense ud og eax vil se således ud:
00010100 00001000 01010010 00000001
Hvis jeg bruger eax i stedet for al, vil jeg i hvert fald få tømt alle 32 bits i registret, så det kun indeholder 1.
Eller jeg kunne bruge RAX i stedet. Men jeg vil gerne have, i skal kunne rette eksemplet til at kunne køre 32 bit, uden de store ændringer.
Den midterste kommando som flytter 0 over i ebx registret, fortæller bare INT, at returkoden til operativ systemet er 0.
For hver af disse int 0x80 instruktioner, er der krav til, og beskrivelser af, hvilke data der skal være i de andre registre,
før man kalder int 0x80.
Man kan se alle 32 bit system kald i filen unistd_32.h, og alle 64 bit system kald i unistd_64.h
Disse findes normalt i /usr/include/x86_64-linux-gnu/asm i Kali Linux. Og sikkert noget nær samme sted på f.ek.s Debian og Ububtu.
Man kan også udføre systemkald i både 32 bit og 64 bit Linux med sysenter kommanden, men det er ret besværligt.
int 0x80 findes både i 32 bit og 43 bit instruktionssættet (ISA), men findes kun i 64 bit, på grund af bagud kompabilitet.
I 64 bit programmer, anbefales det at bruge syscall. Den eksekverer også hurtigere.
Nu til hello world eksemplet:
Som det fremgår af eksemplet, bruger man mange instruktioner på at forberede et kald til kernen.
Vi bruger i alt 7 linier på at skrive en besked ud på konsollen.
Nedenstende variabel msg, indeholder teksten Hello world!. Det efterfølgende 0xa er line feed, for at få et linieskift.
msg: db 'Hello world!', 0xa
db står for define byte, og det er det mest logiske at bruge bytes her. Et alternetiv kunne være define word, som er to bytes.
Enhver adresse som kan divideres lige med 16 kaldes en Paragraph Boundery.
Nedenstående udfører sådan set ikke noget i sig selv, men gør det lidt nemmere for os selv.
len equ $-msg
Man kunne godt hardkode længden af strengen til edx registret direkte, men hvis man så ændrer tekst længden,
passer dette ikke længere.
Det der sker er, at jeg sætte len til forskellen mellem msg starte adresse og len start adresse.
Da len kommer lige efter msg, kan vi trygt bruge det til at beregne længden af msg strengen.
Ovenstående kode, samler de informationer som int skal bruge, for at udføre et write() systemkald.
Vi flytter værdien 1 over i ebx registret, da det fortæller int, hvilken filedescriptor, vi ønsker at skrive beskeden til.
mov ebx,1
I følge POSIX standarden, skal et operativ system have følgende file descriptors:
0 = standard input (stdin)
1 = standard output (stdout)
2 = standard error (stderr)
Dem kender i nok fra jeres arbejde i konsollen, når i bruger sådan noget som "echo 1 > /proc/...", som defaulter til file descriptor 1.
Vi flytter så værdien 4 over i eax, da det er system kaldet write().
mov eax,4
Så flytter vi start adressen på msg, over i ecx, så kernen ved, hvad der skal skrives ud på konsollen.
Det sidste vi udfører, inden vi kalder int, er at fortælle hvor lang strengen som skal skrives ud, er.
Ellers får vi måske skrevet for lidt ud, eller endnu værre skrevet for meget ud. Tilfældige data i ram.
mov edx,len
Så er vi klar til at udføre selve kaldet med int kommandoen.
int 0x80
Så meget for så lidt. Hvis vi har givet int, de rigtige informationer og kaldet går godt, returnerer int en returkode i eax.
Vi læser dog ikke i dette tilfælde, denne værdi.
Hvad gør vi så nu?! Vi gemmer koden i en fil, som vi gør når vi programmerer i andre sprog.
Gem koden i en fil: hello.asm
Så skal vi compile filen, og det gør vi med nedenstående kommando:
nasm -f elf32 ./hello.asm -o ./hello.o -g -Z ./errors.log
Der er selvfølgelig flere måder at gøre det på, men ovenstående er, som jeg gør det.
nasm er compileren og vi giver den argumentet ".f elf32", som fortæller at det er 32 bit Excutable and Linkable Format. elf og elf32 er det samme og begge argumenter kan bruges.
Argumentet "-o ./hello.o" fortæller at vores binære output fil skal hedde hello.o.
Argumentet "-g" gør at nasm genererer debug informationer, hvilket kan være rart, når først man begynder at kore, teste og fejlfinde
sine programmer i en debugger.
Argumentet "-Z ./errors.log" får nasm til at sende fejlbeskederne til filen errors.log. Det er en smagssag, om man vil det,
men prøv lidt forskelligt, og se hvad der passer til dig.
Selv om vi kun har en enkelt fil, i dette eksempel, skal den stadig linkes, og det gør vi med følgende:
ld -o ./hello ./hello.o -melf_i386
Vi fortæller linkeren ld, at vi skal linke en binære output fil ./hello.o over i en eksekverbar fil, som skal hedde ./hello.
Argumentet "-o" står bare for output.
Argumentet "-melf_i386" fortæller linkeren, den binære objekt fil, bruger 32 bit adresser osv. Man vil stadig kunne køre programmet på en 64 bit linux box.
Både compileren og linkeren har mange features og options, som er værd at kigge på.
Begge kan formegentlig hentes med et standard pakkesystem på den distro du nu bruger.
Så er der kun tilbage at køre programmet med ./hello og se, om der kommer det på skærmen, vi forventer.
Der er meget meget mere til assembler og maskinkode, end jeg har skrevet. Det er som i ved, et kæmpe område, så jeg vil efterlade jer, med lidt ekstra informationer.
Når man programmerer assembler, er der meget at se til. En ting man kan tænke over, er hvad der sker, hvis man har værdien 250 i et 8 bit register, værdien 200 i et andet 8 bit register, og lægger dem sammen. Så vil resultatet bliver større end 8 bit.
Her til, har vi nogle flag, som bliver sat. dvs. en bit bliver sat til 1, så programmet kan tage forbehold for disse situationer.
I ovenstående eksempel, vil et Overflow flag blive sat, så programmet kan vide at resultatet af beregningen blev større en resultetet.
Under 32 bit har vi et register med alle disse flag i, der hedder eflags, og 64 bit versionen hedder rflags.
Hvordan skælner man mellem positive og negative tal!? Der er mange små ting, som nok også er en af årsagerne til,
assembler kun bruges når optimal hurtig kode kræves eller begrænset plads er et problem.
Conditional branching:
CMP (Compare) sammen ligner to værdier, og bruges til forgreninger:
cmp eax,ebx ; Sammenligner eax med ebx registret.
I virkeligheden trækker den eax fra ebx og ser, om resultatet bliver 0. Hvis resultatet bliver 0, er de ens.
En anden ting, der sker er, at Zero flaget bliver sat. Det er bl.a. det, nedenstående Jcc kommandoer undersøger.
Zero flager bliver sat, hver gang en matematisk beregning gør, at et register bliver sat til 0.
Vi kan så bruge disse flag til at forgrene med.
Det er ofte en af følgende, men der findes mange:
JA Jump Above
JAE Jump Above or Equal
JB Jump Below
JBE Jump Belov or Equal
JE Jump Equal
JNE Jump Not Equal
JG Jump Greater
JGE Jump Greater or Equal
JL Jump Lower
JLE Jump Lower or Equal
Nogle er til signed værdier og nogle er til unsigned værdier. Altså om værdien kan være positiv eller negativ, eller det ikke har nogen betydning.
Hvis et 8 bit register indeholder 10000000 binært, vil det svare til værdien -1 eller 128 decimalt. Der er jo en del forskel.
Loops:
Nedenstående eksempel viser en simpel loop. Der er flere måder at gøre det på.
Der er tilsvarende en inc(rement) instruks som tæller opad.
Man kan også bruge loop instruktionen til at lave en løkke med:
Kald til en subrutine:
Ud over at have sine variabler i .data og i .bss, er det muligt at overføre værdier ved at smide (pushe) værdierne på stacken inden
man bruger call instruksen.
De kan så poppes igen tilbage, når man er i subrutinen.
Man skal dog være lidt varsom, for når man bruger call, bliver sådan noget som IP, Instruktionspointeren også pushed på stacken,
og det sker efter, de værdier vi selv har lagt på. Så det er noget med at poppe af stacken, tage vores værdier, og pushe adressen,
til ip tilbage på stacken. Stacken er en LIFO. Der findes flere forskellige push og pop instrukser til dette. Man kan også læse data, ved at bruge esp registret, som er vores stack pointer.
esp pejer på toppen af stacken.
Overblik:
De nye 64 bit registre:
R8-R15
Der er også 8 nye 128 bit hurtige matematik SSE registre.
EIP: 32 bit instruction pointer.
RIP: 64 bit instruction pointer.
32 bit Flags register, EFLAGS:
Se bl.a. CF, DF, ZF og OF flagene.
Bonus:
De gamle 32 bit CPUer havde en begrænsning i, hvor meget ram den kunne addresere med 32 bit.
Nu, vi er gået over til 64 bit, er vi ude i overkill 16 exabytes, så man har valgt, ikke at bruge alle 64 bits som tidligere, men at dele CPUens allokering op i Virtuel og Fysisk ram.
Historik frem til nu, hvor nummer 4 er standarden:
Memory models:
1. Real Mode Flat Model.
2. Real Mode Segmented Model. (DOS)
3. Protected Mode Flat Model. (Linux & modern Windows) 80386+
4. Long Mode Flat Model. (64 bit)
Jeg kan kun anbefale at surfe lidt rundt efter noget godt materiale og se at få kodet noget assembler.
Det er på godt og ondt, normalt at bøger om assembler, først præsenterer kode og kommandoer,
når man har passeret de første 100-200 sider om den interne arkitektur. Det er desværre nødvendigt.
Man er også nød til at vide hvilke flag, der bliver sat hvornår.
EDIT:
Jeg vil lige tilføje at Intel godt nok har deres eget 64 bit instruktionssæt, og de faktisk var ude med en 64 bit processor før AMD,
men Intel understøtter og bruger nu primært AMDs instruktionssæt.
Problemet var at Intels 64 bit ISA ikke var sønderligt bagudkompatibelt. Somme tider kunne IA32 kode køre på en Intel 64 bit processor, ofte skulle det dog recompiles og i værste fald, skulle koden rettes til.
EDIT2: Jeg fik lige lyst til at lave et filter:
Som i kan se, bruger dette program uinitialiserede data.
Det læser et tegn fra stdin, kigger om det er mellemrum, 0-9, a-z eller A-Z. Hvis det er det, bliver det sendt til stdout.
I kan her se jeg bruger følgende til at reservere en byte med.
buffer resb 1
Her bliver brugt lidt forskellige jumps.
Man kan også se forskellen på hvordan følgende to kommandoer skælner mellem adressen og indholdet i buffer:
mov ecx, buffer
cmp byte [buffer], 30h
Programmet "cho" eller CharOnly filteret, kan bruges som følger:
"cat EnFilMedTekstOgJunkTegn.txt | ./cho > NyFil.txt"
Alt andet end mellemrum og de almindelige alfanumeriske tegn bliver fjernet.
Hvor anvendeligt det er i den virkelige verden, ved jeg ikke, men jeg synes det er et godt,
men simpelt eksempel.
EDIT: Bonuskode - Et ARM helloworld.s Assembly program:
Compilet på en rPI 2 med GNU assembler:
$ as helloworld.s -o helloworld.o
$ gcc helloworld.o -o helloworld
Links:
Linux System Call Quick Reference - http://www.scribd.com/doc/50665838/LINUX...-Reference
Lidt windows system kald - http://en.wikipedia.org/wiki/INT_21H
Instruktionssæt - http://www.intel.com/content/www/us/en/p...nuals.html
ASCII tabel - http://www.asciitable.com/
NASM doc - http://www.nasm.us/doc/
Linux LD Linker - http://man7.org/linux/man-pages/man1/ld.1.html
man er nød til at læse lidt, før man kan begynde at skrive kode.
Det er sjældent man bruger assembler til at lave et helt program med, med mindre det har meget begrænset funktionalitet.
F.eks. bruges det til bootloaderen som initieres af BIOS, da man kun har 512 bytes tilgængelig.
Oftest vil man lave små moduler i assembler, hvor hastigheden er meget vigtig, og så kalde disse moduler fra et 3. generationssprog.
Det er mest set i grafik drivere og sikkert spil engines osv.
Selve assembler koden, som egentlig bare er tekst, vil først blive compilet til OP koder, eller rigtige maskinkode instruktioner.
Her efter linkes det sammen med eventuelle andre compilede assembler filer, som f.eks. libraries. Når det linkes, vil de faktiske adresser blive fastlagt.
Når forskellige stykker assembler kode bliver compilet seperat, ved compileren ikke, hvilke adresser, funktioner, variabler osv. vil få,
før det hele er linket sammen, til een fil.
Denne lille teaser vil ikke være dybdegående, men forhåbentlig give nogen af jer lidt blod på tanden, og komme i gang.
Der er ikke andre måder at lære assembler på, end at læse og kode.
Tro det eller ej, men i dagens danmark, og resten af verden, er assembler faktisk blevet en del nemmere.
Man skal ikke længere holde øje med, hvilket segment man er i, med mindre du vil kode dit eget operativsystem. Og man kan slippe afsted med at lave systemkald.
Altså kald til funktioner i kernen. Det er også den primære årsag til, der er forskel på assembler til Linux og Windows.
Det vil jeg dog ikke gå i detaljer med.
Der er primært to assembler stilarter til CPUer, som understøtter IA32 (Intel Architecture) og amd64, og disse er Intel-style og AT&T style.
Instruktionerne er ikke alle ens og AT&T-style flytter fra venstre til højre og Intel-style, flytter fra højre til venstre.
For mig, som har kodet en del forskellige sprog, er det underligt at "flytte mod højre", så jeg bruger altid Intel-style,
og det er det, vi vil bruge i dette eksempel.
Uden jeg vil forklare selve koden, er her et eksempel på forskellen:
Kode:
Intel AT&T
MOV EAX, [0100] movl 0x0100, %eax
flyt EAX <-- [0100] flyt 0x0100 --> %EAX
Jeg kan ikke forene mig selv med tanken om at skulle flytte mod højre, så denne guide dækker kun Intel-style assembler.
Inde i CPUen er der nogle registre, som vi ofte fylder værdier i, når vi skal regne på dem. Man bruger ikke helt så meget variabler,
som man kender det fra andre sprog. Disse registre har nogle faste formål, men kan i dag, også bruges mere generelt.
Et af disse registre er det nye 64 bit RAX, som så selvfølgelig indeholder 64 bits. Det er ikke unormalt at tænke på disse som binære registre.
Et tomt RAX register vil se således ud:
RAX: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
Hvis man flytter et tal til RAX, gøres det med "mov(e)" kommandoen, som naturligt nok flytter data.
mov eax, 8
Her efter vil RAX se således ud:
RAX: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00001000
Selv om der med 64 bit CPUen, blev introduceret nye "General Purpose" registre, er det ikke fordi, der er så mange,
at man kan fråse med dem.
Man kan derfor arbejde i de 32 bits til højre i RAX, ved at bruge EAX. Så EAX og RAX er det samme register, og hvis du flytter noget til det ene,
vil det overskrive indholdet i det andet.
Her er et overblik:
Kode:
RAX: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00001000
EAX: 00000000 00000000 00000000 00001000
Kode:
RAX: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00001000
EAX: 00000000 00000000 00000000 00001000
AX: 00000000 00001000
AX registret kan deles op i to, og man kan flytte data til de 8 bits til højre og de 8 bits til venstre, seperat.
Kode:
RAX: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00001000
EAX: 00000000 00000000 00000000 00001000
AX: 00000000 00001000
AH AL
Kode:
RAX: EAX |AH |AL |
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00001000
| AX |
Dette kommer sig af, at man i gamle dage, kun havde de små registre, og som tingene udviklede sig, blev registrene større og større.
Men for at holde bagud kompabiliteten, har man udvidet de eksisterende registre, men stadig beholdt de gamle referencer.
Andre Interessante registre som i gamle dage havde mere bestemte formål, men nu også kan bruges til andre ting:
EAX 32 GP register (General Purpose/Accumulator register)
EBX 32 GP register (General Purpose/Base register)
ECX 32 GP register (General Purpose/Counter register)
EDX 32 GP register (General Purpose/Data)
ESP 32 Stack Pointer (Pejer på det element i stacken, som vil blive fjernet næste gang der fjernes. Toppen af stacken)
EBP 32 Base Pointer (Bruges til at bygge en stack frame med)
EIP 32 Instruction Pointer (Offset)
ESI 32 Source Index
EDI 32 Destination Index
Ovenstående er de mest interessante 32 bit registre, og de findes også med forskellige bit længder. Her under deres 64 bit versioner som f.eks. RAX, RBX osv.
Før vi ser på et par instruktioner, vil jeg præsentere et lille skelet jeg bruger, når jeg starter på noget nyt:
Kode:
BITS 32 ; Dette er et 32 bit eksempel. IA32 er Intel Architecture 32 bit.
GLOBAL _start ; Entry point til ELF linkeren (Det er her, vir fortæller compileren hvor i .text sektionen, eksekveringen starter)
SECTION .data ; Start af data area, hvor vi har vores initialiserede data. Variabler ol.
SECTION .bss ; Her lægger vi vores uninitialized data. Buffers osv.
SECTION .text ; Start af code block. Selve assembler koden skal placeres her under
_start:
; Test din kode her...
mov eax,1 ; Systemkaldet til exit()
mov ebx,0 ; Returkoden til operativsystemet
int 0x80 ; Udfør systemkaldet exit(). Eksekveringen mÃ¥ ikke fortsætte længere end her til
Det mest interessante i skellettet, er de tre SECTION dele.
.data bruger vi til vores initialiserede data.
Tilsvarende pseudo kode kunne være:
string besked = "Hello World!"
Altså variabler som får en værdi fra starten.
Modsat har vi .bss sektionen, hvor vi har vores data som ikke er initialiseret.
F.eks. en buffer til at læse data ind i.
.text sektionen er den del som indeholder vores kode.
Dette kommer også fra de gamle dage, da man havde meget begrænset ram og bagudkompabilitet.
F.eks. kunne man i de gamle 80386 CPUer ikke have et segment som var større end 64K. Hvis man kunne presse både data,
stack og kode, ind i eet segment, havde man et .com program.
Ofte var det ikke nok, så stacken og heapen havde deres egne segmenter, og man havde måske flere kode segmenter.
Og man havde så en .exe fil.
Det slipper vi heldigvis for i dag, og man slipper for at holde øje med hvilket segment man er i, og hvor man skal hen.
Bemærk at heapen er ram, allokeret af operativ systemet, og ikke en del af de tre sektioner.
Som i kan se, bruger man ; til kommentarer.
De tre kode linier i skabelonen gør faktisk ikke andet, end at fortælle kernen at programmet er færdig med at køre.
At eksekveringen ikke længere skal fortsætte. Forunderligt nok, kræver det tre instruktioner, bare at afslutte.
Jeg starter med at forklare den sidste instruktion først, for så giver de andre to god mening.
int 0x80
Int(errupt) kommandoen prikker til CPUen og siger, den har noget, kernen skal udføre. Eller, det gjorde den i gamle dage, i realiteten, er det bare en form for hop (jump) ind i kernel space.
Under 0x80 findes en stak system kald og dette bruges af alt software der kører i userspace.
For at kernen ved hvad, INT instruktionen skal udføre, kigger den i eax registrret. Det er i rax registret i 64 bit systemer
mov eax,1
Vi har med mov(e) kommandoen, lagt et 1-tal over i EAX og det betyder vi ønsker at udføre en exit().
Jeg kunne sådan set godt have udført en mov al,1 kommando. Det ville have givet samme resultat,
men det er god skik at sørge for, hele registret er tomt, og der ikke er nogen gamle data, højere oppe i EAX.
Eller længere til venstre om man vil.
Hvis EAX ser således ud inden mov kommandoen:
00010100 00001000 01010010 00001000
vil mov al,1 ikke rense ud og eax vil se således ud:
00010100 00001000 01010010 00000001
Hvis jeg bruger eax i stedet for al, vil jeg i hvert fald få tømt alle 32 bits i registret, så det kun indeholder 1.
Eller jeg kunne bruge RAX i stedet. Men jeg vil gerne have, i skal kunne rette eksemplet til at kunne køre 32 bit, uden de store ændringer.
Den midterste kommando som flytter 0 over i ebx registret, fortæller bare INT, at returkoden til operativ systemet er 0.
For hver af disse int 0x80 instruktioner, er der krav til, og beskrivelser af, hvilke data der skal være i de andre registre,
før man kalder int 0x80.
Man kan se alle 32 bit system kald i filen unistd_32.h, og alle 64 bit system kald i unistd_64.h
Disse findes normalt i /usr/include/x86_64-linux-gnu/asm i Kali Linux. Og sikkert noget nær samme sted på f.ek.s Debian og Ububtu.
Kode:
mov eax,1 ; Systemkaldet til exit()
mov ebx,0 ; Returkoden til operativsystemet
int 0x80 ; Udfør systemkaldet exit(). Eksekveringen mÃ¥ ikke fortsætte længere end her til
Man kan også udføre systemkald i både 32 bit og 64 bit Linux med sysenter kommanden, men det er ret besværligt.
int 0x80 findes både i 32 bit og 43 bit instruktionssættet (ISA), men findes kun i 64 bit, på grund af bagud kompabilitet.
I 64 bit programmer, anbefales det at bruge syscall. Den eksekverer også hurtigere.
Nu til hello world eksemplet:
Kode:
;title "hello"
;********************************************************
;* Filnavn: hello.asm *
;* Author: iTick *
;* Version: 1.0 *
;* Kernel: 2.6-32-5-amd64 *
;* Dist: Debian (Squeeze) *
;* Initial Date: 2. oktober 2013 *
;* Purpose: Assember hello world - ShellSec.org *
;********************************************************
; Dette program compiles og linkes som nedenstående:
; nasm -f elf32 ./hello.asm -o ./hello.o -g -Z ./errors.log
; ld -o ./hello ./hello.o -melf_i386
; Notes:
; ELF er Excutable and Linkable Format til Linux.
;********************************************************
BITS 32
GLOBAL _start ; Info om Entry point.
SECTION .data ; Start af data area
msg: db 'Hello world!', 0xa ; Vores hello world string, 0xa er line feed. 0xd er carriage return.
len equ $-msg ; Variablen len starter hvor vores streng msg slutter, så den kan bruges til at beregne længden af msg
SECTION .bss ; Her lægger vi vores uninitialized data.
SECTION .text ; Start af code block. Selve assembler koden skal placeres her under
_start:
mov ebx,1 ; File descriptor til stdout
mov eax,4 ; Systemkaldet write()
mov ecx,msg ; Sæt ecx registret til at peje på hello world strengen
mov edx,len ; Flyt længden af beskeden ind i edx registret
int 0x80 ; Udfør kernel kaldet write()
mov eax,1 ; Systemkaldet til exit()
mov ebx,0 ; Returkoden til operativsystemet
int 0x80 ; Udfør systemkaldet exit(). Eksekveringen må ikke fortsætte længere end her til.
Som det fremgår af eksemplet, bruger man mange instruktioner på at forberede et kald til kernen.
Vi bruger i alt 7 linier på at skrive en besked ud på konsollen.
Nedenstende variabel msg, indeholder teksten Hello world!. Det efterfølgende 0xa er line feed, for at få et linieskift.
msg: db 'Hello world!', 0xa
db står for define byte, og det er det mest logiske at bruge bytes her. Et alternetiv kunne være define word, som er to bytes.
Kode:
Collective terms of memory:
Byte 1
Word 2
Double Word 4
Quad Word 8
Ten Byte 10
Paragraph 16
Page 256
Segment 65536
Nedenstående udfører sådan set ikke noget i sig selv, men gør det lidt nemmere for os selv.
len equ $-msg
Man kunne godt hardkode længden af strengen til edx registret direkte, men hvis man så ændrer tekst længden,
passer dette ikke længere.
Det der sker er, at jeg sætte len til forskellen mellem msg starte adresse og len start adresse.
Da len kommer lige efter msg, kan vi trygt bruge det til at beregne længden af msg strengen.
Kode:
mov ebx,1 ; File descriptor til stdout
mov eax,4 ; Systemkaldet write()
mov ecx,msg ; Sæt ecx registret til at peje på hello world strengen
mov edx,len ; Flyt længden af beskeden ind i edx registret
int 0x80 ; Udfør kernel kaldet write()
; al registret sættes til 0 hvis det gik godt. Men det er vi lige glad med i dette eksempel.
Ovenstående kode, samler de informationer som int skal bruge, for at udføre et write() systemkald.
Vi flytter værdien 1 over i ebx registret, da det fortæller int, hvilken filedescriptor, vi ønsker at skrive beskeden til.
mov ebx,1
I følge POSIX standarden, skal et operativ system have følgende file descriptors:
0 = standard input (stdin)
1 = standard output (stdout)
2 = standard error (stderr)
Dem kender i nok fra jeres arbejde i konsollen, når i bruger sådan noget som "echo 1 > /proc/...", som defaulter til file descriptor 1.
Vi flytter så værdien 4 over i eax, da det er system kaldet write().
mov eax,4
Så flytter vi start adressen på msg, over i ecx, så kernen ved, hvad der skal skrives ud på konsollen.
Det sidste vi udfører, inden vi kalder int, er at fortælle hvor lang strengen som skal skrives ud, er.
Ellers får vi måske skrevet for lidt ud, eller endnu værre skrevet for meget ud. Tilfældige data i ram.
mov edx,len
Så er vi klar til at udføre selve kaldet med int kommandoen.
int 0x80
Så meget for så lidt. Hvis vi har givet int, de rigtige informationer og kaldet går godt, returnerer int en returkode i eax.
Vi læser dog ikke i dette tilfælde, denne værdi.
Hvad gør vi så nu?! Vi gemmer koden i en fil, som vi gør når vi programmerer i andre sprog.
Gem koden i en fil: hello.asm
Så skal vi compile filen, og det gør vi med nedenstående kommando:
nasm -f elf32 ./hello.asm -o ./hello.o -g -Z ./errors.log
Der er selvfølgelig flere måder at gøre det på, men ovenstående er, som jeg gør det.
nasm er compileren og vi giver den argumentet ".f elf32", som fortæller at det er 32 bit Excutable and Linkable Format. elf og elf32 er det samme og begge argumenter kan bruges.
Argumentet "-o ./hello.o" fortæller at vores binære output fil skal hedde hello.o.
Argumentet "-g" gør at nasm genererer debug informationer, hvilket kan være rart, når først man begynder at kore, teste og fejlfinde
sine programmer i en debugger.
Argumentet "-Z ./errors.log" får nasm til at sende fejlbeskederne til filen errors.log. Det er en smagssag, om man vil det,
men prøv lidt forskelligt, og se hvad der passer til dig.
Selv om vi kun har en enkelt fil, i dette eksempel, skal den stadig linkes, og det gør vi med følgende:
ld -o ./hello ./hello.o -melf_i386
Vi fortæller linkeren ld, at vi skal linke en binære output fil ./hello.o over i en eksekverbar fil, som skal hedde ./hello.
Argumentet "-o" står bare for output.
Argumentet "-melf_i386" fortæller linkeren, den binære objekt fil, bruger 32 bit adresser osv. Man vil stadig kunne køre programmet på en 64 bit linux box.
Både compileren og linkeren har mange features og options, som er værd at kigge på.
Begge kan formegentlig hentes med et standard pakkesystem på den distro du nu bruger.
Så er der kun tilbage at køre programmet med ./hello og se, om der kommer det på skærmen, vi forventer.
Der er meget meget mere til assembler og maskinkode, end jeg har skrevet. Det er som i ved, et kæmpe område, så jeg vil efterlade jer, med lidt ekstra informationer.
Når man programmerer assembler, er der meget at se til. En ting man kan tænke over, er hvad der sker, hvis man har værdien 250 i et 8 bit register, værdien 200 i et andet 8 bit register, og lægger dem sammen. Så vil resultatet bliver større end 8 bit.
Her til, har vi nogle flag, som bliver sat. dvs. en bit bliver sat til 1, så programmet kan tage forbehold for disse situationer.
I ovenstående eksempel, vil et Overflow flag blive sat, så programmet kan vide at resultatet af beregningen blev større en resultetet.
Under 32 bit har vi et register med alle disse flag i, der hedder eflags, og 64 bit versionen hedder rflags.
Hvordan skælner man mellem positive og negative tal!? Der er mange små ting, som nok også er en af årsagerne til,
assembler kun bruges når optimal hurtig kode kræves eller begrænset plads er et problem.
Conditional branching:
CMP (Compare) sammen ligner to værdier, og bruges til forgreninger:
cmp eax,ebx ; Sammenligner eax med ebx registret.
I virkeligheden trækker den eax fra ebx og ser, om resultatet bliver 0. Hvis resultatet bliver 0, er de ens.
En anden ting, der sker er, at Zero flaget bliver sat. Det er bl.a. det, nedenstående Jcc kommandoer undersøger.
Zero flager bliver sat, hver gang en matematisk beregning gør, at et register bliver sat til 0.
Vi kan så bruge disse flag til at forgrene med.
Det er ofte en af følgende, men der findes mange:
JA Jump Above
JAE Jump Above or Equal
JB Jump Below
JBE Jump Belov or Equal
JE Jump Equal
JNE Jump Not Equal
JG Jump Greater
JGE Jump Greater or Equal
JL Jump Lower
JLE Jump Lower or Equal
Nogle er til signed værdier og nogle er til unsigned værdier. Altså om værdien kan være positiv eller negativ, eller det ikke har nogen betydning.
Hvis et 8 bit register indeholder 10000000 binært, vil det svare til værdien -1 eller 128 decimalt. Der er jo en del forskel.
Loops:
Nedenstående eksempel viser en simpel loop. Der er flere måder at gøre det på.
Kode:
...
mov eax,4 ; Læg 4 over i eax. Dette er vores tæller. I dette eksempel tæller vi nedad.
.loop: nop ; Instruktioner her fra, er med i løkken. nop er den velkendte nop-sled ven, No Operation. Og gør ikke noget.
dec eax ; Så trækker vi en fra eax registret med dec (decrease).
jnz .loop ; Så bruger vi en jnz (Jump Not Zero). Hvis ovenstående dec eax når 0, sættes Zero flaget (ZF) og så vil denne jump ikke hoppe til .loop.
... ; Så hvis eax ikke er 0, så hopper vi til .loop
Der er tilsvarende en inc(rement) instruks som tæller opad.
Man kan også bruge loop instruktionen til at lave en løkke med:
Kode:
...
mov ecx,4 ; Læg 4 over i counter registret ecx. Dette er vores tæller. I dette eksempel tæller vi nedad.
.loop: nop ; Instruktioner her fra, er med i løkken. nop er den velkendte nop-sled ven, No Operation. Og gør ikke noget.
loop .loop ; loop instruktionen læser altid sin tæller fra ecx og sørger selv for at tælle ecx ned for hver iteration
Kald til en subrutine:
Kode:
...
call _myfunc ; Dette er et kald til en subrutine som hedder myfunc og er tilsvarende funktioner i andre sprog
...
_myfunc: ; Her starter funktionen.
...
nop ; Denne funktion gør så ikke andet, end at flytte EIP (Instruktion pointeren) til næste instruktion. Instruktionspointeret for 64 bit programmer, hedder RIP
ret ; Subrutinen afsluttes og forlades med instruksen ret(urn) og den hopper til instruktionen lige efter call.
Ud over at have sine variabler i .data og i .bss, er det muligt at overføre værdier ved at smide (pushe) værdierne på stacken inden
man bruger call instruksen.
De kan så poppes igen tilbage, når man er i subrutinen.
Man skal dog være lidt varsom, for når man bruger call, bliver sådan noget som IP, Instruktionspointeren også pushed på stacken,
og det sker efter, de værdier vi selv har lagt på. Så det er noget med at poppe af stacken, tage vores værdier, og pushe adressen,
til ip tilbage på stacken. Stacken er en LIFO. Der findes flere forskellige push og pop instrukser til dette. Man kan også læse data, ved at bruge esp registret, som er vores stack pointer.
esp pejer på toppen af stacken.
Overblik:
Kode:
General purpose registers:
|RAX|EAX|AX|AH-AL|
|RBX|EBX|BX|BH-BL|
|RCX|ECX|CX|CH-CL|
|RDX|EDX|DX|DH-DL|
|EBP|BP| Basepointer.
|ESI|SI| Source Index (Læsning)
|EDI|DI| Destination Index (Skrivning)
|ESP|SP| Stackpointer.
R8-R15
Der er også 8 nye 128 bit hurtige matematik SSE registre.
EIP: 32 bit instruction pointer.
RIP: 64 bit instruction pointer.
32 bit Flags register, EFLAGS:
Se bl.a. CF, DF, ZF og OF flagene.
Bonus:
De gamle 32 bit CPUer havde en begrænsning i, hvor meget ram den kunne addresere med 32 bit.
Nu, vi er gået over til 64 bit, er vi ude i overkill 16 exabytes, så man har valgt, ikke at bruge alle 64 bits som tidligere, men at dele CPUens allokering op i Virtuel og Fysisk ram.
Historik frem til nu, hvor nummer 4 er standarden:
Memory models:
1. Real Mode Flat Model.
2. Real Mode Segmented Model. (DOS)
3. Protected Mode Flat Model. (Linux & modern Windows) 80386+
4. Long Mode Flat Model. (64 bit)
Jeg kan kun anbefale at surfe lidt rundt efter noget godt materiale og se at få kodet noget assembler.
Det er på godt og ondt, normalt at bøger om assembler, først præsenterer kode og kommandoer,
når man har passeret de første 100-200 sider om den interne arkitektur. Det er desværre nødvendigt.
Man er også nød til at vide hvilke flag, der bliver sat hvornår.
EDIT:
Jeg vil lige tilføje at Intel godt nok har deres eget 64 bit instruktionssæt, og de faktisk var ude med en 64 bit processor før AMD,
men Intel understøtter og bruger nu primært AMDs instruktionssæt.
Problemet var at Intels 64 bit ISA ikke var sønderligt bagudkompatibelt. Somme tider kunne IA32 kode køre på en Intel 64 bit processor, ofte skulle det dog recompiles og i værste fald, skulle koden rettes til.
EDIT2: Jeg fik lige lyst til at lave et filter:
Kode:
;title "cho"
;********************************************************
;* Filnavn: cho.asm *
;* Author: iTick *
;* Version: 1.0 *
;* Kernel: 2.6-32-5-amd64 *
;* Dist: Debian (Squeeze) *
;* Initial Date: 2. Oktober 2013 *
;* Purpose: Filtrere alt andet en alfanummerisk fra *
;********************************************************
; Dette program compiles og linkes som nedenstående: *
; nasm -f elf64 ./cho.asm -o ./cho.o -g -Z ./errors.log *
; ld -o ./cho ./cho.o *
; Notes: *
; mov ecx, buffer tager adressen, hvor *
; cmp byte [buffer], 30h tager indholdet *
;********************************************************
BITS 64 ; Brug 64 bits. Dette er et 64 bit eksempel. IA32 er Intel Architecture 32 bit og kan også bruges.
GLOBAL _start ; Entry point til ELF linkeren (Det er her, eksekveringen starter)
SECTION .data ; Start af data area
SECTION .bss ; Her lægger vi vores uninitialized data.
buffer resb 1
SECTION .text ; Start af code block. Selve assembler koden skal placeres her under
_start:
nop
read: mov eax, 3 ; Forbered sys_read() kald
mov ebx, 0 ; POSIX filedescriptor stdin
mov ecx, buffer ; Kopier adressen på buffer til ecx
mov edx, 1 ; Læs en enkelt byte fra den valgte filedescriptor stdin
int 80h ; Overgiv kontrollen til kernen og lad den udføre sys_read()
cmp eax, 0 ; Læs returkoden fra sys_read() kaldet. Sammenlign returkoden med 0
je exit ; Hvis de er ens, hoppes der til exit. 0 betyder EOF
; Kontroller om det er mellemrum. 20h
cmp byte [buffer], 20h ; Kontroller om bufferen indeholder hex værdien for ascii tegnet mellemrum
je write ; Hvis det er ascii 20h, er det mellemrum. SÃ¥ hop til write. je er Jump Equal
; Komtroller ascii værdierne 0-9
cmp byte [buffer],30h ; Sammenlign den læste værdi med 30h (ascii tallet 0, som i 2, 1, 0)
jb read ; Hvis den er mindre end 0 er det et speciel tegn. Så springer vi til read og læser et nyt tegn
cmp byte [buffer],3Ah ; Hvis vi er kommet her til og vi er laver end hex 3A, er det et tal. Det skriver vi ud
jb write ; Brug jb Jump Below til at hoppe til write.
; kontroller ascii værdierne A-Z
cmp byte [buffer], 41h ; Sammenlign den læste værdi med 41h (A), hvis den er mindre, er det ikke et bogstav
jb read ; Hvis ascii tegnet var mindre, så hop til read
cmp byte [buffer], 5Bh ; Hvis den er mindre end 5B har vi ramt i ascii range med store bogstaver. Det vil vi udskrive
jb write
; Kontroller ascii værdierne a-z
cmp byte [buffer], 61h ; Hvis værdien er mindre end 61 så er det ikke et lille bogstav. Så læser vi et nyt
jb read ; Jumo tilread og læs et nyt tegn hvis ascii værdien er "below" 61h
cmp byte [buffer], 7Bh ; Sammenlign. Hvis tegnet er mindre end 7Bh, vil vi skrive det ud
jb write ; Hop til write, hvis bufferen indeholder et tegn som er mindre end 7Bh
jmp read ; Hop til read og start forfra, lige meget hvad
write:
mov eax, 4 ; Forbered sys_write()
mov ebx, 1 ; Vi vil gerne skrive til filedescriptor 1, stdout
mov ecx, buffer ; ecx skal kende adressen som bufferen har. Her er det tegn vi gerne vil skrive ud
mov edx, 1 ; Dette er antal bytes vi gerne vil skrive med sys_write()
int 80h ; Kald kernens 80h vector og lad den udføre sys_write()
jmp read ; Hop til read lige meget hvad. Vi vil læse det næste tegn, eller kontrollere om der er mere input
exit:
mov eax,1 ; Systemkaldet til exit()
mov ebx,0 ; Returkoden til operativsystemet
int 0x80 ; Udfør systemkaldet exit(). Eksekveringen må ikke fortsætte længere end her til
Som i kan se, bruger dette program uinitialiserede data.
Det læser et tegn fra stdin, kigger om det er mellemrum, 0-9, a-z eller A-Z. Hvis det er det, bliver det sendt til stdout.
I kan her se jeg bruger følgende til at reservere en byte med.
buffer resb 1
Her bliver brugt lidt forskellige jumps.
Man kan også se forskellen på hvordan følgende to kommandoer skælner mellem adressen og indholdet i buffer:
mov ecx, buffer
cmp byte [buffer], 30h
Programmet "cho" eller CharOnly filteret, kan bruges som følger:
"cat EnFilMedTekstOgJunkTegn.txt | ./cho > NyFil.txt"
Alt andet end mellemrum og de almindelige alfanumeriske tegn bliver fjernet.
Hvor anvendeligt det er i den virkelige verden, ved jeg ikke, men jeg synes det er et godt,
men simpelt eksempel.
EDIT: Bonuskode - Et ARM helloworld.s Assembly program:
Kode:
.data // Start af datasektion.
string: .asciz "Hello world" // Vores Hello World string
null: .byte 10 // Brug Line Feed til linieskift.
stringend: // Denne bruges til at beregne stringlen med
.set size, stringend - string // Beregn stringlen
.align 4 // Vi skal have aligned kode segmentet til en 4 byte boundery
.text // Start af kodesegment
.globl main // Vi linker med gcc. Eksporter main ellers kan den ikke kaldes udefra.
main:
// Function Prologue // Entrypoint.
push {r7, lr} // Gem r7 og Link Registeret. Se AAPCS
// Forbered systemkaldet til write()
mov r0, #1 // FLyt 1 over i r0. File descriptor 1 som er stdout
ldr r1, stringaddr // Load helloworld string adresse ind i r1 registret
mov r2, #size // Angiv strlen for helloworld string
mov r7, #4 // Flyt 4 over i r7. Dette er syskaldet write()
swi #0 // Lav en software interrupt. Dette starter systemkaldet
// Function Epilogue
mov r0, #0 // Returkode til operativ systemet
pop {r7, lr} // Genskab r7, vi har brugt og LR - Return Address
bx lr // Branch til Return Address, vi lige har poppet fra stacken
stringaddr : .word string // ARM kan underligt nok ikke se de andre segmenter
$ as helloworld.s -o helloworld.o
$ gcc helloworld.o -o helloworld
Links:
Linux System Call Quick Reference - http://www.scribd.com/doc/50665838/LINUX...-Reference
Lidt windows system kald - http://en.wikipedia.org/wiki/INT_21H
Instruktionssæt - http://www.intel.com/content/www/us/en/p...nuals.html
ASCII tabel - http://www.asciitable.com/
NASM doc - http://www.nasm.us/doc/
Linux LD Linker - http://man7.org/linux/man-pages/man1/ld.1.html