Extra Memory for BASIC (PC Magazine Vol 5 No 8 Apr 29, 1986 PC Tutor) A programmer is trying to develop a multipage screen-buffer technique for a monochrome display that will provide a function similar to the color adapter's ability to keep several different screen images stored in memory and flip them to the screen when appropriate. Using an assembler routine, he is unable to use the highest 12K when running with the BASIC interpreter. However, when the same program is run in compiled form (with appropriate changes such as CALL ABSOLUTE), the machine goes beserk. Is there a way to do this using the memory-management DOS function calls (48h, 49h, and 4Ah) in DOS 2.1? Editor's Response: Programs compiled under the old IBM and Microsoft BASIC compilers use all available memory from the beginning of the program upward. Since these compilers were designed to work under DOS 1.1, they do not free up any of this program memory using function call 4Ah. Conse- quently, a function call 48h from within an assembly language routine will not allocate a memory block since there is no memory left to allocate. Nor could you first deallocate some of the compiled BASIC's memory with an assembler subroutine using function call 4Ah, because the program has already set itself up in the memory and expects it all to be usable. There are a couple of solutions to the problem, however. Under the BASIC compiler you could define a string variable 4000 bytes long to hold the monochrome display contents with: MONOHOLD$ = STRING$(4000,0) You could then use VARPTR to pass the address of this string to your assembly language subroutine. (Note that the first 2 bytes pointed to with VARPTR(MONOHOLD$) contain the length of the string, which is 4000.) However, this would not work under the BASIC interpreter, whose strings are limited to 255 bytes. If you'd like to keep the compiled and interpreted BASIC programs consistent in the way they store data in upper memory, therefore, you need to prevent the compiled BASIC program from taking up all of available memory. This can be done by modifying some of the header information in the compiled .EXE run file, though, as you'll see there may be some problems in this approach. Every .EXE file begins with header information DOS needs to load the program into memory, perform segment fix-ups, and allocate space for it to run. This header information is documented in the DOS manuals for versions 2.0 and earlier and in the DOS Technical Reference manuals for versions 2.1 and later. You can't see this header information if you load the .EXE file directly into DEBUG, because DEBUG uses the header to perform all the space allocation and fix-ups, making the program all ready to run. You'll have to first rename the file to an extension other than .EXE and then load it into DEBUG. (But then you won't be able to run it in DEBUG.) Use the following commands to look at the first part of an .EXE file header: RENAME runfile.EXE runfile.XXX DEBUG -N runfile.XXX -L 0 -D 0 The 2-byte word beginning at offset 000Ah is defined in the DOS Technical Reference manual as the "minimum number of 16-byte paragraphs required above the end of the loaded program." This word will often be at offset 0000h. The 2-byte word at offset 000Ch is the "maximum number of 16-byte paragraphs required above the end of the loaded program." This will usually be FFFFh, which means that the program wants all available memory above where it loads. This extra memory space is used for the "heap" and the "stack." During calculations, for instance, the stack is used to store inter- mediate results. The heap is used by the program mostly for dynamic storage. If your program executes a STRING$ command such as shown above, or if it DIMensions an array, the result has to go somewhere. It goes in the heap. In a program that does a lot of dynamic string and array allocation, the heap can get pretty cluttered up and disorganized. At times, normal execution can grind to a halt while the program cleans up the heap in a process technically referred to as "garbage collection." Of course, most compiled BASIC programs don't really need all available memory and can function normally with less. Thus, you can change the "maximum memory" word at offset 000Ch in the .EXE header to something lower than FFFFh to prevent DOS from allocating all available memory to the program. Let's assume, for example, that you have 256K memory in your machine and that the bottom 128K memory is taken up by DOS and resident programs. (You get this second figure by subtracting the "bytes free" from the "bytes total memory" information provided by CHKDSK.) If your .EXE program is about 32K, you can give it another 64K when it runs and still have 32K left over at the top of memory for your screen swaps. To do this, you'd enter the following DEBUG commands as a direct continuation of the program above (whose last line was: -D 0): -E 000C 00 10 -W -Q RENAME runfile.XXX runfile.EXE Note that the 2 bytes (00 and 10) specified after the E (Enter) command are in reverse order, as usual. The actual word you've entered is 1000h or 4096 decimal. This is the number of 16-byte paragraphs, and it corresponds to 64K bytes. When you now run your .EXE program, the DOS loaders uses this "maximum memory" word in the .EXE header to computer a "top of memory" word (also in paragraph form) that it puts in offset 0002h of the programs Program Segment Prefix. The compiled BASIC prologue uses this value to limit the amount of memory it uses. You can look at the Program Segment Prefix by loading the .EXE file (not the renamed .XXX file) into DEBUG and executing the command: -D 0 If an .EXE header has FFFFh in offset 000Ch, the Program Segment Prefix top of memory word at 0002h will be 4000h for a 256K machine and A000h for a 640K machine (again, you'll see the 2 bytes shown in reverse order). If the amount of real available memory exceeds the length of the program plus the additional memory (plus the memory taken up by DEBUG), then you'll see a lower value. If you have enough high memory for screen swaps while in DEBUG, you'll certainly have enough when running the program outside of DEBUG. How low can you set the value at 000Ch in the .EXE header? Definitely don't set it to 0 -- that indicates to DOS that the program is to be loaded high in memory, and it will crash. With some compiled BASIC programs, it can be set as low as 0060h paragraphs (about 1.5K bytes). If you have a lot of dynamic string and array storage in your program, you'll need something higher, or you'll probably get an "out of memory" error during program execution. Note also that the program may run slower, since it has to perform more frequent garbage collections to clean up the heap. You'll probably have to experiment to get an optimum value. If you do your compilation and linking using a batch file with a replaceable parameter for the filename, you can automate this .EXE header modification with the following batch file code after the LINK: RENAME %1.EXE %1.XXX DEBUG %1.XXX < HEADCHG RENAME %1.XXX %2.EXE where HEADCHG is a file containing the DEBUG commands: E 010C 00 10 W Q Note here that you have to use 0100h plus the header offset for the E (Enter) command because DEBUG is loading the file at offset 0100h. The .EXE file header also contains a word "checksum" value, which is supposed to be used by the DOS loader to ensure .EXE file integrity. If we change a value in the header, then the checksum will not check. However, DOS doesn't use this checksum when loading programs. If it does in the future, you'll have to make an adjustment to that value as well. You may also be interested to learn that the LINK program included with the Microsoft (but not the IBM) Macro Assembler and the Microsoft C Compiler includes a switch called CPARMAXALLOC to set the maximum memory value during the LINK so you don't have to adjust it later. The next version of the Microsoft C Compiler also has an EXEMOD program to change this header information with a one-line command. (Persons tired of having COMMAND.COM reload after running a compiled BASIC program may alter this word to prevent the program from overwriting the transient part of COMMAND.COM at high memory.) Although there's nothing wrong with modifying this part of the .EXE header, you may have problems using memory above the loaded program. First, the value you pick to go in the .EXE header will probably be dependent on your machine's memory configuration. If it's run on a machine with a few more resident programs loaded, you may not have enough high memory to store you screen swaps. Second, in a multitasking system such as TopView, this solution will be absolutely disastrous, because your program is using memory that doesn't belong to it. Only memory up to that top of memory word in the Program Segment Prefix is allocated for your program, and you are deliberately using memory higher than that. TopView may be using this memory for some other program. The same problem exists if you use high memory in the BASICA interpreter. You also don't want to write over the first few bytes right after the top of the extra memory, because these contain a marker DOS uses for memory allocation. There is another solution, but it would require using a separate assembly language program to allocate memory below your loaded program. This assembly language program would perform the following four steps: 1) deallocate all memory above its code by executing the DOS 4Ah function call; 2) allocate a block of memory for screen swaps (or BLOADs or whatever) with function call 48h; 3) store the address of this block in lower memory, probably in one of the user interrupt vectors beginning at 0000:0180h; and, 4) load the compiled BASIC program (or the BASICA interpreter) using the EXEC function call 4Bh. The BASIC program -- in either interpreted or compiled form -- could retrieve the address of this allocated memory block using PEEK and pass this address to the assembly language subroutine. When the BASIC program terminates, control would pass back to the assembler program that loaded it, which would then terminate. ----------------------------------------------------------------- Minding Memory From BASIC (COMPUTE! Magazine June 1986 by D. W. Neuendorf) Memory management in DOS has become an important issue. The new desktop tools and coresident programs are designed to wait in the background to be called during the operation of another program. A number of these utilities may be lurking in memory at once, and programmers can't predict which other programs will be present with their own. The result can be memory conflicts and system crashes. DOS 2.0 and later versions contain several function calls designed to give the operating system control over how the computer's memory is divided among programs residing in memory simultaneously. The most basic of these functions simply attempt to allocate and deallocate blocks of memory at a program's request. These DOS calls are readily available to machine language programmers, just like all other machine- level resources. BASIC programmers, on the other hand, have no direct access to many DOS functions. But there are ways for BASIC programs to call on DOS to perform these memory management tasks. There are two DOS functions we're interested in -- one for allocating memory and another for deallocating memory. In machine language, both functions are called by placing a function number in the microprocessor's AH register and calling Interrupt 21h. (Function numbers indicated to DOS which function is being called. The interrupt then performs the function.) The numbers are 48h for the allocate function and 49h for the deallocate function. In addition to these numbers, each function call requires that you pass an argument. The allocate function requires the number of 16-byte paragraphs of memory to be allocated. This number must be placed in the microprocessor's BX register. The deallocate function requires the segment address of a block to be deallocated. This number must be placed in the ES register. After each function is performed, it returns a value. The allocate function returns, via the AX register, either the segment address of an allocated block or an error code (7 or 8 plus a set carry bit) if the function was unsuccessful. The deallocation routine returns nothing if successful, but sets the carry bit and returns an error code (7 or 9) if unsuccessful. The machine language listings below show the assembler code necessary to call these functions. The BASIC program shows how to call these functions from BASIC. Since the allocate routine is not available initially and therefore can't allocate space for itself, the program reserves a few bytes for it just above BASIC (using the CLEAR statement in line 10). Once the allocate routine has been installed (lines 40-60), it can be used to get memory from DOS for machine language routines and other data. An example of its use is the call in line 70, which gets the segment address of a memory block for the deallocate routine. Finally, line 120 shows an example of using the deallocate routine -- it deallocates its own memory. After studying the BASIC program, you'll see that it's possible to put a machine language subroutine outside BASIC's 64K memory area, thus saving some space for BASIC programs. Better yet, you don't have to worry about where in memory you're hiding the routine -- DOS takes care of it. If you use a lot of machine language subroutines or store large amounts of data in memory, you'll have a lot more room to work with if you don't have to put everything inside BASIC's own segment. If everyone relies on DOS to determine where their programs reside in memory, we can all feel confident that our coresident programs are not overlapping and conflicting with each other. But if too many programmers bypass these DOS functions, the rest of us won't dare to rely on them, either. After all, DOS can protect only the data or programs that it knows about. DOS Memory Allocation: page 50,132 0000 alloc segment para assume cs:alloc assume ds:alloc assume es:alloc 0000 allocate proc far ; ;Routine to allow BASIC to make DOS call to allocate ;a block of memory outside of BASIC's own segment. ;CALL ALLOC(MEMORY) - when BASIC calls the routine, ;MEMORY contains the number of bytes to be allocated. ;When the routine returns to BASIC, MEMORY contains ;the segment address of the allocated block of memory. ;A 7 or 8 indicates allocation failed. ; 0000 55 push bp 0001 8B EC mov bp,sp 0003 8B 5E 06 mov bx,[bp+6] ;get address of MEMORY 0006 8B 1F mov bx,[bx] ;get number of bytes to be allocated 0008 B4 48 mov ah,48h ;DOS function number 000A CD 21 int 21h ;DOS call itself 000C 8B 5E 06 mov bx,[bp+6] ;address of MEMORY 000F 89 07 mov [bx],ax ;put segment address of allocated memory in MEMORY 0011 5D pop bp 0012 CA 0002 ret 2 ; 0015 allocate endp 0015 alloc ends end DOS Memory Deallocation: page 50,132 0000 dealloc segment para assume cs:dealloc assume ds:dealloc assume es:dealloc 0000 dlc proc far ; ;Routine to allow BASIC to make DOS call to deallocate ;a block of memory previously allocated using ALLOC. ;CALL DEALLOC(MEMORY) - when BASIC calls the routine, ;MEMORY contains the segment address of the block of ;memory to be dealloc. When the routine returns to ;BASIC, MEMORY contains either the original segment ;address or an error code. A 7 or 9 indicates ;allocation failed. ; 0000 55 push bp 0001 06 push es 0002 8B EC mov bp,sp 0004 8B 5E 06 mov bx,[bp+8] ;get address of MEMORY 0007 8E 07 mov es,[bx] ;get segment address of block to be deallocated 0009 B4 49 mov ah,49h ;DOS function number 000B CD 21 int 21h ;DOS call itself 000D 8B 5E 06 mov bx,[bp+8] 0010 89 07 mov [bx],ax ;put error code in MEMORY 0012 07 pop es 0013 5D pop bp 0014 CA 0002 ret 2 ; 0017 dlc endp 0017 dealloc ends end DOS Memory Functions in BASIC: 10 CLEAR ,&HFFDF 'Reserve a few bytes just above BASIC for alloc routine 20 DEFINT A-Z 30 DEF SEG:ALLOC=&HFFDF:DMEMORY=2:DEALLOC=0 40 RESTORE 50:FOR X=0 TO 20:READ Y:POKE X+ALLOC,Y:NEXT 'Install alloc. 50 DATA &h55,&h8b,&hec,&h8b,&h5e,&h06,&h8b,&h1f,&hb4,&h48,&hcd 60 DATA &h21,&h8b,&h5e,&h06,&h89,&h07,&h5d,&hca,&h02,&h0 70 CALL ALLOC(DMEMORY) 'DOS call to allocate memory for dealloc routine 80 DEF SEG=DMEMORY 90 RESTORE 100:FOR X=0 TO 22:READ Y:POKE X,Y:NEXT 'Install dealloc 100 DATA &h55,&h06,&h8b,&hec,&h8b,&h5e,&h08,&h8e,&h07,&hb4,&h49,&hcd 110 DATA &h21,&h8b,&h5e,&h08,&h89,&h07,&h07,&h5d,&hca,&h02,&h00 120 CALL DEALLOC(DMEMORY) 130 END