There are at least two$${}^1$$ z/OS C calling conventions, the traditional “LE” OSLINK calling convention, and the newer$${}^2$$ XPLINK convention.  In the LE calling convention, parameters aren’t passed in registers, but in an array pointed to by R1.  Here’s an example of an OSLINK call to strtof():

*  float strtof(const char *nptr, char **endptr);
LA       r0,ep(,r13,408)
LA       r2,buf(,r13,280)
LA       r4,#wtemp_1(,r13,416)
L        r15,=V(STRTOF)(,r3,4)
LA       r1,#MX_TEMP3(,r13,224)
ST       r4,#MX_TEMP3(,r13,224)
ST       r2,#MX_TEMP3(,r13,228)
ST       r0,#MX_TEMP3(,r13,232)
BASR     r14,r15
LD       f0,#wtemp_1(,r13,416)


R1 is pointed to r13 + 224 (a location on the stack). If the original call was:

float f = strtof( mystring, &err );

The compiler has internally translated it into something of the form:

STRTOF( &f, mystring, &err );

where all of {&f, mystring, &err} are stuffed into the memory starting at the 224(R13) location. Afterwards the value has to be loaded from memory into a floating point register (F0) so that it can be used.  Compare this to a Linux strtof() call:

* char * e = 0;
* float x = strtof( "1.0", &e );
400b74:       mov    $0x400ef8,%edi ; first param is address of "1.0" 400b79: movq$0x0,0x8(%rsp)       ; e = 0;
400b82:       lea    0x8(%rsp),%rsi       ; second param is &e
400b87:       callq  400810 <strtof@plt>  ; call the function, returning a value in %xmm0


Here the input parameters are RDI, RSI, and the output is XMM0. Nice and simple. Since XPLINK was designed for C code, we expect it to be more sensible. Let’s see what an XPLINK call looks like. Here’s a call to fmodf:

*      float r = fmodf( 10.0f, 3.0f );
LD       f0,+CONSTANT_AREA(,r9,184)
LD       f2,+CONSTANT_AREA(,r9,192)
L        r6,=A(__fmodf)(,r7,76)
L        r5,=A(__fmodf)(,r7,72)
BASR     r7,r6
NOP      9
LDR      f2,f0
STE      f2,r(,r4,2144)
*
*      printf( "fmodf: %g\n", (double)r );


There are some curious details that would have to be explored to understand the code above (why f0, f2, and not f0,f1?), however, the short story is that all the input and output values in (floating point) registers.

The mystery that led me to looking at this was a malfunctioning call to strtof:

*      float x = strtof( "1.0q", &e );
LA       r2,e(,r4,2144)
L        r6,=A(strtof)(,r7,4)
L        r5,=A(strtof)(,r7,0)
LA       r1,+CONSTANT_AREA(,r9,20)
BASR     r7,r6
NOP      17
LR       r0,r3
CEFR     f2,r0
STE      f2,x(,r4,2148)
*
*      printf( "strtof: v: %g\n", x );


The CEFR instruction converts an integer to a (hfp32) floating point representation, so we appear to have strtof returning it’s value in R3, which is an integer register. That then gets copied into R0, and finally into F2 (and after that into a stack spill location before the printf call.) I scratched my head about this code for quite a while, trying to figure out if the compiler had some mysterious way to make this work that I wasn’t figuring out. Eventually, I clued in. I’m so used to using a C++ compiler that I forgot about the old style implicit int return for an unprototyped function. But I had included <stdlib.h> in this code, so strtof should have been prototyped? However, the Language Runtime reference specifies that on z/OS you need an additional define to have strtof visible:

#define _ISOC99_SOURCE
#include <stdlib.h>


Without the additional define, the call to strtof() is as if it was prototyped as:

int strtof( const char *, char ** );


My expectation is that with such a correction, the call to strtof() should return it’s value in f0, just like fmodf() does. The result should also not be garbage!

Footnotes:

1.  There is also a “metal” compiler and probably a different calling convention to go with that.  I don’t know how metal differs from XPLINK.
2. Newer in the lifetime of the mainframe means circa 2001, which is bleeding edge given the speed that mainframe development moves.