Using Assembly Language Routines in MS-FORTRAN Programs

By Mark Dahmke
BYTE Magazine, October, 1986

Introduction

It is often necessary, in the course of developing a large program, to call assembly language routines to make a critical task efficient or to gain access to features that are machine-specific or not implemented well in the high level language.
This article presents some mechanisms for dealing with the problem of passing parameters and managing the programming environment when working with Microsoft FORTRAN and 8086-family assembler language. All assembler examples are written in Microsoft Assembler, and should work correctly with any version of the assembler.

The Fortran Environment

Microsoft FORTRAN, PASCAL and C share many common features in terms of their environment, parameter passing and data structures. Many of the examples shown here will work well with all these languages, but the emphasis in this article will be MS-FORTRAN.

The MS-FORTRAN runtime environment is shown in figure 1. Depending on which version of FORTRAN you are using, there may be some minor differences. FORTRAN begins by allocating memory at the first available low address, and places the code segment (all user generated code and library routines) there. All segments that are part of a group must reside in the same 64K block of memory, due to the 8086’s segmentation scheme. For example, all FORTRAN subroutines compiled together (from one source file) will be treated as a group. Thus, you could create a program with 256K of code by compiling the main program and subroutines in four pieces, then linking them together with LINK.
Similarly, blocks of constants and variables are put in groups that may not exceed 64K each. The most frequently used is the DGROUP, which includes all constants, variables, unnamed COMMON blocks and strings, but does not include named COMMON blocks. If a program becomes so large that the data space exceeds 64K, you must move variables and arrays into common blocks. The disadvantage of this is that common blocks are less efficient to address. The DGROUP also contains stack space and heap space used by FORTRAN.
Due to the 8086 family’s segmented address space scheme, you must establish addressability before reading or writing memory, or you will likely cause your system to crash. Establishing addressability means loading one of the segment registers with a 16-bit pointer to the base of the block of memory you wish to have access to. Figure 2 shows how this works. The CS, DS, ES and SS registers establish addressability for code, data and stack segments. The ES register is for an “Extra Segment” that allows you to copy data from one segment to another. The segment registers hold the most significant 16-bits of a 20-bit address. This is added to the “offset” address specified in the instruction. For example, the CS and IP (Instruction Pointer) pair of registers always point to the current instruction. Addresses of this form are denoted as CS:IP or segment plus offset.
Parameters passed to subroutines must include the segment portion of the address if the constant or variable is not considered local to the subroutine. Since it is almost impossible to tell in advance whether the DS or ES register will be correctly loaded before calling the routine, FORTRAN always passes the segment and offset to a subroutine.

Passing Parameters

Two cases must be dealt with in FORTRAN: Functions and Subroutines. In both cases FORTRAN uses the call by reference method for passing parameters, even for constants and expressions. This makes it easier to write assembly programs since the values being passed are always addressed in the segment plus offset format.
For both functions and subroutines, the parameters are placed on the stack in the same order they appear in the parameter list of the calling program. Figure 3 shows the stack before a call to a sample function. In the case of functions, results are returned in registers if the result is defined as an integer or real number, single precision. If the result is 16 bits long or less, it is assumed to be in the AX register upon return. If the result is declared INTEGER*4 or REAL*4, it must be in the AX, DX pair. If a number is double precision (REAL*8), the result must be returned in a dummy variable created by the calling program. The offset address of this variable is in the stack at location BP+6.

Subroutines

All variables passed to subroutines, and all results returned from subroutines are passed as parameters in the call list. Figure 4 shows how the stack is loaded prior to calling the sample subroutine shown.

Entry and Exit Requirements

As shown earlier, the addresses must be obtained from the stack and placed in segment and offset registers to gain access to the value of the variable. This can become confusing, especially if the stack contains a large number of parameters. All addressing is done on the basis of the BP register. To simplify the process of computing offsets, I wrote the macros shown in figure 5. SETFRAME performs the housekeeping tasks needed to gain access to the stack. The frame pointer must be saved so it is in the same location when returning to the calling program. Also, the Stack Pointer (SP) must be loaded into the BP so that the offset address to the parameters will be correct.
The GETPARM macro solves the problem of getting an address and putting it in the ES:BX register pair. The first argument of GETPARM is the number (counting from the left) of the parameter you wish to retrieve. The second argument specifies the total number of parameters in the call list. For example, if the following call were made from a program:
CALL SUBR(A,B,C)
and you wanted to retrieve the address of the variable C, you would code:
GETPARM 3,3
which would return the address of the third of three parameters. Figure 6 shows the instructions generated for all three parameters in the call list of this sample.
The third macro shown in figure 5 is for housekeeping at the end of a subroutine. POPRET will make sure the Stack Pointer (SP) and Base Pointer (BP) registers are returned to their starting values, and that the correct number of bytes are popped off the stack before returning. This is necessary because the calling program assumes that the stack is the same as it was before setting up for the subroutine call. The only argument given to POPRET is the number of parameters in the call list. In this example, four parameters were passed, so we would code:
POPRET 4
After 16 bytes are popped off the stack, a far return instruction is executed, which passes control back to the caller.

Examples
Four sample subroutines are shown in figure 7. The first two called SRCHF and SRCHN allow a calling FORTRAN program to scan the current directory for filenames. These subroutines use the old DOS function calls that use FCBs (File Control Blocks), instead of the newer DOS functions that use file handles.
SRCHF and SRCHN are almost identical, except that SRCHF locates the first matching directory entry, and SRCHN finds subsequent matching entries. SRCHF is called just once, while SRCHN can be called repeatedly to get subsequent file names that may or may not be identical to the first one returned.
Because these two routines are nearly identical, only SRCHF will be described in detail. The calling FORTRAN routine must load a string with the filename to be matched. This name may include the question mark (?) character as a wildcard, but not an asterisk (*). While the asterisk is a valid wildcard in many DOS commands, this low-level function call doesn’t recognize it. Normally the asterisk is parsed by the calling program and converted into question marks.
The calling program must pass two other parameters — the drive code and an empty string in which the requested filename is returned. The drive code is simply an integer corresponding to the disk drive letter (1 = A:, 2 = B:, 3 = C: and 0 = default drive).

Upon receiving control, the SRCHF routine first retrieves the drive code value. The GETPARM 1,3 macro will look into the stack and find the double word pointer for the drive code, which is returned in the ES:BX register pair. The next instruction gets the value of the drive code and stores it in the AX register. The FCB template must be initialized with the drive code and filename. The next instruction copies the least significant byte of the drive code (all that really matters anyway) into the first byte of the FCB.

Next, a GETPARM 2,3 macro retrieves the pointer to the second of three parameters passed to SRCHF — the filename itself. The filename must be stored as eleven characters with no period separating the filename from the filetype. For example, the filename “SAMPLE.DAT” would be stored as “SAMPLE DAT”. A second pointer is set up to copy these eleven bytes from the FORTRAN string to the filename area of the FCB. The length is loaded into CX, and a REP MOVSB instruction performs the copy automatically.
After the FCB is loaded, DOS must be told where to find the buffer that will hold the resulting FCB. This is done through the SET DMA ADDRESS function call. Finally, a call to the SEARCH FIRST function will cause DOS to check the directory, and either return a matching filename or an error code. If a filename is found, an error code of zero will be returned, along with the filename in the same eleven byte format as described earlier. If no files were found, the subroutine returns a single question mark in the first character of the returned filename string.

The GETDIR Utility

The next subroutine shown in figure 7 is GETDIR, which will return the current directory name. Three parameters are required: an empty string to store the resulting directory name, the drive code, and an error code variable.
This routine first gets a pointer to the character string and stores the segment and offset in STRING_SEG and STRING respectively. After loading the drive code into the AX register, the string segment and offset addresses are loaded into the DS:DX pair and DOS function 47h is called. This function will store the full directory name in the string space, but will not add a leading backslash (\) or drive letter and colon to the name. Thus, if you are currently in the directory C:\MAIN\SAMPLE this subroutine will return the string “MAIN\SAMPLE”. Also, the string will be terminated with a null or zero byte. After the DOS function call, the return code is stored in the third variable and returned to the calling program.

The CHDIR Subroutine

CHDIR (Change Directory) is almost identical to GETDIR, but the input directory name can be fully qualified, including a drive code, colon and leading backslash character, just as you would enter it in the DOS CD command. The string must be terminated with a null or zero byte to be accepted though.
The GETDFS Subroutine
The GETDFS (Get Disk Free Space) routine returns information about the disk drive. Three values are returned, indicating the number of bytes per sector, sectors per cluster and the number of free clusters on a disk. With these three values, the number of free bytes can be computed easily. The values are returned to the calling program in INTEGER*2 variables, but if the three are multiplied together to get free space in bytes, it is necessary to use an INTEGER*4 variable to hold the result.
If the value returned in the fourth argument of the call list (ICLUST) is equal to -1 (0FFFFh) then an error condition has resulted.

Demonstration FORTRAN Program
Figure 8 shows a sample FORTRAN program that uses all of the above subroutines. The program performs the following functions:
1. Gets and displays the current directory
2. Prompts the user for a new directory name
3. Displays the directory contents
4. Displays the disk free space information
The program also shows how to handle error conditions for each subroutine.
Note the loop at statements 4 and 10. Loop 4 gets rid of the zero byte before the string is displayed on the console, and loop 10 scans the string to find the last non-blank character and inserts a zero byte to properly terminate the string. These functions could also be integrated into the assembler subroutines to make the FORTRAN interface simpler.

Conclusion

Many DOS functions, graphics, screen formatting and resident modules cannot be handled through native FORTRAN code. Coding these functions in assembler is not complicated and can extend the versatility of a program without forcing a complete rewrite in a different language. These sample routines can provide a useful model for creating your own extensions to FORTRAN.

Figure 6: Macro.asm

;---------------------------------------------------------------
; SETFRAME: Sets the environment upon entry to a subroutine.
;
SETFRAME MACRO
PUSH BP ;SAVE FRAMEPOINTER ON STACK
MOV BP,SP
ENDM
;
; POPRET: Restores the BP register and returns to the
; calling FORTRAN routine after cleaning up the
; stack.
;
POPRET MACRO NPARMS ;RETURN FROM SUBR. NPARMS=NUMBER OF PARMS
POP BP
RET NPARMS*4
ENDM
;
; GETPARM: returns a pointer to a parameter in the call list.
;
; Operands: X = the number of the desired parameter
; MAX = the maximum number of parameters in
; the call list.
;
; Result: The ES:BX register pair points to the parameter.
;
GETPARM MACRO X,MAX ;PARAMETER NUMBER (IE, 1,2,3)
LES BX,DWORD PTR SS:[BP+(MAX-X)*4+6]
ENDM
;
;
;--------------------------------------------------------------


Figure 7:

 

 

PAGE 66,132
;
; Utility subroutines for use with MS-FORTRAN
;
; by Mark Dahmke
; May, 1986
;
PUBLIC SRCHF, SRCHN, CHDIR, GETDIR, GETDFS
;
SUBTTL ‘MACROS’
PAGE
;
;—— MACROS ——
;
DOS MACRO
INT 21H ;REQUEST DOS SERVICE
ENDM
;
;—————————————————————
; SETFRAME: Sets the environment upon entry to a subroutine.
;
SETFRAME MACRO
PUSH BP ;SAVE FRAMEPOINTER ON STACK
MOV BP,SP
ENDM
;
; POPRET: Restores the BP register and returns to the
; calling FORTRAN routine after cleaning up the
; stack.
;
POPRET MACRO NPARMS ;RETURN FROM SUBR. NPARMS=NUMBER OF PARMS
POP BP
RET NPARMS*4
ENDM
;
; GETPARM: returns a pointer to a parameter in the call list.
;
; Operands: X = the number of the desired parameter
; MAX = the maximum number of parameters in
; the call list.
;
; Result: The ES:BX register pair points to the parameter.
;
GETPARM MACRO X,MAX ;PARAMETER NUMBER (IE, 1,2,3)
LES BX,DWORD PTR SS:[BP+(MAX-X)*4+6]
ENDM
;
;
;————————————————————–
;
SUBTTL ‘DATA SEGMENT’
PAGE
;——————————-
DATA SEGMENT PUBLIC ‘DATA’
;
SDMA DB 128 DUP(0) ;DMA BUFFER FOR SRCHF AND SRCHN ROUTINES
;
FCB DB 0
DB 8 DUP(0) ;FILE NAME (1-8)
DB 0,0,0 ;FILE TYPE (9-11)
DW 0 ;CURRENT BLOCK (12-13)
DW 0 ;LOGICAL RECORD SIZE (14-15)
DW 0,0 ;FILE SIZE (16-19)
DW 0 ;DATE (20-21)
DW 0,0,0,0,0 ;RESERVED (22-31)
DB 0 ;CURRENT RELATIVE RECORD
DW 0,0 ;RELATIVE RECORD NUMBER
DB 0
DB 0
;
IDRIVE DW 0
STRING DW 0
STRING_SEG DW 0
;
DATA ENDS
DGROUP GROUP DATA
;
SUBTTL ‘CODE SEGMENT’
PAGE
;
CODE SEGMENT ‘CODE’
ASSUME CS:CODE, DS:DGROUP, SS:DGROUP
;
;——————————-
;SEARCH FOR FIRST DIRECTORY ENTRY
;
; PARM LIST: CALL SRCHF(IDRV,FSPEC,RFNAME)
;
; INPUT: IDRV, FSPEC — 0=CURRENT, 1=A, 2=B, FSPEC= ????????.???
; OUTPUT: RFNAME — FILENAME.TYP
;
SRCHF PROC FAR ;SEARCH FOR FIRST DIR ENTRY
SETFRAME
;
GETPARM 1,3 ;GET DRIVE CODE ADDR
MOV AX,ES:[BX] ;GET VALUE
MOV IDRIVE,AX
MOV FCB,AL ;SET UP FCB
;
GETPARM 2,3 ;GET FILESPEC
;
MOV STRING,BX
MOV STRING_SEG,ES ;SAVE PTR AND SEG
;
MOV DI,OFFSET DGROUP:FCB+1
PUSH DS ;SAVE DS
PUSH DS
POP ES ;SET UP DEST
MOV SI,STRING
MOV DS,STRING_SEG
MOV CX,11
REP MOVSB ;COPY FILESPEC 11 CHARS
;
POP DS ;RESTORE DS
MOV DX,OFFSET DGROUP:SDMA
MOV AH,1AH
DOS ;SET DMA ADDRESS
;
MOV DX,OFFSET DGROUP:FCB
MOV AH,11H ;SEARCH-FIRST BDOS COMMAND
DOS
;
CMP AL,0FFH
JZ NO_FILES ;IF NO FILES, SKIP OUT.
;
GETPARM 3,3 ;GET RETURN FILENAME ADDR IN ES:BX
MOV DI,BX ;SET UP ES:DI POINTER FOR MOVE
MOV SI,OFFSET DGROUP:SDMA+1 ;POINT TO FILE NAME
PUSH DS
POP ES ;SET UP DEST SEG
MOV CX,11 ;MOVE 11 BYTES
REP MOVSB
;
JMP S_DONE
;
;
NO_FILES: ;IF NO FILES ARE PRESENT,
GETPARM 3,3
MOV BYTE PTR ES:[BX],’?’ ;PUT A ? IN FIRST CHAR
;OF OUTPUT FILE NAME
S_DONE: POPRET 3
SRCHF ENDP
;
;——————————-
;SEARCH FOR NEXT DIRECTORY ENTRY
;
; PARM LIST: CALL SRCHN(IDRV,FSPEC,RFNAME)
;
; INPUT: IDRV, FSPEC — 0=CURRENT, 1=A, 2=B, FSPEC= ????????.???
; OUTPUT: RFNAME — FILENAME.TYP
;
SRCHN PROC FAR ;SEARCH FOR NEXT DIR ENTRY
SETFRAME
;
GETPARM 1,3 ;GET DRIVE CODE ADDR
MOV AX,ES:[BX] ;GET VALUE
MOV IDRIVE,AX
MOV FCB,AL ;SET UP FCB
;
GETPARM 2,3 ;GET FILESPEC
;
MOV STRING,BX
MOV STRING_SEG,ES ;SAVE PTR AND SEG
;
PUSH DS ;SAVE SEG
PUSH DS
POP ES
MOV DI,OFFSET DGROUP:FCB+1
MOV SI,STRING
MOV DS,STRING_SEG
MOV CX,11
REP MOVSB ;COPY FILESPEC 11 CHARS
;
POP DS
MOV DX,OFFSET DGROUP:SDMA
MOV AH,1AH
DOS ;SET DMA ADDRESS
;
MOV DX,OFFSET DGROUP:FCB
MOV AH,12H ;SEARCH-NEXT BDOS COMMAND
DOS
;
CMP AL,0FFH
JZ NNO_FILES ;IF NO FILES, SKIP OUT.
;
GETPARM 3,3 ;GET RETURN FILENAME ADDR IN ES:BX
MOV DI,BX ;SET UP ES:DI POINTER FOR MOVE
MOV SI,OFFSET DGROUP:SDMA+1 ;POINT TO FILE NAME
MOV CX,11 ;MOVE 11 BYTES
REP MOVSB
;
JMP N_DONE
;
NNO_FILES: ;IF NO FILES ARE PRESENT,
GETPARM 3,3
MOV BYTE PTR ES:[BX],’?’ ;PUT A ? IN FIRST CHAR OF OUTPUT FILE NAME
;OUTPUT FILE NAME
N_DONE: POPRET 3
SRCHN ENDP
;———————————-
; GETDIR: RETURN ASCII STRING CONTAINING CURRENT DIRECTORY PATH
;
;
; CALL GETDIR(PATH,IDRIVE,ICODE)
;
; PATH = CHARACTER*64 (RETURNED PATH NAME)
; ICODE = INTEGER (RETURN CODE)
; IDRIVE= INTEGER (DRIVE, 0=DEFAULT, 1=A, 2=B)
;
; NOTE: PATH IS RETURNED WITH NO STARTING BACKSLASH
; AND WITHOUT THE DRIVE LETTER AND COLON.
; THE PATH STRING IS TERMINATED WITH A ZERO BYTE.
;
GETDIR PROC FAR ;GET PATH NAME ON IDRIVE
;
SETFRAME
;
GETPARM 1,3
MOV STRING,BX ;SAVE PTR TO OUTPUT STRING AREA
MOV STRING_SEG,ES ;AND SEG
;
GETPARM 2,3 ;GET DRIVE CODE ADDR
MOV AX,ES:[BX] ;GET VALUE
MOV IDRIVE,AX ;AND SAVE IT
;
PUSH DS
MOV SI,STRING ;SET UP PATH STRING POINTER
;
MOV DX,IDRIVE ;GET DRIVE NUMBER
MOV AX,STRING_SEG
MOV DS,AX
;
MOV AH,47H ;GET DIRECTORY NAME
DOS
;
GETPARM 3,3
MOV AH,0 ;CLEAR OUT AH
MOV ES:[BX],AX ;STORE IT
;
POP DS
POPRET 3
GETDIR ENDP
;———————————-
; CHDIR: SET DIRECTORY PATH TO STRING FOUND IN PATH.
;
;
; CALL CHDIR(PATH,ICODE)
;
; PATH = CHARACTER*64 (INPUT PATH NAME)
; ICODE = INTEGER (RETURN CODE)
;
; NOTE: PATH MUST CONTAIN THE PATH NAME, TERMINATED BY
; A ZERO BYTE. THE DRIVE LETTER AND COLON AND
; BACKSLASH MAY BE AT THE START OF THE STRING.
;
CHDIR PROC FAR ;SET PATH NAME
;
SETFRAME
;
GETPARM 1,2
MOV STRING,BX ;SAVE PTR TO INPUT STRING AREA
MOV STRING_SEG,ES ;AND SEG
;
PUSH DS ;SAVE DS
PUSH ES
POP DS
MOV DX,BX ;SET UP PATH STRING POINTER
;
MOV AH,3BH ;SET DIRECTORY PATH
DOS
;
GETPARM 2,2 ;POINT TO ICODE
MOV AH,0 ;CLEAR OUT AH
MOV ES:[BX],AX ;STORE IT
;
POP DS ;RESTORE DS
POPRET 2
CHDIR ENDP

 

;———————————-
; GETDFS: GET DISK FREE SPACE IN BYTES.
;
;
; CALL GETDFS(IDRIVE,IBYTES,ISECT,ICLUST)
;
; IDRIVE= INTEGER*2 (DRIVE NUMBER)
; IBYTES= INTEGER*2 (NUMBER OF BYTES /SECTOR)
; ISECT = INTEGER*2 (NUMBER OF SECTORS / CLUSTER)
; ICLUST= INTEGER*2 (NUMBER OF CLUSTERS REMAINING)
;
; IF ISECT = FFFFh THEN ERROR: INVALID DRIVE CODE
;
GETDFS PROC FAR ;GET SPACE REMAINING
;
SETFRAME
;
GETPARM 1,4
MOV DX,ES:[BX] ;GET DRIVE NUMBER
;
MOV AH,36H ;SET DIRECTORY PATH
DOS
;
; THIS FUNCTION RETURNS: AX=FFFF IF ERROR,
; BX = NUMBER OF CLUSTERS AVAILABLE
; CX = NUMBER OF BYTES PER SECTOR
; AX = NUMBER OF SECTORS PER CLUSTER
;
; THEREFORE, IBYTES = AX * BX * CX
;
PUSH BX ;PUSH ICLUST
GETPARM 2,4
MOV ES:[BX],CX ;SAVE IBYTES
;
GETPARM 3,4 ;SAVE ISECT
MOV ES:[BX],AX
;
GETPARM 4,4
POP AX
MOV ES:[BX],AX ;SAVE ICLUST
;
POPRET 4
GETDFS ENDP
;
CODE ENDS
END


Figure 8:

 

 

$STORAGE: 2
C
C
C Demonstration MS-FORTRAN program using assembler
C subroutine calls.
C
C by Mark Dahmke
C May, 1986
C
C This program displays the current directory path,
C then allows you to enter a new directory name.
C Next, it displays all filenames in the directory,
C and also shows the amount of free disk space remaining.
C
C
CHARACTER*65 PATH
CHARACTER*11 FSPEC
CHARACTER*11 FNAME
CHARACTER*1 ZERO
INTEGER IDRIVE,ICODE
INTEGER*4 ISPACE
C
C
DATA PATH /’ ‘/
DATA FSPEC /’???????????’/
DATA FNAME /’ ‘/
C
ZERO = CHAR(0)
C
C GET PATH NAME:
C
C
IDRIVE = 0
C
CALL GETDIR(PATH,IDRIVE,ICODE)
C
IF (ICODE .NE. 0) THEN
WRITE(*,*) ‘ ERROR RETURN: ‘,ICODE
ENDIF
C
C — CLEAR OUT THE ZERO BYTE BEFORE WRITING TO CONSOLE…
C
DO 4 I = 1, 65
IF (PATH(I:I) .EQ. ZERO) PATH(I:I) = ‘ ‘
4 CONTINUE
C
WRITE(*,*) ‘ Current Directory is: ‘,PATH
C
C
C —- CHANGE DIRECTORY
C
PATH = ‘ ‘
5 WRITE(*,*) ‘ Enter name of directory: ‘
READ(*,6) PATH
6 FORMAT(A65)
C
C —- SCAN PATH NAME TO FIND LAST CHARACTER.
C INSERT A ZERO BYTE AT THE END OF THE STRING.
C
I = 64
10 IF (PATH(I:I) .NE. ‘ ‘) GO TO 20
I = I – 1
IF (I .EQ. 0) GO TO 5
GO TO 10
20 I = I + 1
PATH(I:I) = ZERO
C
C
C
CALL CHDIR(PATH,ICODE)
C
IF (ICODE .NE. 0) THEN
WRITE(*,*) ‘ INVALID DIRECTORY NAME OR FORMAT ‘
ENDIF
C
C —- DISPLAY FILE NAMES IN THE CURRENT DIRECTORY.
C
C
CALL SRCHF(IDRIVE,FSPEC,FNAME)
C
IF (FNAME(1:1) .EQ. ‘?’) THEN
WRITE(*,*) ‘ NO FILES’
GO TO 100
ENDIF
C
WRITE(*,40) FNAME(1:8),FNAME(9:11)
40 FORMAT(1X,A8,’.’,A3)
C
C —- CONTINUE TO READ FILE NAMES
C
C
50 CALL SRCHN(IDRIVE,FSPEC,FNAME)
C
IF (FNAME(1:1) .EQ. ‘?’) GO TO 100
C
WRITE(*,40) FNAME(1:8),FNAME(9:11)
GO TO 50
C
C
C —- GET DISK FREE SPACE
C
C
100 CALL GETDFS(IDRIVE,IBYTES,ISECT,ICLUST)
C
ISPACE = IBYTES * ISECT * ICLUST
C
WRITE(*,60) IDRIVE,IBYTES,ISECT,ICLUST,ISPACE
60 FORMAT(‘ Drive ‘,I2,’ has ‘,I6,’ bytes per sector’,/,
& 1X,I6,’ sectors per cluster, and ‘,I8, ‘ free clusters.’,//,
& ‘ Total free space in bytes = ‘,I12)
C
C
STOP
END