\ Lesson 7 - Code Words and DOS I/O \ The Forth Course \ by Richard E. Haskell \ Dept. of Computer Science and Engineering \ Oakland University, Rochester, MI 48309 comment: Lesson 7 CODE WORDS AND DOS I/O 7.1 CODE WORDS 7-2 7.2 CODE CONDITIONALS 7-5 7.3 LONG MEMORY WORDS 7-6 7.4 DOS WORDS 7-7 7.5 BASIC FILE I/O 7-9 7.6 READING NUMBERS AND STRINGS 7-14 7.7 WRITING NUMBERS AND STRINGS 7-20 7.1 CODE WORDS Assembly language instructions can be used to define Forth words when the maximum speed of execution is needed or when direct access to the computers hardware is required. This is accomplished by using the CODE word to define a Forth word. The general form of the CODE word is as follows: CODE <name> <assembly commands> <return command> END-CODE The word CODE takes the place of the colon in a colon definition and builds a header for the name of the Forth word <name>. The word END-CODE takes the place of the semi-colon and ends the code word definition. The <assembly commands> can be written in either POSTFIX or PREFIX notation. We recommend PREFIX which makes the assembly language look very much like standard 8086/8088 assembly language. The Forth word PREFIX needs to be executed before the CODE word is compiled. The <return command> can be any of the following: NEXT JMP >NEXT ( jumps to the inner interpreter >NEXT ) 1PUSH PUSH AX JMP >NEXT ( pushes ax on the stack and jumps to >NEXT ) 2PUSH PUSH DX PUSH AX ( pushes dx and ax on the stack JMP >NEXT and then jumps to >NEXT ) Debugging CODE words is made easier using the 8088 Tutor monitor that is included with this Forth Course. A complete description of how to use the Tutor monitor in the process of learning 8088/8086 assembly language is given in the book "IBM PC - 8088 Assembly Language Programming" by Richard E. Haskell. Instructions for ordering the book are given when you run the program. As an example of using the Tutor monitor to disassemle and single step through a CODE word consider the following definition of the Forth word CMOVE that moves a string of <count> bytes from the address <source> to the address <dest>. CODE CMOVE ( source dest count -- ) CLD \ move up in memory MOV BX, SI \ save SI (IP) MOV AX, DS \ copy DS for setting ES POP CX \ cx = count POP DI \ di = destination address POP SI \ si = source address PUSH ES \ save es MOV ES, AX \ point es to code segment REPNZ \ repeat until count is zero MOVSB \ copy DS:SI to ES:DI MOV SI, BX \ restore si POP ES \ restore es NEST \ done, jmp to >NEXT END-CODE When you FLOAD this lesson the following Forth code will store the hex values 11 22 33 44 55 at the offset address "source.addr" in the code segment. The actual value of the code segment is given by the Forth word ?CS: and will be printed on the screen when you type the word "show.addrs". A five byte space is reserved at the offset address "dest.addr". The offset addresses for "source.addr", "dest.addr", the top of the stack, and the CFA of CMOVE will also be printed on the screen when you type "show.addrs". comment; HEX CREATE source.addr 11 C, 22 C, 33 C, 44 C, 55 C, CREATE dest.addr 5 ALLOT 5 CONSTANT #bytes : test ( -- ) source.addr dest.addr #bytes CMOVE ; : show.addrs ( -- ) HEX CR ." code segment = " ?cs: u. CR ." source addr = " source.addr u. CR ." dest addr = " dest.addr u. CR ." top of stack = " SP0 @ U. CR ." address of CMOVE = " [ ' CMOVE ] LITERAL U. CR DECIMAL ; comment: The words [, ] and LITERAL will be discussed in Lesson 9. Assume the values printed when you type "show.addrs" are the following: code segment = E74 source addr = 6929 dest addr = 6931 top of stack = FFE2 address of CMOVE = 41C Your values may be different. If they are, use your corresponding values in the following exercise. Type debug test. Type test. Step through the first three word which will put the following values on the stack: 6929 6931 5 Press F to go to Forth. Type SYS TUTOR - This will execute the TUTOR program From the TUTOR memory display Type >SE74 to display the code segment. Type /GSE74 to display the data segment = code segment. Type /GO6929 to display the "source addr" in the data segment. Note that 11 22 33 44 55 is displayed. Type /RSSE74 to make the stack segment the same as the code segment. Type /RPSFEDC to set the stack pointer equal to the top of stack (FFE2) minus 6. Type >O41C to go to the start of the CMOVE code. Single step through this program by pressing key F1. Note that when you get to the REP instruction, pressing key F1 five times will move the five bytes from "source.addr" to "dest.addr". To exit TUTOR, type /QD. This should take you to DOS. If you had not changed the stack (which you had to do to get to the values that the Forth program "test" had put on the stack) then typing /QD from TUTOR will take you back to Forth where you had typed "sys tutor". The Forth word CMOVE> ( source dest count -- ) is similar to CMOVE except that the bytes are moved in the opposite direction. That is, the highest address byte is moved first. It is necessary to use this word if you are moving a string up in memory where the destination string may overlap the source string. The use of CMOVE will cause the overlapped portion of the source string to be destroyed before it has a chance to be moved. 7.2 CODE CONDITIONALS When using the Forth assembler jump instructions are achieved by using the Forth words IF...ELSE...THEN, BEGIN...WHILE...REPEAT, and BEGIN...UNTIL together with the following code conditionals: Forth Assembled Code 0= JNE/JNZ 0<> JE/JZ 0< JNS 0>= JS < JNL/JGE >= JL/JNGE <= JNLE/JG > JLE/JNG U< JNB/JAE/JNC U>= JB/JNAE/JC U<= JNBE/JA U> JBE/JNA OV JNO As an example, consider the definition of the Forth word ?DUP which duplicates the value on top of the stack only if the value is non-zero. CODE ?DUP ( n -- n n | 0 ) POP AX CMP AX, # 0 0<> IF PUSH AX THEN 1PUSH END-CODE Note that when this definition gets assembled into machine code the statement 0<> is assembled as JE to the instruction following THEN. 7.3 LONG MEMORY WORDS The following long memory words are useful for accessing data in segments other than the code segment. CODE @L ( seg off -- n ) \ Fetch 16-bit value from seg:off POP BX \ BX = offset address POP DS \ DS = segment address MOV AX, 0[BX] \ AX = data at DS:BX MOV BX, CS \ Restore DS to CS value MOV DS, BX 1PUSH \ push value on stack END-CODE CODE !L ( n seg off -- ) \ Store 16-bit value at seg:off POP BX \ BX = offset address POP DS \ DS = segment address POP AX \ AX = n MOV 0[BX],AX \ Store n at DS:BX MOV BX, CS \ Restore DS to CS value MOV DS, BX NEXT END-CODE The following are other useful long memory words: C@L ( seg off -- byte ) \ Fetch 8-bit byte from seg:off C!L ( byte seg off -- ) \ Store 8-bit byte at seg:off CMOVEL ( sseg soff dseg doff count ) \ move a block of count bytes from sseg:soff to dseg:doff CMOVEL> ( sseg soff dseg doff count ) \ move a block of count bytes from sseg:soff to dseg:doff \ moves last byte first to avoid overwriting moved data 7.4 DOS WORDS F-PC has a large number of Forth words for handling DOS file I/O. These words are defined in the source files HANDLES.SEQ and SEQREAD.SEQ. In this and the next section we will develop a set of file I/O words that you can use and extend to handle a variety of file I/O and other DOS operations. These words can be used in place of, or in conjunction with, the F-PC DOS and file I/O words. comment; VARIABLE ITEMS \ used to record stack depth VARIABLE handl \ file handle VARIABLE eof \ TRUE if end-of-file was read CREATE fname 80 ALLOT \ 80 byte buffer containing ASCII filename : { ( -- ) DEPTH ITEMS ! ; : } ( -- c ) DEPTH ITEMS @ - ; comment: { . . . } Used to keep track of the number of elements put on the stack. For example, { 5 2 8 } will leave the following values on the top of the stack: 5 2 8 3 The 3 on top of the stack is the number of items entered between { and }. comment; : $>asciiz ( addr1 -- addr2 ) \ change counted string to ASCIIZ string DUP C@ SWAP 1+ TUCK + 0 SWAP C! ; \ DOS 2.0+ disk I/O functions comment: ---------------------------------------------------------- 2fdos calls the DOS INT 21H function with ax=ah:al, bx, cx and dx on the stack. It returns ax, dx and an error flag on the stack. If the error flag is TRUE, the error code is in ax (3rd element on the stack). If the error flag is FALSE, then ax and dx will have values that depend on the function call. fdos is similar to 2fdos, but does not return an error flag. It should be used for DOS INT 21H calls that do not use the carry flag to indicate an error. ******************************************************************* comment; PREFIX HEX CODE 2fdos ( ax bx cx dx -- ax dx f ) POP DX POP CX POP BX POP AX INT 21 \ DOS function call U>= IF \ if carry = 0 MOV BX, # FALSE \ set error flag to false ELSE \ else MOV BX, # TRUE \ set error flag to true THEN PUSH AX PUSH DX PUSH BX NEXT END-CODE CODE fdos ( ax bx cx dx -- ax dx ) POP DX POP CX POP BX POP AX INT 21 \ DOS function call PUSH AX PUSH DX NEXT END-CODE DECIMAL comment: 7.5 BASIC FILE I/O The following words can be used for basic file I/O operations such as opening, creating, closing and deleting files, as well as reading and writing bytes from and to the disk file. ----------------------------------------------------- open.file ( addr -- handle ff | error.code tf ) Opens a file. Returns handle under a false flag or returns error code under a true flag. addr points to an asciiz string. Access code is set to 2 to open for reading and writing. comment; HEX : open.file ( addr -- handle ff | error.code tf ) 3D02 \ ah = 3D; al = access.code=2 0 ROT 0 SWAP \ 3D02 0 0 addr 2fdos \ DOS function call NIP ; \ nip dx comment: ----------------------------------------------------- close.file Closes file whose handle is on the stack. Prints error message if unable to close. comment; : close.file ( handle -- ) 3E00 \ ah = 3E SWAP 0 0 \ bx = handle 2fdos NIP \ nip dx IF ." Close error number " . ABORT THEN DROP ; comment: ----------------------------------------------------- create.file Creates file -- returns values as in open.file addr points to an asciiz string attr is the file attribute: 0 - normal file 01H - read only 02H - hidden 04H - system 08H - volume label 10H - subdirectory 20H - archive comment; : create.file ( addr attr -- handle ff | error.code tf ) 3C00 \ ah = 3C 0 2SWAP SWAP \ 3C00 0 attr addr 2fdos NIP ; \ nip dx comment: ------------------------------------------------------ open/create Opens a file if it exists, otherwise creates a new normal file. "addr" points to an asciiz string. Returns a handle for the opened file. Prints error messages if unable to open. comment; : open/create ( addr -- handle ) DUP open.file IF DUP 2 = IF DROP 0 create.file IF ." Create error no. " . ABORT THEN ELSE ." Open error no. " . DROP ABORT THEN ELSE NIP THEN ; : delete.file ( addr -- ax ff | error.code tf ) 4100 0 ROT 0 SWAP 2fdos NIP ; : erase.file ( $addr -- ) \ erase file with counted string at $addr $>asciiz delete.file IF CR ." Delete file error no. " . ELSE DROP THEN ; comment: ----------------------------------------------------- read.file Reads '#bytes' bytes from file with 'handle' into buffer at 'buff.addr'. Returns #bytes actually read. If this value is 0 then the end of file was read. Prints error message if unsuccessful. comment; : read.file ( handle #bytes buff.addr -- #bytes ) >R 3F00 \ handle #bytes 3F00 -ROT R> \ 3F00 handle #bytes addr 2fdos NIP \ nip dx IF ." Read error no. " . ABORT THEN ; comment: ----------------------------------------------------- write.file Writes '#bytes' bytes from buffer at 'buff.addr' to file with 'handle'. Prints error message if unsuccessful. comment; : write.file ( handle #bytes buff.addr -- ) >R 4000 \ handle #bytes 4000 -ROT R> \ 4000 handle #bytes addr 2fdos NIP \ nip dx IF ." Write error no. " . ABORT ELSE DROP THEN ; comment: ------------------------------------------------------- mov.ptr Moves the file pointer of the file with 'handle'. doffset is a double number (32-bit) offset code is the method code: 0 - move pointer to start of file + offset 1 - increase pointer by offset 2 - move pointer to end of file + offset comment; : mov.ptr ( handle doffset code -- dptr ) 42 FLIP + \ hndl offL offH 42cd ROT >R \ hndl offH 42cd -ROT R> \ 42cd hndl offH offL 2fdos IF DROP ." Move pointer error no. " . ABORT THEN ; comment: ------------------------------------------------------- rewind.file Moves the pointer of file with 'handle' to the start of file. comment; : rewind.file ( handle -- ) 0 0 0 mov.ptr 2DROP ; comment: ------------------------------------------------------- get.length Returns the 32-bit length of the file with 'handle'. comment; : get.length ( handle -- dlength ) 0 0 2 mov.ptr ; comment: ------------------------------------------------------- read.file.L Reads the next "#bytes" bytes from the opened file with handle "handle" and stores these bytes in extended memory at seg:offset. comment; CODE read.file.L ( handle #bytes seg offset -- ax f ) POP DX POP DS POP CX POP BX MOV AH, # 3F INT 21 U>= IF MOV BX, # FALSE ELSE MOV BX, # TRUE THEN MOV CX, CS \ restore DS MOV DS, CX PUSH AX PUSH BX NEXT END-CODE comment: ------------------------------------------------------- write.file.L Writes "#bytes" bytes from extended memory at seg:offset to the opened file with handle "handle". comment; CODE write.file.L ( handle #bytes seg offset -- ax f ) POP DX POP DS POP CX POP BX MOV AH, # 40 INT 21 U>= IF MOV BX, # FALSE ELSE MOV BX, # TRUE THEN MOV CX, CS \ restore DS MOV DS, CX PUSH AX PUSH BX NEXT END-CODE comment: ------------------------------------------------------- findfirst.dir Search the directory for the first match of the file specified by the asciiz string at "addr". comment; CODE findfirst.dir ( addr -- f ) \ search directory for first match POP DX \ dx = addr of asciiz string PUSH DS \ save ds MOV AX, CS MOV DS, AX \ ds = cs MOV CX, # 10 \ attr includes subdirectories MOV AX, # 4E00 \ ah = 4E INT 21 \ DOS function call JC 1 $ \ if no error MOV AX, # FF \ flag = TRUE JMP 2 $ \ else 1 $: MOV AX, # 0 \ flag = FALSE 2 $: POP DS \ restore ds PUSH AX \ push flag on stack NEXT END-CODE comment: ------------------------------------------------------- findnext.dir Search the directory for the next match of the file specified by the asciiz string at "addr". comment; CODE findnext.dir ( -- f ) \ search directory for next match PUSH DS \ save ds MOV AX, CS MOV DS, AX \ ds = cs MOV AX, # 4F00 \ ah = 4F INT 21 \ DOS function call JC 1 $ \ if no error MOV AX, # FF \ flag = TRUE JMP 2 $ \ else 1 $: MOV AX, # 0 \ flag = FALSE 2 $: POP DS \ restore ds PUSH AX \ push flag on stack NEXT END-CODE comment: ------------------------------------------------------- set-dta.dir Set the disk transfer area address. comment; CODE set-dta.dir ( addr -- ) \ set disk transfer area address POP DX \ dx = dta address PUSH DS \ save ds MOV AX, CS MOV DS, AX \ ds = cs MOV AX, # 1A00 \ ah = 1A INT 21 \ DOS function call POP DS \ restore ds NEXT END-CODE DECIMAL comment: 7.6 READING NUMBERS AND STRINGS The following words can be used to read bytes, numbers and strings from a disk file. ------------------------------------------------------ get.fn enter a filename from the keyboard and store it as an asciiz string in fname. comment; : get.fn ( -- ) QUERY BL WORD \ addr DUP C@ 1+ \ addr cnt+1 2DUP + \ addr len addr.end 0 SWAP C! \ make asciiz string SWAP 1+ SWAP \ addr+1 len fname SWAP \ from to len CMOVE ; comment: ------------------------------------------------------ open.filename Enter a filename, open it, and store its handle in the variable 'handl'. comment; : open.filename ( -- ) get.fn fname open/create handl ! ; comment: ------------------------------------------------------ eof? If an end-of-file was read (eof = true) then exit word containing eof?. comment; : eof? ( -- ) eof @ IF 2R> 2DROP EXIT THEN ; comment: ------------------------------------------------------- get.next.byte Get the next byte from the disk file whose handle is in 'handl'. Sets eof variable to true if eof. comment; : get.next.byte ( -- byte ) handl @ 1 PAD read.file IF FALSE eof ! PAD C@ ELSE TRUE eof ! THEN ; comment: ------------------------------------------------------- get.next.val Read the next 16-bit value (2 bytes) from the disk file whose handle is in 'handl'. Sets eof variable to true if eof. Useful if actual numbers, rather than ASCII data, is stored on the disk file. comment; : get.next.val ( -- n ) handl @ 2 PAD read.file IF FALSE eof ! PAD @ ELSE TRUE eof ! THEN ; comment: ------------------------------------------------------- get.next.dval Read the next 32-bit value (4 bytes) from the disk file whose handle is in 'handl'. Sets eof variable to true if eof. Useful if actual numbers, rather than ASCII data, is stored on the disk file. comment; : get.next.dval ( -- d ) handl @ 4 PAD read.file IF FALSE eof ! PAD 2@ ELSE TRUE eof ! THEN ; comment: ------------------------------------------------------- parenchk If the byte on the stack is a '(' read the file until the byte following the next ')' is read. Exits if eof is read. comment; : parenchk ( byte -- byte ) DUP ASCII ( = IF DROP BEGIN get.next.byte eof? ASCII ) = UNTIL get.next.byte eof? THEN ; comment: ------------------------------------------------------- quotechk If the byte on the stack is a quote (") read the file until the byte following the next quote (") is read. Exits if eof is read. comment; : quotechk ( byte -- byte ) DUP ASCII " = IF DROP BEGIN get.next.byte eof? ASCII " = UNTIL get.next.byte eof? THEN ; comment: ------------------------------------------------ ?digit Checks to see if the byte on the stack is the ASCII code of a valid digit in the current base. comment; : ?digit ( byte -- byte f ) DUP BASE @ DIGIT NIP ; comment: ------------------------------------------------ get.next.digit Gets the next valid ASCII digit from the disk file. Exits if eof is read. comment; : get.next.digit ( -- digit ) BEGIN get.next.byte eof? parenchk eof? quotechk eof? ?digit NOT WHILE DROP REPEAT ; comment: ------------------------------------------------ get.digit/minus Gets the next valid ASCII digit or a minus sign from the disk file. Exits if eof is read. comment; : get.digit/minus ( -- digit or - ) BEGIN get.next.byte eof? parenchk eof? quotechk eof? DUP ASCII - = SWAP ?digit ROT OR NOT WHILE DROP REPEAT ; comment: --------------------------------------------------- get.next.number gets the next signed integer stored as an ASCII string on the disk and converts it to a signed 16-bit integer. exits if eof is read. comment; : get.next.number ( -- n ) { get.digit/minus eof? \ uses { } to store BEGIN \ consecutive digits get.next.byte eof? \ on the stack. parenchk eof? \ ignore (...) quotechk eof? \ and "..." ?digit NOT UNTIL DROP } DUP PAD C! DUP PAD + BL OVER 1+ C! SWAP 0 DO \ move digits on stack SWAP OVER C! 1- \ to counted string as PAD LOOP NUMBER DROP ; \ convert to number comment: ---------------------------------------------------- ?period Checks to see if a byte is a period. Note that the flag is left as the second element on the stack. comment; : ?period ( byte -- f byte ) DUP ASCII . = SWAP ; comment: ---------------------------------------------------- get.next.dnumber Gets the next signed real number stored as an ASCII string on the disk and converts it to a signed double number on the stack. The number of digits after the decimal point is stored in the variable DPL. Exits if eof is read. comment; : get.next.dnumber ( -- dn ) { get.digit/minus eof? BEGIN get.next.byte eof? parenchk eof? \ similar to quotechk eof? \ get.next.number ?period \ but include period ?digit ROT OR NOT \ in number string UNTIL DROP } DUP PAD C! DUP PAD + BL OVER 1+ C! SWAP 0 DO SWAP OVER C! 1- LOOP NUMBER ; \ convert to double number comment: ---------------------------------------------------- get.next.string Reads the next string enclosed between double quotes "....." in the disk file and stores it as a counted string at "addr". comment; : get.next.string ( -- addr ) \ counted string BEGIN get.next.byte eof? ASCII " = UNTIL 0 PAD 1+ BEGIN \ cnt addr get.next.byte eof? DUP ASCII " <> WHILE OVER C! SWAP 1+ SWAP 1+ REPEAT 2DROP PAD C! PAD ; comment: 7.7 WRITING NUMBERS AND STRINGS --------------------------------------------------- send.byte Sends a byte to the opened disk file whose handle is in 'handl'. comment; : send.byte ( byte -- ) PAD C! handl @ 1 PAD write.file ; comment: --------------------------------------------------- send.number Sends a signed 16-bit number as an ASCII string to the opened disk file whose handle is in 'handl'. comment; : send.number ( n -- ) (.) 0 DO DUP C@ send.byte 1+ LOOP DROP ; comment: --------------------------------------------------- send.number.r Sends a signed 16-bit number as an ASCII string to the opened disk file whose handle is in 'handl'. The number will be right-justified in a field of width "len", padded with leading ascii blanks. comment; : send.number.r ( n l -- ) >R (.) R> OVER - 0 DO BL send.byte LOOP 0 DO DUP C@ send.byte 1+ LOOP DROP ; comment: --------------------------------------------------- send.dnumber Sends a signed 32-bit number as an ASCII string to the opened disk file whose handle is in 'handl'. The decimal point is positioned according to the contents of DPL. comment; : send.dnumber ( d -- ) \ DPL = #digits after dec. point TUCK DABS <# DPL @ ?DUP IF 0 DO # LOOP ASCII . HOLD THEN #S ROT SIGN #> 0 DO DUP C@ send.byte 1+ LOOP DROP ; : send.val ( n -- ) \ send 16-bit value PAD ! handl @ 2 PAD write.file ; : send.dval ( d -- ) \ send 32-bit value PAD 2! handl @ 4 PAD write.file ; : send.string ( addr -- ) \ addr of counted string DUP C@ SWAP 1+ SWAP 0 DO DUP I + C@ send.byte LOOP DROP ; : send.crlf ( -- ) 13 send.byte 10 send.byte ; : send.lf ( -- ) 10 send.byte ; : send.cr ( -- ) 13 send.byte ; : send.tab ( -- ) 9 send.byte ; : send.( ( -- ) ASCII ( send.byte ; : send.) ( -- ) ASCII ) send.byte ; : send., ( -- ) ASCII , send.byte ; : send." ( -- ) ASCII " send.byte ; : send."string" ( addr -- ) send." send.string send." ;