DCL Command RECALL Extensions
Clyde Digital Systems
Since VAX/VMS V4.0, DCL has kept a “history” of your last 20 DCL commands, allowing you to easily retrieve past commands. You could either use your up-arrow and down-arrow keys to step forwards and backwards through the list, or you could use the DCL command RECALL to “recall” an old command. The RECALL command is quite flexible, allowing you to list the 20 commands and then select one by giving a number (“$ RECALL 5”), or allowing you to specify the first few characters of the command to be used in a search (e.g., “$ RECALL LINK” to retrieve the most recent LINK command. Many people have commented that one feature that is missing from DCL is a means of flushing the buffer (for security purposes). Last fall, I wrote a program that would flush the buffer, as well as allow you to save commands and restore them in a different interactive session.
The VMS terminal driver (TTDRIVER) has a last-command recall capability that DCL disables; DCL stores the commands in its own buffer in a user’s P1 address space. I started by using the DCL command EXAMINE to look around my P1 space to see if I could find the buffer DCL uses to store old commands. I succeeded in locating the buffer (luckily, it is not protected from user-mode reads) and set out trying to figure out how DCL stored them (I didn’t have access to the VMS micro-fiche, which may or may not have made my task easier!).
A 1024-byte buffer starting at virtual address 7FFE350F (hex) is used to store the DCL commands. Since the buffer is quite large, you will note that you probably have more commands stored there than you can access via the DCL RECALL command (depending on the length of your commands, as many as 100 or more could be stored). You can see this buffer by entering
$ EXAMINE/ASCII 7FFE3500:7FFE3910
Figure 1 is a partial dump of the command buffer (I used my own program to dump the bytes in a DUMP-like format—the DCL EXAMINE won’t allow you to see both the hexadecimal and ASCII representation of memory at the same time).
$ ! The hex longwords should be read right to left. The right-most $ ! longword on the first line is the address of the null byte following $ ! the last command entered (in this case, the MEM command below). $ ! $ mem 7ffe350b:7ffe390f 776F6873 0A000372 69640300 7FFE3608 .6....dir...show 7FFE350B 49545F57 4F485309 000A7372 65737520 users...SHOW_TI 7FFE351B 2F444D43 0800046C 69616D04 0009454D ME...mail...CMD/ 7FFE352B 08455641 532F444D 43080008 5453494C LIST...CMD/SAVE. 7FFE353B 64756109 00085453 494C2F44 4D430800 ..CMD/LIST...aud 7FFE354B 206E6F6D 756B7708 00096666 6F2F7469 it/off...wkumon 7FFE355B ........... .......... 00000000 00000000 00000000 00000000 ................ 7FFE38EB 00000000 00000000 00000000 00000000 ................ 7FFE38FB 01000C00 00810350 03000000 00000000 ......<P>....... 7FFE390B $ !The buffer ends here ^ @7FFE390F Figure 1 - Partial dump of DCL command buffer
The commands are stored beginning at 7FFE350F and extending through 7FFE390F. The longword at location 7FFE350B is a pointer to the end of the last command stored in the buffer. Each command is preceded andfollowed by a count byte, with a NULL (0) byte separating commands (See Figure 2). The count byte (which holds the length of the command) is used to let DCL know how many bytes to show as you use your cursor keys to step through the list. When you press the up-arrow key, DCL uses the pointer (at 7FFE350B) as the starting location (the pointer is actually the address of the NULL byte following the last command). The pointer is decremented so that it points to the length of the last command, which is then used as the number of bytes to output to the terminal. After the command has been displayed, the command pointer is positioned at the NULL byte separating the last two commands. Then, depending on whether the up-arrow or down-arrow key is pressed, DCL moves forward or backward through the buffer. DCL stops stepping backwards after 20 commands have been RECALLed. Whenever you press RETURN to execute a command, the command string is stored in the buffer starting at the byte after the last NULL byte and the command pointer at 7FFE350B is updated to point to the byte past it. Got it? Good!
. . C M D / S A V E . . . C M D / L I S T . . . audit/off... 0008434D 442F5341 56450800 08434D44 2F4C4953 54080009 ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ 1 2 3 4 1 2 3 4 1 2 1 - Null byte separating commands 2 - Leading length of command 3 - Start of command 4 - Trailing length of command Figure 2 - Format of commands stored in the buffer
When the command buffer is full, DCL wraps the buffer so that it starts over again at 7FFE350F. Any resulting partial command is then zeroed out, so that DCL doesn’t get confused and output part of a command. In Figure 3, the first command gets wiped out by the wrapping of the command text (and the command pointer points to the end of the last command, which has been wrapped).
$ mem 7ffe350b:7ffe390f 66303933 65666637 3A623035 7FFE351C L5..50b:7ffe390f 7FFE350B 54030000 00000000 00000000 00000015 ...............T 7FFE351B 206C6C61 2F657571 20687314 00035550 PU...sh que/all 7FFE352B ........... .......... 09000A41 544F5551 20574F48 530A000A ...SHOW QUOTA... 7FFE38EB 206D656D 15000945 4D49545F 574F4853 SHOW_TIME...mem 7FFE38FB 01000C00 00810350 03000033 65666637 7ffe3..<P>....... 7FFE390B $ !The buffer ends here ^ and wraps back to 7FFE350F Figure 3 - Dump of DCL command buffer showing command wrap Note that the null bytes after 7FFE351C were written to wipe out the command previously stored there.
CMD.MAR is a MACRO program that will allow a user to flush his command buffer, list all the commands stored in the buffer, save the commands by writing them out to a file, and restore commands previously SAVEd. CMEXEC privilege (or CMKRNL) is required to FLUSH and RESTORE the command buffer, but LISTing and SAVE-ing can be done without privilege. The memory used as a buffer is not protected from user-mode reads, but is protected against user-mode and supervisor-mode writes. Therefore, you can see your old commands, but you can’t do anything with them unless you are running in a executive or kernel mode. CMD.EXE can be installed with CMEXEC privilege, if desired.
To use CMD.MAR, simply assemble and link the program and set up a foreign DCL command:
$ MACRO CMD $ LINK CMD $ CMD :== $dev:[dir]CMD.EXE
A simple (but easy to code) command line parser allows you to specify which operation CMD is to perform. The valid qualifiers are:
/LIST - List all commands stored /FLUSH - Flush the command buffer /SAVE - Write the buffer to a file for later restoration /RESTORE - Restore commands that were previously SAVEd /BOTH - SAVE, then FLUSH the command buffer
Only one qualifier may be specified, and at least 3 characters must follow the “/”.
To LIST old commands, CMD simply steps backwards through the buffer, displaying on the terminal all commands in the buffer (much as the DCL RECALL command does). To FLUSH the buffer, CMD zeroes out all 1024 bytes of the buffer, and resets the pointer at 7FFE350B to point to 7FFE350F. When SAVE-ing the commands, CMD writes the 1040-byte buffer starting at 7FFE3500 out to SYS$LOGIN:SAVE_CMDS.TXT. When you wish to RESTORE the commands, CMD simply reads the old buffer from SYS$LOGIN:SAVE_CMDS.TXT and copies all 1040 bytes to 7FFE3500, writing over the current DCL command buffer. After commands have been RESTOREd, your DCL RECALL buffer will be exactly as it was when it was SAVEd. See Figure 4 for an example of CMD’s actions.
$ recall/all ! Display the commands in the buffer 1 audit/virt 2 evespn 3 Clock 4 wait 00:00:13 5 SHOW_TIME 6 WKUMON $ CMD/SAVE ! Now SAVE those commands $ CMD/FLUSH ! ... and FLUSH the buffer $ recall/all ! Display the buffer to show it was FLUSHed $ CMD/RESTORE ! RESTORE the commands saved above $ recall/all ! Display them now 1 CMD/SAVE 2 audit/virt 3 evespn 4 Clock 5 wait 00:00:13 6 SHOW_TIME 7 WKUMON $ CMD/LIST ! Use CMD/LIST to display them CMD/LIST CMD/SAVE audit/virt evespn Clock wait 00:00:13 SHOW_TIME WKUMON $ ! Voila! Figure 4 -- Sample CMD usage
Please note that all of the addresses used by CMD were determined by observation alone and could change with any version of VMS. They are, however, correct for versions 4.1 through 4.5 of VMS. I probably have just overlooked the proper symbols defined for those addresses.
Hunter Goatley, a graduate in Computer Science from Western Kentucky University, is currently working as a programmer/analyst for Clyde Digital Systems, Orem, Utah.
;======================================================================== ;= = ;= Programmer: Hunter Goatley, Clyde Digital Systems, Orem, UT = ;= Program: CMD.MAR = ;= Purpose: Save, restore, flush, & list DCL command buffer = ;= Language: VAX-11 MACRO32 assembly language = ;= System: VAX/VMS v4.x = ;= Date: October 8, 1986 = ;= = ;======================================================================== ;= = ;= This program allows the user to flush the command buffer, = ;= list all of the commands in the buffer, write the contents = ;= of the buffer to a file, and restore the buffer from a file. = ;= = ;= To use this program, set up a foreign symbol, such as: = ;= = ;= $ CMD :== $dev:[dir]CMD.EXE = ;= $ CMD/qualifier = ;= = ;= The valid qualifiers are: = ;= = ;= /LIST - List all commands in the DCL command buffer = ;= /SAVE - Save the commands in SYS$LOGIN:SAVE_CMDS.TXT = ;= /RESTORE- Restore commands previously SAVEd = ;= /FLUSH - Flush the command buffer = ;= /BOTH - SAVE, then FLUSH the DCL command buffer = ;= = ;======================================================================== ; ;********************* ; WARNING!!!! These addresses were found by searching through P1 space -- ; they may be very VMS-version dependent. ; CMD_BUF_BEG = ^X7FFE350F ; First usable byte of the command data area CMD_PTR = ^X7FFE350B ; Address of pointer to end of most recent cmd CMD_BUF_END = ^X7FFE3910 ; One byte past cmd buffer (with autodecrement, ; ... will become addr of last byte of buffer) CMD_BUFFER_ADDR = ^X7FFE3500 ; These symbols are based on observation CMD_BUFFER_LEN = 1040 ; only!! No guarantees CMD_BUF_LEN = CMD_BUF_END-CMD_BUF_BEG ; ; This macro compares the current address (in R10) with the beginning address ; of the data area -- if they are equal, set the current address = to the end ; of the data block (wrap). ; .MACRO CHECK ?HERE CMPL R11,R10 ; Have we gone past the beginning? BNEQ HERE ; No -- skip next instruction MOVL #CMD_BUF_END,R10 ; Make R10 point to one byte past the ; ... end of the data area. With next ; ... autodecrement, it will point to ; ... the last byte of the table. HERE: .ENDM CHECK ; ; ; This macro compares the first four bytes of FOR_BUFF with a valid qualifier. ; If there is a match, the appropriate routine is called and control branches ; to return to VMS. ; .MACRO PRESENT QUAL,ROUTIN1,ROUTIN2=0,?LABEL CMPL #^A\QUAL\,FOR_BUFF ; Was /QUAL given? BNEQU LABEL ; No - try next CALLS #0,ROUTIN1 ; Call the proper routine .IF DIF 0,ROUTIN2 ; Was a second routine given? BLBC R0,BYE ; Error? CALLS #0,ROUTIN2 ; No - call the second routine .ENDC ; ... BRW BYE ; Return to VMS (only one qualifier LABEL: .ENDM PRESENT ; ... at a time!!! .PSECT DCL_CMDS_DATA,NOEXE,WRT,LONG ; $SSDEF ; Include status symbols $RMSDEF ; Include RMS symbols $FABDEF ; Include $FAB symbols $RABDEF ; Include $RAB symbols $PRVDEF ; Include privilege symbols ; ;======================================================================= ; CMDFAB: $FAB FNM=<SYS$LOGIN:SAVE_CMDS.TXT&rt;, - ; File name FAC=<GET,PUT&rt;, - ; File ACcess (R/W only) MRS=1420, - ; Maximum Record Size RAT=CR, - ; Record ATtributes ORG=SEQ ; File Organization (sequential) ; ;*** Record Access Block for SAVE_CMDS.TXT ; CMDRAB: $RAB FAB=CMDFAB, - ; Record Access Block RBF=CMDREC, - ; If writing, write from CMDREC RSZ=CMD_BUFFER_LEN, - ; Record size USZ=CMD_BUFFER_LEN, - ; Input buffer is 1420 chars long UBF=CMDREC ; Address of SAVE_CMDS record buffer ; ; CMDREC: . = .+CMD_BUFFER_LEN ; Buffer for DCL commands ; ;======================================================================= ; MAXCMDLEN = 256 ; Maximum length of a DCL command ; CMDBUFFER_D: ; Descriptor for the command buffer .WORD MAXCMDLEN ; ... .BYTE DSC$K_DTYPE_T ; ... .BYTE DSC$K_CLASS_S ; ... .ADDRESS .+4 ; ... CMDBUFFER: ; The command buffer .BYTE ^A/ /[MAXCMDLEN] ; ... ; FOR_BUFF_D: ; Descriptor for the command line .WORD MAXCMDLEN ; ... returned by LIB$GET_FOREIGN .BYTE DSC$K_DTYPE_T ; ... .BYTE DSC$K_CLASS_S ; ... .ADDRESS .+4 ; ... FOR_BUFF: ; BUffer holding the command line .BLKB MAXCMDLEN ; ... FOR_LEN:.LONG 0 ; Length of command line returned USAGE_D: ; Usage message .ASCID - \Must give valid qualifier: /LIST, /FLUSH, /SAVE, /RESTORE, /BOTH (save,flush)\ CMEXEC: .QUAD PRV$M_CMEXEC ; Privilege needed to flush & restore CMKRNL: .QUAD PRV$M_CMKRNL ; Alternative privilege needed ; ;=============================================================================== ; .PSECT DCL_CMDS,EXE,NOWRT .ENTRY DCL_CMDS,^M<&rt; ; ; A simple command line parser follows. The code simply checks for a "/" ; followed by at least 3 characters of the command qualifier. Not very ; forgiving, but easy to code. ; PUSHAW FOR_LEN ; Get any commands on command line PUSHL #0 ; Don't want to prompt for anything PUSHAQ FOR_BUFF_D ; Buffer for command line CALLS #3,G^LIB$GET_FOREIGN ; Get the command line TSTW FOR_LEN ; Was anything given? BNEQ 10$ ; Yes - see what was PUSHAQ USAGE_D ; No - print usage message CALLS #1,G^LIB$PUT_OUTPUT ; ... BRW BYE ; ... ; 10$: PRESENT </RES&rt;,RESTORE_CMDS ; Was /RESTORE given? PRESENT </LIS&rt;,LIST_CMDS ; Was /LIST? PRESENT </FLU&rt;,FLUSH_CMDS ; Was /FLUSH? PRESENT </SAV&rt;,SAVE_CMDS ; Was /SAVE? PRESENT </BOT&rt;,SAVE_CMDS,FLUSH_CMDS ; Was /BOTH (save and flush)? PUSHAQ USAGE_D ; If here, a valid qualifier was not CALLS #1,G^LIB$PUT_OUTPUT ; ... given - print USAGE message BYE: $EXIT_S CODE=R0 ; Exit to VMS ; ;=============================================================================== ; Subroutine SAVE_CMDS - Save the DCL commands in SYS$LOGIN:SAVE_CMDS.TXT ; .ENTRY SAVE_CMDS,^M<&rt; ; $CREATE FAB=CMDFAB ; Create the output file BLBC R0,100$ ; Error? $CONNECT RAB=CMDRAB ; Connect the RAB BLBC R0,100$ ; Error? MOVC3 #CMD_BUFFER_LEN, - ; Get the DCL previous command @#CMD_BUFFER_ADDR,CMDREC ; ... buffer (not protected ; ... against user-mode reads) $PUT RAB=CMDRAB ; Write the buffer to the file BLBC R0,100$ ; Error? $CLOSE FAB=CMDFAB ; Close the file 100$: RET ; Return to caller ; ;=============================================================================== ; Subroutine RESTORE_CMDS - Restore DCL commands in SYS$LOGIN:SAVE_CMDS.TXT ; Calls EXEC_RESTORE from EXECUTIVE mode ; .ENTRY RESTORE_CMDS,^M<&rt; ; CALLS #0,GETPRV ; Turn on the CMEXEC to do this BLBC R0,100$ ; If error (No priv), return $OPEN FAB=CMDFAB ; Open SAVE_CMDS.TXT for reading BLBC R0,100$ ; Error? $CONNECT RAB=CMDRAB ; Connect the RAB BLBC R0,100$ ; Error? $GET RAB=CMDRAB ; Read the only record in the ; ... file (the command buffer) BLBC R0,100$ ; Error? $CMEXEC_S - ; Go move the command buffer ROUTIN=EXEC_RESTORE ; ... to process space $CLOSE FAB=CMDFAB ; Close the file 100$: RET ; Return to caller ; ;=============================================================================== ;**** This executive mode routine moves the previous commands from CMDREC ;**** to the CLI data area. ; .ENTRY EXEC_RESTORE,^M<&rt; ; MOVAL HANDLER,(FP) ; Unnecessary handler, but... MOVC3 #CMD_BUFFER_LEN,CMDREC, - ; Move the prev command block @#CMD_BUFFER_ADDR ; ... to the CLI data area MOVZBL #SS$_NORMAL,R0 ; Set up a success return RET ; Return to caller ; .ENTRY HANDLER,^M<&rt; ; Exit handler in case of $EXIT_S ; ... $ACCVIO (Kill process) ; ;=============================================================================== ; Subroutine LIST_CMDS - List all commands stored in DCL RECALL buffer ; .ENTRY LIST_CMDS,^M<R2,R3,R4,R5,R6,R7,R8,R9,R10,R11&rt; MOVL @#CMD_PTR,R10 ; Get addr of end of most recent cmd MOVL R10,R9 ; Make a copy of it MOVL #CMD_BUF_BEG,R11 ; Get the beginning of the data block LLOOP: MOVAB CMDBUFFER,R8 ; Get the buffer address MOVC5 #0,#0,#^A/ /,#MAXCMDLEN,(R8) ; Clear the buffer MOVZBL -(R10),R7 ; Get the length of the first command BEQLU LFIN ; 2 null bytes in a row? End MOVW R7,CMDBUFFER_D ; Move the length to the descriptor CHECK ; Check to see if this command wraps ; ... to the end of the data area ADDL2 R7,R8 ; Make R8 point to end of buffer LLOOP1: MOVB -(R10),-(R8) ; Move the first byte CHECK ; Check our addresses SOBGTR R7,LLOOP1 ; Loop until command is moved PUSHAQ CMDBUFFER_D ; Write it to the terminal CALLS #1,G^LIB$PUT_OUTPUT ; .... DECL R10 ; Should now point to prefix count CHECK ; Check our addresses DECL R10 ; Should now point to 0 byte CHECK ; Check our addresses CMPL R10,R9 ; If current addr = starting addr BEQL LFIN ; ... end of data BRB LLOOP ; Loop until all commands printed LFIN: MOVL #SS$_NORMAL,R0 ; Set return code RET ; Return to caller ; ;=============================================================================== ; Subroutine FLUSH_CMDS - Flush the DCL command RECALL buffer ; .ENTRY FLUSH_CMDS,^M<&rt; CALLS #0,GETPRV ; Turn on the CMEXEC to do this BLBC R0,5$ ; If error (No priv), return $CMEXEC_S - ; Flush the DCL command buffer ROUTIN=EXEC_FLUSH ; ... (zero it out) 5$: RET ; Return to caller ; ; Zero out the DCL command recall buffer ; .ENTRY EXEC_FLUSH,^M<R2,R3,R4,R5&rt; MOVC5 #0,#0,#0,#CMD_BUF_LEN,- ; Zero out the DCL command @#CMD_BUF_BEG ; ... recall buffer MOVL #CMD_BUF_BEG,@#CMD_PTR ; Reset DCL command pointer MOVL #SS$_NORMAL,R0 ; Set return status RET ; Return to caller ; ;=============================================================================== ; Subroutine GETPRV - Turn on either CMEXEC or CMKRNL to do FLUSH and RESTORE ; .ENTRY GETPRV,^M<&rt; $SETPRV_S - ; Turn on CMEXEC privilege ENBFLG=#1, - ; ... PRVADR=CMEXEC ; ... CMPL #SS$_NORMAL,R0 ; Was CMEXEC turned on? BEQLU 5$ ; Yes - return $SETPRV_S - ; If not, try to turn on CMKRNL ENBFLG=#1, - ; ... PRVADR=CMKRNL ; ... CMPL #SS$_NORMAL,R0 ; Was CMEXEC turned on? BEQLU 5$ ; Yes - return MOVL #SS$_NOCMEXEC,R0 ; If not, tell that CMEXEC is needed 5$: RET ; Return to caller .END DCL_CMDS