Explainer 4 of 11

Byte-extent aliasing

In COBOL, memory is bytes, not variables. Treat every field as a byte range and aliasing follows from one test: do two ranges overlap?

Where this shows up in reverse engineering

Any format where more than one name can describe the same bytes needs this test, not just COBOL — the technique is the same wherever the hiding happens.

The problem

The reaching definitions page tracked writes by variable name. That works when every name owns its own storage. In COBOL many names share storage. A group like REC is just the bytes of its children laid end to end, so writing REC writes WS-A and WS-B. REDEFINES gives the same bytes a second name with a different shape. A move into one child changes half of the group. Tracking by name gets every one of these wrong.

The intuition

Lay the program's data out as the compiler does: each field is a start offset and a length in some storage region. Forget names. A write paints the bytes it covers. A read looks at the bytes it covers and asks which writes painted any of them. Two fields alias when their ranges overlap — whatever they are called, however they were declared.

The example

REC is eight bytes: WS-A is bytes 0–4 and WS-B is bytes 4–8. WHOLE redefines the same eight bytes as one field. The program writes the group, then one child, then reads each child and the redefinition.

       IDENTIFICATION DIVISION.
       PROGRAM-ID. BYTEEXT.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  REC.
           05  WS-A             PIC X(4).
           05  WS-B             PIC X(4).
       01  WHOLE REDEFINES REC  PIC X(8).
       01  OUT-A                PIC X(4).
       01  OUT-B                PIC X(4).
       01  OUT-ALL              PIC X(8).
       PROCEDURE DIVISION.
       MAIN-PARA.
           MOVE 'AAAABBBB' TO REC.
           MOVE 'XXXX' TO WS-A.
           MOVE WS-A TO OUT-A.
           MOVE WS-B TO OUT-B.
           MOVE WHOLE TO OUT-ALL.
           STOP RUN.

Step by step

The strip is bytes 0 to 8 of working storage. Each byte shows the statement that last wrote it. Below it are the definitions the analysis keeps, one per write, each with the range it wrote.

The byte strip, one statement at a time (5 steps)
  1. MOVE 'AAAABBBB' TO REC: a literal, so no definers. It writes REC [0:8].

    0
    1
    2
    3
    4
    5
    6
    7
    REC
    WS-A
    WS-B
    WHOLE
    /0
    /0
    /0
    /0
    /0
    /0
    /0
    /0

    Definitions kept (amber: overlaps this read)

    MAIN-PARA/0 REC [0:8]
  2. MOVE 'XXXX' TO WS-A: a literal, so no definers. It writes WS-A [0:4]. It overlaps MAIN-PARA/0 without covering it, so that definition stays.

    0
    1
    2
    3
    4
    5
    6
    7
    REC
    WS-A
    WS-B
    WHOLE
    /1
    /1
    /1
    /1
    /0
    /0
    /0
    /0

    Definitions kept (amber: overlaps this read)

    MAIN-PARA/0 REC [0:8]MAIN-PARA/1 WS-A [0:4]
  3. MOVE WS-A TO OUT-A reads WS-A [0:4]. Definitions overlapping it: MAIN-PARA/0, MAIN-PARA/1. Bytes last written by: MAIN-PARA/1. It writes OUT-A, outside the record.

    0
    1
    2
    3
    4
    5
    6
    7
    REC
    WS-A
    WS-B
    WHOLE
    /1
    /1
    /1
    /1
    /0
    /0
    /0
    /0

    Definitions kept (amber: overlaps this read)

    MAIN-PARA/0 REC [0:8]MAIN-PARA/1 WS-A [0:4]

    Who wrote what this read sees

    byte by byte (exact)MAIN-PARA/1
    per write (cobble)MAIN-PARA/0, MAIN-PARA/1
  4. MOVE WS-B TO OUT-B reads WS-B [4:8]. Definitions overlapping it: MAIN-PARA/0. Bytes last written by: MAIN-PARA/0. It writes OUT-B, outside the record.

    0
    1
    2
    3
    4
    5
    6
    7
    REC
    WS-A
    WS-B
    WHOLE
    /1
    /1
    /1
    /1
    /0
    /0
    /0
    /0

    Definitions kept (amber: overlaps this read)

    MAIN-PARA/0 REC [0:8]MAIN-PARA/1 WS-A [0:4]

    Who wrote what this read sees

    byte by byte (exact)MAIN-PARA/0
    per write (cobble)MAIN-PARA/0
  5. MOVE WHOLE TO OUT-ALL reads WHOLE [0:8]. Definitions overlapping it: MAIN-PARA/0, MAIN-PARA/1. Bytes last written by: MAIN-PARA/0, MAIN-PARA/1. It writes OUT-ALL, outside the record.

    0
    1
    2
    3
    4
    5
    6
    7
    REC
    WS-A
    WS-B
    WHOLE
    /1
    /1
    /1
    /1
    /0
    /0
    /0
    /0

    Definitions kept (amber: overlaps this read)

    MAIN-PARA/0 REC [0:8]MAIN-PARA/1 WS-A [0:4]

    Who wrote what this read sees

    byte by byte (exact)MAIN-PARA/0, MAIN-PARA/1
    per write (cobble)MAIN-PARA/0, MAIN-PARA/1

The rule

extent = (region, start, end)
may_alias(a, b) = same region ∧ a.start < b.end ∧ b.start < a.end
must_cover(w, d) = same region ∧ w.start ≤ d.start ∧ d.end ≤ w.end ∧ w is exact
kill(s) = { d | s's write must_cover d }
definers(read r at s) = { d ∈ in(s) | may_alias(d, r) }

Two different tests, on purpose. A definition is a possible source of a read when it overlaps any byte of it. A definition is certainly gone only when a later write covers every byte it wrote. Anything between — a partial overwrite — leaves it alive.

What goes wrong

In cobble

Every dataflow stage works on these extents. Each field's extent comes from its declaration alone — level, usage, OCCURS, REDEFINES. The two tests are the ones above: whether two extents may overlap, and whether one covers all of the other. A move of a literal has no source field, so cobble names its source LITERAL.