En förståelse för hur datorer är organiserade, hur de verkar fungera på en mycket låg nivå, behövs för att förstå hur ett program i assembleringsspråk fungerar. På den mest förenklade nivån har datorer tre huvuddelar:
- Huvudminne eller RAM-minne som innehåller data och instruktioner,
- en processor, som bearbetar uppgifterna genom att utföra instruktionerna, och
- In- och utdata (ibland förkortat I/O), som gör det möjligt för datorn att kommunicera med omvärlden och lagra data utanför huvudminnet så att den kan hämta tillbaka data senare.
Huvudminne
I de flesta datorer är minnet uppdelat i bytes. Varje byte innehåller 8 bitar. Varje byte i minnet har också en adress som är ett nummer som anger var byten befinner sig i minnet. Den första byten i minnet har adressen 0, nästa byte har adressen 1 och så vidare. Genom att dela upp minnet i bytes blir det byteadresser eftersom varje byte får en unik adress. Adresser till byte-minnen kan inte användas för att hänvisa till en enskild bit i en byte. En byte är den minsta delen av minnet som kan adresseras.
Även om en adress hänvisar till en viss byte i minnet kan processorer använda flera byte i minnet i rad. Den vanligaste användningen av denna funktion är att använda antingen 2 eller 4 bytes i en rad för att representera ett tal, vanligtvis ett heltal. Enstaka bytes används ibland också för att representera heltal, men eftersom de bara är 8 bitar långa kan de bara rymma 28 eller 256 olika möjliga värden. Genom att använda 2 eller 4 bytes i en rad ökar antalet olika möjliga värden till 216 , 65536 eller 232 , 4294967296.
När ett program använder en byte eller ett antal bytes i en rad för att representera något som en bokstav, ett nummer eller något annat kallas dessa bytes för ett objekt eftersom de alla är en del av samma sak. Även om alla objekt lagras i identiska minnesbytes behandlas de som om de har en "typ", som anger hur bytesna ska uppfattas: antingen som ett heltal eller ett tecken eller någon annan typ (t.ex. ett icke heltalsvärde). Maskinkod kan också betraktas som en typ som tolkas som instruktioner. Begreppet typ är mycket, mycket viktigt eftersom det definierar vilka saker som kan och inte kan göras med objektet och hur objektets bytes ska tolkas. Det är till exempel inte tillåtet att lagra ett negativt tal i ett positivt talobjekt och det är inte tillåtet att lagra ett bråk i ett heltal.
En adress som pekar på (är adressen till) ett objekt med flera byte är adressen till den första byten i objektet - den byte som har den lägsta adressen. En viktig sak att notera är att du inte kan avgöra vilken typ ett objekt har - eller ens dess storlek - genom dess adress. Faktum är att du inte ens kan avgöra vilken typ ett objekt är genom att titta på det. Ett assemblerprogram måste hålla reda på vilka minnesadresser som innehåller vilka objekt och hur stora dessa objekt är. Ett program som gör detta är typ-säkert eftersom det bara gör saker med objekt som är säkra att göra på deras typ. Ett program som inte gör det kommer förmodligen inte att fungera korrekt. Observera att de flesta program faktiskt inte uttryckligen lagrar vilken typ ett objekt har, de har bara konsekvent tillgång till objekt - samma objekt behandlas alltid som samma typ.
Processorn
Processorn kör (utför) instruktioner som lagras som maskinkod i huvudminnet. De flesta processorer har inte bara tillgång till minnet för lagring, utan har också några små, snabba utrymmen av fast storlek för att hålla objekt som för närvarande bearbetas. Dessa utrymmen kallas register. Processorer utför vanligtvis tre typer av instruktioner, även om vissa instruktioner kan vara en kombination av dessa typer. Nedan följer några exempel på varje typ i x86-assembleringsspråk.
Instruktioner som läser eller skriver i minnet
Följande instruktion i x86-assembleringsspråk läser (laddar) ett 2-byteobjekt från byteadressen 4096 (0x1000 i hexadecimaltal) till ett 16-bitarregister som kallas "ax":
I det här monteringsspråket betyder hakparenteser runt ett nummer (eller ett registernamn) att numret ska användas som en adress till de data som ska användas. Användningen av en adress för att peka på data kallas indirektion. I nästa exempel, utan de fyrkantiga parenteserna, får ett annat register, bx, faktiskt värdet 20 inläst i det.
Eftersom ingen indirektion användes, sattes det faktiska värdet in i registret.
Om operanderna (de saker som kommer efter mnemotekniken) visas i omvänd ordning skriver en instruktion som laddar något från minnet det istället till minnet:
Här får minnet vid adress 1000h värdet bx. Om det här exemplet utförs direkt efter det föregående exemplet kommer de två bytena vid 1000h och 1001h att vara ett heltal på två byte med värdet 20.
Instruktioner som utför matematiska eller logiska operationer.
Vissa instruktioner gör saker som subtraktion eller logiska operationer som inte:
Exemplet med maskinkod tidigare i denna artikel skulle vara detsamma i assemblerspråk:
Här adderas 42 och ax och resultatet lagras i ax. I x86-assemblering är det också möjligt att kombinera en minnesåtkomst och en matematisk operation på det här sättet:
Den här instruktionen lägger till värdet av det 2-bytes heltal som är lagrat på 1000h till ax och lagrar svaret i ax.
Denna instruktion beräknar or av innehållet i registren ax och bx och lagrar resultatet i ax.
Instruktioner som bestämmer vad nästa instruktion ska vara.
Vanligtvis utförs instruktionerna i den ordning de visas i minnet, vilket är den ordning de är skrivna i assemblerkoden. Processorn utför dem bara en efter en. Men för att processorer ska kunna göra komplicerade saker måste de utföra olika instruktioner beroende på vilka data de har fått. Processorernas förmåga att exekvera olika instruktioner beroende på något resultat kallas för förgrening. Instruktioner som bestämmer vad nästa instruktion ska vara kallas för förgreningsinstruktioner.
I det här exemplet antar vi att någon vill beräkna hur mycket färg som behövs för att måla en kvadrat med en viss sidlängd. På grund av stordriftsfördelar kommer dock färgbutiken inte att sälja mindre än den mängd färg som behövs för att måla en kvadrat på 100 x 100.
För att räkna ut hur mycket färg de behöver utifrån längden på kvadraten de vill måla, tar de fram följande steg:
- subtrahera 100 från sidlängden
- Om svaret är mindre än noll, sätt sidlängden till 100.
- multiplicera sidlängden med sig själv
Denna algoritm kan uttryckas i följande kod där ax är sidlängden.
mov bx, ax sub bx, 100 jge continue mov ax, 100 continue: mul ax
I det här exemplet introduceras flera nya saker, men de två första instruktionerna är bekanta. De kopierar värdet av ax till bx och subtraherar sedan 100 från bx.
En av de nya sakerna i det här exemplet kallas etikett, ett koncept som finns i assemblerspråk i allmänhet. Etiketter kan vara vad programmeraren vill (om det inte är namnet på en instruktion, vilket skulle förvirra assemblerprogrammet). I det här exemplet är etiketten "continue". Den tolkas av assemblerprogrammet som adressen till en instruktion. I det här fallet är det adressen till mult ax.
Ett annat nytt koncept är flaggor. I x86-processorer sätter många instruktioner "flaggor" i processorn som kan användas av nästa instruktion för att bestämma vad som ska göras. I det här fallet, om bx var mindre än 100, kommer sub att sätta en flagga som säger att resultatet var mindre än noll.
Nästa instruktion är jge, vilket är en förkortning för "jump if greater than or equal to" (hoppa om större än eller lika med). Det är en greninstruktion. Om flaggorna i processorn anger att resultatet var större än eller lika med noll, kommer processorn istället för att bara gå till nästa instruktion att hoppa till instruktionen vid continue label, vilket är mul ax.
Det här exemplet fungerar bra, men det är inte vad de flesta programmerare skulle skriva. Subtraktionsinstruktionen satte flaggan korrekt, men den ändrar också värdet den arbetar på, vilket krävde att ax kopierades till bx. De flesta assemblerspråk tillåter jämförelseinstruktioner som inte ändrar något av de argument de får, men som ändå ställer in flaggorna korrekt, och x86-assembler är inget undantag.
cmp ax, 100 jge continue mov ax, 100 continue: mul ax
Istället för att subtrahera 100 från ax, se om talet är mindre än noll och tilldela det tillbaka till ax, lämnas ax oförändrat. Flaggorna sätts fortfarande på samma sätt och hoppet görs fortfarande i samma situationer.
Inmatning och utmatning
Inmatning och utmatning är en grundläggande del av databehandling, men det finns inte ett enda sätt att göra det på i assemblerspråk. Detta beror på att sättet som I/O fungerar på beror på hur datorn är konfigurerad och vilket operativsystem den har, inte bara på vilken typ av processor den har. I exempelavsnittet nedan använder Hello World-exemplet MS-DOS-operativsystemanrop och exemplet efter det använder BIOS-anrop.
Det är möjligt att göra I/O i assemblerspråk. Assemblerspråk kan i allmänhet uttrycka allt som en dator kan göra. Även om det finns instruktioner för att lägga till och förgrena i assembler som alltid gör samma sak finns det dock inga instruktioner i assembler som alltid gör I/O.
Det är viktigt att notera att det sätt på vilket I/O fungerar inte är en del av något assemblerspråk eftersom det inte är en del av hur processorn fungerar.