$ lib/extract=$PRVDEF/out=prvdef.mar sys$library:starlet.mlb
$ open infile prvdef.mar

	Then a template is created which copied to the start of 
	PRCPRV_CLI.MAR.

$ create prcprv_cld_mar.mar
$ deck/dollars="$EOD"
; PRCPRV_CLI_TEMPLATE.MAR	- Ehud Gavron -		20-Sep-1991
.
.
	This template file contains a macro which is pased a keyword
	(name of a privilege) plus a symbolic value of the bit location
	in the quadword privilege mask.  It then generates a piece of
	code which sets the appropriate privilege bits, or all of them
	if the symbolic value is 64 (PRIV=ALL or NOALL).  It changes
	two masks, ENBMSK and DISMSK which are used by the main code
	to determine which privileges to enable and disable.

.macro cli_check keyword,symbol ?l ?l2 ?l3 ?l4 ?a

	keyword is the name of the privilege (eg. ALLSPOOL)
	symbol is the symbolic bit location (eg. 4)
	l, l2, l3, and l4 are labels for internal branches
	a is a label to a descriptor constructed by the macro which
	  contains the keyword.
	
	First it checks if the keyword is present:
	
    pushal	a				; Push address of keyword desc
    calls	#1,g^cli$present		; Is it here?
    cmpl	r0,#cli$_present		; Yes?
    beql	l				; If so goto l

	If the keyword is not present in affirmative form, it
	checks to see if it is negated:

    cmpl	r0,#cli$_negated		; Is it negated?
    bneq	l2				; No - not here, exit

	Then it checks to see if the negation is for the ALL keyword
	(symbolic value 64)

    cmpl	#symbol,#64			; Is ALL negated?
    bneq	l4				; no, something else is

	Since ALL is being negated (NOALL) then the entire privilege
	disable mask is set:

    mnegl	#1,(r4)				; negate all of dismsk
    mnegl	#1,4(r4)
    brb		l2				; and exit

	L4 is the branch when a specific privilege is being negated.
	We negate the right bit and exit:

l4: bbss	#symbol,(r4),l2			; Something is negated
    brb	   	l2				; so do it

	L is the branch when something is being affirmed.
	We first check to see if it was ALL:

l:  cmpl	#symbol,#64			; is ALL here?
    bneq	l3				; no, something is here

	ALL is being set, so we set all bits in the enable mask:

   mnegl	#1,(r5)				; set all bits on
   mnegl	#1,4(r5)			; in enbmsk
   brb	l2				; and exit

	L3 is the branch for affirming a particular privilege.
	We set the appropriate bit and exit:

l3: bbss	#symbol,(r5),l2			; enable something
   brb	l2				; exit

	Here we build the descriptor which includes the
	keyword privilege name:

a: .ascid	/keyword/

	Finally, the branch that all parts of the macro
	end at, the end:
l2:
   .endm

	We then provide a main entry point, and the code to move
	the addresses of both masks into the appropriate registers.
	This will allow the CLI_CHECK macro to be able to set them:

   .entry	parse_keywords,^m<r4,r5>
   movl		4(ap),r4		; address of dismsk
   movl		8(ap),r5		; address of enbmsk
;
; End of template!!!
;
$EOD


	We then open the template for writing to it, and assign
	a channel (OUTMAR) to it:

$ open/append outmar prcprv_cld_mar.mar

	Then we create the template of the command language definition
	file. 

$ create prcprv_cld.cld
$DECK/dollars="$EOD"

	First we define the verbs and the qualifiers

define verb PRCPRV
  qualifier IDENTIFICATION
     value (required)
  qualifier PRIVILEGES
     value (required,list,type=PRIVILEGES)

	Then we define the privileges themselves, only putting in the
	ALL keyword.  The reason is that ALL is a construct used by
	the DCL SET command and is not a true privilege in the sense
	that it has no bit position or mask.

define type PRIVILEGES
   keyword ALL,negatable
$EOD

	We now open a channel to this routine, calling it OUTCLD.
	
$ open/append outcld prcprv_cld.cld

	In order to generate the code, we must now loop through
	INFILE, parsing entries and placing lines in OUTMAR and
	OUTCLD.  First though a special case call for the ALL
	keyword:

$ write outmar "	cli_check ALL,64"

	Then we start the loop:

$rloop:
$ read/end=eof infile line
	
	We trim and convert the line to uppercase, making sure that
	it's a symbol definition line:

$ upcase = f$edit(line,"upcase,compress,trim")
$ if f$element(0," ",upcase) .nes. "$EQU" then goto rloop

	We then extract the symbol name of the format PRV$x_y
	and make sure that we're not using the x="V" symbols.
	This gives us PRV$M_y symbols, which indicate the bit
	position of the privilege.  We do this for greater
	ease, since some privileges reside in the second longword
	and would be difficult to set otherwise.

$ symbol  = f$element(1," ",upcase)
$ under   = f$locate("_",symbol)
$ m = f$extract(under-1,1,symbol)
$ if m .eqs. "M" then goto rloop

	We then get the keyword from using the y part of PRV$M_y.
	We do not use F$ELEMENT(1,"_",symbol) because some of the
	privileges have an underscore in their name (PHY_IO, LOG_IO...)

$ length  = f$length(symbol)
$ keyword = f$extract(under+1,length-under,symbol)
$ value   = f$element(2," ",upcase)
$ write sys$output "Inserting keyword ''keyword'..."

	We place the keyword in the CLD file:

$ write outcld "   keyword ''keyword', negatable"

	We then are ready to put it into the Macro code.  However,
	it is necessary to check if we are dealing with the second
	longword.  If we are, then we should temporarily increment
	the address of the masks such that they will point to the
	second longword:

$ if value .lt. 32 then goto writeit
	
	Value is greater than or equal to 32, indicating the second
	longword.  We get the offset in the second longword:

$ val = value - 32

	Increment the addresses of the masks:

$ write outmar "	addl	#4,r4"
$ write outmar "	addl	#4,r5"

	Call the macro:

$ write outmar "	cli_check ''keyword',''val'"

	And return the addresses to what they were.

$ write outmar "	subl	#4,r4"
$ write outmar "	subl	#4,r5"
$ goto rloop

	If the bit position was in the first longword, we just
	call the macro:

$writeit:
$ write outmar "	cli_check ''keyword',''symbol'"
$ goto rloop
$!

	Finally we terminate all files and close all channels:

$eof:
$ write outmar "	ret"
$ write outmar "	.end"
$ close outmar
$ close outcld
$ close infile

	At the point we have the following files:
		PRCPRV.MAR         - the mainline code
		PRCPRV_CLD.CLD     - our recently created .CLD file
		PRCPRV_CLD_MAR.MAR - our recently created .MAR parser
		
	The assembly and linking then takes place:

$ mac prcprv
$ mac prcprv_cld_mar
$ set command/obj prcprv_cld
$ link prcprv,prcprv_cld,prcprv_cld_mar

	In our code we will parse the command line, get the identification
	and privileges qualifiers, and then build and trigger an AST.

	The code begins by establishing a condition handler:

moval   g^lib$sig_to_ret,(fp)

	We do this because the CLI$ routines will signal errors 
	themselves if no condition handler is set.  As we'd rather
	exit gracefully, lib$sig_to_ret will convert any signal to
	a return status we can check.

	We then get the command line:

pushl   #0                      ; flags
pushal  cmd_desc                ; resultant-length
pushl   #0                      ; prompt
pushal  cmd_desc                ; resultant-string
calls   #4,g^lib$get_foreign

	The cmd_desc descriptor will include all elements of the original
	command line except the original verb or command.  We add this
	manually by concatenating it in front of the line:

pushal  cmd_desc
pushal  cmd_name
pushal  cld_desc
calls   #3,g^str$concat

	We then call CLI$DCL_PARSE which will scan the line and properly
	be able to identify qualifiers and keywords:

pushal  g^lib$get_input         ; prompt_routine
pushl   #0                      ; param_routine
pushal  prcprv_cld              ; table
pushal  cld_desc                ; command_string
calls   #5,g^cli$dcl_parse

	As the image is called with a foreign command symbol, we must
	manually enforce the required qualifier (/PRIVILEGES):

pushal  priv_desc               ; entity_desc
calls   #1,g^cli$present
check
	
	Check is used here not only to make sure the call completed
	successfully, but also to make sure the returned status 	
	indicates the qualifier is present. 

	We then get the /IDENTIFICATION qualifier value:
pushal  pid_desc                ; retlength
pushal  pid_desc                ; retdesc
pushal  ident_desc              ; entity
calls   #3,g^cli$get_value

	That value is in the form of a string descriptor containing
	a hexadecimal number (eg "22003344").  We need to convert
	this to an unsigned longword:
pushal  pid_long                ; output-value
pushal  pid_desc                ; input-string
calls   #2,g^ots$cvt_tz_l

	We then call our pre-built routine to parse the keywords and
	set the privilege enable and disable masks:
pushal  enbmsk
pushal  dismsk
calls   #2,parse_keywords

	Now we're ready to work with the AST.  Since we intend to
	raise IPL (Interrupt Priority Level) to synchronize with
	VMS memory management structures, we need to make sure that
	our code is entirely memory resident.  That way we will not
	incur any page faults at high IPL and thereby crash the system.
	We do this by locking all the code pages into our working set:

lock_adr:                               ; Lock information to lock down the
        .address        lock_start      ; code pages in memory so we don't
        .address        lock_end        ; fault at high IPL

$lkwset_s       inadr=lock_adr

	We then jump into kernel mode to allocate the memory, build
	an ACB (AST Control Block) and queue that AST to our target 
	process:
$cmkrnl_s       routin=prcprv_k,arglst=k_arg

	Inside the kernel mode routine we first label the start of our
	locked pages range and then get the requested PID:
lock_start:
   	jsb  g^exe$nampid

	EXE$NAMPID will raise IPL to 8, acquire the SCHED spinlock,
	and return an internal PID (IPID) in R1.

	We then store that on the stack for later use:
pushl	r1			; Save the IPID for the ACB

	We allocate a region of nonpaged pool to contain both the
	ACB and the code:
movl    #prcprv_ast_size,r1     ; Size of ACB and code
jsb     g^exe$alononpaged       ; Allocate nonpaged dynamic memory
blbc    r0,scram                ; Failed allocation

	We move our entire code in there.  (Note that as MOVC3
	affects general registers 0-5, we save them and restore
	them around it):
pushr   #^m<r0,r1,r2,r3,r4,r5>  ; Save registers across MOVC3
movc3   #prcprv_ast_size,prcprv_ast,(r2)        ; Do the copy
popr    #^m<r0,r1,r2,r3,r4,r5>  ; restore registers

	We then fill in the size and type of the dynamic memory 
	structure we are building (the ACB) so that it can be
	easily deallocated later:
movw	r1,acb$w_size(r2)	; Fill in size for later deallocation
movb	#dyn$c_acb,-		; Set dynamic memory type as an ACB
	acb$b_type(r2) 

	We then plug in the address of the executable code, and
	move the privilege masks into the AST:
moval	<prcprv_ast_code-acb>(r2),- ; Put pointer to code in ACB
	acb$l_kast(r2)
movq	@enbadr(ap),-		; Put enable mask in ast
	<enbmsk_a-acb>(r2)
movq	@disadr(ap),-		; Put disable mask in ast
	<dismsk_a-acb>(r2)

	We then set it up to be a special kernel mode AST:
clrb	acb$b_rmod(r2)		; Clear request mode bits
bisb	#acb$m_kast,-		; And set as a special kernel mode AST
	acb$b_rmod(r2)

	We take the target PID off of the stack and stick it in the ACB:
popl    acb$l_pid(r2)           ; Copy target PID into ACB

	Finally to queue the ast we make R5 point to the ACB, make R2
	contain the priority boost upon AST delivery, and queue it:
movl    r2,r5                   ; R5 must point to ACB for SCH$QAST
movl    #pri$_ticom,r2          ; Boost process priority upon delivery
jsb     g^sch$qast              ; Queue the AST

	That being done, we set a success status:
movl    #ss$_normal,r0          ; Set success status

	Now we release our spinlocks, lower IPL, and preserve R0
	across the call, just in case we got here via the Scram
	label on an earlier error:
scram:
    unlock lockanme=SCHED,-	; Release spinlock and lower IPL
    	   newipl  = (sp)+,-	; (preserve status in case we got here
	   preserve= YES	; as a result of an error)

	Then we provide the termination point for the locked pages, and
	return back to user mode:	
lock_end:
     ret                          

	The AST part is also equally simple.  We allocate enough space
	for the ACB, our two privilege masks (enable and disable masks),
	and then write the AST:
prcprv_ast:
acb:      .blkb   acb$k_length            ; Allocate storage for ACB
enbmsk_a: .blkq   1                       ; Privilege Mask enable quadword
dismsk_a: .blkq   1                       ; Privilege Mask disable quadword
prcprv_ast_code:

	We first call the code to disable privileges listed in the disable
	mask, and then the enable mask.  We do this so that the combination
	of /PRIVILEGES=(NOALL,TMPMBX,NETMBX) will have the desired effect.
	A more correct, yet significantly slower method would be to loop
	through every element in the original privileges keyword list, and
	issue a separate $SETPRV for it.  Instead we just do it for the masks:
$setprv_s       enbflg=#0,prmflg=#1,prvadr=dismsk_a
$setprv_s       enbflg=#1,prmflg=#1,prvadr=enbmsk_a

	Finally we move the pointer to the ACB into R0 and go deallocate
	the chunk:        
movl    r5,r0                   ; Set up pointer for deallocation
jmp     g^exe$deanonpaged       ; Deallocate pool and cease execution

	Note that the JMP at the end also dismisses the AST.  That's
	all it takes to modify another process' privileges.

	All in all, it takes two original files, PRCPRV.MAR and
	BUILD.COM to put together the current VMS version.  If
	a future VMS version introduces new privileges, or changes
	old ones, all that will be needed is a new invocation of
	the command @BUILD.COM.

	The inherent simplicity of VAX MACRO-32 and DCL allow
	building any complex procedure as a superset of simpler
	building blocks.



	.title	prcprv	Set Process Privs (for someone else)
	.ident /V1.1-14Jul92/
;++
;
; I'm tired of trying to demonstrate software, and telling someone to
; run this and that, only to have to give them privs, have them log
; in and out, etc.  Why can't I set the privilegs of another process?
; Well... now I can :-)
;
;	V1.0.0	19-Sep-1991	Ehud Gavron	Created
;				gavron@ACES.COM
;
;	V1.1.0	14-Jul-1992	Ehud Gavron	Modified
;				gavron@ACES.COM
;
;			- Enforce the required /PRIVILEGES qualifier
;			- Gracefully signal invalid keywords in CLI$DCL_PARSE
;			- Cleaned up code moving quadword masks into the AST
;			- Replaced PCB vector table search with EXE$NAMPID call
;			- Cleaned up and fixed inconsistent symbols and names
;			- Removed spaghetti code in kernel mode error handling
;
;--
	.library "sys$library:lib.mlb"			; Give me $xxxdefs
	.link	 "sys$system:sys.stb"/selective_search	; Give me xxx$xx_xxxs

	$pcbdef		;Process Control Block offsets
	$dyndef		;Dynamic Memory types 
	$ipldef		;Interrupt Priority Level
	$acbdef		;AST Control block offsets
	$pridef		;Priority boost classes
	$spldef		;Spinlock types

	.macro	check	?l
	blbs	r0,l
	$exit_s	r0
l:
	.endm	check

cmd_len = 80
cmd_desc:	.long	cmd_len
		.address cmd_buf
cmd_buf:	.blkl	cmd_len

cld_len = cmd_len + 10
cld_desc:	.long	cld_len
		.address cld_buf
cld_buf:	.blkl	cld_len

cmd_name:	.ascid "PRCPRV"
ident_desc:	.ascid "IDENTIFICATION"
priv_desc:	.ascid "PRIVILEGES"

enbmsk:		.blkq	1		; Enable privs mask
dismsk:		.blkq	1		; Disable privs mask
pid_desc:	.ascid	"00000000"	; A PID descriptor.  Filled with 0's
pid_long:	.blkl	1		; Longword PID
k_arg:					; argument list for kernel mode routine
	.long	4			; to pass the PID address, a null for
	.address	pid_long	; process name, and privilege enable
	.long		0		; and disable masks
	.address	enbmsk
	.address	dismsk
lock_adr:				; Lock information to lock down the
	.address	lock_start	; code pages in memory so we don't
	.address	lock_end	; fault at high IPL

	.entry	prcprv,^m<>
;
; Establish a condition handler for CLI$ calls that will return to us
;
	moval	g^lib$sig_to_ret,(fp)
;
; Get the command line
;
	pushl	#0			; flags
	pushal	cmd_desc		; resultant-length
	pushl	#0			; prompt
	pushal	cmd_desc		; resultant-string
	calls	#4,g^lib$get_foreign
	check
;
; Append the newly gotten string to the command name
;
	pushal	cmd_desc
	pushal	cmd_name
	pushal	cld_desc
	calls	#3,g^str$concat
	check
;
; Get DCL to parse it for us
;
	pushl	#0			; prompt_string
	pushal	g^lib$get_input		; prompt_routine
	pushl	#0			; param_routine
	pushal	prcprv_cld		; table
	pushal	cld_desc		; command_string
	calls	#5,g^cli$dcl_parse
	check
;
; Make sure that the PRIVILEGES qualifier was entered
;
	pushal	priv_desc		; entity_desc
	calls	#1,g^cli$present
	check
;
; Now get the process ID we're interested in
;
	pushal	pid_desc		; retlength
	pushal	pid_desc		; retdesc
	pushal	ident_desc		; entity
	calls	#3,g^cli$get_value
	check
;
; Convert from hexadecimal text to binary longword
;
	pushal	pid_long		; output-value
	pushal	pid_desc		; input-string
	calls	#2,g^ots$cvt_tz_l
	check
;
; Parse the privilege keywords to set the masks
;
	pushal	enbmsk
	pushal	dismsk
	calls	#2,parse_keywords
;
; Make sure we don't fault at high IPL by locking down pages in working set
;
	$lkwset_s	inadr=lock_adr
	check
;
; Call routine to allocate memory, put ACB there, and queue AST
;
	$cmkrnl_s	routin=prcprv_k,arglst=k_arg
	$exit_s	r0

;
; Kernel mode routine to do the dirty deeds.  Some of this is based on
; Bruce Ellis' WSBLASTER routine.
;
	.entry	prcprv_k,^m<>
;
; PRCPRV_K - The kernel mode routine to create an AST and queue it to
;	     the target process.
;
; Calling:
;	PIDADR(AP)	- Address of target PID	
;	PRCNAM(AP)	- Address of process name (not used)
;	ENBADR(AP)	- Address of enable-privs mask 
;	DISADR(AP)	- Address of disable-privs mask
;

PIDADR = 4
PRCNAM = 8	; <-- unused except to force exe$nampid to parse a PID of 0
ENBADR = 12
DISADR = 16


lock_start:
;
; Use EXE$NAMPID to convert our target PID to a PCB address.  Note that 
; IPL will be at IPL$_SYNCH with the SCHED spinlock held as a result of
; this call.  (The only two exceptions to an IPL raise would be an invalid
; process name or unreadable PID on stack.  Neither of these can occur here.)
;
	jsb	g^exe$nampid
	blbs	r0,10$
	ret
10$:
	pushl	r1			; Save the IPID for the ACB
;
; Allocate memory for the Ast Control Block (ACB) and the AST code
;
	movl	#prcprv_ast_size,r1	; Size of ACB and code
	jsb	g^exe$alononpaged	; Allocate nonpaged dynamic memory
	blbc	r0,scram		; Failed allocation 
;
; Copy the AST code and ACB into the allocated region
;
	pushr	#^m<r0,r1,r2,r3,r4,r5>	; Save registers across MOVC3
	movc3	#prcprv_ast_size,-	; Copy the ACB+AST into the pool region
		prcprv_ast,(r2)
	popr	#^m<r0,r1,r2,r3,r4,r5>	; Restore registers
;
; Fill in appropriate ACB fields so we know what it is, how big it is,
; and where to start executing on AST delivery
;
	movw	r1,acb$w_size(r2)	; Fill in size for later deallocation
	movb	#dyn$c_acb,-		; Set dynamic memory type as an ACB
		acb$b_type(r2) 
	moval	<prcprv_ast_code-acb>(r2),- ; Put pointer to code in ACB
		acb$l_kast(r2)
	movq	@enbadr(ap),-		; Put enable mask in ast
		<enbmsk_a-acb>(r2)
	movq	@disadr(ap),-		; Put disable mask in ast
		<dismsk_a-acb>(r2)
	clrb	acb$b_rmod(r2)		; Clear request mode bits
	bisb	#acb$m_kast,-		; And set as a special kernel mode AST
		acb$b_rmod(r2)
	popl	acb$l_pid(r2)		; Copy target PID into ACB
	movl	r2,r5			; R5 must point to ACB for SCH$QAST
	movl	#pri$_ticom,r2		; Boost process priority upon delivery
	jsb	g^sch$qast		; Queue the AST
	movl	#ss$_normal,r0		; Set success status
scram:	unlock	lockname=sched,-	; Release spinlock and lower IPL
		newipl  = (sp)+,-	; (preserve status in case we got here
		preserve= YES		; as a result of an error)
lock_end:
	ret

;
; The ACB
;
prcprv_ast:
acb:	  .blkb	acb$k_length		; Allocate storage for ACB
enbmsk_a: .blkq	1			; Privilege Mask enable quadword
dismsk_a: .blkq 1			; Privilege Mask disable quadword
;
; The actual Asynchronous System Trap (AST) code
;
prcprv_ast_code:
	$setprv_s	enbflg=#0,-	; Disable requested privs
			prmflg=#1,-
			prvadr=dismsk_a
	$setprv_s	enbflg=#1,-	; Enable requested privs
			prmflg=#1,-
			prvadr=enbmsk_a
	movl	r5,r0			; Set up pointer for deallocation
	jmp	g^exe$deanonpaged	; Deallocate pool and cease execution
	prcprv_ast_size=.-prcprv_ast
	.end	prcprv

Figure 2: BUILD.COM
$! Build.COM	- Builds PRCPRV
$!
$! Ehud Gavron		Gavron@Spades.ACES.COM
$! 20-Sep-1991
$!
$!-----------------------------------------------------------------------------
$! First create the .CLD and keyword parser:
$!
$!
$ library/extract=$PRVDEF/output=prvdef.mar sys$library:starlet.mlb
$ open infile prvdef.mar
$ create prcprv_cld_mar.mar
$ deck/dollars="$EOD"
; PRCPRV_CLI_TEMPLATE.MAR	- Ehud Gavron -		20-Sep-1991
;
; This template file is copied into the start of PRCPRV_CLI.MAR.
; It contains the macro CLI_CHECK.  CLI_CHECK is called with two
; arguments - the CLI keyword to check for, and the name of the
; symbol which contains the mask value to set or unset.
;
; One of two masks (ENBMSK,DISMSK) is modified based on whether
; the keyword is present or negated respectively.  The code
; produced after the template is done by processing $prvdef.
;
	.library	"sys$Library:lib.mlb"
	.link		"sys$system:sys.stb"/selective_search
	$climsgdef
	$prvdef

	.macro cli_check keyword,symbol ?l ?l2 ?l3 ?l4 ?a
	pushal	a				; Push address of keyword desc
	calls	#1,g^cli$present		; Is it here?
	cmpl	r0,#cli$_present		; Yes?
	beql	l				; If so goto l
	cmpl	r0,#cli$_negated		; Is it negated?
	bneq	l2				; No - not here, exit
	cmpl	#symbol,#64			; Is ALL negated?
	bneq	l4				; no, something else is
	mnegl	#1,(r4)				; negate all of dismsk
	mnegl	#1,4(r4)
	brb	l2				; and exit
l4:	bbss	#symbol,(r4),l2			; Something is negated
	brb	l2				; so do it
l:	cmpl	#symbol,#64			; is ALL here?
	bneq	l3				; no, something is here
	mnegl	#1,(r5)				; set all bits on
	mnegl	#1,4(r5)			; in enbmsk
	brb	l2				; and exit
l3:	bbss	#symbol,(r5),l2			; enable something
	brb	l2				; exit
a:	.ascid	/keyword/
l2:
	.endm

	.entry	parse_keywords,^m<r4,r5>
	movl	4(ap),r4		; address of dismsk
	movl	8(ap),r5		; address of enbmsk
;
; End of template!!!
;
$EOD
$ open/append outmar prcprv_cld_mar.mar
$ create prcprv_cld.cld
$DECK/dollars="$EOD"
define verb PRCPRV
  qualifier IDENTIFICATION
     value (required)
  qualifier PRIVILEGES
     value (required,list,type=PRIVILEGES)

define type PRIVILEGES
   keyword ALL,negatable
$EOD
$ open/append outcld prcprv_cld.cld
$!
$! We do the chicanery with keyword below because we can't use 
$! f$element(1,"_",symbol) since PHY_IO and LOG_IO have an "_" in them.
$!
$ write outmar "	cli_check ALL,64"
$rloop:
$ read/end=eof infile line
$ upcase = f$edit(line,"upcase,compress,trim")
$ if f$element(0," ",upcase) .nes. "$EQU" then goto rloop
$ symbol  = f$element(1," ",upcase)
$ under   = f$locate("_",symbol)
$ m = f$extract(under-1,1,symbol)
$ if m .eqs. "M" then goto rloop
$ length  = f$length(symbol)
$ keyword = f$extract(under+1,length-under,symbol)
$ value   = f$element(2," ",upcase)
$ write sys$output "Inserting keyword ''keyword'..."
$ write outcld "   keyword ''keyword', negatable"
$ if value .lt. 32 then goto writeit
$ val = value - 32
$ write outmar "	addl	#4,r4"
$ write outmar "	addl	#4,r5"
$ write outmar "	cli_check ''keyword',''val'"
$ write outmar "	subl	#4,r4"
$ write outmar "	subl	#4,r5"
$ goto rloop
$writeit:
$ write outmar "	cli_check ''keyword',''symbol'"
$ goto rloop
$!
$eof:
$ write outmar "	ret"
$ write outmar "	.end"
$ close outmar
$ close outcld
$ close infile
$!
$! Now we compile the sources
$!
$ macro prcprv
$ macro prcprv_cld_mar
$ set command/object prcprv_cld
$ link/notrace prcprv,prcprv_cld,prcprv_cld_mar
$ delete prvdef.mar;*
$ purge prcprv_cld.cld,prcprv_cld_mar.*
$ exit


Figure 3: Getting it all to work

	1. Build the necessary executable:
	   $ @BUILD.COM

	2. Define a symbol, making it a foreign command:
	   $ PRCPRV == "$DISK$TOOLS:[PRCPRV]PRCPRV.EXE"

	3. Use the command of the format
		$ PRCPRV /IDENTIFICATION=n /PRIVILEGES=privilege-list

	   Examples:
		$ PRCPRV/IDENT=22003344/PRIV=(NOALL,TMPMBX,NETMBX)
		$ PRCPRV/IDENT=22003344/PRIV=(SYSPRV,WORLD)
		$ PRCPRV/IDENT=22003344/PRIV=ALL

	Note that CMKRNL privilege is required for PRCPRV to work.
	It is linked /NOTRACE, so it can be installed with that
	privilege if ordinary users are to use it.
