Revisiting the Extended DCL RECALL by Hunter Goatley Western Kentucky University goathunter@WKUVX1.BITNET In December of 1988, VAX Professional published my article "Extending DCL RECALL: A DCL Patch to Get the Most Out of RECALL." As you may remember, the article described some patches to DCL.EXE that would allow you to recall as many as 62 previous commands, instead of DCL's limit of 20. Once my patches had been applied, the extended DCL recall was available through the normal means of recalling commands: the up- and down-arrow keys and the DCL RECALL command. Although it did require an unofficial patch to DCL, the benefits it provided were well worth the trouble. The first time I generated the patches, I started by looking at the micro-fiche source for DCL to find out how the 20-command limit was imposed. I discovered that the value was hardcoded, instead of stored somewhere in the DCL data area. I then used Andy Pavlin's DISM32 to disassemble DCL.EXE. (DISM32 is a VMS disassembler that can be found on the DECUS VAX SIG tapes from the last few years. It understands most VMS images, including device drivers, and produces MACRO source code that can oftentimes be assembled without modification.) Once I had the disassembled "source" for DCL.EXE, I was able to quickly find the instructions containing the hardcoded values. From there, it was pretty simple to use PATCH to locate the offsets within DCL of the instructions. One of the largest drawbacks to my method is that DCL.EXE changes in most VMS upgrades, making it necessary to relocate the new offsets after each upgrade. Since it is a fairly trivial process, I never worried much about coming up with a quicker way. Recently though, after devising the new patches for VMS versions 5.4 and 5.4-1, I decided that it was getting old. Obviously, DEC doesn't plan to change the limit since my article was written two-and-a-half years ago and it hasn't been changed yet. The reason for the twenty-command limit is that twenty commands (assuming each is 77 characters or less) will fit on a standard 24-line screen. However, as DECwindows gains in popularity, more people will be creating DECterms (the DECwindows terminal emulator) with more than 24 lines---on a 19-inch VAXstation monitor, I found 35 lines to be a comfortable size. In any case, the reason for the limit is quickly becoming obsolete. Ideally, a new DCL SET command would be introduced to let you set the limit to an arbitrary value. In lieu of that, my patches provided a reasonable solution. After posting my patches for VMS version 5.4 to INFO-VAX, I received a message from Michael Kimura of Hughes Aircraft Company. Mike had modified my patches to make them a little more generic: instead of hardcoding the offsets, Mike's patches defined symbols corresponding to the internal DCL routine DCL$RECALL and specified the instruction addresses as offsets from the DCL$RECALL address. While simplifying the process a great deal, it was still necessary to consult the micro-fiche for the updated address of DCL$RECALL. Since it looks like we'll be stuck with 20-command recall for awhile (and having been inspired by Mike), I decided to devise a simple, painless way to apply the patches that would be independent of all VMS versions. The result was DCLPATCH.MAR (Program 1), a VAX MACRO program that reads the DCL image into memory, applies the patches, and creates a new image file. While I considered several ways to do this (including a TPU routine to do the patch), I decided to go with the most straight-forward approach. The program will be described in some detail below; while it is a simple program, it has some "neat tricks" that you might be able to use in your own MACRO programs. BUT FIRST, A BRIEF REFRESHER For a full explanation of the patches involved, please refer to my previous article, "Extending DCL RECALL" (VAX Professional, December 1988). To refresh your memory, the reason there is a limit of 62 commands is that the hardcoded limits are stored in the image as short literals. A short literal is stored as a byte whose two high bits must be zero to identify it as a short literal. This leaves 6 bits for the actual number, making the largest possible short literal 63 (2 raised to the 6th minus 1). Because the code itself actually uses WRK_C_RECALLMAX+1, the patched value can be no larger than 63, and the new recall maximum becomes 62. Note that there may not actually be 62 commands available for recall. The command recall buffer is 1024 bytes long; the length of the commands themselves determines how many commands will fit in the buffer. If your commands are very short, you could have more than 100 in the buffer; if they were very long you could have as few as 4. The number of commands available for recall is actually the number of commands still in the buffer, but no more than 62. (For more information on the DCL command recall buffer, please consult my October 1987 VAX Professional article entitled "Flushing the Buffer"). HOW DCLPATCH.MAR WORKS The program's logic flow is very straight-forward. The main routine first branches to the internal subroutine READ_OLD_IMAGE. The target image, SYS$SYSTEM:DCL.EXE in this case, is opened using RMS calls ($OPEN and $CONNECT). An Extended Attribute Block (XAB) is provided so RMS will return the File Header Characteristics ($XABFHC). Once the file has been opened, the XABFHC contains, among other data, the number of the end-of-file block---which also happens to be the size of the file in blocks. This value is multiplied by 512 to convert the size from blocks to bytes. The program then dynamically allocates a buffer large enough to hold the entire file by calling the Run-Time Library routine LIB$GET_VM. If the image is too large (not enough contiguous memory can be allocated), the program simply returns the error to VMS. Once the buffer has been successfully allocated, the Record Access Block (RAB) for the file is modified to point to the new buffer; this buffer will be used to receive each block read by RMS. The $GET RMS routine is called to read the file in 512-byte chunks. As each record is read, the address of the input buffer is increased by 512, so that the RAB points to the next 512-byte piece of memory. Modifying the RAB directly with the following instruction eliminates the need for extra registers to keep track of the next available byte: ADDL2 #512,RAB$L_UBF(R6) A separate counter is incremented for each successful block that is read. Instead of using XAB$L_EBK, I used a separate counter to ensure that I know exactly how many blocks were read. The value returned in XAB$L_EBK may actually point to the block just past the end-of-file block, if the last block in the file is full. While this is not normally true for .EXE files, I think it's good programming practice. NOTE: I used $GET (record I/O) instead of $READ (block I/O) purely for convenience. Using $GET to read 512-bytes is actually faster than using $READ to read 512-byte blocks, though the code could have been written to read in larger chunks with block I/O. Had I been seriously concerned about overhead, using block I/O would have been better. Once the entire file has been read into the buffer, the program starts replacing instructions by calling REPLACE_STREAM. Actually, I wrote a macro named REPLACE that sets up the input registers before branching to the REPLACE_STREAM subroutine. I chose the macro to increase the program's readability. The name STREAM refers to a contiguous stream of bytes. The input parameters that are passed to REPLACE_STREAM include the length of the stream to replace, the address of the old stream, and the address of the new stream. REPLACE_STREAM simply starts at the beginning of the buffer and steps through it byte-by-byte, looking for a match on the first byte of the stream to be replaced. Once a match is made, the following bytes are compared until either a match is found or a dissimilar byte is found. If the entire stream is not matched, the routine proceeds through the buffer. Once a match is made, the pointers are adjusted to point to the beginning of the stream and the new bytes are copied into the buffer, overlaying the old bytes. A message is printed to the terminal indicating the offset of the replaced stream. Once the stream has been replaced, the search for the next match resumes. Again, there are other ways this could have been coded, using the LOCC (LOCate Character) instruction. I chose the simple byte-by-byte loops so that I could easily maintain pointers and counters. Once all of the instruction streams have been replaced, the program calls WRITE_NEW_IMAGE to create a new image called DCL_RECALL.EXE in the default directory. This is accomplished by writing 512-byte chunks of the contiguous memory buffer to the output file using $PUT. THE FINE POINTS There are a few points that I wanted to emphasize, plus a couple of assumptions that the program has to make in order to work as simply as it does: 1. The instruction streams are assumed to be the same size. If two different-size streams were involved, the physical size of the output file would have to change, which would, basically, trash the image being patched. This program was designed to apply the patches in a manner similar to the REPLACE command in PATCH. If you need to add instructions, PATCH is the only way to go, since it will create patch buffers, adjust pointers, etc. If you look at the data area in DCLPATCH.MAR, you'll find the instructions that are to be replaced (labelled OINST1, OINST2, etc.). The replacement instructions are labelled NINST1, NINST2, etc. To ensure that the old and new streams are the same size, I used the ASSUME macro: OINST3: MOVL S^#WRK_C_RECALLMAX,R6 ;Old instruction 3 OINST3_L = .-OINST3 ;... NINST3: MOVL S^#NEW_C_RECALLMAX,R6 ;... NINST3_L = .-NINST3 ;... ASSUME OINST3_L EQ NINST3_L ;... In this fragment, the first line is the old instruction that is to be replaced. The second line generates a symbol, OINST3_L, that is equated with the length of the instruction. The "." represents the current address at assembly-time; specifying ".-OINST3" tells the assembler to subtract the address of OINST3 from the current address, which happens to be the byte after the instruction. This difference, 3 in this case, is then assigned to the symbol OINST3_L. The third line is the new instruction, and the fourth calculates the length of it. The fifth line invokes the ASSUME macro to ensure that the two lengths are equal (it can also be used to check for greater than, less than, etc.; the ASSUME macro is not documented, but can be found in the STARLET macro library). If the two values are not equal, the assembler will generate an error stating so. 2. Because the program is written in MACRO, I was able to specify the two instruction streams in normal mnemonic format. In a high-level language, each of the instructions would have to be broken down into their individual bytes, which would mean looking up opcodes and the internal storage format for the operands. 3. To help ensure that the program doesn't have to be modified for new versions of DCL.EXE, the program is linked with the DCL symbol table. The following line in DCLPATCH.MAR tells the linker to link with DCLDEF.STB to resolve the following external symbols: .LINK "SYS$SYSTEM:DCLDEF.STB"/selective_search ; ; Global variables used here: ; .EXTRN WRK_B_RECALLCNT ; Symbol from DCLDEF.STB .EXTRN WRK_C_RECALLMAX ; Symbol from DCLDEF.STB 4. The largest assumption made by the program is that the instructions to be replaced are unique. Since the program replaces all occurrences of a stream, care must be taken to ensure that the correct instructions have been located. In the case of DCL.EXE, I got lucky: the only occurrences of the instructions are the ones that need to be replaced. This would not have been true if one of the instructions had been something common, like: CLRL R0 In that case, the instruction streams could be expanded to include the two or three instructions preceding or following the one to replace in order to qualify it better. For example, if I needed to replace only one CLRL R0 and it is preceded or followed by an uncommon instruction like "EXTZV #3,#16,R0,R10", specifying the EXTZV would help ensure that we only replace the instruction we want to replace. Just in case one of the instructions is duplicated in some future version of DCL, the program checks that only a certain number of each were found. If the number of replacements doesn't match the expected number, no new file is created. 5. Unlike PATCH, my program does not create a patch history. Since the patch is made in place, the only difference between DCL_RECALL and DCL is that a few bytes have been changed. Using the PATCH instructions from my previous article, a patch history was added to DCL.EXE which showed the patches that were made (using ANALYZE/IMAGE). The drawback to this is that it's more difficult to tell whether or not the extended RECALL patch has been applied. The advantage is that future "official" patches to DCL will go smoothly, since there will be no indication that the "unofficial" patch was made. (Not that this ever caused a problem before during upgrades, but it could have.) Note that when using PATCH/ABSOLUTE, PATCH does not write a patch history. 6. The address printed to the terminal by REPLACE_STREAM is the offset that would have been supplied to the PATCH utility. If you feel better about using my old patches, you can just plug the printed addresses into the old patch command file. (If you wanted to use PATCH/ABSOLUTE, you'd need to add 200 (hex) to each address to allow for the image header.) 7. If you don't want 62 commands to be recallable, you can change the following definition in DCLPATCH.MAR to specify any number from 0 to 62: NEW_C_RECALL_MAX = 62 8. As you look at the main routine, you'll notice that informational messages are printed to the screen using the PRINT macro. The macro is defined as: .MACRO PRINT STRING,?TEXT ;* Macro to print text .SAVE_PSECT LOCAL_BLOCK ;* Save this PSECT .PSECT _DCLPATCH_DATA,NOEXE,WRT,LONG,SHR ;* Change to data PSECT .ALIGN LONG ;* Align on longword TEXT: .ASCID ~STRING~ ;* Create .ASCID string PRINT_TEXT = TEXT ;* Save the address .RESTORE_PSECT ;* Go back to code PUSHAQ PRINT_TEXT ; Write the string CALLS #1,G^LIB$PUT_OUTPUT ; ... to SYS$OUTPUT .ENDM PRINT ;* End of PRINT macro When invoked, the macro will cause the assembler to create a .ASCID string (a string with a descriptor) in the PSECT (program section) named _DCLPATCH_DATA, and then return to the code PSECT to call LIB$PUT_OUTPUT to print the string. For more information on how this works, you can study the comments above and consult the VAX MACRO AND INSTRUCTION SET REFERENCE MANUAL for more information about .SAVE_PSECT and .RESTORE_PSECT. Essentially, it provides a quick way to hop between a data area and a program area with in-line assembler directives. CUSTOMIZING DCLPATCH.MAR As you may have noticed by now, this program has several other possible applications. It would be trivial to modify the program to patch other images and replace other streams. One of the most useful applications for the program is to change ASCII strings within an image. For example, suppose you have an executable image that has a logical name that doesn't suit your site (or even worse, a full device/directory file specification). You could modify DCLPATCH.MAR to replace all occurrences of the string with a more suitable string. For example, the following would replace all occurrences of DUA0:[UTILS.SYS] with the logical SYSTEM_UTILITYS: OINST1: .ASCII /DUA0:[UTILS.SYS]/ OINST1_L = .-OINST1 NINST1: .ASCII /SYSTEM_UTILITYS:/ NINST1_L = .-NINST1 ASSUME OINST1_L EQ NINST1_L Even in this case, you must make sure the two streams are the same length because you don't know how many times the length of the original string is stored (in descriptors, in the code, etc). You sometimes have to be creative when generating the logical names (like the misspelling of SYSTEM_UTILITYS above), but at least it provides some way to reduce site-dependencies when you don't have the source code. BUILDING DCLPATCH To build DCLPATCH.EXE (either as-is or customized), simply use the following DCL commands: $ MACRO DCLPATCH $ LINK DCLPATCH Here is a sample run under VMS v5.3-1: $ macro dclpatch $ link dclpatch $ run dclpatch Extending recall capability of SYS$SYSTEM:DCL.EXE.... Replacing "CMPB WRK_B_RECALLCNT(R10),#WRK_C_RECALLMAX+1" (3 occurrences).... Patching at image address 000067B1 Patching at image address 000067D4 Patching at image address 00006822 Replacing "CMPL R1,#WRK_C_RECALLMAX" (1 occurrence).... Patching at image address 00006CBF Replacing "MOVL #WRK_C_RECALLMAX,R6" (1 occurrence).... Patching at image address 00006CE5 Replacing "MOVL #WRK_C_RECALLMAX,R9" (1 occurrence).... Patching at image address 00006CFD Replacing RECALL output code (1 occurrence).... Patching at image address 00006DAC Creating file DCL_RECALL.EXE Extended DCL RECALL image DCL_RECALL.EXE created $ Once the patch has been made, you can test it by copying DCL_RECALL.EXE to SYS$SYSTEM: and installing it. Once it has been installed, an account can be set up to use it by running AUTHORIZE and specifying the DCL_RECALL as the CLI the user is to use: $ INSTALL ADD DCL_RECALL/OPEN/HEADER/SHARED $ SET DEFAULT SYS$SYSTEM: $ RUN AUTHORIZE UAF> MODIFY HUNTER/CLI=DCL_RECALL User record(s) updated UAF> EXIT Now when someone logs in under username HUNTER, he/she will be using the patched version of DCL. You can test the patches by entering more than twenty commands and then using RECALL/ALL and the arrow keys. To access the command numbered 56, simply give the following command: $ RECALL 56 Once you are confident that the patch works, you can rename it as DCL.EXE and reinstall DCL so that all users will get it by default (I suggest keeping a copy of the original around, "just in case...."): $ INSTALL REMOVE DCL_RECALL $ COPY SYS$SYSTEM:DCL.EXE SYS$SYSTEM:DCL_ORIGINAL.EXE $ COPY SYS$SYSTEM:DCL_RECALL.EXE SYS$SYSTEM:DCL.EXE $ INSTALL REPLACE DCL As usual, you use this program at your own risk. If you don't feel comfortable using the patches, then don't apply them. I've been using the patches since VMS version 4.2 without ever having a problem. By finally taking the time to write DCLPATCH.MAR, I've eliminated the minor headache of regenerating a new patch each time a VMS upgrade arrives. With some very minor changes, the program has many other possible uses. And remember: there are other ways it could have been written. Perhaps you'll explore some of the others? Hunter Goatley, VAX Systems Programmer E-mail: goathunter@WKUVX1.BITNET Academic Computing, STH 226 Voice: (502) 745-5251 Western Kentucky University Bowling Green, KY 42101