Installing Data Gatherer on z/OS

OpenTelemetry is a technology for providing trace data as work flows between systems. It is often called OpenTel or OTel.

As work flows around a network, status(“where the work is”) information is sent to a central location, and tools at this central location can sew the data together and produce visualisations of the flow of data, and where the work was delayed.

The data is normally sent over TCP/IP.

Products/programs could emit their own data in the required format, and sent it over TCP/IP to the server. On z/OS data can be written to In-Memory SMF, and a data collector reads the SMF records and sends the data over TCP/IP to the central site.

IBM provides a Data Gatherer. This post is about configuring it and getting it working. It feels a bit rough around the edges, and I would have designed some of it differently.

Where is it?

The code has been forced into the RMF data collector. The data collector code is usually installed in the Unix directory /usr/lpp/grb.

Where is the documentation?

In the z/OS Data Gatherer User’s Guide (SC31-5703-70) – Chapter 3 are instructions on creating security profiles.

The first sentence in this chapter is:

Instructions for setting up the z/OS OpenTelemetry Emitter are available in the file system in your z/OS installation at /usr/lpp/grb/opentelemetry_emitter/dt/README.

These instructions are not clear, and sometimes wrong. (For example to disable TLS, you set a flag, and then define a certificate!) Thanks to Joern Thyssen from Rocket Software for his help in getting it working.

I created some JCL called MYDG and submitted it. You could use a started task. The userid running the data gatherer needs access to the SMF data. If the access to the SMF data is restricted you may want to use a started task with its own restricted userid. Instructions for this are in the Chapter 3 above.

My JCL

//IBMOT32  JOB  (OTEL),MSGLEVEL=(1,1),NOTIFY=&SYSUID
// EXPORT SYMLIST=*
// SET JARFILE='zos-otel-emitter-dt.jar'
// SET $OTLENDP='http://10.1.0.2:4317'
// SET $OTLPRT='grpc'
// SET REGSIZE='0M'
// SET $OTLEXPC='false'
// SET $TLSENBL='false'
// SET $MTLS='false'
// SET $TLSCERT=''
// SET $TLSCLKY=''
// SET $TLSCLCR=''
// SET $SMFRDBS=''
//*
// SET VERSION='21'
// SET JAVADIR='/usr/lpp/java/java21/current_64'
// SET APPHOME='/usr/lpp/grb/opentelemetry_emitter/dt'
// SET $INMRESL='IFASMF.MQOTEL'
// SET $SMFDUMP='false'
// SET $SMFDUMP='true' dump SMF record binary
// SET $SMFRDFL='0'
//JAVAJVM EXEC PGM=JVMLDM&VERSION,REGION=&REGSIZE PARM='/+I'
//STEPLIB DD DISP=SHR,DSN=JAVA.V21R0M0.SIEALNKE
...

Java 21 and higher

If you are using Java 21 or higher, some of the output from Java comes out by default in ASCII (and so is not easily readable). You need to specify

IJO="$IJO -Dfile.encoding=IBM-1047" 

Identifing the SMF data

This reads SMF data identified as IFASMF.MQOTEL, and sends it over http (not https) to http://10.1.0.2:4317.
MQ writes its OpenTel data to SMF records type 1158.

My SMFPRMxx in parmlib has

RECORDING(LOGSTREAM) 
...
INMEM(IFASMF.MQOTEL,RESSIZMAX(128M),TYPE(1158))

This gives a user specified name IFASMF.MQOTEL to records with type 1158. You protect the name IFA.IFASMF.MQOTEL with your security manager. (The Chapter 3 documentation is confusing).

The Data Gatherer accesses the data through the label IFASMF.MQOTEL.

Collecting data

Once Java has started (it takes about 15 seconds to start on my baby zD&T machine), it will listen for new records sent to the SMF resource (IFASMF.MQOTEL). It does not drain existing records.

You can send the data over TCP/IP or write it locally.

If you are sending the data over TCP/IP, once the first record has been read from SMF, the data collector starts a TCP/IP session to the remote collector. If the IP address is not active (or is misconfigured) it can take many seconds ( > 15 seconds for me) before the UnknownHostException is thrown.

Personally I would have connected at startup, so you know if you have a configuration error. It is not good when you start the server at midnight, but only find there is a problem at 0800 when the work starts. It would be better to report the error when the server is started, because it gives you more time to fix any problems.

If the connection is successful, there is no notification.

What is sent?

You can specify the option

 SET $SMFDUMP='true'  

and it dumps the data in //STDOUT

00000000 13 60 00 00 7E 7E 00 61 5A DC 01 26 17 7F E5 E2 .-..==..!...."VS
00000010 F0 F1 00 00 00 00 00 01 00 20 01 00 00 E2 E3 BE 01...........ST.
00000020 DD C1 37 4E 40 00 00 00 01 00 00 01 00 00 00 00 .A.+ ...........
00000030 00 00 00 00 04 86 00 00 00 00 00 40 00 00 00 02 .....f..... ....
00000040 00 01 0A 00 E2 D7 C1 D5 00 E2 E3 BE DD C0 C0 60 ....SPAN.ST..{{-
00000050 80 00 00 00 00 00 00 00 00 E2 E3 BE DD C0 CC 83 .........ST..{.c
00000060 80 00 00 00 00 00 00 00 F0 81 86 F7 F6 F5 F1 F9 ........0af76519
00000070 F1 F6 83 84 F4 F3 84 84 F8 F4 F4 F8 85 82 F2 F1 16cd43dd8448eb21
00000080 F1 83 F8 F0 F3 F1 F9 83 85 F2 85 F3 82 85 84 84 1c80319ce2e3bedd
00000090 83 F0 83 F0 F6 F0 F8 F0 85 F2 85 F3 82 85 84 84 c0c06080e2e3bedd
000000A0 83 F0 F1 86 F9 F7 83 F0 00 04 00 2F 00 18 0C 01 c01f97c0........
000000B0 A2 85 99 A5 89 83 85 4B 95 81 94 85 00 04 01 F4 service.name...4
000000C0 C3 E2 D8 F9 00 20 09 01 A2 97 81 95 4B 95 81 94 CSQ9....span.nam
000000D0 85 00 00 00 00 0B 01 F4 D4 D8 C7 C5 E3 40 C3 D6 e......4MQGET CO
000000E0 D3 C9 D5 00 00 28 18 01 94 85 A2 A2 81 87 89 95 LIN.....messagin
...

Once you have proved it is working – I suggest you set SMFDUMP=’false’.

TLS support

The Data Gatherer has support for TLS, but the backend I was using Jaeger does not have TLS support. The documentation says install the cassandra product; I could not install this on my Linux machine.

Why doesn’t this valid C program compile?

I wanted to use some C code I found, and when I tried to compile the source, it kept complaining because of a syntax error.

... 
void printMD()
{
printf("ABC ");
int i;
i = 0;
printf("I is %i\n",i);
}
...

The original code was several thousand lines long. The (first) error message was

ERROR CCN3275 ./perfutic.c:4     Unexpected text 'int' encountered.                                        

There were two challanges

  1. Create the smallest program to isolate
  2. Find out why the code failed to comile

Isolate the problem

I put #ifdef temp…. #endif around blocks of code to remove irrelevant code. This worked, but I quickly got into a mess where I had many #ifdef…#endif and matching them up.

I saved a copy of the program, then used #ifdef..#endif to ignore blocks of code. If this made no change to the compile, then delete the block, and add more #ifdef…#endif. If it caused other error messages, remove the #ifdef..#endif statements, and try a smaller block of code.

Eventually I got down to the code above.

Why did the code fail to compile?

I spent half a day scratching my head, and when I came back next morning, I had a flash of inspiration.

Later versions of C are more flexible in some areas.

In early days of the C compiler you had to define all variables before you did any work. With later versions of the C compiler this restriction was relaxed, so you could call a function, the define a variable.

See intermingled declarations and code: variable declaration is no longer restricted to file scope or the start of a compound statement (block) in the ISO C99 specification

The makefile I was using was

cparms ="-Wc,SO,LIST(lst31),XREF,ILP32,DLL,SSCOM,SHOWINC,DEFINE(MVS=1)"  
cc -c -o $@ -I"//'MQCD94.SCSQC370'" -I'/usr/include' -I. $(cparms) $<

You control the level of C using the LANGLVL option.

The cc command produced (in the listing)

Language level. . . . . . . . : *COMMONC:NOTEXTAFTERENDIF 

With the c99 command, the code compiled, and the listing had

 Language level. . . . . . . . : *STDC99:NOTEXTAFTERENDIF 

With the original compile, I was using the options which did not allow variables to be defined after executable code.

Instead of using cc, I could have used cc with langlvl(EXTENDED).

Why doesn’t this valid C program compile?

The short answer is : I was using the wrong compiler options.

Whoosh goes my z/OS console

I was having problems IPLing z/OS. Once you can logon to TSO you can use SDSF to display the console. The problem is when the system fails before you can logon to TSO.

When z/OS is IPLed a few messages are displayed, then the console goes into wrap mode, and message roll past faster than I can read them.

You can change the roll mode using the command to not delete messages automatically

K S,DEL=N

You can then use

K

to clear the screen.

You can use K S,REF to display and overtype values. For example RTME=5 says roll every 5 seconds, RTME=1/4 says roll every quarter of a second.

How much free space do my ZFS have?

I was running into a problem where my file systems were running out of space. I knew I had some spare space – but where was it?

The command

zfsadm aggrinfo

produced output like

COLIN.ZFS.ZOWE.CONFIG (R/W COMP): 1943183 K free out of total 2975040      
COLIN.ZFS.ZOWE33.CONFIG (R/W COMP): 7327 K free out of total 421920
COLIN.OPENSSL.ZFS2 (R/W COMP): 2407159 K free out of total 7200000
ZFS.S0W1.SYSTEM (R/W COMP): 1263 K free out of total 1440
ZFS.Z31B.CNJ (R/O COMP): 80605 K free out of total 648000
ZFS.S0W1.WEB (R/W COMP): 9839 K free out of total 10080

You can sort it to give the largest space at the bottom.

  • -n means treat the value as a number
  • -k 4,4 means sort by the 4th field.
zfsadm aggrinfo |sort -n -k4,4

Gave

ZFS.Z31B.VERSION (R/W COMP): 33 K free out of total 3823200
ZFS.Z31B.ZEDC (R/O COMP): 367 K free out of total 1440
...
COLIN.OPENSSL.ZFS2 (R/W COMP): 2407159 K free out of total 7200000
COLIN.ZOPEN.ZFS (R/W COMP): 2446835 K free out of total 6144480

Formatting a spread sheet column so it spans multiple columns.

I have some data in two columns in Google sheets (also in Libre office Calc). How do I print it so it is two columns (of two columns) , down the first column, down the second column, and onto another page?

The easiest way for me was

  • Copy the column of data into the clip board.
  • Go to libre office writer
  • Paste the data
  • Top line of options, Format -> Page style ( or Shift+Alt+P)
  • Select the Columns option on the top line
  • Columns + (to make it two columns)
  • OK
  • Set a heading
    • Insert -> header and footer ( near the bottom)
    • Header – > all
  • Set a page number
    • Insert -> Page number ( 5th from the bottom)

Using an external function

My “simple” problem was to have one application, using functions from another application, and allow me to upgrade both, and continue working. I want to write some code for openssl to call my program. I’m changing my code frequently, and I do not want to have to recompile and rebind openssl every time I change my program. (The recompile takes about 3 hours on my baby machine).

It took me a couple of days to get working; I learned a lot.

The simple solution which does not really work

I have a suite of programs called mycode, and I want to use stuff from another package which I’ll call otherstuff.

I can simply bind mycode, and otherstuff together to resolve all of the functions entry points. It works, but if I upgrade mycode, or otherstuff, I will not pick up the latest versions, and the code which is in the final load module may be incompatible with the newer versions.

Use fetch!

In mycode I can use the C function fetch, to load a load module, and give me the entry point. I then call the functions using the entry point.

This works – after a fashion. The key word is load module. A load module exists in a PDS or a PDSE. A Program Object can be in Unix or in a PDS, so the fetch solution does not work with file files in Unix.

The code worked if the module was in a load module – which I didn’t want.

I could not use fetch to load a program object from Unix.

Putting the code in a load module

I used the binder to create a load module.

xlc  dummy.o-o "//'COLIN.LOAD(dummy)'"  ...

To get my program to run I had to use

EXPORT STEPLIB=COLIN.LOAD
./myprog

The fetch worked, and my program was called.

Use Dynamic Link Library

The Unix eqivilent to a load module is the DLL.

When you create a DLL, two parts are created.

  • The .x file which is used for C to find how to call a function (stub code)
  • The .so file which contains the executable code.

The .x file

The .x file contains data like

IMPORT DATA,'cpfopen','ascii_tab'
IMPORT CODE,'cpfopen','cpfopen1'
IMPORT CODE,'cpfopen','printHex'

Which says

  • there is an entry point cpfopen1 in program object cpfopen
  • there is an external data object called ascii_tab in program object cpfopen
  • there is an entry point printHex in program object cpfopen

This .x file contains information for C to create stub code to load the actual code.

My calling program has

#pragma linkage(cpfopen1 ,OS)
...
int myopen ;
myopen = cpfopen1("ABC","DEF");
...

This needs to be complied with option

-Wc,DLL  

If you do not use the DLL option it ignores the IMPORT statement, and reports cpfopen1 and printHex are not found.

I bound it using

xlc  -o fopen  myfile.o cpfopen1.x  V 

The executed code.

You need to compile your code and specify which function names you want to make visible to other programs(export).

You can use

  • the compiler option EXPORTALL or
  • #pragma export(function1) #pragma export(function2) within your code.

At bind time

l1="-Wl,DLL  " 
xlc cpfopen1.o -o cpfopen1 -V $l1 1>a

This creates

  • the executable with the name in the -o parameter (cpfopen1)
  • and cpfopen1.x using the name of the object in the -o parameter

With

l1="-Wl,DLL  " 
xlc cpfopen1.o -o cpfopen1.so -V $l1 1>a

the file cpfopen1.x has

IMPORT CODE,'cpfopen1.so','cpfopen1'

Doing it this way, and letting the C and the binder resolve the entry point is the easy way of doing it.

Advanced DLL

You can load a DLL yourself

void * hDLL; 
int (*fptr)(const char * filename, const char *mode);

hDLL = dlopen("cpfopen1", RTLD_LOCAL | RTLD_LAZY );
if (!hDLL) {
fprintf(stderr, "%s\n", dlerror());
exit(99);
}
dlerror(); /* Clear any existing error */

The find the entry point using dlsym. This uses the handle of the DLL object loaded above, and you pass the entry point name. (It might not exist).

Define a pointer to the function

int   (*fptr)(const char * filename, const char *mode); 

This defines a pointer to a function fptr, which returns an int, and has two char * parameters.

fptr = (int (*)(const char *, const char *)) dlsym(hDLL, "cpfopen1"); 

if ( fptr != 0)
{
myopen = (*fptr)("ABC","DEF");
printf("result %i\n",myopen);
}

It took me some time to get the definitions of the function parameters correct.

The called function is

#pragma linkage(cpfopen1, fetchable)
int cpfopen1(char * filename, char *mode)
{
printf(">>>>>>>>>>>>>>>In cpfopen1 %s %s\n",filename,mode);
return 7 ;
}

It was much easier to include the .x file at bind time, and let C run time sort it out.

Clean up

You should use dlclose(handle) to close the object and free its resources

Sharing status information between functions.

My high level problem is that I want to extend a very large package to be able to have my own code process some file requests, such as fopen(), fread() and fclose(). Then I wanted to share information such file name in the fclose routine.

I decided to map fopen() to cpfopen() and process the request through my own code.

The first step was easy; I changed fopen to invoke my program.

#define fopen(a,b) cpfopen(a,b)
FILE * cpfopen(const char *__restrict__filename, const char *__restrict__mode);

I include that code in the package routines, and recompile them all.

The problem is that I want information used in the fopen function, to be available in the fclose function. I could change these routines not to return a FILE * handle, but return a COLINFILE , but this makes it very messy.

At first glance the use of C global storage looks as if it may work. However because I can open two files, opening a second file will overwrite information from the first file, I need to be able to keep status on each file.

I found a neat, self contained solution – the use of name tokens.

You can define a name token pair to z/OS, and you can retrieve the token by giving the name.

How does it work?

Define the token

I did this in my cpfopen routine using IEANTCR.

  • The “name” field is 16 bytes long. You can specify any value for the identifying label. In the example below I used the value COLIN.
  • The “token” field is 16 bytes long. You can specify any value to be stored under the label. In the example below I used the value MYVALUE.
#pragma linkage(IEANTCR ,OS) 
...
char name[16];
char token[16];
memset(&name,0,sizeof(name));
memcpy(&name[0],"COLIN",5);

memset(&token[0],0,sizeof(token));
memcpy(&token[0],"MYVALUE",7);
int task = 1; // for TCB level
int persist = 0; // Not relevant when TCB or address space
int rc; // return code passed back
IEANTCR(&task,&name,&token,&persist,&rc);
printf("Return code from ieantcr %i\n",rc);

Retrieve the token

I did this in my cpfread routine using IEANTRT.

#pragma linkage(IEANTRT ,OS) 
...
char name[16];
char token[16];
memset(&name,0,sizeof(name));
memcpy(&name[0],"COLIN",5);// this label
int task = 1;
int persist = 0;
int rc;
IEANTRT(&task,name,token,&rc);
printf("IEANTRT %i\n",rc);
if ( rc == 0)
{
printf("read token %7.7s\n",token);
}

When this code ran it printed out MYVALUE, as expected.

You can have name token pairs on each TCB, or name token pairs available for any TCB within the address space, (and authorised users can make name token pairs available across address spaces).

Each name token pair is chained off a TCB/Address space/system control block. If you have many name token pairs, the CPU needed to locate an entry will increase as the number of entries increases.

In this case you have to do more work. Allocate a hash table of input value -> value, and put the address of the hash table as the value of the name token. You then use a unique token name such as “COLINSNT” to locate it the hash table.

How to bind

The SYSLIB of the bind step needs SYS1.CSSLIB to find the stub for the name token code to work.

My actual code for the fopen etc

I wanted to pass the file name used in fopen, to the fread and fclose routines. I used the FILE * handle as the identifying label.

I wanted to pass the variable length file name, and not just a constant, so I needed to make a copy of the file name, and pass the address of the copy of the name in the token.

cpfopen

#pragma linkage(IEANTCR ,OS) 
...
FILE * cpfopen(const char * filename, const char *mode)
{
printf("In cpfopen %s,%s\n",filename, mode);
FILE * handle;

handle = fopen(filename,mode);
if (handle > 0)
{
printf("handle %8.8x\n",handle);
char name[16];
char token[16];
memset(&name,0,sizeof(name));
memcpy(&name[0],handle,4);
char * dupname ;
dupname = strdup(filename);
// save the file name
memset(&token[0],0,sizeof(token));
memcpy(&token[0],&dupname,4);
// 31 bit
int task = 1;
int persist = 0;
int rc;
IEANTCR(&task,&name,&token,&persist,&rc);
printf("Return code from ieantcr %i\n",rc);

}
return handle;
}

cpfread

#pragma linkage(IEANTRT ,OS)
...
size_t cpfread(void * buffer, size_t size, size_t count, FILE * stream)
{

char name[16];
char token[16];
memset(&name,0,sizeof(name));
memcpy(&name[0],stream,4);
// use the passed stream value( handle)
char * dupname;

int task = 1;
int persist = 0;
int rc;
IEANTRT(&task,name,token,&rc);
printf("IEANTRT %i\n",rc);
if ( rc == 0) // it was found
{
// copy from the token to my variable
memcpy(&dupname,&token[0],4);
printf("read file name %s\n",dupname);
if (dupname != 0)
printf("In Read: Value from NT is %s\n",dupname);
else printf("dupname is 0\n");
}
// Do the read file read
size_t n = fread(buffer, size, count, stream);
printf("Bytes read %i\n",n);
return n;
}

cpfclose

You need to free any storage you allocated, and delete the name token pair using IEANTDL.

#pragma linkage(IEANTRT ,OS)
...
size_t cpfclose(FILE * handle)
{

char name[16];
char token[16];
memset(&name,0,sizeof(name));
memcpy(&name[0],handle,4);
// use the passed stream value( handle)
char * dupname;

int task = 1;
int persist = 0;
int rc;
IEANTRT(&task,name,token,&rc);
printf("IEANTRT %i\n",rc);
if ( rc == 0) // it was found
{
// copy from the token to my variable
memcpy(&dupname,&token[0],4);
printf("close file name %s\n",dupname);

gg
}
// now the close
rc = fclose(handle);
return rc;
}

Binding in Unix on z/OS. A whole new world

I had been happy compiling and binding small programs in Unix on z/OS. It got a bit more complex as my programs got bigger and needed more files at bind time.

I stumbled into an area which I knew nothing about, (the pirate world of ar!) and which may help with doing binds.

My simple compile and bind script

name=wr 
rm ${name}.o
export _C89_CCMODE=1
p1="-Wc,arch(8),target(zOSV2R4),list,source,ilp32,gonum,asm,float(ieee) "

p8="-Wc,LIST(c.lst),SOURCE,NOWARN64,XREF,SHOWINC"

xlc $p1 $p8 -c $name.c
xlc $name.o -o wr -V 1>a

This script

  • compiles a program (wr.c) putting the compile listing into c.lst
  • binds the .o file and outputs (-o) into a file wr, and puts the output listing into a

You can use the ld command to invoke the binder, or use the xlc command. The xlc command looks at the types of the files to decide the action. For example *.c means compile *.o means bind.

My messy compile and bind script

name=colinm 
rm ${name}.o
export _C89_CCMODE=1
p1="-Wc,arch(8),target(zOSV2R4),list,source,ilp32,gonum,asm,float(ieee) "
p7="-Wc,ASM,ASMLIB(//'SYS1.MACLIB') "
p8="-Wc,LIST(c.lst),SOURCE -Wa,LIST(133),RENT"

# compile it

xlc $p1 $p7 $p8 $p2 -c $name.c -o $name.o

as -mgoff -aegmrsx=a.lst BPXTPOST.s
as -mgoff -aegmrsx=a.lst BPXTWAIT.s
as -mgoff -aegmrsx=a.lst DUMMY.s
as -mgoff -aegmrsx=a.lst br14.s
as -mgoff -aegmrsx=a.lst cpfetch.s

# create a load module in a PDS

include=" BPXTPOST.o BPXTWAIT.o pcexit.o br14.o DUMMY.o DUMMY2.o mywto.o cpfetch.o "
l1="-Wl,LIST,MAP,XREF,AC=1 "

/bin/xlc $name.o $include -o "//'COLIN.LOAD(colinm)'" -V $l1 1>a

This script

  • compiles colinm.c into colinm.o, puts the listing into c.lst
  • assembles the files BPXTPOST… putting the listing into a.lst
  • binds the list of “text files” into a load module COLIN.LOAD(COLINM), setting AC=1, and producing a load map.

Linkedit control statements

name=pcexit 
export _C89_CCMODE=1
l2="-l //'CEE.SCEESPC' "
l3="-O EDCXSTRT"
# create a load module in a PDS
l1="-b LIST,MAP,XREF,AC=1 "
ss="//'CEE.SCEESPC.OBJ(EDCXSTRT)'"
ld $ss $name.o -v -o "//'COLIN.LOAD(PCEXIT)'" -V $l1 $l2 $l3 1>ab

This script generates binder like JCL statements. (ld does not use them – it is a JCL equivilent of what is used)

//* ld  --------------------------------------------------------------- 
//LINKEDIT EXEC PGM=IEWBLINK,
// PARM='TERM=YES,PRINT=NO,MSGLEVEL=4,STORENX=NEVER,LIST=NOIMP,XREF=YESc
// ,MAP=YES,PRINT=YES,MSGLEVEL=0,LIST,MAP,XREF,AC=1'
//LDIN0001 DD DSN=CEE.SCEESPC.OBJ(EDCXSTRT),DISP=SHR
//LDAT0001 DD DSN=CEE.SCEESPC,DISP=SHR,DCB=DSORG=DIR
//SYSLMOD DD DSN=COLIN.LOAD(PCEXIT),DISP=REP,DATACLAS=,DSNTYPE=LIBRARc
// Y,MGMTCLAS=,SPACE=,STORCLAS=,UNIT=,DCB=(RECFM=U,LRECL=0,c
// BLKSIZE=6144,DSORG=PO)
//SYSPRINT DD PATH='/dev/fd1',PATHOPTS=(ORDWR,OCREAT,OAPPEND),FILEDATAc
// =TEXT,PATHMODE=(SIROTH,SIRGRP,SIRUSR,SIWOTH,SIWGRP,SIWUSc
// R)
//SYSTERM DD PATH='/dev/fd2',PATHOPTS=(ORDWR,OCREAT,OAPPEND),FILEDATAc
// =TEXT,PATHMODE=(SIROTH,SIRGRP,SIRUSR,SIWOTH,SIWGRP,SIWUSc
// R)
//SYSLIN DD *
INCLUDE LDIN0001
INCLUDE './pcexit.o'
LIBRARY LDAT0001
ORDER EDCXSTRT
/*

Where

  • -l //’CEE.SCEESPC’ becomes a binder LIBRARY statement which points to a PDS, which gets searched before SYSLIB
  • -O EDXCSTRT becomes the binder ORDER statement. For some programs, some CSECTS have to be in the right order. You can specify -O name1,name2.
  • -b…. pass these parameters to the binder
  • //’CEE.SCEESPC.OBJ(EDCXSTRT)’ contains some precompiled code. It gets mapped to INCLUDE … from a JCL dataset
  • $name.o is the precompiled object input to the binder.

    Using an archive file (the ar command)

    An archive file is something between a tar file, and a PDS with the output of compiles. (I have a dataset COLIN.OBJLIB in ISPF).

    * compile an assembler file
    as -mgoff -aegmrsx=a.lst BPXTPOST.s
    ar -rc arch.a BPXTPOST.o

    This

    • assembles the program BPXTPOST.s, creating BPXTPOST.o
    • stores it in the archive arch.a

    I can then bind with it

    /bin/xlc colinm.o   arch.a                 -o out.load   -V $l1    1>a 

    This takes the colinm.o and any .o files it needs from the arch.a archive.

    What is in the archive?

    The ar command

    ar -tv  arch.a

    gives

    rw-r--r-- 990021/990000     220 May 14 09:36 2026 __.SYMDEF  
    rw-r--r-- 990021/0 1200 Apr 27 10:29 2026 BPXTPOST.o
    rw-r--r-- 990021/0 1200 Apr 28 19:18 2026 BPXTWAIT.o
    rw-r--r-- 990021/0 720 May 11 15:11 2026 br14.o

    The command nm (Display symbol table of object, library, or executable files)

    nm -M   arch.a

    gives more information

    BPXTPOST.o:                               
    0 d --- --- - BPXTPOST
    0 T ANY ANY - BPXTPOST
    0 d --- --- - B_IDRL
    0 d --- --- - B_PRV
    0 d ANY ANY - B_TEXT
    0 U --- --- - CEESTART

    Where

    • 0 is offset from the start
    • d is Initialized data (bbs), local
    • T is Text symbol, global
      • ANY ANY – is RMODE(ANY) AMODE(ANY) Compiler_options(-)
    • U is undefined

    Using the nm command on an executable gave

    ...     
    128 U --- --- - B_TEXT
    1312 T MIN MIN - CEEARLU
    1312 U --- --- - CEEARLU
    728 U --- --- - CEEBETBL
    728 T MIN MIN - CEEBETBL
    ...
    104 D --- --- - locs
    152 T ANY ANY - main
    152 U --- --- - main
    40 U --- --- - optarg
    40 D --- --- - optarg
    ...

    we can see the offset of the main entry point is at offset 152 in the module.

    Assembler programming without using using – it is all relative

    Before Jump

    For many years a program on z architecture looked like

    MYPROG  CSECT
    LR 12,15
    USING MYPROG,12
    B OVER
    DC C'*MYPROG *'
    OVER DS 0H
    ...

    LA 3,MYAUTOSTORAGE

    USING MYDATA,3
    L 4,FIELD2
    MYDATA DSECT
    FIELD1 DS F
    FIELD2 DS F

    To reference a field you have to do it off a register. For example

    • B OVER is converted to BR 0x10 off register 12
    • L 4,FIELD2 is converted to Load offset 4 from register 3

    Each base register could cover 4KB, so for large programs, or large data you needed multiple registers.

    Post Jump

    A significant leap in the instructions was the introduction of the “… relative” instruction. For example the Jump (or as it is properly know the BRANCH RELATIVE ON CONDITION) is jump the number of half words from the start of the instruction

    OFFSET  
    0000 MYPROG CSECT
    0000 LR 12,15
    0000 USING MYPROG,12
    0002 J OVER (A7F4 0007)
    0006 DC C'*MYPROG *'
    0010 OVER DS 0H F

    The Jump instruction A7M4 xxxx) has condition M (F is always branch) and xxxx in this case if 0007. The address of the instruction is 0002, so jump to h2 + 2 * 7 = 0010 — which is the label for OVER.

    This is a faster instruction than the B OVER, because it has to do less work. It can jump to +- 65534 bytes, and does not use any base registers. Not using a base register means that more registers can be used to access data.

    But ..

    I was writing some assembler code within a C program, and hit a few snags. The C code does not make a base register available to access fields.

    With code like

    MYLABEL  DC C'HELLO'
    LA R2,MYLABEL

    With base registers, this code will work and create the LA instruction with the offset of MYLABEL from the base register.

    However, because there is no base register and USING, the LA instruction fails.

    I found two ways of doing it when using relative instructions

    Branch and save

              BRAS 2,OVERWTOL
    SWTO WTO TEXT=label,MF=L
    OVERWTOL DS 0H

    This jumps to the label OVERWTOL, and saves the “return address” in register 2. Register 2 points to label SWTO.

    Load address relative

    OFFSET
    0004 SWTO WTO TEXT=label,MF=L
    0074 LARL 2,SWTO

    Which becomes load address with value -38 half words from the start of the instruction, so “offset” FFFFFFC8

    This is half the solution

    I was using the WTO macro to write message on the console.

       WTO   text=(Mydata),MF=(E,(4)) 
    ...
    MYDATA DC H'5',CL8'HELLO'

    This failed because there was no base register.

    I had to use

             LARL   2,MYDATA   
    WTO text=((2),MF=(E,(4))
    ...
    MYDATA DC H'5',CL8'HELLO'

    and this worked successfully.