Walking z/OS Control Blocks in Java
Clochán an Aifir (Giant's Causeway), County Antrim, Northern Ireland

Walking z/OS Control Blocks in Java

Have you ever wondered how to walk z/OS control blocks in a Java application? No, me neither. Then a friend (I don't want to embarrass him, so let's just call him "Max") wanted a way to determine the account number of the job in which his Java application was being executed.

Turns out this is a non-trivial problem, but is a useful exercise for exposing the joys of walking control blocks in a language that has a scrupulous aversion to pointers.

As with any journey through z/OS control blocks, the first step is to determine where to find what you are looking for. Here's what the journey looks like:

  • Start at the Prefixed Save Area (PSA, see: https://www.ibm.com/docs/en/zos/3.2.0?topic=rqe-psa-information), which is found at virtual address zero (0).
  • Field PSATOLD (at zero-based offset 540) of the PSA contains a 31-bit pointer to the current Task Control Block (TCB, see: https://www.ibm.com/docs/en/zos/3.2.0?topic=xtl-tcb-information) for our task.
  • Field TCBJSCB (at offset 180) of the TCB holds a pointer to the Job Step Control Block (JSCB, see: https://www.ibm.com/docs/en/zos/3.2.0?topic=rqe-jscb-information) for the job step running our Java program.
  • Field JSCBJCTA (at offset 261) of the JSCB holds a 24-bit token (SVA). This SVA maps to the Job Control Table (JCT, see: https://www.ibm.com/docs/en/zos/3.2.0?topic=rqe-jct-information) for the job that is running our application.
  • Field JCTACTAD (at offset 40) of the JCT holds the SVA that maps to the first (and typically, only) Account Control Table (ACT) for the job. Note that the ACT is documented as being a part of the JCT, starting at offset 176. This is hogwash (a technical term). The ACT is not (typically) contiguous with the JCT.
  • Field ACTJNFLD (at offset 31 = 207 - 176) holds the 1-byte count of the number of accounting fields (which may be zero - beware!).
  • If ACTJNFLD is greater than zero, Field ACTACCNT (at offset 32 = 208 - 176) of the ACT holds the account number, as a 1-byte length, followed by the value.

I'll come back to the thorny issue of SVAs, but for now, let's look at how we deal with the good old-fashioned 31-bit pointers in the journey above. Fortunately, Java on z/OS comes with the Java Batch Toolkit for z/OS (JZOS, see: https://www.ibm.com/docs/en/semeru-runtime-ce-z/21.0.0?topic=reference-java-batch-toolkit-zos-jzos). Among the hundreds of helper methods in this library, is the hero of our particular cause - peekOSMemory. As the name implies, this method allows the extraction of a specified number of bytes of memory at a given address into a Java byte array or a long (8-byte) integer.

There are three versions or "overloads" of this method - one which infers the number of bytes to be extracted from the size of the byte array provided to receive the contents of z/OS memory (see: https://www.ibm.com/docs/api/v1/content/SSA3RN_21.0/com.ibm.java.api.21.doc/com.ibm.jzos/ibm.jzos/com/ibm/jzos/ZUtil.html#peekOSMemory(long,byte%5B%5D), one general form, which allows the number of bytes to be extracted (and an offset into the receiving byte array) to be explicitly specified (see: https://www.ibm.com/docs/api/v1/content/SSA3RN_21.0/com.ibm.java.api.21.doc/com.ibm.jzos/ibm.jzos/com/ibm/jzos/ZUtil.html#peekOSMemory(long,byte%5B%5D,int,int)), and a third overload of this method for retrieving a "long" (8-byte) value from up to 8 bytes of contiguous memory (see: https://www.ibm.com/docs/api/v1/content/SSA3RN_21.0/com.ibm.java.api.21.doc/com.ibm.jzos/ibm.jzos/com/ibm/jzos/ZUtil.html#peekOSMemory(long,int)).

In addition to not supporting pointers, Java "thinks" in ASCII (strictly UTF-8), not EBCDIC. Did I mention that I don't like Java that much? To get around this, I define a method of my own, peekString, to "wrap" the peekOSMemory method, to make extracting EBCDIC strings more convenient.

private static String peekString(long addr, int len) {
    byte[] stringbuf = new byte[len];
    ZUtil.peekOSMemory(addr, stringbuf);
    return new String(stringbuf, Charset.forName("IBM037"));
}        

Now we have the machinery to walk control blocks to obtain the SVA of the JCT:

long addrPSA = 0;
long addrTCB = peekOSMemory(addrPSA + 540, 4);
/* get JSCB address from TCB */
long addrJSCB = peekOSMemory(addrTCB + 180, 4);
/* get job step program name from JSCB (for giggles) */
System.out.printf("JSCB step program name:  '%s'\n", peekString(addrJSCB + 360, 8));
/* get SVA of JCT from JSCB */
long svaJCT = peekOSMemory(addrJSCB + 261, 3);
System.out.printf("JSCB JCT SVA:            %08X\n", svaJCT);        

However, what is this SVA thing, and how do we convert it to an address? In the early days of z/OS (or MVS) and the batch environment (JES), the world was smaller (24-bit) and no one contemplated that this 16MB virtual world wouldn't be big enough one day. Anyway, that day came (decades ago), and a new, exciting, 31-bit address space came to z/OS, but JES structures were (evidently) set in stone. To allow JES to take advantage of 31-bit (and later, 64-bit) memory, while continuing to hold "pointers" in 24-bit (3-byte) fields in control blocks, some bright spark in IBM noted that the JES control blocks were always (at worst) fullword-aligned (i.e. address is always a multiple of 4), so setting the low-order bit of the 24-bit control block pointer could be used as an indicator that the 24-bit address was a "token" and not an address. And so, the SVA was born - if the SVA has its low-order bit set, do some gymnastics to find the "actual" address of the control block, otherwise the SVA value is the address (in 24-bit memory) of the control block. Note that this address is actually the address of the Scheduler Work Area (SWA) prefix in front of the control block. The SWA prefix is always 16 bytes, the last 4 bytes of which contains the "eyecatcher" of the control block. Hence, once we convert a SVA to an address, we add 16 to the address to get a pointer to our control block.

Now for the gymnastics required when the low-order bit of the SVA is set...

First, we go back to the JSCB and fetch the address of the SWA Manager Parameter Area (QMPA, see: https://www.ibm.com/docs/en/zos/3.2.0?topic=rqe-qmpa-information) in field JSCBQMPI (at offset 244).

The QMPA holds the address of the (first) SWA Manager Address Table (QMAT, see member IEFQMAT in SYS1.MACLIB). Field QMSTA (at offset 16) of the QMPA is a 1-byte flag field. If bit 5 (numbering from 0 as the most-significant bit) is set, the QMAT has a 64-bit address, which is split across 3 fields: the high-order 2 bytes in field QMADD01 (at offset 10), the next 2 bytes in field QMADD23 (at offset 18) and the low-order 4 bytes in field QMADDR (at offset 24). If bit 5 of QMSTA is reset, the 4-byte (31-bit) address of the QMAT is in field QMADD (at offset 24) of the QMPA.

Now that we have located the first QMAT, we examine the 3-byte SVA. The first byte is the "extent index" (0 to 255), identifying which QMAT (numbering from zero as the first QMAT) holds the address corresponding to our SVA. The QMATs are a linked list, with the 8-byte pointer to the next QMAT in field QMATFW64 (at offset 16) in the current QMAT. So we walk the QMAT chain "extent index" times to arrive at the right QMAT. If the extent index is zero, the first QMAT is the one we want, so there is no need to walk the chain.

Having located the QMAT that contains the address of our control block, the next 15 bits of the SVA is the zero-based index of the address table entry in the current QMAT (i.e. ignore the last bit of the SVA, which is always set, if the SVA is a token). The address table starts at field QMATENTR (offset 72) of the QMAT, and each entry is 24 bytes in length. The first field of each address table entry, QMATAD64, holds the 8-byte address of a control block (again, it actually points at the SWA prefix, with the control block following 16 bytes later).

Now we have the recipe for converting an SVA to an address:

private static long sva2Addr(long addrJSCB, long sva) {
    long result = 0;
    if ((sva % 2) == 1) {
        /* gymnastics required */
        long addrQMPA = ZUtil.peekOSMemory(addrJSCB + 244, 4);
        long status = ZUtil.peekOSMemory(addrQMPA + 16, 1);
        long addrQMAT;
        if ((status & 0x04) == 0x04) {  /* 64-bit QMAT address */
            byte[] qmat = new byte[8];
            ZUtil.peekOSMemory(addrQMPA + 10, qmat, 0, 2);
            ZUtil.peekOSMemory(addrQMPA + 18, qmat, 2, 2);
            ZUtil.peekOSMemory(addrQMPA + 24, qmat, 4, 4);
            addrQMAT = ByteBuffer.wrap(qmat).getLong();
        }
        else {
            addrQMAT = ZUtil.peekOSMemory(addrQMPA + 24, 4);
        }
        int extent = (int)sva / 65536;
        int index  = (((int)sva) % 65536) / 2;
        for (int i = 0; i <= extent; i++) {
            if (i == extent) {
                result = ZUtil.peekOSMemory(addrQMAT + 72 + index * 24, 8);
            }
            else {
                addrQMAT = ZUtil.peekOSMemory(addrQMAT + 16, 8);  /* point at next extent */
            }
        }
    }
    else {
        result = sva;
    }
    return (result + 16);   /* point to control block beyond 16-byte SWA prefix */
}        

Now we can code up the "journey" to find the job account number as follows:

public static void main(String[] args) {
    /* walk control blocks to find current job's account number */
    /* get TCB address from PSA (which is at address 0) */
    long addrPSA = 0;
    long addrTCB = ZUtil.peekOSMemory(addrPSA + 540, 4);
    /* get JSCB address from TCB */
    long addrJSCB = ZUtil.peekOSMemory(addrTCB + 180, 4);
    /* get job step program name from JSCB */
    System.out.printf("JSCB step program name:  '%s'\n", peekString(addrJSCB + 360, 8));
    /* get SVA of JCT from JSCB */
    long svaJCT = ZUtil.peekOSMemory(addrJSCB + 261, 3);
    System.out.printf("JSCB JCT SVA:            %08X\n", svaJCT);
    long addrJCT = sva2Addr(addrJSCB, svaJCT);
    System.out.printf("JCT eyecatcher:          '%s'\n", peekString(addrJCT - 4, 4));
    /* get data set ENQ block, as it will have a "token" SVA if job class has SWA=ABOVE */
    long svaDSEN = ZUtil.peekOSMemory(addrJCT + 88, 3);
    System.out.printf("JCT DSEN SVA:            %08X\n", svaDSEN);
    long addrDSEN = sva2Addr(addrJSCB, svaDSEN);
    System.out.printf("DSEN eyecatcher:         '%s'\n", peekString(addrDSEN - 4, 4));
    /* get SVA of first ACT */
    long svaACT = ZUtil.peekOSMemory(addrJCT + 40, 3);
    System.out.printf("first ACT SVA:           %08X\n", svaACT);
    long addrACT = sva2Addr(addrJSCB, svaACT);
    System.out.printf("ACT eyecatcher:          '%s'\n", peekString(addrACT - 4, 4));
    /* get programmer's name from job card */
    System.out.printf("Programmer's name:       '%s'\n", peekString(addrACT + 184 - 176, 20));
    /* get number of accounting fields in ACT */
    long nFields = ZUtil.peekOSMemory(addrACT + 207 - 176, 1);
    System.out.printf("Accounting fields:       %d\n", (int)nFields);
    if (nFields > 0) {
        /* get first field */
        int fieldLength = (int)ZUtil.peekOSMemory(addrACT + 208 - 176, 1);
        System.out.printf("Job account number:      '%s'\n", peekString(addrACT + 209 - 176, fieldLength));
    }
}        

To demonstrate the above in action, I submitted a job with the following job card:

//JVMJCL86 JOB (42),'Andrew',NOTIFY=&SYSUID        

And the output that came back looked like this:

JSCB step program name:  'JVMLDM86'
JSCB JCT SVA:            008D5028  
JCT eyecatcher:          'JCT '    
JCT DSEN SVA:            0000003F
DSEN eyecatcher:         'DSEN'
first ACT SVA:           008D5568  
ACT eyecatcher:          'ACT '    
Programmer's name:       'Andrew              '
Accounting fields:       1         
Job account number:      '42'              

Here is the complete Java source:

/* get job account number for current task */

import com.ibm.jzos.ZUtil;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;

public class GetAcct {

    /* convert EBCDIC character string in z/OS memory to a Java (UTF-8) String */
    private static String peekString(long addr, int len) {
        byte[] stringbuf = new byte[len];
        ZUtil.peekOSMemory(addr, stringbuf);
        return new String(stringbuf, Charset.forName("IBM037"));
    } 
   
    private static long sva2Addr(long addrJSCB, long sva) {
        long result = 0;
        if ((sva % 2) == 1) {
            /* gymnastics required */
            long addrQMPA = ZUtil.peekOSMemory(addrJSCB + 244, 4);
            long status = ZUtil.peekOSMemory(addrQMPA + 16, 1);
            long addrQMAT;
            if ((status & 0x04) == 0x04) {  /* 64-bit QMAT address */
                byte[] qmat = new byte[8];
                ZUtil.peekOSMemory(addrQMPA + 10, qmat, 0, 2);
                ZUtil.peekOSMemory(addrQMPA + 18, qmat, 2, 2);
                ZUtil.peekOSMemory(addrQMPA + 24, qmat, 4, 4);
                addrQMAT = ByteBuffer.wrap(qmat).getLong();
            }
            else {
                addrQMAT = ZUtil.peekOSMemory(addrQMPA + 24, 4);
            }
            int extent = (int)sva / 65536;
            int index  = (((int)sva) % 65536) / 2;
            for (int i = 0; i <= extent; i++) {
                if (i == extent) {
                    result = ZUtil.peekOSMemory(addrQMAT + 72 + index * 24, 8);
                }
                else {
                    addrQMAT = ZUtil.peekOSMemory(addrQMAT + 16, 8);  /* point at next extent */
                }
            }
        }
        else {
            result = sva;
        }
        return (result + 16);   /* point to control block beyond 16-byte SWA prefix */
    }

    public static void main(String[] args) {
        /* walk control blocks to find current job's account number */
        /* get TCB address from PSA (which is at address 0) */
        long addrPSA = 0;
        long addrTCB = ZUtil.peekOSMemory(addrPSA + 540, 4);
        /* get JSCB address from TCB */
        long addrJSCB = ZUtil.peekOSMemory(addrTCB + 180, 4);
        /* get job step program name from JSCB */
        System.out.printf("JSCB step program name:  '%s'\n", peekString(addrJSCB + 360, 8));
        /* get SVA of JCT from JSCB */
        long svaJCT = ZUtil.peekOSMemory(addrJSCB + 261, 3);
        System.out.printf("JSCB JCT SVA:            %08X\n", svaJCT);
        long addrJCT = sva2Addr(addrJSCB, svaJCT);
        System.out.printf("JCT eyecatcher:          '%s'\n", peekString(addrJCT - 4, 4));
        /* get data set ENQ block, as it will have a "token" SVA if job class has SWA=ABOVE */
        long svaDSEN = ZUtil.peekOSMemory(addrJCT + 88, 3);
        System.out.printf("JCT DSEN SVA:            %08X\n", svaDSEN);
        long addrDSEN = sva2Addr(addrJSCB, svaDSEN);
        System.out.printf("DSEN eyecatcher:         '%s'\n", peekString(addrDSEN - 4, 4));
        /* get SVA of first ACT */
        long svaACT = ZUtil.peekOSMemory(addrJCT + 40, 3);
        System.out.printf("first ACT SVA:           %08X\n", svaACT);
        long addrACT = sva2Addr(addrJSCB, svaACT);
        System.out.printf("ACT eyecatcher:          '%s'\n", peekString(addrACT - 4, 4));
        /* get programmer's name from job card */
        System.out.printf("Programmer's name:       '%s'\n", peekString(addrACT + 184 - 176, 20));
        /* get number of accounting fields in ACT */
        long nFields = ZUtil.peekOSMemory(addrACT + 207 - 176, 1);
        System.out.printf("Accounting fields:       %d\n", (int)nFields);
        if (nFields > 0) {
            /* get first field */
            int fieldLength = (int)ZUtil.peekOSMemory(addrACT + 208 - 176, 1);
            System.out.printf("Job account number:      '%s'\n", peekString(addrACT + 209 - 176, fieldLength));
        }
    }
};        

A few things to note:

  • The QMAT structure is only documented in SYS1.MACLIB member, IEFQMAT, for "diagnostic purposes" (see: https://www.ibm.com/support/pages/apar/OA48915) - it is "not a programming interface" (IBM speak for something that might change without notice). The robust way to convert an SVA token to an address is to call the SWAREQ macro, or to copy an SVA-referenced control block into your own (176-byte) memory area, you invoke the IEFQMREQ macro (for a full discussion, see: https://www.ibm.com/docs/en/zos/3.2.0?topic=area-using-iefqmreq-swareq-macros). I have done the SVA-to-address translation "by hand" to avoid writing a JNI routine to call a macro.
  • A call to peekOSMemory can raise a com.ibm.jzos.RcException, typically to indicate a protection exception. Accordingly, it is good practice to wrap calls to peekOSMemory in a try...catch block, if you want to handle such an error elegantly.
  • I have fetched the SVA of the data set ENQ control block in the JCT (field JCTDETDA at offset 88). This is done to test the sva2Addr method when the SVA is a token, not a 24-bit address. Just as JES is stuck with 24-bit fields for addressing its control blocks, it also hasn't evolved to allow the common or garden control blocks (JCT, ACT, SCT) to be located above the line (in 31-bit or 64-bit storage). More exotic control blocks, like DSENQ, are eligible to be stored above the line, provided the job class of the running job has SWA=ABOVE in the JOBCLASS definition (see: https://www.ibm.com/docs/en/zos/3.2.0?topic=jisd-jobclass-job-started-task-time-sharing-user-class).
  • All z/OS tasks are "jobs", with "JCL" associated with them, even if you can't see it! The above Java program can be run from the z/OS UNIX command prompt, for example, and it will execute successfully, although it probably won't return an account number (i.e. ACTJNFLD will be zero). This means you can test this application from an interactive session - submitting JCL is optional.

$ java -cp . GetAcct
JSCB step program name:  'BPXPRECP'
JSCB JCT SVA:            008D9028
JCT eyecatcher:          'JCT '
JCT DSEN SVA:            0000003F
DSEN eyecatcher:         'DSEN'
first ACT SVA:           008D93E8
ACT eyecatcher:          'ACT '
Programmer's name:       '                    '
Accounting fields:       0        

I hope you find this information useful one day!

Thank you! Java is not my favorite either, great explanation.

Like
Reply

To view or add a comment, sign in

More articles by Andrew Mattingly 🦖

Others also viewed

Explore content categories