Atrevida Game Programming Tutorial #16
Copyright 1998, Kevin Matz, All Rights Reserved.
Prerequisites:
Let's expand our assembler vocabularies by learning some more instructions. In this chapter, we'll learn about the multiplication and division instructions, the shift instructions, and the hardware port instructions.
To multiply a byte by a byte, we put one of the values to be multiplied in the AL register. (Mathematics fanatics call this value the multiplicand.) Then we use either the MUL or IMUL instruction. We supply one operand -- the value (the multiplier) to multiply the first value by. This operand can use virtually any addressing mode except for the immediate mode (that means you can say "MUL AL" or "MUL [x]" or "MUL [BYTE DS:BX + SI + 3]" but not "MUL 5" or "MUL 03Dh"). The result (the product) of the multiplication is returned in AX. (Note that the largest possible value obtained by multiplying two bytes is 255 dec * 255 dec = 65025 dec, which is very close to the range of a word; 65535 - 255 - 255 = 65025 dec, right?)
Let's multiply 3 and 8.
MOV AL, 3 ; Multiply 3... MOV BL, 8 ; ...by 8... MUL BL ; ...storing the result in AX. ; The result, 24 dec, is now in AX.
Let's multiply two signed, byte-sized variables called x and y.
MOV AL, [x] ; Multiply [x]... IMUL [y] ; ...by [y]; signed result is in AX.
Personally, when I think of multiplication, I automatically think of the MUL instruction. But using a MUL instruction when an IMUL instruction should be used instead can be a hard-to-find bug. Remember, use MUL for unsigned (all positive) values, and use IMUL for signed (positive and negative) values.
To multiply a word by a word, place the multiplicand operand in the AX register. Then use either the MUL or IMUL instruction with a word-sized operand. Here's the fun part. The result of multiplying two sixteen-bit values together can produce a value with up to thirty-two bits. So the processor splits the product into two word-sized halves. The least-significant half is placed in the AX register, and the most-significant half is placed in the DX register.
Let's multiply -2000 dec by 30000 dec:
MOV AX, -2000 MOV BX, 30000 IMUL BX ; Most-siginificant word of the ; result is now in DX; least- ; significant word of the result ; is in AX.
Division works similar to multiplication. DIV is used for dividing one unsigned value by another unsigned value. IDIV is used for dividing one signed value by another signed value. DIV and IDIV permit two "modes": you can divide a word by a byte, or you can divide a doubleword by a word.
To divide a word by a byte, place the word-sized dividend (the value that gets divided) into AX. Then use either DIV or IDIV with the byte-sized divisor (the value the dividend is divided by) as an operand. When the division is finished, the result (the quotient) is available in AL. Also, the remainder or modulo of the dividend and divisor is calculated and is available in AH. (So if you want to do a "mod" operation, use DIV or IDIV and read the remainder out of AH.)
To divide a doubleword by a word, break the doubleword-sized dividend into least-significant and most-significant halves. Place the most-significant word into DX and the least-significant word into AX. Then you can use either DIV or IDIV as appropriate; supply a word-sized divisor as the operand. The quotient will be available in AX, and the remainder or modulo will be available in DX.
Here's a quick assembler program that demonstrates the use of the multiplication and division instructions. It doesn't do anything noticeable, but of course, if you have a debugger, you can watch the values in the registers change as you step through the program:
------- TEST11.ASM begins -------
%TITLE "Assembler Test Program 11 -- Multiplying and Dividing" IDEAL MODEL small STACK 256 LOCALS DATASEG ecks DB 11 why DW 1000h CODESEG Start: ; Let DS point to the start of the data segment: MOV AX, @data MOV DS, AX ; Multiply the byte-sized [ecks] by 9, giving a word-sized product: MOV AL, 9 MUL [ecks] ; AX should now contain 99 dec. (Note: IMUL could be used here ; instead.) ; Multiply the contents of AX (99 dec) by the word-sized [why], ; giving a doubleword-sized product: MUL [why] ; The product of 99 dec by 1000 hex (1000 hex = 4096 dec) is ; 405504 dec, or 63000 hex. The most-significant word is 6, ; so DX should contain 6. The least-significant word is 3000 hex, ; so AX should contain that value. ; Divide a word, 80 dec, by a byte, 10 dec: MOV AX, 80 MOV BL, 10 DIV BL ; The quotient, 8, should be in AL. The remainder, 0, should be ; in AH. ; Divide a doubleword, -8000 dec, by the word -999 dec: MOV AX, -8000 ; Least-significant word MOV DX, 0FFFFh ; Most-significant word... All ; bits are set; this "extends" ; the negative value to 32 bits. ; More on this later. MOV BX, -999 IDIV BX ; The quotient, 7, should be in AX. The remainder, -8, should be ; in DX. EndProgram: ; Terminate the program: MOV AX, 04C00h INT 21h END Start
------- TEST11.ASM ends -------
We'll see example programs later that actually do things with the values calculated by MUL/IMUL and DIV/IDIV.
Keep in mind that the multiplication and division instructions have a reputation for being slow. After all, if you do addition and multiplication on paper, the multiplication generally takes much longer, because there are more steps to do. On the 8088, multiplication or division instructions often took more than a hundred clock cycles to execute. On newer processors these are much faster -- on a 486, for instance, using MUL or IMUL to multiply two word-sized values can take between 13 and 26 clock cycles. Surely Pentium-class processors do much better still.
MOV AL, 3 ; Now we have 3 in AL. MOV AH, 0 ; Zero out AH. ; Now, AX contains 3.
That's easy enough. But what about signed numbers? What if we were to have -3 in AL, and we wanted to convert this value to a word-sized value (ie. we want AX to contain -3)?
Let's try the above method...
MOV AL, -3 ; AL contains -3. MOV AH, 0 ; Zero out AH.
But did this work? No. Recall two's complement notation for negative numbers. When we put -3 into AL, we were actually storing 1111 1101 bin in AL. 1111 1101 bin represents -3 in a signed byte context. (How did we get 1111 1101 bin? First, we take a positive number, such as 3, in binary form, and we apply a NOT operation to it. That means we flip all the bits; ones become zeroes and zeroes become ones. Then we add one. Look back to chapter two, "Binary Operations", for the full story on two's complement.)
Then we clear AH. So AX contains 0000 0000 1111 1101 bin. And what does that represent in a signed word context? Well, the sign bit (the leftmost bit) is zero, so we interpret the number as an unsigned value. 0000 0000 1111 1101 bin equals 128 + 64 + 32 + 16 + 8 + 4 + 1, which is 253 dec. 253 dec certainly isn't equal to -3 dec. So the above method doesn't work with negative values.
Fortunately, there are instructions that will handle this task for us correctly.
The CBW (Convert Byte to Word) instruction takes the byte in the AL register and extends it to a 16-bit value. This 16-bit value is then placed in AX.
Let's convert -3 (in a byte context) to its equivalent word-sized respresentation:
MOV AL, -3 ; AL = -3 CBW ; Convert byte in AL to word in AX ; Now AX contains -3.
Note that it's not possible to use registers other than AL and AX.
If you need to convert a word to a doubleword, you can use the CWD instruction. It will take the word in AX, and convert it to a 32-bit doubleword value, with the most-significant word in DX, and the least-significant word in AX. For example:
MOV AX, 0ABCDh ; AX = 0ABCDh (a negative number) CWD ; Convert word in AX to doubleword ; with MSW in DX and LSW in AX
The CBW and CWD instructions simply copy the sign bit to all of the leftmost positions in the new value. If the sign bit is 1, all of the bits to the left become ones. If the sign bit is 0, all of the bits to the left becomes zeroes.
The SHL instruction is the shift-left instruction. It can left-shift either bytes or words. There are two ways in which SHL can be used:
If you want to shift a register or memory location left by one bit, you can specify the register or memory location as the first operand, and 1 for the second operand. For example:
SHL AX, 1 ; Shift AX left by 1 SHL CH, 1 ; Shift CL left by 1 SHL [Count], 1 ; Shift variable Count left by 1 SHL [BYTE ES:BX + SI + 3], 1 ; Shift the byte at location ; "ES:BX + SI + 3" left by 1
If you want to shift left by some constant other than 1, you have to put that constant into the CL register. Then you use CL as the second operand:
MOV CL, 3 SHL BX, CL ; Shift BX left by 3 MOV CL, 5 SHL [x], CL ; Shift the variable x left by 5
Sadly, you can't use any other register than CL, and you can't specify an immediate value other than 1. (Intel at its best.)
SHR is the shift-right instruction. It works the same way as SHL, but, of course, it performs right shifts. For example:
SHR DL, 1 ; Shift DL right by 1 SHR [WORD SS:BP], 1 ; Shift the word at SS:BP right by 1 MOV CL, 4 SHR AX, CL ; Shift AX right by 4 MOV CL, [x] SHR [Amount], CL ; Shift variable Amount right by ; the value specified in variable x
Remember that bit-shifting causes bits to be lost when they are shifted out of the byte or word's range. Also recall that left-shifts can be used to multiply numbers by powers of two, while right-shifts can be used to divide numbers by powers of two. If you want to review bit shifts, read the first section of Chapter 3, "Binary Manipulations".
Let's look at left rotations. If we were to rotate a byte to the left by one bit, we would move all of the bits to the left once. The last bit, in bit 7, would be copied back to bit 0.
7 6 5 4 3 2 1 0 +---+---+---+---+---+---+---+---+ /----| | | | | | | | |<----\ (Left rotation) | +---+---+---+---+---+---+---+---+ | | <- <- <- <- <- <- <- | | | \------------------------------------------/
For example, if we had the following byte...
10010110 bin
...rotating it left once would give us:
00101101 bin
And rotating it left once again:
01011010 bin
And again:
10110100 bin
If we were to rotate it eight times, we would get the original byte.
Right rotations are similar, but of course the rotation occurs towards the right. The bits that fall out of position 0 are copied back to bit 7:
7 6 5 4 3 2 1 0 +---+---+---+---+---+---+---+---+ /---->| | | | | | | | |----\ (Rotate Right) | +---+---+---+---+---+---+---+---+ | | -> -> -> -> -> -> -> | | | \------------------------------------------/
If we had the following byte...
00100101 bin
...rotating it to the right once would give us:
10010010 bin
And again:
01001001 bin
And again:
10100100 bin
Again, if there were a total of eight rotations, we would get the original byte again.
Rotations work in a similar fashion for word-sized values -- for left rotations, bits that leave position 15 are transferred to bit 0, and for right rotations, bits that leave position 0 are transferred to position 15.
ROL is the rotate-left instruction. ROR is the rotate-right instruction. The operands work exactly the same way as they do for SHL and SHR -- if you want to rotate by a value larger than 1, you must put that value in CL:
MOV AL, 23h ; AL = 23h ROL AL, 1 ; Rotate AL left once, so AL = 46h MOV DX, 0FADEh ; DX = FADEh ROR DX, 1 ; Rot. DX right once, so DX = 7D6Fh ; (Before: 1111 1010 1101 1110 ; After: 0111 1101 0110 1111) MOV BX, 1 ; BX = 1 MOV CL, 3 ; CL = 3 ROL BX, CL ; Rotate BX left by CL, so BX = 8 MOV CL, 5 ; CL = 5 ROR BYTE [Array + 25], CL ; Find the 26th byte of the "Array" ; array, and rotate that byte right ; by CL
One last thing: the carry flag, CF, gets modified whenever you use ROL or ROR. For ROL, the diagram actually looks like this:
CF 7 6 5 4 3 2 1 0 +---+ +---+---+---+---+---+---+---+---+ | |<---+----| | | | | | | | |<----\ (ROL) +---+ | +---+---+---+---+---+---+---+---+ | | <- <- <- <- <- <- <- | | | \------------------------------------------/
The bit that is copied from the leftmost position (position 7 for bytes, position 15 for words) is also copied to the carry flag, CF. I normally ignore this, but you should just be aware that CF gets modified.
For ROR, the diagram should actually look like this:
7 6 5 4 3 2 1 0 CF +---+---+---+---+---+---+---+---+ +---+ /---->| | | | | | | | |----+--->| | (ROR) | +---+---+---+---+---+---+---+---+ | +---+ | -> -> -> -> -> -> -> | | | \------------------------------------------/
Notice again that the bit that is copied from the rightmost position, bit 0, is also copied to CF. Again, you can ignore this, unless you have a clever plan for using this bit.
SAR stands for "Shift Arithmetic Right". SAR is similar to SHR, but SAR preserves the most significant bit. How is this useful?
In Chapter 3, "Binary Manipulations", I very briefly indicated that caution should be used when shifting signed numbers in order to do multiplication or division. Actually, using left-shifts (SHL) to multiply signed numbers by powers of two produces the correct results (as long as there is no overflow or "underflow"). But using right-shifts (SHR) to divide signed numbers by powers of two doesn't work, because the sign bit gets dragged out of the most-significant position. To preserve the sign bit, we can use SAR instead of SHR.
SAR works this way for a single right shift: it saves a copy of the number's sign bit. Then it shifts the number to the right, the same way SHR does. Then it copies the saved sign bit back into the most-significant position. For right shifts by two or more, this procedure is repeated an appropriate number of times.
Let's divide the byte-sized number -20 dec by two, using SAR:
MOV AL, -20 ; AL = -20 dec SAR AL, 1 ; Signed-right-shift AL by 1
Now let's work out what happens. The two's complement representation of -20 is:
20 dec = 00010100 bin NOT ----------------- 11101011 bin + 1 ----------------- 11101100 bin = -20 dec
Now, let's signed-right-shift 11101100 bin:
11101100 bin becomes 11110110 bin
And what does 11110110 bin represent?
11110110 bin NOT ----------------- 00001001 bin + 1 ----------------- 00001010 bin = 8 + 2 = 10 dec, so 11110110 bin = -10 dec.
It works: -20 dec divided by two is indeed -10 dec. Note that I have used a byte in this example, but word-sized values work as well.
Now, what about SAL? SAL is actually exactly the same as SHL, because SHL works correctly for multiplying signed numbers by powers of two. Because SAL and SHL are the same, each can be used in place of the other. But it's best to use the SHL instruction for unsigned numbers and the SAL instruction for signed numbers, because it helps to make your assembler code more understandable.
Now, are there any other rotation or shift instructions? I personally manage to get by using only SHL, SHR, SAL, SAR, ROL, and ROR. But you might find the other instructions useful, so I'll very briefly list them here. Check your instruction set listing if you need more information.
The RCL (Rotate Left through Carry Flag) instruction rotates a value and takes each bit that falls out of the most-significant position and puts it into the carry flag, CF. Those bits then fall out of CF and are transferred back to the least-signficant position:
CF 7 6 5 4 3 2 1 0 +---+ +---+---+---+---+---+---+---+---+ /---| |<----| | | | | | | | |<----\ (RCL) | +---+ +---+---+---+---+---+---+---+---+ | | | \---------------------------------------------------/
The RCR (Rotate Right through Carry Flag) instruction is similar:
7 6 5 4 3 2 1 0 CF +---+---+---+---+---+---+---+---+ +---+ /---->| | | | | | | | |---->| |---\ (RCR) | +---+---+---+---+---+---+---+---+ +---+ | | | \---------------------------------------------------/
There are also a number of shift and rotate instructions that only work on the 386 and higher processors.
The hardware port output instruction is OUT. OUT takes two operands: the first specifies the hardware port number, and the second specifies the value to send to that port.
If the hardware port number is between 0 and 255 dec, you can specify the port number in "immediate mode", like this:
OUT 20h, AL ; Send the contents of AL to port 20h
(Please note that executing some of these examples could cause strange results!)
But if the port number is above 255 dec, you must put the port number in DX:
MOV DX, 2000h ; DX = 2000h OUT DX, AL ; Send the contents of AL to port ; 2000h
(Of course, values between 0 and 255 dec can be put in DX too.)
OUT is equally picky about its second operand: you can specify either AL or AX. Recall that hardware ports are only 8 bits wide. If you specify AL, then the contents of AL are sent to the hardware port that you specify. If you instead use AX, then the least-significant byte, AL, is sent to the specified hardware port, and the most-significant byte, AH, is sent to the next hardware port (the specified hardware port plus one). For example:
MOV AL, 123 MOV DX, 1234 OUT DX, AL ; Send 123 dec to port 1234 dec. MOV AX, 0ABCDh OUT 50, AX ; Send CDh to port 50 dec. Then ; send ABh to port 51 dec.
To input values from hardware ports, use the IN instruction. It has two operands, which are the same as OUT's but are switched around: the first operand is the place to store the value read in, and the second operand is the hardware port number.
The first operand must be either AL or AX. If it's AL, a single byte is read from the hardware port and stored in AL. If it's AX, one byte is read from the hardware port and stored in AL, and then one byte is read from the next hardware port (the hardware port plus one), and that byte is stored in AH.
The hardware port number must be given in the same way as with the OUT instruction -- the port number must be in DX unless it is between 0 and 255 dec.
For example:
MOV DX, 0A3B7h ; DX = 0A3B7h IN AL, DX ; Input one byte from port 0A3B7h ; and store it in AL. MOV DX, 4000h ; DX = 4000h IN AX, DX ; Input one byte from port 4000h ; and store it in AL, then input ; a byte from port 4001h and store ; it in AH. IN AL, 0FFh ; AL = byte read from port 0FFh
Note that not all ports are readable. For that matter, not all ports can be written to. There are read-only and write-only ports, as well as read-and-write ports... and, of course, there are many unused ports.
That's basically all there is to using hardware ports in assembler. Let's try using these instructions in an actual program.
There are three hardware ports related to the printer:
OUTPUT DATA 378 hex Write only INPUT STATUS 379 hex Read only OUTPUT CONTROL 37A hex Read and write
Note: Some very old, early 80's PC's had a video card called the MDPA (Monochrome Display and Printer Adapter) -- it was for users who couldn't afford the CGA graphics adapter. On this card there are different port assignments: Output Data is at 3BC hex, Input Status is at 3BD hex, and Output Control is at 3BE hex.
7 6 5 4 3 2 1 0 +-----+-----+-----+-----+-----+-----+-----+-----+ OUTPUT CONTROL | XXX | XXX | XXX | INT | SEL | INI | AUF | STR | Port 37Ah +-----+-----+-----+-----+-----+-----+-----+-----+ Bit 0: Strobe: 0 = Normal setting Set to 1 and then 0 to output data to the printer Bit 1: Auto Feed: 0 = Does not send a line-feed character (ASCII 10 dec) after carriage returns (ASCII 13 dec); this is the default 1 = Sends a line-feed character after each CR (double-spacing) Bit 2: Init: 1 = Normal setting Set to 0 and then 1 to initialize printer Bit 3: Select: 1 = Printer accepts characters sent to it 0 = Printer ignores data sent to it? Bit 4: Enable Int: 0 = Printer interrupts disabled (default) 1 = IRQ7 interrupt enabled on printer ack. pulse Bits 5..7: Unused 7 6 5 4 3 2 1 0 +-----+-----+-----+-----+-----+-----+-----+-----+ INPUT STATUS | BZY | ACK | PAP | ONL | ERR | XXX | XXX | XXX | Port 379h +-----+-----+-----+-----+-----+-----+-----+-----+ Bits 0..2: Unused Bit 3: Error: 0 = Printer error 1 = Printer normal Bit 4: On-Line: 0 = Printer is not on-line 1 = Printer is on-line Bit 5: Paper: 0 = Printer has paper 1 = Printer is out of paper Bit 6: Acknowledge: 0 = Acknowledge pulse 1 = Normal input Bit 7: Busy: 0 = Printer is busy (stop sending data) 1 = Printer is ready to accept more data
First, we must initialize the printer. We do this by "strobing" bit 2 of the Output Control port. Bit 2 is normally set to 1. To strobe this bit, we set the bit to 0 and then back to 1. We can send one byte, containing a zero in that position, and then we can send a second byte, containing a one in that position.
We can read the Input Status port to get information on the status of the printer. If there is any error at all, bit 3 is set to 0. If we want to find the cause of the printer error, we can can test each bit corresponding to the different possible conditions. For example, if we wanted to see if the printer is out of paper, we can test bit 5.
To get the printer to print characters, we send each character, one at a time, to the Output Data port. After each character is written to this port, we must strobe bit 0 in of the Output Control port. Bit 0 is normally set to 0, so to strobe this bit, we write a 1 and then a 0.
Here's a short example program demonstrating the use of the IN and OUT instructions with the printer's hardware ports:
------- TEST12.ASM begins -------
%TITLE "Assembler Test Program 12 -- Printer control using IN and OUT" IDEAL MODEL small STACK 256 LOCALS DATASEG PrinterPort_OutputData DW 0378h PrinterPort_InputStatus DW 0379h PrinterPort_OutputControl DW 037Ah CR DB 13 ; Carriage return: ASCII 13 dec FormFeed DB 12 ; Form feed: ASCII 12 dec PrinterStatus DB ? CODESEG Start: ; Make variables in the data segment addressable: MOV AX, @data MOV DS, AX CALL InitializePrinter ; Get the printer status: MOV DX, [PrinterPort_InputStatus] IN AL, DX MOV [PrinterStatus], AL ; If you wish, you could test the bits in the PrinterStatus variable ; here, to check for errors... ; (put your code here) XOR AX, AX ; AX = 0 ; Write a character to the printer: MOV AL, 'S' PUSH AX CALL SendCharacterToPrinter MOV AL, 'a' PUSH AX CALL SendCharacterToPrinter MOV AL, 'l' PUSH AX CALL SendCharacterToPrinter MOV AL, 'u' PUSH AX CALL SendCharacterToPrinter MOV AL, 't' PUSH AX CALL SendCharacterToPrinter MOV AL, 'o' PUSH AX CALL SendCharacterToPrinter MOV AL, 'n' PUSH AX CALL SendCharacterToPrinter MOV AL, '!' PUSH AX CALL SendCharacterToPrinter MOV AL, [CR] PUSH AX CALL SendCharacterToPrinter ; Comment out the following if you don't want to eject the page. (If ; you have a laser printer, it may not start printing until the form ; feed character is received). MOV AL, [FormFeed] PUSH AX ; Formfeed char. will eject the page CALL SendCharacterToPrinter ; Terminate the program: EndProgram: MOV AX, 04C00h INT 21h ; ------------------------------------------------------------------------ ; InitializePrinter ; ------------------------------------------------------------------------ ; Desc: Initializes the printer. ; Pre: None. ; Post: None. Registers and flags are not affected. ; ------------------------------------------------------------------------ PROC InitializePrinter PUSH AX ; Save affected registers and flags PUSH DX PUSHF ; First, get the current output control byte: MOV DX, [PrinterPort_OutputControl] IN AL, DX ; Now, turn off bit 2, as a "pulse" to initialize the printer, and ; ensure that bit 3, "Select", is turned on so that the printer ; will accept data: AND AL, 0FBh ; AL = AL AND 11111011 hex OR AL, 08h ; AL = AL OR 00001000 hex ; Send back this altered byte: OUT DX, AL ; Complete the pulse or strobe by sending back the same byte, but with ; bit 2 set to on again: OR AL, 04h ; AL = AL OR 00000100 hex OUT DX, AL POPF ; Restore registers and flags POP DX POP AX RET ENDP InitializePrinter ; ------------------------------------------------------------------------ ; ------------------------------------------------------------------------ ; SendCharacterToPrinter ; ------------------------------------------------------------------------ ; Desc: Sends a character to the printer using hardware ports. ; Pre: Ensure the printer has been initialized. ; Post: The character is printed. No registers or flags are affected. ; ------------------------------------------------------------------------ PROC SendCharacterToPrinter ARG @@CharToPrint:BYTE = @@ArgBytesUsed PUSH BP ; Save BP MOV BP, SP ; Let arguments, locals be reached PUSH AX ; Save affected flags and registers PUSH DX PUSHF ; Send the character to the Output Data port: MOV DX, [PrinterPort_OutputData] MOV AL, [@@CharToPrint] OUT DX, AL ; Now, perform the "strobing" in the output control byte, to send the ; character to the printer: ; First, get the current output control byte: MOV DX, [PrinterPort_OutputControl] IN AL, DX ; Set bit 1: OR AL, 01h ; AL = AL OR 00000001 hex ; Send back this altered byte: OUT DX, AL ; Complete the pulse or strobe by sending back the same byte, but with ; bit 1 cleared again: AND AL, 0FEh ; AL = AL AND 11111110 hex OUT DX, AL POPF ; Restore registers and flags POP DX POP AX POP BP RET @@ArgBytesUsed ENDP ; ------------------------------------------------------------------------ END Start
------- TEST12.ASM ends -------
If you're wondering, saluton means hello in Esperanto, the international language.
Do you want more practice with loops and procedures? You might want to add a procedure to the above program to print a string to the printer. You would need to pass the address of the string to the procedure, and you would print out characters until you reached a terminating character, such as "$" (just like the string print routine INT 21 Service 9) or ASCII 0 (the null-terminator in C/C++).
In the following assembler chapters, we'll look at string instructions, macros, equates, and how to interface assembler code with C and C++. And have you found these example programs rather boring? Starting in the next assembler chapter, we'll start seeing some actual game-programming-related example programs -- we'll find out how to draw Mode 13h graphics using assembler, for example. (If you're adventurous, you might even want to give this problem some thought and give it a try. Here's some important hints... Remember how to switch to Mode 13h? Yes, INT 10h Service 0 certainly works in assembler! And the video memory where the Mode 13h pixels are stored begins at A000:0000. You can set up some of the registers to point to this area, and you can read and write bytes using MOV and certain addressing modes. To calculate addresses for pixels, you can either use MUL/IMUL to do the multiplication by 320 dec, or you can use the bit shifting tricks.)
Willen, David C., and Jeffrey I. Krantz. "8088 Assembler Language Programming: The IBM PC, Second Edition". USA: Howard W. Sams & Co., 1983-84. ISBN: 0-672-22400-3.
(Page numbers unknown.)