Monday, April 5, 2010

assembler tutorial: intro 2 of 2


the lovers moved to flee from heaven's tears


warning: this post contains frequent references to explicit hex, and may be inappropriate for readers under the age of 11h

last time i said, "before we begin in earnest, two things." here comes thing number two.



second, a word about how assembler works. you are no doubt aware that your computer has long-term storage (disks) and short-term storage (random access memory, or ram.) if we use an office allegory to describe a computer, we might say that the disks are like the filing cabinets in the back room: they can hold lots and lots of stuff, and are generally pretty well organized, but inconvenient. constantly going to them to fetch new work or to put something away would be a chore, so we tend to use them only when we need to grab something we plan to use soon or to put something away when we won't be using it for a good long while. ram is like the in/out trays on my desk: i can stack all kinds of stuff there (though much less than i can put in the filing cabinets) and my work is quickly and easily accessible. the cpu is like my desktop, where all the work actually happens. to do some work i have to take it from the trays and move it to the desktop, and to clear the desk for some other task i need to move what's on the desktop back to the trays. so where in the cpu do we store this really temporary stuff while working on it?

registers

cpus have built-in memory storage spaces called registers. in the intel x86 architecture, 16-bit general purpose registers go by the names ax, bx, cx and dx. each 16-bit register can be broken into two parts, a high-order byte and a low-order byte. for register ax, these would be called ah and al respectively. the specific meanings of high- and low-order aren't too important right now, and the topic delves deep into ancient religious wars of cpu design, but suffice it to say that putting the 16-bit word c725h into register ax will load c7h into ah and 25h into al.

in modern cpus each 16-bit register is only half of one of the 32-bit registers, which bear the names eax, ebx, ecx and edx. there are other special purpose registers that we'll talk about as we move along, but you get the idea.

so writing an assembly language program is like shuffling paperwork around. you copy data into a register, you tell the cpu to process it, then you do something (or nothing, if you wish) with the result. here is a simple set of instructions that you'll see frequently in assembler. we'll talk about what it does next time.

1 ; These instructions are commonly found in DOS programs.
2 mov ax, 4c00h
3 int 21h


segments

another thing you must know is how dos accesses memory. to mov (copy) data to or from ram you need an address. since dos uses 16-bit registers, the largest address it can work with is 16 bits long, so dos can address up to 65,536 (64k) bytes of ram. that's it. a long time ago 64k was a lot. remember all the great programs we ran on the commodore 64? but as consumers demanded more from their applications 64k became a barrier. dos handles this by viewing memory as a series of segments, each 64kb in size. a program can contain many segments of code and data so long as none of them exceeds 64kb.

to address memory, then, we need two registers: a special segment register for the segment address and a normal general purpose register for the offset within that segment. if the ds register, which points to a data segment, contains 24a0h and we mov 0fh into register dx, then ds:dx refers to the 16th byte of that segment, written as 24a0:000fh. if we later load ds with 4110h we'll find that ds:dx now points to 4110:000fh. it's not too complicated, but it's up to the programmer to keep track of which segment he's using at any point in time. fortunately you need not know the exact addresses of your segments (dos actually determines that at runtime, so there is no way you could know as you're writing your source.) in assembler we use labels, friendly names to refer to addresses. so you may see code like the following to initialize the data segment register:

4 ; Load the DS register with the address of the data segment.
5 mov ax, data ; "data" is the address of our
6 mov ds, ax ; data segment


stack

finally, there is the stack. this is a handy little place in ram to put things temporarily, such as when you want to pass data from one procedure to another. we push data onto the stack to store it, and we pop data off of the stack to retrieve it. the stack is like that little cart with the clean plates at the head of a buffet line. the most recently cleaned plates are warm, damp and on the top of the stack, and the ones that have been there awhile and are much drier are at the bottom. when you take a plate off the top, you're taking the one that was most recently placed on the stack.

data stacks work the same way. the topmost item is the most recently pushed data, the oldest data is at the bottom. data is always popped in reverse order from how it was pushed onto the stack.

of course, we all know what happens when you put too many plates in a stack on one of those carts. bad, loud things happen. if we were to overfill our stack segment in our program, we could overwrite some other segment, or worse, some other program's segment. this could also lead to bad, loud things, so the intel cpu does a funny thing when it sets up the stack: it fills it backward, from the top down. you need to see this to get it…

let's say that you decide to create a stack segment for your program that is only four bytes long (don't use such a small stack in real life.) the ss register (stack segment) will contain the address of the beginning, or bottom of the stack. the first byte would be at ss:0h, the second at ss:1h, the third at ss:2h and the fourth at ss:3h. the stack pointer (another register called sp) will point to the top of the stack, 4h.

"wait!" you cry. "4h isn't in the stack segment, because it's only four bytes long!" you're right. what's at the address that sp is pointing to right now? we don't know for sure. "isn't that dangerous?" you ask. perhaps, but wait until you see how the thing comes off.

when we push a byte onto the stack, sp is first decremented, so now it points to 3h. then the pushed data is copied to 3h. see? everything is okay, because we don't actually write to 4h, so we don't corrupt some other program's stuff. when we push a second byte, sp is decremented and the new data is written to 2h. when we pop the data off of the stack, the data that sp points to at 2h is copied and sp is incremented to point to 3h. but what if we try to push more than four bytes onto the stack? well, consider that when the fourth byte is pushed sp has been decremented four times, so it now points to 0h. any attempt to decrement sp again will set the overflow flag in the flags register, and dos will crash your program with a stack overflow error. your program valiantly falls on its own sword to keep from doing bad things to other programs. of course, there is nothing to keep you from popping the data at 4h before you've pushed anything onto the stack. just expect really bad things to happen when you try to use that unknown value. it's best to not go there.

why use the stack if it's so much potential trouble? remember that programs love it, more than programmers love their buffets. sometimes procedures pass parameters by the stack so that they can do things. after branching to another line of execution the stack can act like a trail of breadcrumbs, helping your code to wend its way back to where it came from. even if you never consciously use it, the services you request through dos or bios interrupts will use the stack. buck up, young padawan: you can't escape your destiny.

now then, you're all set to write your first program in assembly language…

No comments:

Post a Comment