software 1996 · whisky-ware · a beer

ASM Tutorial — Chapter I

🌐 Leer esta página en español →

I wrote this in 1996, when I was 15 or 16, for the crowd on a BBS called Public Enemy. It was whisky-ware: if it worked for you, you owed me a whisky. The style is what it is — zero corrections, zero censorship. What you're looking at is exactly what got distributed, in a Word .DOC file inside a ZIP with its FILE_ID.DIZ and everything. If it looks cheesy to you, you didn't live through the 90s.

░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░
░▒▓                                                                        ▓▒░
░▒▓                              ASM Tutorial                              ▓▒░
░▒▓                                                                        ▓▒░
░▒▓      Sami, 1996 (going by Manolo Buitre to keep dad in the dark)       ▓▒░
░▒▓                                                                        ▓▒░
░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░

Hey to everyone who's downloaded this little file... first off, let me say this isn't going to be difficult at all, but you've gotta take it all in together... if you find you're getting stuck somewhere... keep reading... 'cause chances are one chapter ties into another, and the next one ends up explaining better whatever was in the one before, and so on and so forth 3:)...

This first chapter is going to try to be an introduction to the "inner world" of the PC, so don't go thinking it's dead easy (I don't want to fool you), but you do need to understand what little is written in this chapter to be able to keep going. This is, without a doubt, => the most important <= chapter, so you've got to master it 3;DDDDDDDD... I'm sorry, but that's how it is... but cheer up and keep going, because once you've read this everything else is like games... you're lost to begin with, but then your level goes up reaaaally fast 8*)))))))

01 — Introduction

I'll skip explaining why you'd use ASM in the first place... but I'll give you the short version in case you have no clue what this ASM (assembler, same thing) business is about. ... Turns out you've got a chip (a microprocessor — an 8086, a 286, a 386, a 486, or if you're feeling fancy even a 586) from that whole PC "family". All of them get programmed with a handful of very simple instructions (like the letters of the alphabet)... but ones you can combine to build complex functions (exactly like with human vocabulary). Just so you get an idea... you can multiply, say, a 5 by a 5 easily with a single processor instruction:

mul 5,5

... but you'll be asking yourself... and where does it put the result?... well, let's just say this instruction we just wrote (mul 5,5) doesn't actually exist. Inside the chip (whichever one — a 386, a 486, whatever you fancy) you've got a handful of "registers"... these are... well, like the memory keys on a calculator (so you get the picture), except stupidly fast.

Before we start writing simple little programs you're going to need to know the data types that exist... you'll need to know how to convert a number from hexadecimal to binary and back (but don't worry... it's simpler than it "sounds" ;)

02 — Data types

I don't really know where to start explaining this if you have no clue... but let's give it a shot... Let's start with how you encode a number for your microprocessor, so it can understand it. This gets encoded in bits...

        7       6       5       4       3       2       1       0
        *       *       *       *       *       *       *       *

Each of these asterisks is a bit... a bit is, like a mate of mine used to say... a little piece of information... "something" that can be on or off (a 1 or a 0, as people usually say... called "set" in the technical jargon, in case you come across the term elsewhere) and the number I put above the asterisk is the bit number... (which, by convention, goes left to right starting at 0)

So what happens then... imagine we've got one bit. Turns out this bit can be either on... or off... two combinations, right?...

What happens if we add another bit next to it:

        1       0       <== This is the bit number (left to right)
        *       *
        0       0       <== this is the first combination
        0       1       <== the second
        1       0       <== and so on and so forth, obviously
        1       1       ==> Until we see that with two bits we
                            can get up to 4 different combinations.

If we add another bit to these two... we'll have eight different combinations (try it yourself if you don't believe me ;) ... and to wrap this up... let me tell you a byte has 8 bits... and a word is two bytes... that is... 16 bits.

To save you some time thinking about this... take a look at this:

        7       6       5       4       3       2       1       0
        *       *       *       *       *       *       *       *
        128     64      32      16      8       4       2       1

Getting the idea?... still not???... alright then... let's keeeeep going....

Add up the values below the bits that are on (*)... what do you get?... 255?... very good... if we count the 0... that's 256 combinations :)

And now you'll be thinking... well great, so what... all those combinations... but what the hell do I need so many combinations and so much nonsense for.......... well here we go!! (relax, it's simple... and if not, well, you already know who Manolo Buitre is 3;)

Let's store a number in binary... for example... the number 1:

        7       6       5       4       3       2       1       0 <= bit no.
        *       *       *       *       *       *       *       * <= nothing
        128     64      32      16      8       4       2       1 <= value
      -------------------------------------------------------
        0       0       0       0       0       0       0       1 <= encoding

Not sure if you're getting what I mean... one more example then..... the number 4:

        0       0       0       0       0       1       0       0 <= encoding

... the number 128:

        1       0       0       0       0       0       0       0 <= encoding

... the number 129:

        1       0       0       0       0       0       0       1 <= encoding

... the number 96:

        0       1       1       0       0       0       0       0 <= encoding

(so much copy-pasting 3;)

Enough examples, right?... simple enough?... and now you'll say... "you're not seriously telling me I'm gonna have to work with those ones and zeros!???? :-((("... of course not... decimal gets used a lot in practice... binary for certain reasons we'll get to further ahead, and especially hexadecimal, because of how well, well, well it gets along with binary... you know hexadecimal (hex to friends) runs from 1 to 16, but with these digits:

        0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  <= hexadecimal
        0  1  2  3  4  5  6  7  8  9  10 11 12 13 14 15 <= decimal value

As you can see... once you go past 9 you move on to A, then B to represent an 11, and all in all with a single digit we can express a value from 0 to 15 (16 combinations) instead of the 0 to 9 of decimal (10 combinations)... But that's beside the point... picture a binary number... 24 for example:

        7       6       5       4       3       2       1       0 <= bit no.
        *       *       *       *       *       *       *       *
        128     64      32      16      8       4       2       1 <= value
      -------------------------------------------------------
        0       0       0       1       1       0       0       0 <= encoding

... and in binary 24 is "00011000"... and to convert it to hexadecimal... we take the binary "00011000" and split it into groups of 4 bits (remember that with 4 bits we can get 0 to 15 combinations... see where I'm going with this? ;-).....

        8       4       2       1   |    8       4       2       1   <= VALUE
        0       0       0       1   |    1       0       0       0   <= binary
        ----------------------------------------------------------
                                1   |    8

So 00011000 in binary (written 00011000b) is written 18h in hexadecimal (which is nowhere near 18 in decimal)...

Let me explain a bit why hexadecimal and binary get used more than decimal...

a) all the documentation for PC cards
   (soundblaster and all that) comes to you with numbers in hexadecimal...

b) if you learn to encode in binary you'll be able to see how
   EVERYTHING in the computer works, and it's not remotely useless, since
   you'll be able to "think" like the gadget sitting right under your nose,
   and therefore save a lot of memory, understand ASCII perfectly,
   communications, bytes, words, and so on...

c) because I say so, "dammit"... and since theory that's not good for
   anything makes me sick... well, you can imagine that this, which looks
   like a load of nonsense... does get used later on (and if it didn't,
   rest assured you'd drop the whole subject 3;)

As a summary of this first section... as useful facts...

A byte is 8 bits... and therefore 256 possible combinations
A word is 16 bits... and therefore 65536 possible combinations
A letter (yes, an ASCII character) takes up a byte... and just so
you get an idea, the computer stores an "A" with the code 41h, an "a"
with the code 61h, and so on.
A byte in hexadecimal never has more than two digits... 256 is 0FFh
If you write a value in hexadecimal, ALWAYS make sure you put the
letter "h" (lowercase if possible, since it's more readable) right
after the value, and if the hex number starts with a letter
ALWAYS ==> put a 0 in front of it... you'll understand why further on...
The sign of a number is bit no.7... if it's set to 1, either the number
is negative or it's worth 128 or more... try it out and you'll see why };)

Right... it's about 12 (I think, more or less)... it's Saturday the 16th or 17th of August... I'm trying to get the details right since I've just come back from a park, and you know how lively parks get on Saturday nights 38*)... and even though I'd rather still be in the little park drinking, gimme fizzy pop, mum, fizzy pop! 3;)... well the lot back home want me at least a little bit sober... see you next chapter!...

03 — Assembling and linking

No... I get it, you probably want to curse my entire family when you hear these words, but what can you do... alright, let's get to it... To write a program in ASM you need a text editor that saves in ASCII (is there one that doesn't?)... the DOS edit, for instance, is the one I usually use. Then you need what's called the compiler (TASM, MASM, A86... any one that lets you target an 80XXX processor will do, though personally I like TASM, or Turbo Assembler, better than the rest) and finally a linker: Turbo Link, TLink, Link, or whichever you like best (doesn't much matter which one you use here, since they're all pretty similar).

And now the questions:

A) What's a compiler?

You write a program with a plain text editor, right?:

        .MODEL Tiny
        .CODE
            mov ax,4Ch
            int 21h
        .STACK
        END

All this program does is exit to DOS, that is, quit... (don't worry if you don't understand the program, because right now that's beside the point).

This is => NOT <= machine code, meaning code that the machine (your processor) can understand directly. What the compiler does is take what you've written and turn it into machine code. Machine code, just so you get an idea, is exactly the same as what you're writing, just in this form:

        mov ax , 05h       <== puts a 5 into ax (this is what you write)
                               (takes up 12 bytes)
        15h 01h  05h       <== this only takes up 3 bytes

If you still haven't caught what I'm telling you... you could say (quite accurately) that every instruction in ASM (MOV, for example) corresponds to an instruction code ("opcode" in English), for instance 15h. As you can imagine this gets more complex later on, but let's skip that topic, since it's basically never used for writing normal, or even complicated, programs... this was simply an explanation of what the compiler does.

B) And what does the linker do?

When you write a program in ASM you usually give it the extension .ASM (mi_prog.asm, for example), and compiling it creates an object file (mi_prog.obj). I recommend you start running some tests right away, compiling other people's examples or copying someone else's code so you start hitting bugs and questions. But you'll notice the .obj isn't executable... so you have to link it to make an EXE or a COM.

To sum up the steps for compiling and linking your first programs:

a) Create the program with edit
        edit mi_progr.asm

b) Compile it
        tasm mi_progr.asm

c) Link it into a DOS .EXE
        tlink mi_progr.obj

d) Run the program if you want to
        mi_prog.exe

And a P.S. to this little chapter... don't worry if you still don't know why it's done this way... for now just do it, and once we've written a few programs we'll dig a bit deeper into linking, obj files and the rest (which, as you'll see, is simple, don't worry ;-)

04 — The processor's registers

The processor's registers are "spots" where you can put values to operate on: add, subtract, multiply and other things we'll get to in a bit :) Let's say registers are like variables... but with a fixed name, and each one is used for a specific purpose. Let's get started then...

We've got what are called the general-purpose registers:

AX, BX, CX and DX... all of them 16 bits, meaning we
                   can fit numbers from 0 to 65535 (or negative ones)

A general trait of these registers is that they can also be handled in groups of 8 bits:

AX => AH and AL          BX => BH and BL
CX => CH and CL          DX => DH and DL
             15                 0       <= Bit number
        AX => ****|****|****|****
               |       | |       |
               --------- ---------
                  AH        AL

If you didn't get the drawing (which, by the way, is designed to torture you) well, AL and AH are 8 bits each (BX, CX and DX are all exactly the same as AX)... so let's move on to some more examples, which are the ones that really show how useful this stuff is:

        mov     ax,5                    ; This puts a 5 into AX
                                       ^^^
* Note: The ";" means a comment. If you put it in your program, everything
after the ";" on that line doesn't get compiled.

AX would end up like this:

     AX=0000|0000|0000|0101b    <= in binary that's this

        AH would be 00000000b
        AL would be 00000101b

We could also write:

        mov ah,1
        mov al,2

        AL => ****|****
              0000|0010b

        AH => ****|****
              0000|0001b

        So AX ends up as: 0000|0001|0000|0010b

I think that's all pretty clear, right?... if not, well, you know what to do... ask! So, with that cleared up, let's get to the special features of the "general-purpose" registers...

AX: for multiplying and dividing
BX: memory addressing (pointing to a byte in memory...
    I'll get to that in the next section... RAM (MEMORY)) and
    for multiplying AX by BX.
CX: counter (loops... if you don't know what that is,
    don't worry, you'll see it in the next chapter)
DX: usually used to store the offset of an address
    (jump ahead to the RAM part if you don't know what that means)
SI: 16 bits... Source Index (in English)... usually used to
    point to a string (a string is a set of bytes,
    for example... "Manolo wants to be a carpenter")
DI: 16 bits... Destination Index... same as SI, but for
    pointing to the place you want to copy a string to.

And those are the 4 general-purpose registers. If you're working with a 386 or higher... things change a bit (if you want them to, of course)... the registers are 32 bits, giving you a huge number of combinations and very fast data transfers. These 32-bit registers are called EAX, EBX, ECX and so on for all the registers. AX in this case would be the lowest 16 bits (the least significant ones, bit no.0 through no.15)

05 — RAM (memory)

RAM stands for Random Access Memory... this means that to access a given byte of it, you need that byte's address. And now here comes the "hard" part of ASM (or rather, of the PC's own layout)...

Say we've got, oh, 8 megs of RAM, alright?... Let's also assume every byte is going to have an address (which is numeric)... If we gave the first byte the address 0, the second one 1, the third one 2... with our 16-bit registers we couldn't address more than 65536 bytes, right?... so what happens to the rest of the memory then?... do we just lose it?... no, of course not... we've got more registers! (don't cry yet 3;)... These new registers are called segment registers.

How do you solve the 64Kb problem?... through segmentation... (hey! leave my family out of this, alright? 3;)... It's simple... it's done with 2 16-bit registers.

Syntax examples:

        mov bx,5        ; bx=5
        add bx,2        ; adds a 2 to bx
                        ; bx is now worth 7

        mov bx,1        ; we put a 1 into bx
        mov ax,bx       ; ax is worth 1

Imagine you want to read byte number 64 from memory:

        mov bx,64
        mov al,[bx]     ; al grabs the value at memory address 64
                        ; NOT the value that happens to be in BX!

Worth pointing out that this can ONLY be done with BX, SI and DI... which means writing:

        mov al,[cx]

... doesn't exist and won't work. The computer splits memory into 64Kb blocks called segments:

        |---------|
        | 64Kb    |
        |---------|--> Segment 3
        | 64Kb    |
        |---------|--> Segment 2
        | 64Kb    |
        |---------|--> Segment 1
        | 64Kb    |
        |---------|--> Segment 0

The segment value (0, 1, 2, 3, etc.) goes into the segment registers: DS, CS, SS and ES. So the address ends up being made of a pair of registers, Segment:offset:

DS:BX for data
CS:IP is the address where your program is currently
      running, so it's not really a good idea to change it
      by hand (I can pretty much guarantee you'll screw it up)
ES:DI, ES:SI, DS:SI, DS:DI for data as well

What about SS:BP?... that's for another chapter... don't worry about them for now. Also worth mentioning that the segment registers aren't directly accessible, so to put the value 1 into DS you'd have to write:

        mov ax,1
        mov ds,ax

06 — The stack

It's a memory segment of whatever size you assign to it (you'll see how to create one in our first program). Here you can store 16-bit integers just by writing:

        push ax                 ; or push whatever 16-bit value you want

And to get them back:

        pop ax                  ; Grabs the last item you pushed
                                ; onto the stack and puts it into AX

What's this good for... well it has plenty of uses... from the sheer speed of push and pop, to passing parameters between different functions, going through something like:

        push    ds

        push    cs
        pop     ds

        ; YOUR PROGRAM GOES HERE for whenever you need the code
        ; segment and the data segment to be the same, and then
        ; restore the data segment to where it was.

        pop     ds

07 — Interrupts

Come on then! this is the last bit of theory and from here on everything's simple. If you get interrupts, which are veeeery simple... after that all you'll need is to know how whatever you want to program works: SB, VGA, etc. etc. but don't worry... that's all just a matter of "tables" and having the right information at hand. Once we're done with interrupts we'll write our first program, and you'll finally start getting into what ASM programming actually is, but as you'll understand, these previous 620 lines were necessary for you to understand the reasoning behind the machine's instructions.

Right... let's get to it... interrupts... these little buggers are small routines sitting in memory (for now)... that save you work when it comes to printing text to the screen or the simplest PC functions... we're mainly talking about DOS and BIOS functions here.

The IBM-PC family is controlled through interrupts, which can be triggered by hardware or by software. When an interrupt happens, the program saves CS:IP onto the stack (see, another use for the stack) and keeps running the interrupt routine until it hits an IRET.

To call a software interrupt all you have to do is write:

        int 10h ; 10h because it's a BIOS service

And then, say we want to set MCGA mode (320x200x256 colours), we need to put a 13h into AX... see... there's your first program already:

        .Model tiny     <== This is a *directive*... tiny means
                            we're only going to use one code segment
                            and DS is going to equal CS.

        .code Principal <== Another directive: says the code
                            segment called "Principal" starts here

          mov ax,0013h  <== The service number is 13h
          int 10h       <== and it's one of the BIOS video routines

          mov ah,4Ch    <== DOS service 4Ch ends the program
          int 21h           and whatever's in AL gets put into that
                            variable you'll have seen in bat files: errorlevel

        end             <== Tells the compiler to stop compiling
                            from here on. (mandatory to include)

Note: DOS and BIOS interrupts are looooads, so I'd recommend getting hold of some list that has all of them... because even though almost all the interrupts are optional, they massively boost the program's future compatibility, on top of making the program smaller... although they also slow it down.

It's a good idea to get hold of some other tutorial too, although every single one I've seen was in English. A really good one aimed at Basic was in a file called BTU, and that's the one I learned with, along with a ton of patience reading through theory that, honestly, turned out not to be worth much 8(

... until next time (assuming I don't die beaten to death at a Wednesday or Sunday gig 3;)... by the way... meetup this Friday, yeah?

Ah!... and as a teacher I'm worse than my dog at a checkout till, so any doubts you've got (and I imagine there'll be quite a few) should go to "Manolo Buitre", who doesn't have email or anything of the sort, so... :-)

- END OF THE FIRST CHAPTER -

08 — Extras and templates

A COM file compiles just like any .ASM, but when linking you need to add the /t option. The differences from an .EXE are that a COM isn't relocatable, meaning DOS can't just stick it at whatever CS offset it feels like — only at the one you set yourself with the "org 100h" directive.

;  Pause in a COM. For use in Bat files

Codigo Segment
        ASSUME CS:codigo, DS:codigo     ; here we're telling it that the
                                        ; segment called codigo is going to be
                                        ; both data and code (think about why)

               org  100h                ; Origin at offset 100h

            Comienzo:                   ; Just a plain label

               MOV  AH,7               ; As an exercise, look up what
               INT  21h                ; these two lines do

               MOV  AH,4Ch             ; You already know what these
               INT  21h                ; do, right?

Codigo EndS
        END Comienzo

I'd recommend having, as your programming "kit":

- an assembler and a linker    (without this you're screwed, obviously)
- an 80XXX processor           (if you don't have one I don't know how you're
                                reading this, 3;)
- a disassembler                (for messing around with other people's
                                programs)
- a debugger                    (to see what the processor's doing with
                                your programs or someone else's, and squash
                                the bugs you've got)
- smartdrv loaded
- the DOS edit.com itself
- a bat file along these lines:
        asm.bat => edit  %1.asm
                   tasm  %1.asm
                   tlink %1.obj
- and then if you've got the interrupt list handy, plus a few
  cheat sheets for whatever you know you're going to forget, well
  that'll do. :)

Whisky-ware document, so... you know what that means 3;) (just kidding, but donations of said beverage are, of course, welcome 3*)