Writing an execlet or how a simple question
can lead to some serious system programming.

Kris G. G. Clippeleyr. Quadratrix, Belgium
qkcl@quadratrix.be



Being a seasoned system manager, and having done some system programming, I get a lot of questions from application programmers how certain things are done on an OpenVMS system.


Recently, one of them asked what DCL command he could use to show the current working directory, aka the default device & directory specification of a process.


There are two obvious possibilities: SHOW DEFAULT and SHOW PROCESS.


SHOW DEFAULT, however, can only be used in the context of the current process; and SHOW PROCESS /IDENTIFICATION does not show the information for "Default file spec" if the process specified is not the one issuing the command.


I did not want to give the user access to ANALYZE/SYSTEM, and furthermore, neither the SHOW PROCESS nor the CLUE PROCESS commands within SDA return the desired information in a neatly fashion.


So, I embarked on a mission to offer the programmer a new DCL command, a sort of SHOW DEFAULT /IDENTIFICATION.


Developing the utility QDDDS

I needed a name for the utility. At first, DDDS came to mind, but since the name of all my utilities begin with Q, I decided on QDDDS (Query Default Device & Directory Specification).


Command line parsing

The user interface should be kept simple. At first only one command line parameter was allowed: the identification of the process. Looking at the DCL command SHOW PROCESS, however, I thought a utility with the necessary DCL bells and whistles had to be developed.

Writing a .CLD-file is not that complicated, and I decided that the utility would accept one parameter and a number of qualifiers:

  • a process name as (optional) parameter;
  • /IDENTIFICATION qualifier to be able to indicate a process outside of the own UIC-group;
  • /DEVICE qualifier to show the default device specification;
  • /DIRECTORY to show the default directory specification.

Some other qualifiers were also added to provide the bells and whistles:

  • /HELP to display information about the utility from within the utility;
  • /VERSION to display the version of the executable;
  • /OUTPUT to be able to capture the information provided into a file;
  • /SYMBOLS to store the information in DCL symbols.

Interpretation of the rules described in a .CLD-file, can only be done in the program if the command is added to the command tables by SET COMMAND, and the program is then invoked by its verb. If the program is invoked using "RUN image", using the MCR command, or as a foreign command, parsing the command line is not that straightforward.


Fortunately, I already had written code that allows a program to be invoked as a real DCL command, as a foreign command or using the DCL RUN command, or even using the MCR command, and still parse and interpret the command line the DCL way, using the CLI facility. So, I borrowed the command line processing from the QDSB utility.


The routines rely on inclusion of the command definition as an object module in the image, and also rely on the undocumented system service SYS$CLI.

SYS$CLI accepts 3 arguments, first of which is a "clidef1" structure. A byte in this structure (cli$b_rqstat) tells how the program is invoked. A quadword (cli$q_rqdesc) describes (as a descriptor) the command used to invoke the program. The condition value returned by SYS$CLI should be checked (odd is good, and also value 196608, which is actually CLI$_NORMAL minus 1, and indicates that "RUN" has been used).


vl_clidef1_r.cli$b_rqtype = CLI$K_GETCMD; vl_sts = sys$cli ( &vl_clidef_r, 0, 0 ); if (( vl_sts & 1 ) || ( vl_sts == ( CLI$_NORMAL – 1 ))) {     vl_how = vl_clidef1_r.cli$b_rqstat;    vl_sts =     str$copy_dx ( &vl_cmd_d, &vl_clidef1_r.cli$q_rqdesc ); }


Once you got hold of the command line, it should be prepared for parsing. If the program is invoked by MCR, the command line returned by SYS$CLI also contains the name of the program. The system service SYS$FILESCAN can be used to search for the full file specification. It can then be stripped from the command line. What remains of that command line are the parameters and the qualifiers.


vl_valuelst_r[0].code = FSCN$_FILESPEC; vl_sts = sys$filescan ( &vl_cmd_d, &vl_valuelst_r, 0, 0, 0 ); if ( vl_sts & 1 ) {     vl_cmd_d.dsc$w_length -= vl_valuelst_r[0].len;     vl_cmd_d.dsc$a_pointer += vl_valuelst_r[0].len; }


If the program is invoked using the technique of the foreign command, only the parameters and qualifiers are returned by SYS$CLI.


So, in both cases, we prefix the QDDDS command verb in front of the (modified) command line, and let CLI$DCL_PARSE do the job.


In case the program is invoked as a real DCL command verb (by first doing SET COMMAND QDDDS, and then QDDDS), all this command line manipulation is not necessary, and we can go about our business.


I decided not to support the "RUN image" option.


Command line interpretation
To see what the user wants, I had to write the code to interpret the command line.


Based on the .CLD-file, I had to support one parameter (the process name), and several qualifiers.


To simplify things, global variables were used to indicate the presence (or absence) of the various entities. For the (value of the) parameter a descriptor was provided, as was for the (optional) value specified with the /IDENTIFICATION or /OUTPUT qualifier.


The presence (or absence) of the various entities can be easily checked with the CLI$PRESENT routine. The routine CLI$GET_VALUE is used to obtain the value specified for certain items.


vl_sts = cli$present ( &vl_ent_d );
switch ( vl_sts )
{
    case CLI$_ABSENT:
        vl_abs = 1;
        vl_sts = CLI$_NORMAL;
        break;
    case CLI$_DEFAULTED:
        vl_def = 1;
        vl_sts = CLI$_NORMAL;
        break;
    case CLI$_NEGATED:
        vl_neg = 1;
        vl_sts = CLI$_NORMAL;
        break;
    case CLI$_PRESENT:
        vl_prs = 1;
        vl_sts = CLI$_NORMAL;
        break;
    default:
break;
}


The (value of the) parameter identifies the target process. Also supported to identify the target process, is the /IDENTIFICATION qualifier. If neither the parameter, nor the /IDENTIFICATION qualifier is specified, I assumed that the current process should be the target.


Getting to work

The processing of the bells and whistles stuff, I borrowed from another utility I wrote (QJPI).


Providing information (the /HELP qualifier) relies on the presence of a help library, in this case QDDDS.HLB, and uses the LBR$OUTPUT_HELP routine to do the job.


To show the version of the utility, I decided to define a global symbol in a LINK-er OPTIONS file with a numeric value that could be interpreted as a string. I used the symbol name QDDDS_VRSN and the value %x312E3056, which, interpreted as a string, reads (from right to left) as "V0.1".


I also found it appropriate to give the utility its own condition values by incorporating a compiled .MSG-file in the program. By doing so I could use the LIB$SIGNAL routine to display a neat message telling the user what version of QDDDS she or he is running.


globalvalue int QDDDS_VRSN;
globalvalue int QDDDS_VERSION;


/*...*/


int vl_m;
int vl_v;
struct dsc$descriptor_s vl_vrsn_d;


vl_m = QDDDS_VERSION;
vl_v = QDDDS_VRSN;


vl_vrsn_d.dsc$w_length = sizeof ( vl_v );
vl_vrsn_d.dsc$b_dtype = DSC$K_DTYPE_T;
vl_vrsn_d.dsc$b_class = DSC$K_CLASS_S;
vl_vrsn_d.dsc$a_pointer = (char *) &vl_v;


lib$signal ( vl_m, 1, &vl_vrsn_d );


To support the /OUTPUT qualifier, I had to decide what to show. Having a look at how SHOW PROCESS displays things, I went for a somewhat similar presentation.


$ SHOW PROCESS/IDENTIFICATION=0000D230

22-JUL-2011 10:57:41.36    User: SYSTEM    Process ID: 0000D230         Node: SAM     Process name: "DTWM"

Terminal:
User Identifier:    [SYSTEM]
Base priority:    4
Default file spec:    Not available
Number of Kthreads: 1

Soft CPU Affinity: off
$
$ QDDDS/IDENTIFICATION=0000D230
   22-JUL-2011 10:58:01.04
   Process ID:    0000D230
   Process name:    "DTWM"    Default device:    SYS$SYSROOT:
   Default directory:    [SYSMGR]
$


As with most utilities, omitting /OUTPUT, or not specifying a value for this qualifier, should cause the output to be written to the current SYS$OUTPUT device. Specifying a partial file specification as a value to /OUTPUT should also pose no problem. Therefore, I decided to use RMS while processing this qualifier.


The program acts as if the output is in any case written to a file. The RMS data structures are initialized thus that in whatever circumstance the output is written to the intended device or file. Fortunately, (re)opening SYS$OUTPUT using SYS$CREATE is no problem.


Now, one of the last minute whistles added to the program, is the /SYMBOLS qualifier.


I decided to use the same conventions as with the QJPI utility. If /SYMBOLS was specified, the program would have to create some local symbols of which the name should begin with the string "QDDDS$" and end with a string somehow hinting at what the value of the symbol represents. I went for:


  • QDDDS$DEVICE
    to hold the default device specification
  • QDDDS$DIRECTORY
    to hold the default directory specification
  • QDDDS$PID
    to hold the process identification
  • and QDDDS$PRCNAM
    to hold the process name.

To create the symbols, the program calls the LIB$SET_SYMBOL routine.


To obtain the important data (default device and directory specification) and to display the information, I needed the process name, and the process identification. To get hold of either of these data, I used the SYS$GETJPI system service (with appropriately initialized item list(s)).


Using this system service had the by-product that no extra code had to be written to check for the necessary privileges. SYS$GETJPI fails if you do not have the GROUP privilege and you want to look at another process (than your own) in the same group. It also fails if you want to look at another process outside of your group, and you do not have the WORLD privilege.


Now that all trivial things were put behind us, I could concentrate on retrieving the information the programmer initially asked for.


In order to test my utility, I decided first to throw everything I already wrote together, and postpone the difficult stuff to later. Therefore, I wrote two routines ("QDDDS_GetDDevice" and "QDDDS_GetDDirectory") to hide my ignorance and indecisiveness, incorporated them in the utility and started testing.


The API

Once the initial testing completed, I returned to investigating how to retrieve the important information.

Going through the OpenVMS source listings, I found that SHOW DEFAULT translates the logical name "SYS$DISK", and calls the system service SYS$SETDDIR. This system service can be used to change or obtain the current default directory specification. It therefore accesses a location within the PIO data cells (process I/O segment), specifically PIO$GT_DDSTRING.


In order not to have to recompile (parts of) QDDDS, and to avoid re-linking it, I created a shareable image containing two public entry points "QExt_TrnLnm" and "QExt_GetDDir".


The first routine would be used to translate "SYS$DISK" while the latter would access the data cell PIO$GT_DDSTRING to retrieve the default directory specification.


The initial version of "QExt_GetDDir" prepared some data structures, and called the system service SYS$CMKRNL to transfer control (in kernel mode) to the routine "_qext_getddir". That routine first converted its argument "epid" (the extended process identification) into an "ipid" (internal process identification), and then called the barely documented routine EXE$READ_PROCESS to obtain what was stored in PIO$GT_DDSTRING.


vl_astcnt = 0;
vl_ipid = exe_std$cvt_epid_to_ipid ( vl_epid );
vl_sts = exe$read_process (
   vl_ipid, PQB$S_DDSTRING, &PIO$GT_DDSTRING
   , &vl_ddstring[0], EACB$K_MEMORY, &vl_astcnt
   );


Note that the PIO$GT_DDSTRING denotes a counted string, so the first byte contains the length.


This approach seemed satisfactory, but implied that the user needed the CMKRNL privilege, or that the shareable image was installed with the CMKRNL privilege.


I wasn’t going to bother with this, since I had to figure out how to find the equivalence name of the logical name "SYS$DISK" for another process. This logical name is defined in the process private logical name table LNM$PROCESS_TABLE.


I decided to have a look at how the command CLUE PROCESS/LOGICAL within ANALYZE/SYSTEM works. Unfortunately, the method used by ANALYZE/SYSTEM to display the logical names for a process, couldn’t readily be used in QDDDS. So, I had to think of something else.


But, first things first, I had to get rid of the SYS$CMKRNL call in the shareable image. This proved to be easy by creating a privileged shareable image, containing so-called user-written system services.


System services

Since I already had some experience in writing system services for OpenVMS VAX, it seemed not that difficult to do the same for OpenVMS Alpha or OpenVMS I64.


I started with the privileged library vector block. The definition on OpenVMS Alpha was quite different from what it was on OpenVMS VAX.


struct _plv {
   unsigned int plv$l_type;
   unsigned int plv$l_version;
   unsigned int plv$l_kernel_routine_count;
   unsigned int plv$l_exec_routine_count;
   void *plv$ps_kernel_routine_list;
   void *plv$ps_exec_routine_list;
   int (*plv$ps_kernel_rundown_handler)();
   int (*plv$ps_exec_rundown_handler)();
   int (*plv$ps_rms_dispatcher)();
   int *plv$ps_kernel_routine_flags;
   int *plv$ps_exec_routine_flags;
};

Fortunately, the necessity of having specific dispatcher code completely disappeared.


In order to be somewhat flexible, I decided to have eight kernel mode services, and eight executive mode services. And, since I started from existing OpenVMS VAX code, the definition of the privileged library vector is the only thing written in Macro.


The first system service (_qext_getddir) didn’t require much effort. A simple revamping of the routine of the same name I already mentioned above, sufficed.


The second one (_qext_trnlnm) proved to be quite a different cup of tea. My investigation into ANALYZE/SYSTEM was unsuccessful; as was my initial search on the web.


The idea of searching for the data structures defining the LNM$PROCESS_TABLE of the target process, and then finding the equivalence name for SYS$DISK, I discarded.


The only solution seemed to force the target process to translate SYS$DISK and to make that translation somehow available to the process executing the QDDDS utility.


In the good old days of the VAX, when I took the course VAX/VMS System Programming, we wrote code that was meant to execute in the context of another process. During my professional life as a system manager, I hadn’t had the need of writing such code on OpenVMS VAX, let alone on OpenVMS Alpha or OpenVMS I64. But could it be done?


The information I found in the OpenVMS Programming Concepts Manual was not very promising:


  • "On OpenVMS VAX systems, a system programmer must sometimes develop code that performs various actions (such as performance monitoring) on behalf of a given process, executing in that process's context. To do so, a programmer typically creates a routine consisting of position-independent code and data, allocates sufficient space in nonpaged pool, and copies the routine to it. On OpenVMS VAX systems, such a routine can execute correctly no matter where it is loaded into memory.
  • On OpenVMS Alpha systems, the practice of moving code in memory is more difficult and complex. It is not enough to simply copy code from one memory location to another. On OpenVMS Alpha systems, you must relocate both the routine and its linkage section, being careful to maintain the relative distance between them, and then apply all appropriate fixups to the linkage section.

The OpenVMS Alpha system provides two mechanisms to enable one process to access the context of another:


  • Code that must read from or write to another process's registers or address space can use the EXE$READ_PROCESS and EXE$WRITE_PROCESS system routines, as described in Section 4.7.1.
  • Code that must perform other operations in another process's context (for instance, to execute a system service to raise a target process's quotas) can be written as an OpenVMS Alpha executive image, as described in Section 4.7.2."
  • Mechanism 1 (the EXE$READ_PROCESS path) I already used for the default directory specification, but I could not use that for the translation of SYS$DISK. So, the only way to go was to develop an executive image, aka an execlet.

The execlet

I must admit that reading through the section 4.7.2 of the OpenVMS Programming Concepts Manual did not help me much. I needed an example.


In search of examples

Google is your friend, as was AltaVista in the second half of the 1990s.


Searching for "execlet", I found a .PDF-file ("portable document format", not "product description file") containing what seemed to be a presentation by John R. Covert, he had given at an "IT Symposium" probably organised by Decus Germany. It contained some snippets of code related to "Execlets in C geschrieben".


Searching for "vms execlet", I found on the old stale "HP OpenVMS Ask the Wizard" site, an interesting answer to the exact same question I had: "How can I force another process to execute a particular routine?" (http://h71000.www7.hp.com/wizard/swdev/execlet.html), probably written by Stephen Hoffman ("Hoff"). So, it seemed that the SCH$QAST routine was not made obsolete on OpenVMS Alpha after all.


In the group comp.os.vms, I found a thread started by Brian Schenkenberger ("am I the only one hacking VMS on Itanium?"), where in the answer of Ian Miller, I found a link to Ian’s "MBMON" package.


Studying Ian’s code, I felt compelled to taking his approach:

  • Writing a control program to load or unload the execlet (instead of using SYSMAN);
  • Using the mandatory initialization routine to set up and initialize a data structure;
  • Using the (undocumented) routine LDR$REF_INFO to obtain the necessary code entry points (through the previously initialized data structure);
  • And finally, writing a routine that forced the target process to translate a logical name and send the result back.

Control program

After reading up on executive loadable images in the comments of the examples I already downloaded from the net, I realised I definitely needed a program to load and unload the execlet, thus avoiding the use of SYSMAN, and many reboots of my AlphaStation.


Starting with the source code of the loader program Ian had provided with his "MBMON" package, I soon realised I had to devise the control program from scratch. I decided to write a command line utility akin to SYSMAN, that recognized and processed the commands LOAD and UNLOAD.


I incorporated (a modified version of) the command line parsing code from QDDDS into this control program that I named QEXTCP. In contrast with QDDDS, the QEXTCP program should also support being invoked by the RUN command. Furthermore, it should prompt the user should no command be given directly. Therefore, if no command is given, the QEXTCP utility forces itself into a loop around calls to CLI$DCL_PARSE and CLI$DISPATCH. The latter routine will transfer control to the appropriate subroutine within QEXTCP if a command is recognized.


while ( vl_sts != RMS$_EOF )
{
    vl_sts = cli$dcl_parse (
      &vl_cmd_d, &QEXTCP_CLD
      , lib$get_input, lib$get_input
      , &vl_prmt_d
    );
   if ( vl_sts & 1 )
   {
       vl_sts = cli$dispatch ();
   }
}


Note that (the address of) LIB$GET_INPUT is specified for both the "parameter" routine and the "prompt" routine.

Loading the execlet

According to the OpenVMS Programming Concepts Manual (dated June 2002),


  • "An executive image can contain one or more initialization procedures that are executed when the image is loaded."
  • and

  • "Because an initialization routine is executed when the executive image is loaded, it serves as an ideal mechanism for locating the callable routines of an executive image in memory."

Then some guidelines are given on how this can be achieved.


This information has been omitted from the HP OpenVMS Programming Concepts Manual (dated January 2005).


So, the execlet needed an initialization routine.


Based on Ian’s code, I wrote a simple function (__qext_init) that initialized a data block, and returned through its arguments the necessary data to its caller.


I also wrote a third system service that I used to invoke the LDR$LOAD_IMAGE routine. By doing so, I could not only make sure that the execlet was loaded, but also obtain the necessary information about it.


The LDR$LOAD_IMAGE routine expects three or four arguments, depending on the value of its second argument. The first argument is the name of the executive image for which I specified a logical name, thus avoiding the need of putting the file in SYS$LOADABLE_IMAGES. The second argument is a bit vector. I made sure that LDR$V_UNL and LDR$V_USER_BUF were set, indicating that the image may be removed from memory, and that (the address of) the user buffer (the fourth argument) should be passed to the executive image's initialization routine.


The third argument to LDR$LOAD_IMAGE is the address of a reference handle, a three-longword buffer that, upon successful completion of the routine, contains the address of the loaded image in the first longword, the address of a loaded image data block (LDRIMG) in the second longword, and a sequence number in the third longword.


struct _qx_vec {
   unsigned int qv_vrsn_min;
   unsigned int qv_vrsn_maj;
   unsigned int qv_cnt;
   unsigned int qv_filler_0;
   unsigned int (*qv_rtn_00) ( void * );
};
typedef struct _qx_vec t_qx_vec;

unsigned int vl_refhdl[3];
LDRIMG vl_ldrimg_r;
t_qx_vec vl_usrbuf;
unsigned int vl_flags = LDR$M_UNL | LDR$M_USER_BUF;

/*...*/

vl_sts = ldr$load_image (
      &vl_fn_d, vl_flags, &vl_refhdl[0], &vl_usrbuf    );
if ( vl_sts & 1 )
{
/*...*/
}


The call to LDR$LOAD_IMAGE triggers the initialization routine of the execlet, and there the necessary initialization is done to tell where the callable routines are located.


t_qx_vec *vl_vec_p;

vl_vec_p = &v_QX_Vec;
vl_vec_p->qv_vrsn_min = QXLT_MINOR_ID;
vl_vec_p->qv_vrsn_maj = QXLT_MAJOR_ID;
vl_vec_p->qv_cnt = 1;
vl_vec_p->qv_filler_0 = 0;
vl_vec_p->qv_rtn_00 = __qext_dispatcher;


*a_vp = vl_vec_p;

To complete the first version of the control program, I had to code the unload functionality. The information about the LDR-routines in the HP OPENVMS PROGRAMMING CONCEPTS MANUAL, specifically about LDR$UNLOAD_IMAGE, left me puzzled.


On the one hand, the LDR$LOAD_IMAGE returns through its argument "ref_handle" the necessary info about the execlet. On the other hand the LDR$UNLOAD_IMAGE expects this information to perform the unload operation. So, in between calling the routines, I had to keep the information stored in the "ref_handle" safe.


Fortunately, Ian’s code showed that there was no need for safekeeping that information. It could be obtained through an undocumented LDR-routine namely LDR$REF_INFO. That routine was documented in the article I found on the "HP OpenVMS Ask the Wizard" site.


vl_sts = ldr$ref_info ( &vl_fn_d, &vl_refhdl[0] );
if ( vl_sts & 1 )
{
   );vl_sts = ldr$unload_image ( &vl_fn_d, &vl_refhdl[0] );
}


This hurdle taken, up to the next.

Invocation

Now that I was able to load and unload the execlet, I could concentrate on writing code that actually called routines hidden in the execlet.


Inside the routine calling LDR$LOAD_IMAGE, I had all information I wanted since (the initialization code of) the execlet passed the address of the "dispatch"-routine through the "user buffer" argument. How to obtain that same information, once the execlet well and truly loaded?


I still had some uncharted territory to explore, and from one of the slides of John’s presentation I learned that the "ref_handle" argument could be interpreted as much more than a three-longword buffer.


The "ref_handle" is also returned by LDR$REF_INFO, so I started investigating.


According to the OpenVMS Programming Concepts Manual, the second longword of the "ref_handle" argument to LDR$LOAD_IMAGE is


  • "Address of loaded image data block (LDRIMG). See the $LDRIMGDEF macro definition in SYS$LIBRARY:LIB.MLB and VMS FOR ALPHA PLATFORMS INTERNALS AND DATA STRUCTURES for a description of the LDRIMG structure."

After extracting the module LDRIMGDEF from SYS$LIBRARY:SYS$LIB_C.TLB, and studying both the "struct _ldrimg" and Ian’s code in MBMON, I noticed that via the item "ldrimg$l_nonpag_w_base" the exact same data structure, that the initialization routine passed to the loader code, could be found.


Once that figured out, it wasn’t difficult to write some code that sort of mapped (references to) the execlet.


struct _ldr_refhdl {
   void *lr_addr;
   LDRIMG *lr_lid;
   long lr_seqnum;
};
typedef struct _ldr_refhdl t_ldr_refhdl;

t_qx_vec *vl_vec_p;
t_ldr_refhdl vl_rh;

vl_sts = ldr$ref_info ( &vl_fn_d, &vl_rh );
if ( vl_sts & 1 )
{
   vl_vec_p = (t_qx_vec *) vl_rh.lr_lid->ldrimg$l_nonpag_w_base;

   /*...*/
}


From that point on it wasn’t difficult to find the "dispatch"-routine.


I decided on using a "dispatch"-routine that accepted only one argument, the address of a data block, so I had the opportunity to add additional code without changing the framework.


Executing code in the context of another process

To write the code that forces another process to translate a logical name (and send the result back), I studied the routine EXE$READ_PROCESS in module PROC_READ_WRITE.


The logic of EXE$READ_PROCESS seemed quite simple:

  • perform some basic verifications;
  • allocate and initialize an ACB;
  • queue a special kernel mode AST to the target process;
  • set up a timer;
  • wait for the response (from the target process) or a timeout.

It looked simple enough, and proved to be not that difficult to code this in C. But I had to provide a routine of my own as kernel AST-routine to be executed by the target process.


That routine should have access to the logical name to be translated, the logical name table, and should be provided with ample storage to return the equivalence name. All this could be done by extending the ACB (AST Control Block) and storing the necessary information in the extension, since the address of the ACB is passed to the AST-routine.


Furthermore, once the translation of the logical name performed, the routine had to send the result back. This could also be done by queuing a kernel AST to the requesting process.


To allocate an (extended) ACB, I used the routine EXE_STD$ALONONPAGED to make sure it resided in nonpaged pool. To queue the kernel mode AST to the target process, the address of the AST routine was recorded in the ACB$L_KAST field of the ACB, and a call to SCH_STD$QAST was made.


The system service SYS$SETIMR was used to set up a timer, and SYS$HIBER was called to wait for the outcome.


The end seemed to be in sight, I only had to code the AST-routine. I had made sure the routine had access to all necessary data, and had ample space to store the result of translating a logical name.


Since I had taken the necessary precautions, I was pretty sure that all data and data areas were accessible for reading and writing from kernel mode. So, instead of calling the system service SYS$TRNLNM, I decided to go for LNM_STD$SEARCH_ONE (the latter routine does not do any type of argument verification, and is therefore a little bit faster).


vl_am = PSL$C_USER;
vl_sts = lnm_std$search_one (
   vl_acb_p->_lnl /* length of logical name */
   , &vl_acb_p->_lns[0] /* addr. of logical name */
   , vl_acb_p->_lntl /* length of logical name table */
   , &vl_acb_p->_lnts[0] /* addr. of logical name table */
   , (void *) vl_pcb_p /* addr. of PCB */
   , vl_am /* access mode */
   , &vl_acb_p->_wrk[0] /* addr. of equivalence string */
   );


Once this call succeeded, I could simply queue the (extended) ACB back to the requesting process.


That second AST routine took care of copying the necessary data so that the original caller got what she or he wanted.


Map files
While testing the software on my AlphaStation, I didn’t heed the LINK-er OPTIONS files much. The one I used to link the execlet was based on MBMON_EXECLET.OPT from the MBMON package, and that proved to be all right.


Reading up on executive loadable images, however, I started experimenting with the COLLECT option, to group program sections, and with PSECT_ATTR option to force the attributes to match, to end up with as few image sections as possible.


The OPENVMS PROGRAMMING CONCEPTS MANUAL is very clear on this:

"An executive image can contain at most one image section of the following types and no others:

  • Nonpaged execute section (for code)
  • Nonpaged read/write section (for read-only and writable data, locations containing addresses that must be relocated at image activation, and the linkage section for nonpaged code)
  • Paged execute section (for code)
  • Paged read/write section (for read-only and writable data, locations containing addresses that must be relocated at image activation, and the linkage section for pageable code)
  • Initialization section (for initialization procedures and their associated linkage section and data)
  • Image activator fixup section"

Studying the map files generated by the LINK-er is a great help and I soon found out that the /ATTRIBUTES=RESIDENT qualifier (and value) was necessary to create image sections that would be mapped in nonpaged pool by the image loader; the /ATTRIBUTES=INITIALIZATION_CODE qualifier (and value) was necessary for those image sections that contain code that should execute when the image is loaded.


Privileges and problems

I know I shouldn't be doing this, but I usually test my software with most, if not all, privileges enabled.


Nearing the end of this little project, I had to write the command procedures to install, set up and enable the use of the software. From previous experience I knew that CMKRNL and maybe SYSNAM were necessary to install the software, but what about setting up the software?


Since I am in the habit of putting all files related to one of my utilities in one directory, and defining a logical name to point to that directory in the system logical name table, all "start-up" command procedures should check if the process had the SYSNAM privilege enabled.


The shareable image, the privileged shareable image and the executive loadable image needed "setting up" also. Nothing special needed to be done for the shareable image. The privileged shareable image (the one with the system services) had to be INSTALL-ed. This kind of operation required the CMKRNL privilege. To use the executive image, it had to be loaded. Trial and error learned that loading or unloading the execlet also required CMKRNL.


Last, but not least, the user utility QDDDS. I already knew that the $GETJPI-call needed GROUP or WORLD depending on the relation between the requesting and the target process. Fortunately no other (exotic) privileges were necessary (again found out by trial and error).


Taken care of the privileges, and having a working version of QDDDS on OpenVMS Alpha, it was time to test the software on OpenVMS I64. I rigged up an rx2600, and started compiling the sources.


Everything compiled rather smoothly, until the build job hit the QEXT_PRV.C source file. That file contained the source code for the system services. The C compiler complained


  • "%CC-E-NEEDMEMBER, In this statement, "ldrimg$l_nonpag_w_base" is not a member of "vl_rhp->lr_lid"."
  • This was to be expected. Although the following is stated in PORTING APPLICATIONS FROM HP OPENVMS ALPHA TO HP OPENVMS INDUSTRY STANDARD 64 FOR INTEGRITY SERVERS, January 2005:

  • "HP intends to maintain a strict policy of ensuring forward source compatibility so that ‘‘well-behaved’’ applications that currently run on recommended versions of OpenVMS Alpha run successfully on OpenVMS I64 systems. If an application takes advantage of published system services and library interfaces, there should be a high level of confidence that the application will move without modifications to the latest version of OpenVMS I64."

But above citation does not apply to code that has "knowledge about the calling standards" and "knowledge about image format".


Extracting module LDRIMGDEF from SYS$LIBRARY:SYS$LIB_C.TLB on both OpenVMS Alpha and OpenVMS I64, and comparing the definition of the "struct _ldrimg" revealed substantial differences.


Fortunately, John Covert hinted at where the PSECTs could be found when on OpenVMS I64. In the "struct _ldrimg" a pointer (ldrimg$l_segments) to image section descriptors is present. Starting with this pointer, it is possible to find the PSECT that contains the data structure that we found on OpenVMS Alpha through the "ldrimg$l_nonpag_w_base" item. During testing it appeared to be pointed at by the second element of the ISD array. But how to be sure about that?


Running out of time, and not at all tempted to investigate any further, I decided to add some extra elements to the "struct _qx_vec", initialize them with appropriate values in the execlet's initialization routine and add code to check these elements when referencing the execlet.


struct _qx_vec {
unsigned int qv_finger_print; /* added for I64 */
unsigned int qv_ident; /* added for I64 */
unsigned int qv_vrsn_min;
unsigned int qv_vrsn_maj;
unsigned int qv_cnt;
unsigned int qv_filler_0;
unsigned int (*qv_rtn_00) ( void * );
};
typedef struct _qx_vec t_qx_vec;


/*…*/

vl_vec_p = &v_QX_Vec;
vl_vec_p->qv_finger_print = QXLT_FINGER_PRINT; /* added for I64 */
vl_vec_p->qv_ident = QXLT_IDENT; /* added for I64 */
vl_vec_p->qv_vrsn_min = QXLT_MINOR_ID;
vl_vec_p->qv_vrsn_maj = QXLT_MAJOR_ID;


The code that found the "dispatch"-routine following the call to LDR$REF_INFO was conditionalized.


vl_sts = ldr$ref_info ( &vl_fn_d, &vl_rh );
if ( vl_sts & 1 )
{
#if __alpha
   vl_vec_p = (t_qx_vec *) vl_rh.lr_lid->ldrimg$l_nonpag_w_base;
#elif __ia64
   LDRISD *vl_ldrisd_p;
   vl_i = (int) vl_rh.lr_lid->ldrimg$l_segcount;
   vl_ldrisd_p = vl_rh.lr_lid->ldrimg$l_segments;
   do {
      vl_vec_p = (t_qx_vec *) vl_ldrisd_p->ldrisd$p_base;
      if ( vl_vec_p->qv_finger_print == QXLT_FINGER_PRINT )
      {
      if ( vl_vec_p->qv_ident == QXLT_IDENT )
      {
      break;
      }
   }
   vl_ldrisd_p++;
    vl_i--;
   }
   while ( vl_i );
/*...*/
#else
#error Architecture not supported
#endif
}


After these necessary alterations, the code compiled cleanly, but now the LINK-er wasn't so happy about the OPTIONS file to link the execlet. So, based on the .MAP-files generated by that same LINK-er, I also conditionalized the OPTIONS file for the execlet.


Now that everything was compiled and linked on Integrity, I didn't expect any problems while testing. This proved to be a miscalculation. Everything seemed OK, but when I turned the privileges off, I got


— "%SYSTEM-F-PAGOWNVIO, page owner violation"


Pondering on the meaning of this message, I saw the proverbial light bulb. Could it be that when locking data in the working set, I somehow tripped over a feature in OpenVMS I64? When asking for the translation of "SYS$DISK", the "QDDDS_GetDDevice" routine defines two descriptors thus:


$DESCRIPTOR ( vl_lnm_d, "SYS$DISK" );
$DESCRIPTOR ( vl_lnt_d, "LNM$FILE_DEV" );


These variables are passed as arguments to "QExt_TrnLnm". This routine (before calling the system service _qext_trnlnm) attempts to lock its arguments into the working set, and there it went wrong. The call to SYS$LKWSET returned 492.


I know that it is advisable to replace all calls to SYS$LKWSET with calls to LIB$LOCK_IMAGE, I didn't bother because in Porting Applications from HP OpenVMS Alpha to HP OpenVMS Industry Standard 64 for Integrity Servers is stated:


— "As of OpenVMS Version 8.2, the SYS$LKWSET and SYS$LKWSET_64 system services test the first address passed in. If this address is within an image, these services attempt to lock the entire image in the working set. If a successful status code is returned, the program can increase IPL to higher than 2 and without crashing the system with a PGFIPLHI bugcheck."


I didn't want to spend much time in investigating this problem. Instead I modified the "QExt_TrnLnm" routine so that it first copied its arguments to local variables before locking these in the working set and then calling the system service _qext_trnlnm. This seemed to do the trick, no more violation of the page owner.


Installation and configuration

Now that the first version was virtually finished, I modified a KITINSTAL.COM from one of my other utilities, and created an installation saveset.


After installing the utility on various test and development systems, I integrated the "start-up" procedures in SYSTARTUP_VMS.COM, and provided the programmers with the "set up" command procedures so that they could find out the current working directory of a process other than their own.


Summary

Well, a simple question it was (what’s the default device and directory specification of a process?).


It led to the creation of a new command QDDDS, a utility to process the command and its options, a shareable image to support the utility, a privileged shareable image with a set of so-called user-written system services, and even a loadable executive image, aka an execlet. Along the way, a control program to load and unload the execlet was thrown in.


Several domains of the OpenVMS operating system were touched: command line parsing, help libraries, logical names, kernel mode programming, internal data structures, etc. Even architectural differences sprung up.


Readers who want to study this material in more detail can obtain the sources upon simple request (qkcl@quadratrix.be).