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:
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.
Recommended by LinkedIn
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:
$ 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.
😂