Explainer 11 of 11

Bounding walks by crossings, and flagging what was cut

When an exact walk across programs costs too much, stop it after a fixed number of program crossings, keep what it found, and mark every answer the stop cut directly.

Where this shows up in reverse engineering

Cobble's last dataflow stage, cobble-dataflow-combine, answers one question for a CICS corpus: where can each dynamic jump go? EXEC CICS XCTL PROGRAM(WS-NEXT-PGM) names its target through a field, and that field's bytes may have come from another program. The combine walks those bytes backwards across programs until it finds literals.

The problem

The walk's unit is a place: a program, a statement in it, and a byte range. A statement reached under many different byte ranges is many places. Where programs hand areas to each other in a ring, every program in the ring can reach every other under many ranges, so one cyclic group holds the product of the two. On the large corpus that group grew past a million places.

The memo already answers each place once per round. The remaining cost is the number of places, which is a property of the corpus. No exact fix could be guaranteed faster without building it and measuring it on the corpus. What was needed was a walk whose cost has a known bound, and whose answers say when they may be incomplete.

The intuition

Inside one program a walk is cheap: the per-program memo answers each position once for the whole run. The expensive part is leaving the program. There are three ways to leave, and each is a crossing:

Give each place a counter: how many crossings it took to get here. When a place at the limit wants to cross again, it does not. It keeps the values it found in its own program, drops the crossings, and adds the reason hop-limit to what it reports. That reason travels back to the jump that started the walk, the same way every other reason in a value set does.

The cost is now bounded by the programs within N crossings of the jump, not by the size of the corpus. The answer is a subset of the exact answer. A target whose own walk was cut says so; a target shortened only because a cut elsewhere hid a link does not (see what goes wrong).

A second, separate switch, --skip-readbacks, drops one kind of crossing altogether: RETURNED stops are not followed, and the bytes report readback-skipped.

The example

Three programs in a chain. PROGC stamps 'PROGQ' into the first eight bytes of its commarea and XCTLs to PROGP. PROGP and PROGT have the same shape: each picks its next program either from a literal or from the first eight bytes of the commarea it received, and hands that commarea on. Neither PROGQ nor PROGM is in the analysed set.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. PROGC.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-AREA.
           05  WS-FROM          PIC X(8).
           05  WS-REST          PIC X(12).
       PROCEDURE DIVISION.
           MOVE 'PROGQ' TO WS-FROM.
           EXEC CICS XCTL PROGRAM('PROGP')
                COMMAREA(WS-AREA) LENGTH(20) END-EXEC.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. PROGP.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-NEXT              PIC X(8).
       LINKAGE SECTION.
       01  DFHCOMMAREA.
           05  LK-FROM          PIC X(8).
           05  LK-REST          PIC X(12).
       PROCEDURE DIVISION.
           IF EIBCALEN = 0
               MOVE 'PROGT' TO WS-NEXT
           ELSE
               MOVE LK-FROM TO WS-NEXT
           END-IF.
           EXEC CICS XCTL PROGRAM(WS-NEXT)
                COMMAREA(DFHCOMMAREA) LENGTH(20) END-EXEC.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. PROGT.
      *    Same as PROGP, with 'PROGM' in place of 'PROGT'.

Run to the end, the combine knows two edges: PROGC → PROGP (a literal) and PROGP → PROGT (from PROGP's own MOVE 'PROGT', so it is found at every limit). The question is the target of PROGT's XCTL. Exactly, it is PROGM or PROGQ: PROGQ travels from PROGC through PROGP's commarea untouched and into PROGT.

Step by step

The walk from PROGT's XCTL meets three places. Each row is a place, what its own program gives, and where it would cross next. The last column is the place's hop count.

placefound locallywould cross tohops
PROGT, XCTL, WS-NEXT bytes 0–8{PROGM}RECEIVED: caller PROGP, at its XCTL, commarea bytes 0–80
PROGP, XCTL, DFHCOMMAREA bytes 0–8{}RECEIVED: caller PROGC, at its XCTL, WS-AREA bytes 0–81
PROGC, XCTL, WS-AREA bytes 0–8{PROGQ}nothing2

The second place is PROGP's commarea at the moment PROGP hands it on. PROGP never writes LK-FROM, so the bytes there are what PROGP received, and the walk must cross again.

The target of PROGT at four limits (4 runs)
  1. --max-hops 0. The first place has hops 0, which is the limit, and it has a crossing. It keeps PROGM, drops the crossing to PROGP, and adds hop-limit. The walk ends there.

    placehopsoutcome
    PROGT, XCTL, 0–80cut: {PROGM} + hop-limit
    candidates: PROGMreasons: hop-limit
  2. --max-hops 1. The first place has hops 0, below the limit, so it crosses: PROGP's place gets hops 1. That place is at the limit and has a crossing, so it keeps what it found locally — nothing — and adds hop-limit. The extra hop bought nothing: PROGP only hands the bytes along.

    placehopsoutcome
    PROGT, XCTL, 0–80{PROGM}, crosses
    PROGP, XCTL, 0–81cut: {} + hop-limit
    candidates: PROGMreasons: hop-limit
  3. --max-hops 2. PROGP's place has hops 1, below the limit, so it crosses: PROGC's place gets hops 2. That place is at the limit but has no crossing — PROGQ is a literal in PROGC — so nothing is cut and no reason is added.

    placehopsoutcome
    PROGT, XCTL, 0–80{PROGM}, crosses
    PROGP, XCTL, 0–81{}, crosses
    PROGC, XCTL, 0–82{PROGQ}, nothing to cross
    candidates: PROGM, PROGQreasons: none
  4. No limit. Hops are not counted: every place carries 0. The same three places are expanded the same way, and the answer is the same as at 2. The row at 2 had no hop-limit, which is what promised this.

    placehopsoutcome
    PROGT, XCTL, 0–80{PROGM}, crosses
    PROGP, XCTL, 0–80{}, crosses
    PROGC, XCTL, 0–80{PROGQ}, nothing to cross
    candidates: PROGM, PROGQreasons: none

PROGP's own target needs one crossing fewer: its walk starts at its XCTL with {PROGT} found locally and reaches PROGC at hops 1. These are the rows the test asserts:

--max-hopstarget of PROGTtarget of PROGP
0PROGM · hop-limitPROGT · hop-limit
1PROGM · hop-limitPROGQ, PROGT
2PROGM, PROGQPROGQ, PROGT
no limitPROGM, PROGQPROGQ, PROGT

Read the first two rows for PROGT together: the answer at 0 and at 1 is the same, yet both still carry hop-limit, and at 2 it grows. That is why the reason matters more than the comparison.

The rule

place = (program, statement, bytes, hops)
expand(place) = local values, plus the places its stops cross to
if it would cross and hops ≥ N: keep local values, cross nowhere, add hop-limit
otherwise: each place crossed to gets hops + 1 (hops stays 0 when there is no limit)
--skip-readbacks: a RETURNED stop crosses nowhere and adds readback-skipped
answer(jump) = join of every place reached from (program, jump, target bytes, 0)

Why hops are counted only when a limit is set: the hop count is part of the place. With no limit, a cycle across programs would come back to the same program, statement and bytes with a larger count — a new place on every lap, and the walk would never end. With the count held at 0, the lap returns to a place already seen and the memo closes the cycle. With a limit N, a place can appear under at most N + 1 counts, so laps run out.

Why a target without hop-limit is exact: expansion depends on the hop count only through the cut. If no place reached from a jump was cut, each was expanded exactly as it would be with no limit, so the walk reaches the same programs, statements and bytes, and joins the same values and reasons. (This holds within a round, given the same edges; see below.)

What goes wrong

In cobble

The combine (cobble-dataflow-combine) applies the limit when --max-hops is set; with no limit, a walk follows links as far as they go. A walk cut at the limit carries the reason hop-limit. --skip-readbacks leaves RETURNED stops unfollowed, and the bytes report readback-skipped. Each place is answered once per round, as memoised reachability explains. Both flags are in the field dataflow reference.