Geschrieben von chaoscode am 02.08.2006, 14:48

Assembler.

wer lust hat, kann eine überarbeitete version mit besserer
Rechtschreibung an mich schicken, da ich meine 3 in deutsch mit
mühe erkämpft habe.

Anmerkungen:
hier wird (der verständlich keit wegen) mit unsigned 32 bit werten
ohne fehlererkennung/carry/überläufen gearbeitet.

manchmnal sieht man als label das kürzel de.
das sind die ersten 2 buchstaben meines namens.
(ich heiße dennis)
das leigt aber daran, dass mir nix besseres einfällt.

hallo.
vorweg gesagt sei, dass ich hier von 32 bit rede.
64 bit kommen noch irgendwann, und 16 auch evtl mal.
und wir arbeiten unter windows.
für dieses tutorial brauchen wir ein tool, einen assembler.
ich empfehle fasm.
(google -> fasm -> http://flatassembler.net )
und auch gute musik ist zu empfehlen.
ich programiere eigentlich nur, wenn ich gute musik höre.
daher empfehle ich Winamp und/oder radio ripper.
(achtung. für radio ripper braucht man das .netframework
(welches weiß ich nichgt mehr. aber nicht das 2 er.)
das ladet er runter, bei der instalation.)

ein assembler ist der "compiler" für unser programm.
in anderen sprachen nennt man das compilieren,
aber in assembler heißt das assemblieren.
der assembler, der assembliert, ist der übersetzer.
er übersetzt unser programm von assembler in Maschinencode.
denn assembler ist kein maschienencode.
maschienencode ist das was der pc verarbeitet.
das sind dann für menschen unleserliche zahlenkombinationen.
assembler selber ist aber trotzdem sehr nahe am maschienencode.
eigentlich ist assembler das gleiche wie maschinencode.
aber es ist in einer für menschlichen form dargestellt.
wenn wir eine zahl in ein register laden wollen, sieht das in assembler so aus:

(wir lernen schon noch, was ein register ist, und was der befehl
bedeutet.)
mov eax,0x12345678
in maschienen code ist das dann ¸xV4
oder in hex auch 0xB8 0x78 0x56 0x34 0x12
da ist assembler doch schon die bessere wahl... oder?

also, jezt kommen wir zu dem aufbau einer cpu.
denn bevor man mit etwas arbeitet, muss man wissen, wie man damit arbeitet....

ein prozessor hat register.
in den registern speichert er ergebnisse.
die register sind speicherzellen, in denen die cpu ergebnisse speichert.
es gibt nur wenige register im 32 bit x86 prozessor.
register heißen
eax
ebx
ecx
edx

edi
esi

esp
ebp

es bibt noch mehr register wie cr0, dr7, CS, oder das flag register.
aber das sind controll/degub/segment/status register und für uns
MOMENTAN nicht von bedeutung.
und das wichtigste register:
eip.
das register eip zeigt, wo das aktuelle programm ausgeführt wird.

jedes dieser register hat platz für 32 bit.
wenn wir mal zusammen zählen, dann sind das 8*4byte(32 bit) = 32 byte.
aber wenn wir von pc reden, dann sind da doch immer von zahlen im
512 millionen byte(512 Megabyte) oder mehr die rede.
stimmt.
aber das ist der arbeitsspeicher.
im arbeitsspeicher ist massig platz,
aber der ist langsamer, als die register.
aber wie wir den benutzen, lernen wir noch.

Syntax:
Deer Syntax gibt an, wie der code geschrieben wird.
in diesem tutorial wird der intel syntax benuzt.
wenn ein ; da steht, dann ist alles hinter dem semilikolon ein kommentar.
das heißt, dass dieser bereich nicht im programm eingebaut wird.
das ist dazu gut, dass man den code kommentiern kann,
so dass man den code besser lesen und verstehen kann.
bsp:

code
code
code ; das ist ein kommentar
code ; das mache ich, damit man das programm besser verstehen kann.
code
code

1. die wichtigsten befehle.

eine studie hat mal gezeigt, dass 95 % des programmcodes mit
durchschnittlich 5 % der befehle gemacht werden(o.ä.).

daher lernen wir jezt mal ein paar der wichtigen befehle kennen,
ohne die (fast) kein programm laufen kann.

1. mov:
mov steht für move und bedeutet auf englisch bewegen.
dieser befehl bewegt (eigentlich kopiert er diese zahl eher)
von einem in das andere register.

wir nehmen an, im register Eax ist die zahl 0xde00f561
wenn wir den befehl mov ebx,eax ausführen, dann ist im
register ebx jezt auch die zahl 0xde00f561
d.h. am ende dieses befehles sind in beiden registern die werte 0xDE00F561

bsp:
wir wollen von einem register den wertt in ein anderes hineinkopieren.

mov eax,ebx

in dem register eax ist jezt der wert von ebx.

der befehl ist so aufgebaut:
mov [ziel],[Quelle]

2. add

wie der name es schon sagt, addiert dieser befehl.
eine cpu soll ja rechnen, und dafür ist diese befehl gut.
so sieht ein beispiel aus:

add edx,ecx

in diesem beispiel wird zu dem wert in edx der wert ecx addiert.
d.h.
edx = edx + ecx.

der syntax ist so:
add [zielregister und opperand 1],[operand 2]

3.sub
sub steht für subtrahieren.
wenn wir einen wert subtrahieren wollen,
dann nehmen wir diesen befehl.

sub ebx,eax

hier wird von ebx der wert eax subtrahiert.
d.h.
ebx = ebx - eax

aufbau:
sub [ziel + opperand1],[opperand2]

4.or
or???
was denn das??
das ist englisch und heiß oder.
was hast oder mit rechnen zu tun???
ganz einfach.
bei or werden jeweils die bits einzeln verglichen, und
gesezt.
es gibt bei or 2 eingangs bits.
und ein ausgangs bit.

wenn eines der beiden eingangsbits, oder auch beide 1 ist,
dann ist auch das ausgangsbit 1.
d.h.:
eingang: Ausgang:
bit 1 bit2
0 0 0
0 1 1
1 0 1
1 1 1

und wie arbeitet das jezt im prozessor???
nehmen wir an, eax = 0x0000436A
und ebx = 0xF7435A31
und wir füren den befehl
or eax,ebx
aus, dann kommt 0xf7435B7B raus.
aber wie das???
schauen wir uns das mal genauer an.
das erste register hat den wert 0x0000436A
das ist binär dargestellt
00000000000000000100001101101010
und das 2. register hast den wert 0xF7435A31
das ist binär dargestellt
11110111010000110101101000110001
und daS Ergebnis ist
11110111010000110101101101111011
machen wir mal das , was die cpu macht.

1 1110111010000110101101000110001
0 0000000000000000100001101101010
\-> hier ist eines von beiden bits gesezt. also 1

ergebnis: 1
danach kommt das nächste bit-paar dran.

1 1 110111010000110101101000110001
0 0 000000000000000100001101101010
\-> hier das gleiche. also 1
1
ergebnis: 11

das geht ein paar mal so weiter, bis:

1111 0 111010000110101101000110001
0000 0 000000000000100001101101010
\ hier ist kein bit 1. also 0.
1111
ergebnis:
11110

das geht ein paar mal so weiter, bis:
11110111010000110 1 01101000110001
00000000000000000 1 00001101101010
\-> hier sind beide bits 1. ergebnis 1
11110111010000110

ergebnis:
111101110100001101

irgend wann, sind alle bits durchgearbeitet,
und als ergebnis haben iwr dann

11110111010000110101101101111011
aber die cpu verarbeitet jedes bit parralel,
so dass das in einem takt abgearbeitet werden kann.
ich habe das jezt nur so dargestellt, um das besser zu verdeutlichen.

5.and
bei and (englich und bedeutet "und")
ist das ähnlich.
(ich verzichte hier auf die rieseige schritt für schritt
erklärung von oben.)
and hat auch 2 eingangsbits, und ein ausgangsbit.
aber bei and müssen 2 eingangsbits 1 sein, damit 1 rauskommt.
eingang: Ausgang:
bit 1 bit 2
0 0 0
0 1 0
1 0 0
1 1 1

d.h. wenn wir binär
001101101100110 und
110111101110010 haben, und das ganze mit and verarbeiten,
dann kommt
000101101100010 raus.

verstanden?
ich hoffe, dass schon.

6.
xor
xor, xor...
was könnte man sich darunter vorstellen?
was haben sich dies kranken X-treme pc spinner denn da schon
wieder ausgedacht...
was bedeutet das denn überhaupt?
xor bedeutet im englischen "exclusive or".
diese or schon wieder....
aber was heißt hier exclusive?
Antwort:
bei xor kommt am ausgang nur 1 raus, wenn nur einer(!) der beiden
eingänge 1 hat.
d.h.:
eingang: Ausgang:
bit1 bit2
0 0 0
0 1 1
1 0 1
1 1 0

2 binäre zahlen:
00101101101001111011011
11011111011100111001011
ergebnis:
11110010110101000010000

verstanden?
gut.

1. aufgabe:
wir wollen zu dem wert in eax den wert aus edx addieren,
und das ergebnis in ecx speicher.
wie?

2. aufgabe
schreibe in assembler:
ecx = eax+edx+edx+ecx-ebx

3. aufgabe:
wir wollen eax mit ecx mit xor verknüpfen.
dazu noch edx addieren und ebx subtrahieren.

********************************************************

1. lösung:
add eax,edx
mov ecx,eax

2. lösung:
add ecx,eax
add ecx,edx
add ecx,edx
sub ecx,ebx

3. lösung:
xor eax,ecx
add eax,edx
sub eax,ebx

********************************************************

wenn wir ein programm schreiben, werden manche
sachen mehrfach gebraucht.
und damit man die nicht immer wieder schreiben muss,
gibt es die möglich keit, zu einem teil vom programm
hinzuspringen, und danach wieder zurückl zu kehren.
d.h.
code
code
code
code
code
call subrout >\
code <---\ |
code | |
code | |
code | |
| |
subrout: | |
code <---+----/
code |
code |
code |
code |
ret >----/

erklärung:

subrout:
das ist ein label.
das wir später im code nicht mehr vorhanden sein.
das ist nur dazu da, um den assembler anzuzeigen,
an welcher position er weiter machen soll.

call subrout:
das ist der befehl.
hier wird folgendes gemacht:
die position des nächsten befehls wird auf dem stack gespeichert.
(was ein stack ist, wird noch erklärt.)
danach führt der prozessor die befehle von der position
aus, die angegeben wurde.
(das subrot ist eine position, die angegeben wird.)

ret:
ret heißt return.
der prozessor arbeitet an der position weiter,
die auf dem Stack gespeichert ist.

der call befehl legt die position vom nächsten befehl auf den stack
ret holt sich diese position und arbeitet da weiter.

hier ein bespiel:

call de
call plus
mov eax,ecx
call de

de:
xor eax,ebx
xor eax,ecx
xor eax,edx
ret
plus:
add eax,eax
add eax,ebx
add eax,ecx
add eax,edx
ret

aber:
schauen wir uns ma mal an, wie das programm nacher im pc aussieht:
0001: call 0005
0002: call 0009
0003: mov eax,ecx
0004: call 0005
0005: xor eax,ebx
0006: xor eax,ecx
0007: xor eax,edx
0008: ret
0009: add eax,eax
000A: add eax,ebx
000B: add eax,ecx
000C: add eax,edx
000D: ret

1.:
er führt 0001 aus. -> call 0005
-> auf den stack: 0002
-> weiter arbeitenm an 0005
2. Stack: 0002
er führt die befehle 0005 - 0007 aus.
er führt den befehl 0008 aus
->weiter arbeiten an der position auf dem stack.
3.
er arbeiten an der position 0002 weiter
-> call 0009
-> lege adresse vom nächsten befehl auf den stack:
-> weiterarbeiten an position 0009
4. Stack:0003
befehle ausführen und ret.
5.
an position 0003 weiter machen.
6.
mov eax,ecx
7. call 0005
-> auf den stack: 0005
-> weiter arbeiten an position 0005
8. Stack:0005
ausführen der befehle
9.
ret arbeite an adresse vom stack weiter.
-> springe nach 0005
10.
führe die befehle aus.
11.
an position 0008 angelangt:
ret.
arbeite an der position weiter, die auf dem stack liegt.
-> fehler.
Stack ist leer!!!

also immer aufpassen, auch wenn für einem das programm fertig ist,
die cpu macht immer weiter!

3.
wenn wir einen anderen teil ausführen wollen, ohne dass
der prozessor wieder zurückkommt, dann nehmen wir jmp.
jmp heißt jump.
bei jmp arbeitet der prozessor and er position weiter,
die angegeben wurde.
aber, es kommt nicht mehr zurück.
ein beispiel prog:

mov eax,ecx
add eax,edx
jmp de
add ebx,eax ; der teil vom programm wird nie ausgeführt
xor eax,eax ; der hier auch nicht.

de:
add edx,edx ; edx = edx+edx

das sieht dann so aus:
0000: mov eax,ecx
0001: add eax,edx
0002: jmp 0005
0003: add ebx,eax
0004: xor eax,eax
0005: add edx,edx

d.h.:
1.
befehl 0000
2.
befehl 0001
3.
befehl 0002
4.
befehl 0005
5.
befehl 0006
!!!achtung!!!
hier ist kein code mehr.
das kann sehr gefährlich werden.
oder auch nur einen fehler beim windows auslösen.

aufgabe:
1.
das folgende programm ist sehr groß.
mache es kleiner, mit hilfe von call.
add eax,ecx
xor ecx,edx
or eax,ecx
add eax,ecx
xor ecx,edx
or eax,ecx
add eax,ecx
xor ecx,edx
or eax,ecx
add eax,ecx
xor ecx,edx
or eax,ecx
add eax,ecx
xor ecx,edx
or eax,ecx
add eax,ecx
xor ecx,edx
or eax,ecx
add eax,ecx
xor ecx,edx
or eax,ecx
add eax,ecx
xor ecx,edx
or eax,ecx
add eax,ecx
xor ecx,edx
or eax,ecx
mov ebx,eax

*********************************************************
1.lösung

call de
call de
call de
call de
call de
call de
call de
call de
call de
mov ebx,eax
call EXIT_PROGRAMM ;beendet das programm.
; nicht traurig sein, das lernen wir noch.
de:
add eax,ecx
xor ecx,edx
or eax,ecx
ret
*********************************************************

RAM.
Ram. er ist resig (im vergleich zu unseren 8 registern)
und da der ram so riesig ist, muss die cpu wissen, welchen
teil des ram wir meinen.
deshalb hat jedes byte eine adresse.
und wie adressieren wir das?
ganz einfach.
ihr kennt ja noch den befehl mov.
oder?
die adresse wird immer in eckige klammern gesachrieben.
[addrese]
so können wir den wert aus eax in dem ram schreiben,
in dem wir den befehl "mov [ram_adreese],eax" nehmen.
wenn wir z.b. an die adresse 0x2F4DD6c etwas schreiben wollen,
dann schreiben wir in assembler einfach

mov [0x2F4DD6C],eax

hier sind die adresse jezt vorgegeben.
aber was ,wenn wir eine adresse im register eax bekommen,
und mit der arbeiten sollen?

mov [eax],eax. das funktioniert auch.
es gibt auch die möglichkeit, das etwas komplizierter zu machen.

mov [eax+edi],ecx
oder noch komplizierter:

mov [eax+edi+0x400],ecx

aber wie können nicht nur in den ram schreiben,
wir können auch lesen.

mov eax,[ecx]
mov eax,[0x40000]
mov edx,[ecx+edi]
usw....

STACK

der stack.
was ist das?
es ist ein sogenannter lifo speicher.
lifo?
lifo.
last in first out.
das lezte was abgespeichert wurde, ist das erste, was wieder
heraus geholt wird.
bsp:
Stack
*-----*
| |
*-----*
| |
*-----*
| |
*-----*
lege eax auf den stack
Stack
*-----*
| EAX |
*-----*
| |
*-----*
| |
*-----*
lege ebx auf den stack
Stack
*-----*
| EAX |
*-----*
| EBX |
*-----*
| |
*-----*

rechnen

hole ebx vom stack
hole eax vom stack

dafür gibt es die befehle push und pop.
und auch die register ebp und esp sind hier sehr wichtig.
mit push können wir einen wert auf den stack legen,
und mit pop einen abrufen.
push eax
pop ecx
ist das gleiche wie mov ecx,eax.
nur viel langsamer.

der call befehl und der stack.
der call befeh besteht eigentlich aus 2 befehlen.

push eip ;der zeiger,der den nächsten befehl anzeigt
jmp ziel

und ret ist
pop eip ; der wert, der auf dem stack liegt

stack organisierung:

das register esp zeigt in den speicher.
Speicher
(eine zelle ist 1 byte)
*-----*
| | <- esp zeigt hier hin.
*-----*
| |
*-----*
| |
*-----*
| |
*-----*
| |
*-----*
| |
*-----*
| |
*-----*
| |
*-----*
ich gehe mal davon aus, dass eax 0x45FE7C2B enthält
wenn wir push eax ausführen, passiert das:
1.
eax wird in den ram geschrieben, an die adresse,
die esp anzeigt.
2.
esp wird um 4 erhöht
(eax ist ja 4 byte groß.)

*-----*
|0x45 |
*-----*
|0xFE |
*-----*
|0x7C |
*-----*
|0x2B |
*-----*
| | <- esp zeigt hier hin.
*-----*
| |
*-----*
| |
*-----*
| |
*-----*
wenn wir jetzt den befehl pop eax ausführen, dann
nimmt pop eax den wert von dem stack, und der
prozessor arbeitet da weiter.

*-----*
|0x45 | <- esp zeigt hier hin.
*-----*
|0xFE |
*-----*
|0x7C |
*-----*
|0x2B |
*-----*
| |
*-----*
| |
*-----*
| |
*-----*
| |
*-----*

eax = 0x45FE7C2B
Anmerkung:
ret ist nix anderes als pop eip
mit push und pop klann man parameter übergeben, an unterprogramme
oder auch register sichern.

z.b.
de:
push eax
push ebx
push ecx
[rechnen]
pop ecx
pop ebx
pop eax
ret

in diesem programm wird gerechnet, und die Register sind am ende unverändert.

das erste richtige programm!

also.
wenn wir programieren, dann wollen wir manchmal
(nicht immer, aber dieses mal) etwas ausgeben.
und wir arbeiten hier mit wondows.
evtl habt ihr schon mal was von DLL gehört?
nein?
dann ist egal.
ich erkläre es kurz.
also.
wenn es keine dll's gäbe, dann müsste jedes programm seine
eigenen funktionen mitbringen.
und es würde nur auf einem bestimmten system laufen.
aber der vorteil von dll's ist der:
man muss nur den namen der funktion und die dll kennen.
mehr nicht.
den rest übernimmt das system.
das hat den vorteil, auch wenn das system die schnittstellen geändert hat,
kann man immer noch die dll nehmen, und das programm läuft.
das hat auch den vorteil dass viele standart sachen schon beim windows
dabei sind, und man nicht jedes mal das rad neu erfinden muss.

aber jezt zu dem programm.
habt ihr fasm runtergeladen???
wenn nein, tut das.
entpackt das.
und dann müssen wir noch etwas verändern.
startet das programm fasmw, und beendet es.
jezt sollte im verzeichnis eine datei neu sein.
fasmw.ini
die öffnet ihr.
und fügt ganz unten einen neuen eintrag hinzu.

[Environment]
Include = "c:\Dennis\fasm 1.66\INCLUDE"

(ich habe das jezt in c:dennis/fasm 1.66 entpackt.
ihr müsst den pfad natürlich ändern.
wenn ihr z.b. das ganze in "e:programieren/asm" speichert,
dann sollte das so aussehen:

[Environment]
Include = "e:programieren/asm\INCLUDE"

ok. das wars.
jezt wir angefangen.
starte winamp, gute musik zu hören, und öffnte fasmw.exe

das erste programm:

1.
was sind die ziele?
ein application erstellen die startet, rechnet, und ohne fehler beendet.

fasm ist für mehrere ausgabeformate programmiert worden.
daher muss er wissen, dass wir windoof wollen.

unsere erste zeile ist "format pe gui 4.0"
foremt sagt dem assembler, dass wir das ausgabe format angeben wollen.
damit weiß er, dass es eine .exe wird, und was für informationen es beinhaltet.

die nächste zeile ist "use32"
der assembler soll ja schließlich erfahren, dass wir eine 32 bit
application programmieren wollen.
anmerkung:
in dos und win 3.11 wurden noch 16 bit applicationen programmiert.
das hatte viele nachteile, wie z.b. den auf 1 mb begrenzten speicher.

"entry start"
mit dieser zeile wird gesagt, dass das programm an der position "start"
anfäng.
es könnte auch genau so gut auch entry main
oder entry Dennis1 heißen.

die nächste zeile heißt
section ".code" executable readable
das wort section heißt, dass hier ein neuer teil des programmes anfäng.
die exe file ist in verschiedene sectionen unterteilt.
eine section für die daten, eine für den code,
eine für tabellen, und noch mehr.
jetzt müssen wir noch das label start definieren.

start:

das label start haben wir schon mal benutzt.
-> "entry start"
das müssen wir definieren, dass der assembler weiß,
welche position wir mit "start" meinen.
wenn wir das nicht machen, gibt der assembler einen fehler aus,
dass er nicht weiß, wo sich das label befindet.
danach kommen endlich die befehle.

in unserem erstem programm werden wir etwas sinnloses machen,
nur um zu testen, ob es klapt.

mov rcx,0x00FFFFFF
loop $
ret

(das $ zeichen in dem befehl loop heißt,
dass die aktuelle position gemeint ist.)
das programm macht fast nix.
es spring "nur" 16,7 millionen mal auf sich selber,
decrementiert bei jedem sprung ecx um 1,
und wenn ecx 0 ist, springt es nicht, sondern
läuft weiter nach ret.
und ret beendet das ganze, und gibt die kontrolle an windows zurück.
das volle programm lautet dann:

format pe gui 4.0
use32
entry start

section ".code" executable readable
start:
mov ecx,0x00FFFFFF
loop $
ret

das wars dann mal fürs erste.

wenn ihr win 20/xp habt, (oder eine andere möglichkeit habt,
um die cpu auslastung zu messen)
könnt ihr bei älteren pc's sehen, dass die cpu auslastung einen kurzen
moment erhöhtist.
(man merkt es eigentlich garnicht.)
aber wenn ihr die zahl erhöht, die bei mov ecx steht, dann
werdet ihr unterschiede feststellen.
(bei mov ecx,0xFFFFFFFF muss das programm 4,2 milliarden
mal die operation ausführen.
d.h. auch auf highend pc's dauert dieser eine befehl an die 2 sekunden.
auch bei älteren systemen (400 mhz) dauert es dann an die 10 - 15 sekunden.
ps:
an die win 95/98(evtl auch me) user, die es im jahre 2006 immer noch gibt,
wenn ihr ein cli vor dem loop befehl einführt,
ist der pc eingefrohren, für die dauer des programmes.
d.h. wenn ihr 400 mhz habt, (wie ich auf dem entwicklungsrechner für pos)
dann ist der pc für ca 10,5 sekunden eingefroren.
(der sound hängt auch.)
danach geht es weiter, als ob nix wäre.

das wrs ann aber wirklich.

mfg Dennis

(anmerkung an die x86 profis:
das ist für noobs!
da kann man nicht alles auf eineml schreiben!!!)

(anmerkung für risc programierer:
auch wenn ihr es nicht glaubt, aber auch Cisc's
können zum programieren reizen,
ps, ich hab auch schon 2 verschiedene risc's in asm programiert,
mcore 2107 und ein anderes. (ram = register, nur 127 byte ram,
organisiert in speicherbänken.)

Bewertung Anzahl
6
100,0 %
3 Bewertungen