For at forstå, hvordan computere er organiseret, hvordan de tilsyneladende fungerer på et meget lavt niveau, er det nødvendigt at have en forståelse for, hvordan et assemblerprogram fungerer. På det mest forsimplede niveau har computere tre hoveddele:
- hovedhukommelse eller RAM, som indeholder data og instruktioner,
- en processor, som behandler dataene ved at udføre instruktionerne, og
- input og output (undertiden forkortet til I/O), som gør det muligt for computeren at kommunikere med omverdenen og gemme data uden for hovedhukommelsen, så den kan hente dataene tilbage senere.
Hovedhukommelse
I de fleste computere er hukommelsen opdelt i bytes. Hver byte indeholder 8 bits. Hver byte i hukommelsen har også en adresse, som er et tal, der angiver, hvor byten befinder sig i hukommelsen. Den første byte i hukommelsen har adressen 0, den næste byte har adressen 1 og så videre. Opdelingen af hukommelsen i bytes gør den byteadresserbar, fordi hver byte får en unik adresse. Adresser i bytehukommelser kan ikke bruges til at henvise til en enkelt bit i en byte. En byte er den mindste del af hukommelsen, der kan adresseres.
Selv om en adresse henviser til en bestemt byte i hukommelsen, giver processorer mulighed for at bruge flere bytes hukommelse i træk. Den mest almindelige anvendelse af denne funktion er at bruge enten 2 eller 4 bytes i en række til at repræsentere et tal, normalt et heltal. Enkelte bytes bruges undertiden også til at repræsentere hele tal, men da de kun er 8 bit lange, kan de kun rumme 28 eller 256 forskellige mulige værdier. Ved at bruge 2 eller 4 bytes i en række kan antallet af forskellige mulige værdier øges til henholdsvis 216 , 65536 eller 232 , 4294967296.
Når et program bruger en byte eller et antal bytes i en række til at repræsentere noget som et bogstav, et tal eller noget andet, kaldes disse bytes for et objekt, fordi de alle er en del af den samme ting. Selv om objekter alle er gemt i identiske bytes i hukommelsen, behandles de som om de har en "type", der siger, hvordan bytesne skal forstås: enten som et heltal eller et tegn eller en anden type (f.eks. en ikke-taltværdi). Maskinkode kan også opfattes som en type, der fortolkes som instruktioner. Begrebet type er meget, meget vigtigt, fordi det definerer, hvilke ting der kan og ikke kan gøres med objektet, og hvordan objektets bytes skal fortolkes. Det er f.eks. ikke gyldigt at gemme et negativt tal i et positivt talobjekt, og det er ikke gyldigt at gemme en brøk i et heltal.
En adresse, der peger på (er adressen på) et objekt med flere byte, er adressen på den første byte i dette objekt - den byte, der har den laveste adresse. Som en sidebemærkning er det vigtigt at bemærke, at man ikke kan se, hvilken type et objekt er - eller endda dets størrelse - ud fra dets adresse. Faktisk kan man ikke engang se, hvilken type et objekt er, ved at se på det. Et assemblerprogram skal holde styr på, hvilke hukommelsesadresser der indeholder hvilke objekter, og hvor store disse objekter er. Et program, der gør dette, er type-sikkert, fordi det kun gør ting med objekter, som er sikre at gøre på deres type. Et program, der ikke gør det, vil sandsynligvis ikke fungere korrekt. Bemærk, at de fleste programmer faktisk ikke eksplicit gemmer, hvad typen af et objekt er, de tilgår blot objekter konsekvent - det samme objekt behandles altid som samme type.
Processoren
Processoren kører (udfører) instruktioner, som er gemt som maskinkode i hovedhukommelsen. Ud over at kunne få adgang til hukommelsen til lagring har de fleste processorer nogle få små, hurtige rum af fast størrelse til at opbevare objekter, der er under bearbejdning. Disse rum kaldes registre. Processorer udfører normalt tre typer instruktioner, selv om nogle instruktioner kan være en kombination af disse typer. Nedenfor er der nogle eksempler på hver type i x86-assemblersprog.
Instruktioner, der læser eller skriver hukommelse
Følgende x86-assemblerinstruktion læser (indlæser) et 2-byte objekt fra byteadressen 4096 (0x1000 i hexadecimalt format) til et 16-bit register kaldet "ax":
I dette samlesprog betyder firkantede parenteser omkring et tal (eller et registernavn), at tallet skal bruges som en adresse til de data, der skal bruges. Brugen af en adresse til at pege på data kaldes indirektion. I det næste eksempel uden de firkantede parenteser får et andet register, bx, faktisk værdien 20 indlæst i det.
Da der ikke blev brugt nogen omledning, blev selve den faktiske værdi sat ind i registret.
Hvis operanderne (de ting, der kommer efter mnemoteket) vises i omvendt rækkefølge, vil en instruktion, der indlæser noget fra hukommelsen, i stedet skrive det til hukommelsen:
Her får hukommelsen på adressen 1000h værdien bx. Hvis dette eksempel udføres lige efter det foregående, vil de 2 bytes på 1000h og 1001h være et helt tal på 2 bytes med værdien 20.
Instruktioner, der udfører matematiske eller logiske operationer
Nogle instruktioner udfører ting som subtraktion eller logiske operationer som ikke:
Maskinkodeeksemplet tidligere i denne artikel ville være dette i assembler-sprog:
Her lægges 42 og ax sammen, og resultatet gemmes tilbage i ax. I x86-assembler er det også muligt at kombinere en hukommelsesadgang og en matematisk operation på denne måde:
Denne instruktion tilføjer værdien af det hele tal på 2 byte, der er gemt i 1000h, til ax og gemmer svaret i ax.
Denne instruktion beregner or af indholdet af registrene ax og bx og gemmer resultatet tilbage i ax.
Instruktioner, der bestemmer, hvad den næste instruktion skal være
Normalt udføres instruktioner i den rækkefølge, de vises i hukommelsen, hvilket er den rækkefølge, de er skrevet i assemblerkoden. Processoren udfører dem bare en efter en. Men for at processorer kan udføre komplicerede ting, skal de udføre forskellige instruktioner afhængigt af, hvilke data de har fået. Processorernes evne til at udføre forskellige instruktioner afhængigt af resultatet af noget kaldes branching. Instruktioner, der bestemmer, hvad den næste instruktion skal være, kaldes forgreningsinstruktioner.
I dette eksempel kan du antage, at en person ønsker at beregne den mængde maling, der skal bruges til at male et kvadrat med en bestemt sidelængde. På grund af stordriftsfordele vil farvehandleren imidlertid ikke sælge mindre end den mængde maling, der er nødvendig for at male et kvadrat på 100 x 100 kvadrat.
For at regne ud, hvor meget maling de skal bruge ud fra længden af den firkant, de vil male, finder de frem til dette sæt trin:
- Træk 100 fra sidelængden
- hvis svaret er mindre end nul, sættes sidelængden til 100
- ganges sidelængden med sig selv
Denne algoritme kan udtrykkes i følgende kode, hvor ax er sidelængden.
mov bx, ax sub bx, 100 jge continue mov ax, 100 continue: mul ax
Dette eksempel introducerer flere nye ting, men de to første instruktioner er velkendte. De kopierer værdien af ax til bx og trækker derefter 100 fra bx.
En af de nye ting i dette eksempel kaldes en label, et begreb, der findes i assembler-sprog generelt. Labels kan være hvad som helst, som programmøren ønsker (medmindre det er navnet på en instruktion, hvilket ville forvirre assembleren). I dette eksempel er etiketten "continue". Det fortolkes af assembleren som adressen på en instruktion. I dette tilfælde er det adressen på mult ax.
Et andet nyt koncept er flag. På x86-processorer sætter mange instruktioner "flag" i processoren, som kan bruges af den næste instruktion til at beslutte, hvad der skal ske. I dette tilfælde vil sub, hvis bx var mindre end 100, sætte et flag, der siger, at resultatet var mindre end nul.
Den næste instruktion er jge, som er en forkortelse for "jump if greater than or equal to" (spring hvis større end eller lig med). Det er en forgreningsinstruktion. Hvis flagene i processoren angiver, at resultatet var større end eller lig med nul, vil processoren i stedet for at gå til den næste instruktion springe til instruktionen ved continue-labelet, som er mul ax.
Dette eksempel fungerer fint, men det er ikke det, som de fleste programmører ville skrive. Subtraktionsinstruktionen satte flaget korrekt, men den ændrer også værdien, som den opererer på, hvilket krævede, at ax blev kopieret til bx. De fleste assembler-sprog tillader sammenligningsinstruktioner, som ikke ændrer nogen af de argumenter, de får overdraget, men som stadig sætter flagene korrekt, og x86-assembler er ingen undtagelse.
cmp ax, 100 jge continue mov ax, 100 continue: mul ax
I stedet for at trække 100 fra ax, se, om tallet er mindre end nul, og tildele det tilbage til ax, forbliver ax uændret. Flagene sættes stadig på samme måde, og springet foretages stadig i de samme situationer.
Input og output
Ind- og uddata er en grundlæggende del af computerarbejde, men der er ikke én måde at gøre det på i assembler. Det skyldes, at den måde, hvorpå I/O fungerer, afhænger af computerens opsætning og det operativsystem, den kører, og ikke blot af, hvilken slags processor den har. I eksemplet Hello World-eksemplet i nedenstående afsnit anvendes MS-DOS-operativsystemopkald, og eksemplet efter det anvender BIOS-opkald.
Det er muligt at lave I/O i assemblagesprog. Assemblersprog kan faktisk generelt udtrykke alt, hvad en computer kan gøre. Men selv om der er instruktioner til at tilføje og forgrene i assembler, som altid vil gøre det samme, er der ingen instruktioner i assembler, som altid udfører I/O.
Det er vigtigt at bemærke, at den måde, hvorpå I/O fungerer, ikke er en del af noget assembler-sprog, fordi det ikke er en del af processorens funktion.