Explainer 6 of 11

Area need: which handed-over bytes anyone reads

A commarea is handed from program to program, but most of its bytes never decide where control goes next. Work out, per program, which bytes do, and track only those.

Where this shows up in reverse engineering

Recovering a CICS application's call graph means resolving its dynamic transfers: CALL WS-PGM, EXEC CICS XCTL PROGRAM(WS-NEXT-PGM). On a large CICS corpus (about 3,800 programs and 19,000 jumps) the target names come in three shapes: a literal VALUE in working storage, a MOVE 'LITERAL' TO WS-FIELD, and, for about 5% of sites, a field the program reads out of its DFHCOMMAREA and then transfers to. That last shape is the reason to follow values across programs at all.

The problem

A caller writes a few fields of a 100-byte commarea and transfers to a callee. The callee reads one 8-byte field of it and uses it as the next program name. To resolve that name, the forward analysis has to carry the caller's writes into the callee. If it carries every byte of every commarea, it does work for 92 bytes it will never need. You want to know, for each program, which bytes of what it is handed actually feed a call target, so the forward analysis can drop the rest.

The catch is that "feeds a call target" crosses programs. A callee's need on entry decides what its caller must keep live at the call; that can make the caller's own commarea live on its entry, which its own callers must then keep. The need has to flow backward along call chains until it stops changing.

The intuition

Inside one program this is ordinary backward liveness: start from the reads you care about and walk the control flow graph backward, asking at each statement which bytes are still needed. A write that fully covers a needed byte satisfies it, so that byte is no longer needed above the write; the bytes the write copied from become needed instead. Where paths split, anything needed on either path is needed. It is the mirror image of reaching definitions, run with the same worklist, over byte extents rather than whole fields.

What makes it an area need is the seed and the summary.

Each row is indexed by a slot: the kind of area and its position. COMMAREA is what LINK and XCTL hand over (always position 0); USING is a CALL's arguments, by position; RETRIEVED is what a START hands over in its FROM area and the started program gets back with RETRIEVE INTO. Bytes are stored relative to the start of the area, so a caller's 100-byte group and the callee's DFHCOMMAREA line up by offset, not by field name.

Every program starts needing nothing. Walk all of them, rebuild the table from what they report, and walk again the ones whose inputs changed. Rows only ever grow, so this stops. How the rounds pick which programs to walk again is on its own page.

The example

PROGA fills two fields of a 100-byte commarea and transfers to PROGB. PROGB copies its whole DFHCOMMAREA into working storage and transfers to whatever program name sits in bytes 8 to 16.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. PROGA.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-COMMAREA.
           05  WS-USER-ID       PIC X(8).
           05  WS-NEXT-PGM      PIC X(8).
           05  WS-REST          PIC X(84).
       PROCEDURE DIVISION.
       MAIN-PARA.
           MOVE 'USER0001' TO WS-USER-ID.
           MOVE 'PROGC' TO WS-NEXT-PGM.
           EXEC CICS XCTL PROGRAM('PROGB')
                COMMAREA(WS-COMMAREA) END-EXEC.
       IDENTIFICATION DIVISION.
       PROGRAM-ID. PROGB.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-COMMAREA.
           05  WS-USER-ID       PIC X(8).
           05  WS-NEXT-PGM      PIC X(8).
           05  WS-REST          PIC X(84).
       LINKAGE SECTION.
       01  DFHCOMMAREA          PIC X(100).
       PROCEDURE DIVISION.
       MAIN-PARA.
           MOVE DFHCOMMAREA TO WS-COMMAREA.
           EXEC CICS XCTL PROGRAM(WS-NEXT-PGM) END-EXEC.

Byte ranges are written region[start:end), counted from 0: ws[8:16) is WS-NEXT-PGM, linkage[0:100) is DFHCOMMAREA. Only statements are shown; the graph's other nodes write nothing and hand their live set on unchanged. Both XCTLs transfer for good, so neither program reads anything back and both back rows stay empty.

Step by step

Within a program the walk visits statements last to first (the worklist takes the highest position each time). Each table shows what is live on entry to the statement, that is, just above it.

Two rounds of area need over PROGA and PROGB (6 steps)
  1. Every program starts with an empty row. PROGA receives no area; PROGB receives one in slot COMMAREA/0. Both programs are dirty, so round 1 walks both.

    memberreceivesentryback
    PROGAnone{}{}
    PROGBCOMMAREA/0{}{}

    To walk

    PROGAPROGB
  2. Round 1, PROGA. The XCTL names PROGB by a literal, so its target reads no storage. It hands over ws[0:100) in slot COMMAREA/0, and is charged PROGB's entry row for that slot: empty. Nothing is live anywhere.

    statementdemanded herelive on entry
    XCTL PROGRAM('PROGB')PROGB's entry row on ws[0:100): none{}
    MOVE 'PROGC' TO WS-NEXT-PGMnone{}
    MOVE 'USER0001' TO WS-USER-IDnone{}

    PROGA's report

    entry: none (receives nothing)
  3. Round 1, PROGB. The dynamic XCTL demands its operand, ws[8:16). The MOVE above it writes all of ws[0:100), which covers ws[8:16), so that range is no longer live above it. The MOVE is a byte-for-byte group copy that starts at linkage byte 0, so only the source bytes lined up with the live target bytes become live: linkage[8:16), not all 100.

    statementdemanded herelive on entry
    XCTL PROGRAM(WS-NEXT-PGM)ws[8:16){ws[8:16)}
    MOVE DFHCOMMAREA TO WS-COMMAREAnone{linkage[8:16)}

    PROGB's report: live at entry, relative to DFHCOMMAREA = linkage[0:100)

    entry COMMAREA/0: [8, 16)
  4. End of round 1. PROGB's entry row changed. PROGA's call site reaches PROGB, so PROGA is walked again. PROGB is not: nothing it reads from the table changed. Its one dynamic site hands over no area, so it is not charged the union of every member's need either.

    memberreceivesentryback
    PROGAnone{}{}
    PROGBCOMMAREA/0{COMMAREA/0: [8, 16)}{}

    To walk

    PROGA
  5. Round 2, PROGA. The XCTL is now charged PROGB's entry row: offsets [8, 16) of ws[0:100), which is ws[8:16). The MOVE of 'PROGC' fully covers it and reads only a literal, so above it nothing is live. MOVE 'USER0001' writes bytes nobody needs.

    statementdemanded herelive on entry
    XCTL PROGRAM('PROGB')ws[8:16){ws[8:16)}
    MOVE 'PROGC' TO WS-NEXT-PGMnone{}
    MOVE 'USER0001' TO WS-USER-IDnone{}

    PROGA's report

    entry: none (receives nothing)
  6. End of round 2. No row changed, so no program is dirty: the need has settled. This is the file cobble-dataflow-area-need writes, one JSON row per member.

    memberrow on disk
    PROGA{"member":"PROGA","receives":[],"entry":[],"back":[],"sites":[]}
    PROGB{"member":"PROGB","receives":[["commarea",0]],"entry":[[["commarea",0],[8,16]]],"back":[],"sites":[]}

    To walk

    empty — settled after 2 rounds

Fed this file, cobble-dataflow-fixpoint --call-targets-only --narrow-areas demands only ws[8:16) at PROGA's XCTL instead of all of ws[0:100). PROGA then keeps one write target (WS-NEXT-PGM) instead of two, and its edge count drops from 2 to 1. The cross-program combine still follows linkage[8:16) in PROGB back to 'PROGC' in PROGA. These rows and counts are what cobble produced on these two programs.

Without the byte-for-byte copy rule, the MOVE in PROGB would make all of linkage[0:100) live, PROGB's entry row would be [0, 100), and PROGA would have to keep MOVE 'USER0001' too, along with whatever wrote the other 84 bytes.

The rule

live_in(s) = demand(s) ∪ (live_out(s) − bytes s certainly overwrites)
           ∪ needed_sources(s, live_out(s)) ∪ targets of s's weak writes that touch live_out(s)
live_out(s) = ⋃ live_in(t) for every successor t of s

demand(s) = operand of a dynamic call at s, and a TRANSID data name
          ∪ for each area s hands over: what the callee reads of it on entry
          ∪ at GOBACK / EXIT PROGRAM / STOP RUN / CICS RETURN: this member's back row on its received areas

callee reads on entry = literal callee in the corpus that receives the slot: its entry row
                      callee outside the corpus, or not receiving the slot: every byte
                      resolved dynamic site: the union of its candidates' rows (none if it resolved to nothing)
                      unresolved dynamic site: the union of every member's entry row

entry(m)[slot] = live_in(entry of m) ∩ m's received area, as offsets into the area
                ∪ for each RETRIEVE INTO: live_out(RETRIEVE) ∩ the INTO area, in slot RETRIEVED/0
back(c)[slot] = ⋃ over LINK/CALL sites reaching c: live_out(site) ∩ the handed area
                ∪ what every unresolved LINK/CALL reads back

repeat rounds from all-empty rows until no row changes

Byte-for-byte copies. A MOVE copies byte for byte when either side is a group, or both are alphanumeric, the source has no subscript, and its reference-modification start (if any) is known to the compiler. For such a write, the walk maps each live target byte back to the one source byte that landed on it, and keeps only those. Any other write needs all its sources.

LENGTH OF in bounds. A reference-modification bound such as WS-AREA(LENGTH OF WS-HDR + 1 : LENGTH OF WS-BODY) is folded to a number, because the compiler knows each field's length from its declaration. Literals, LENGTH OF and + - * / over them fold. With both bounds known the extent is exact and narrow. Without folding it would be a clamped extent over the whole base field.

START and RETRIEVE. A START hands over its FROM area the way a LINK hands over a commarea, in slot RETRIEVED/0. Its transaction id is resolved to a program through the CSD, as a RETURN TRANSID's is. The started program's need in that slot is the bytes of its RETRIEVE INTO area live right after the RETRIEVE. The RETRIEVE itself demands nothing: it writes the area, it does not read it.

How the result narrows the fixpoint. --call-targets-only keeps only the writes a dynamic call target or a handed-over area depends on. Without --narrow-areas every handed-over area counts whole. With it, each call is charged only the bytes the area-need file says its callees read, so the forward run tracks those bytes and nothing else of the area.

What goes wrong

In cobble

The command is cobble-dataflow-area-need. It runs the rounds over the whole corpus and stops when a round changes no row, or after --max-rounds N. --sites adds a report of each dynamic site and where it sits in the source. The fixpoint reads the result under cobble-dataflow-fixpoint --call-targets-only --narrow-areas, and tracks only the bytes of each handed-over area that some program reads. The command is documented in field dataflow.

Next: resolving a dynamic call site inside its own program, and rounds across programs, and walking only what changed.