The Minimum You Need to Know Before Reading COBOL Source Code
· Updated: · Go Komura · COBOL, Legacy Technology, Business Systems, Maintenance, Mainframe
Revision history (1 updates, last updated Sep 1, 2026)
A log of the changes made to this article. Where a pre-update version was archived, it stays readable at a permanent DOI link.
- Retranslated as a full translation of the Japanese original. The previous English version was an abridgement that carried only part of the source, so sections, tables, Mermaid diagrams, figure captions and FAQ entries were missing. All of them have been restored to match the Japanese original, and the technical claims are the same as in the Japanese version. Read the version before this update (DOI: 10.5281/zenodo.21614531)
- First published
Cite this article(DOI: 10.5281/zenodo.21614530)
This article is archived on Zenodo. Below are both the DOI that always resolves to the latest version and the DOI pinned to the version you are reading.
Go Komura (2026). The Minimum You Need to Know Before Reading COBOL Source Code. KomuraSoft LLC. https://doi.org/10.5281/zenodo.21614530 https://comcomponent.com/en/blog/2026/03/17/001-cobol-minimum-reading-guide/
- DOI (latest version)
- 10.5281/zenodo.21614530
- DOI (this version)
- 10.5281/zenodo.22217168
Handovers, incident response, maintaining a vendor package. In situations like these, one day a pile of COBOL source code suddenly lands on your desk.
- The file names end in
.cblor.cpy - The variable names are all uppercase
- Rows of
01,05,77,88 - Things like
PIC S9(7)V99 COMP-3appear, somewhere between an incantation and accounting software - And it is full of
COPY, so the file you opened does not even show you the whole picture
This is usually the point where people stall.
But the map you need to read it is not that big. COBOL varies between compilers and products, yet the skeleton you should grasp first when reading an existing business system is largely the same everywhere. With IBM-style and typical business COBOL in mind, this article lays out the minimum set for people who suddenly have to read COBOL source code.
flowchart TB
accTitle: Why you stall and how big the map really is
accDescr: A diagram showing that all-uppercase names, level numbers, notation such as PIC S9(7)V99 COMP-3, and source full of COPY make readers stall, but that the skeleton to grasp first when reading an existing business system is largely common across compilers, so the map is not that big.
iv1["Unfamiliar notation makes you stall"] --> iv2["But the skeleton is common across compilers"]
iv2 --> iv3["Get the minimum map first"]
iv1 -.-> iv4["COPY everywhere hides the whole picture"]
Figure 1: What looks like an incantation comes down to a handful of shared concepts.
1. The Conclusion First (In One Breath)
Putting it rather crudely up front, but in a way that actually helps in practice:
- Before being a language of logic, COBOL is very much a language of record definitions
- Reading only the
PROCEDURE DIVISIONgives you half the story. Look at theDATA DIVISIONfirst PICis the shape of an item;USAGEis how it is representedCOMP-3is packed decimal. It shows up constantly in the world of amounts and counts88is not a separate variable; it is a condition-name attached to the values of the preceding itemREDEFINESis a mechanism for viewing the same memory in a different shape. It is not a copy- If there is a
COPY, the source you are looking at is not yet complete. You cannot see the whole until you open the copybooks - If you can follow
PERFORM,IF,EVALUATE,READ,WRITE, andCALL, you can grasp most of the flow - Old source is fixed format, where column positions carry meaning. The whitespace you see is not just decoration1
In short: DIVISION, PIC, USAGE, COMP-3, REDEFINES, OCCURS, 88, COPY, PERFORM. Once you can read these, your odds of getting lost drop considerably.
Knowledge map for this article
Before COBOL is a language of processing steps written in the PROCEDURE DIVISION, it is a language that defines the shape of records in the DATA DIVISION, where each data item gets its shape from the PICTURE clause and its internal representation, such as COMP-3, from the USAGE clause. COMP-3 is packed decimal, which packs two decimal digits into each byte, so it looks like a meaningless sequence of bytes when the file is opened as text. REDEFINES reinterprets the same storage area with a different layout, OCCURS expresses an array, and an 88 level expresses a condition that gives a name to a value of the item immediately above it. Because the COPY statement pulls an external copybook in at compile time, the source file on screen does not show the whole picture on its own, and reading COBOL means following the flow of processing through PERFORM statements and scope terminators and pinning down the external boundaries through FILE STATUS, EXEC SQL, and EXEC CICS.
flowchart LR
accTitle: How to read COBOL
accDescr: Diagram showing how COBOL rests on the DATA DIVISION and the PROCEDURE DIVISION, and how data definition elements such as the PICTURE clause, the USAGE clause, COMP-3, REDEFINES, OCCURS, 88 levels, and the COPY statement relate to control elements such as the PERFORM statement and scope terminators and to external boundary elements such as EBCDIC, FILE STATUS, EXEC SQL, and EXEC CICS
cobol["COBOL"]
cobol_data_division["DATA DIVISION"]
comp_3["COMP-3 (packed decimal)"]
cobol_procedure_division["PROCEDURE DIVISION"]
cobol_picture_clause["PICTURE clause (PIC)"]
cobol_usage_clause["USAGE clause"]
comp_5["COMP-5"]
cobol_redefines["REDEFINES clause"]
cobol_occurs["OCCURS clause"]
cobol_occurs_depending_on["OCCURS DEPENDING ON"]
cobol_88_level["Level 88 (Condition Name)"]
cobol_copy["COPY Statement"]
copybook["Copybook"]
cobol_perform["PERFORM statement"]
cobol_scope_terminator["Scope terminator"]
cobol_move["MOVE statement"]
cobol_fixed_format["Fixed format (reference format)"]
ibm_enterprise_cobol["IBM Enterprise COBOL"]
ebcdic["EBCDIC"]
cobol_file_status["FILE STATUS Clause"]
exec_sql["EXEC SQL"]
exec_cics["EXEC CICS"]
cobol -->|"uses"| cobol_data_division
cobol -->|"uses"| cobol_procedure_division
cobol_data_division -->|"uses"| cobol_picture_clause
cobol_data_division -->|"uses"| cobol_usage_clause
cobol_usage_clause -->|"uses"| comp_3
cobol_usage_clause -->|"uses"| comp_5
cobol_data_division -->|"uses"| cobol_redefines
cobol_data_division -->|"uses"| cobol_occurs
cobol_occurs_depending_on -->|"uses"| cobol_occurs
cobol_data_division -->|"uses"| cobol_88_level
cobol -.->|"uses"| cobol_copy
cobol_copy -->|"uses"| copybook
cobol_procedure_division -->|"uses"| cobol_perform
cobol_procedure_division -->|"uses"| cobol_scope_terminator
cobol_procedure_division -->|"uses"| cobol_move
cobol -.->|"uses"| cobol_fixed_format
ibm_enterprise_cobol -->|"uses"| ebcdic
ibm_enterprise_cobol -.->|"uses"| cobol_fixed_format
cobol -.->|"uses"| cobol_file_status
cobol -.->|"uses"| exec_sql
cobol -.->|"uses"| exec_cics
In the diagram a solid line marks a relation that always holds and a dashed line marks a conditional one (the conditions are given per relation on the detail page). The full list of relations (21 in total, with evidence and certainty) and the definitions of the main concepts are collected on the knowledge map detail page (in Japanese). Data: JSON-LD / Turtle
2. Think of COBOL First as a Language About the Shape of Data
If you read it with C# or Java instincts, you will first want to chase the ifs, fors, and function calls.
But with COBOL, before going there, it is faster to grasp “what records does this program receive, what records does it produce, and what buffers does it hold?”
A typical business COBOL program flows roughly like this:
- Read records from a file or DB
- Move them into
WORKING-STORAGEitems - Branch on conditions
- Repack them into another record
- Write them out
In other words, layout tends to come before algorithm.
flowchart TB
accTitle: The flow of a typical business COBOL program
accDescr: A diagram showing the flow of a typical business COBOL program, which reads records from a file or DB, moves them into WORKING-STORAGE items, branches on conditions, repacks them into another record, and writes them out.
f1["Read a record"] --> f2["Move it into WORKING-STORAGE"]
f2 --> f3["Branch on conditions"]
f3 --> f4["Repack into another record"]
f4 --> f5["Write it out"]
Figure 2: The record flow is what matters, and the algorithm sits in between.
For example, here is a typical skeleton.
IDENTIFICATION DIVISION.
PROGRAM-ID. SAMPLE01.
ENVIRONMENT DIVISION.
INPUT-OUTPUT SECTION.
FILE-CONTROL.
SELECT SALES-FILE ASSIGN TO ...
DATA DIVISION.
FILE SECTION.
FD SALES-FILE.
01 SALES-REC.
05 SALE-ID PIC 9(8).
05 SALE-AMOUNT PIC S9(7)V99 COMP-3.
WORKING-STORAGE SECTION.
01 WS-EOF PIC X VALUE 'N'.
88 EOF VALUE 'Y'.
PROCEDURE DIVISION.
PERFORM UNTIL EOF
READ SALES-FILE
AT END
SET EOF TO TRUE
NOT AT END
PERFORM PROCESS-SALE
END-READ
END-PERFORM
STOP RUN.
When reading this code, the first things to look at are the type of SALE-AMOUNT and the meaning of EOF, before the PERFORM.
Read COBOL in that order and it suddenly goes quiet.
3. Look at the Four DIVISIONs First
COBOL source is first divided into four large DIVISIONs.
| DIVISION | What to look at first |
|---|---|
IDENTIFICATION DIVISION |
Program name, old comments, provenance |
ENVIRONMENT DIVISION |
Files, external resources, I/O assumptions |
DATA DIVISION |
Record definitions, working areas, parameters |
PROCEDURE DIVISION |
The actual processing steps |
The especially important parts are these.
FILE SECTIONContains the record definitions for input/output filesWORKING-STORAGE SECTIONContains everyday variables, flags, counters, work buffersLOCAL-STORAGE SECTIONMay contain areas re-initialized on each invocationLINKAGE SECTIONMay contain parameters passed in from outside, the receiving end of a subprogram
If you see a LINKAGE SECTION and PROCEDURE DIVISION USING ..., there is a strong chance the program is not self-contained and runs on data received from outside.
flowchart TB
accTitle: What a LINKAGE SECTION tells you
accDescr: A diagram showing that when a LINKAGE SECTION and a PROCEDURE DIVISION USING are visible, there is a strong chance the program is not self-contained and instead runs on data received from outside.
lk1["A LINKAGE SECTION is present"] --> lk3["Likely runs on data received from outside"]
lk2["A PROCEDURE DIVISION USING is present"] --> lk3
lk3 -.-> lk4["Not a self-contained program"]
Figure 3: Once you see the receiving end defined, read the code assuming a caller exists.
4. Do Not Be Intimidated by the Look of Fixed Format
In old COBOL, the column positions themselves in a source line carry meaning. If you look at it without knowing this, you will never figure out “why is there this weird margin on the left?”1
In fixed format, roughly:
- Columns 1 - 6: sequence number
- Column 7: indicator
- Columns 8 - 11: Area A
- Columns 12 - 72: Area B
Column 7 is especially important.
*or/: comment line-: continuation lineD: debugging line*>: a comment that can also appear mid-line
With a ruler above it, the mapping from columns to content looks like this. The first line marks the tens and the second line the ones.
1 2 3 4 5 6 7 8
12345678901234567890123456789012345678901234567890123456789012345678901234567890
SSSSSSIAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB........
S= sequence number (columns 1 - 6)I= indicator (column 7)A= Area A (columns 8 - 11)B= Area B (columns 12 - 72).= column 73 onward. Some compilers use it as an identification field, but it plays no part in the meaning of the program
Applied to real source, it comes out like this.
000100* This line is a comment because column 7 holds an asterisk
000200 IDENTIFICATION DIVISION.
000300 PROGRAM-ID. SAMPLE01.
000400 DATA DIVISION.
000500 WORKING-STORAGE SECTION.
000600 01 WS-ORDER.
000700 05 WS-ORDER-ID PIC 9(8).
000800 05 WS-LONG-NAME PIC X(30) VALUE 'ABCDEFGHIJKLMNOPQRST
000900- 'UVWXYZ0123'.
There are four points to reading it.
- Columns 1 - 6 are the sequence number. That is what
000100and friends are in the example above, and they have nothing to do with how the program runs. They are sometimes blank. - Column 7 is the indicator. An
*means a comment, a-means a continuation line. Line000900above is one, continuing the string literal from the previous line. - Columns 8 - 11 are Area A.
DIVISION,SECTION, paragraph names,FD, and the level numbers01and77start here. In the example,IDENTIFICATION DIVISION.and01 WS-ORDER.begin in Area A. - Columns 12 - 72 are Area B. Ordinary statements and lower levels such as
05are written here. In the example,05 WS-ORDER-IDbegins in Area B.
The whitespace here is not “formatting” in the modern sense; parts of it are syntax. Converting tabs in an editor, shifting things left, or copy-pasting carelessly will simply break it. When looking at old source, first question whether the file is fixed format or free format. Run a modern auto-formatter over fixed-format source and the boundary between Area A and Area B collapses, and the code no longer compiles.
flowchart TB
accTitle: Before you touch fixed-format source
accDescr: A diagram showing that with old source you first confirm whether it is fixed format or free format, because running a modern auto-formatter or a tab conversion over fixed-format source collapses the boundary between Area A and Area B and the code no longer compiles.
fx1["Opened some old source"] --> fx2{"Fixed or free"}
fx2 -->|"Fixed format"| fx3["Column positions are syntax"]
fx3 -.-> fx4["Auto-formatting or tab conversion breaks it"]
fx2 -->|"Free format"| fx5["Column rules are loose"]
Figure 4: Settle the format of the file before you reformat anything.
5. The Bare Minimum of the DATA DIVISION
5.1 Level Numbers
COBOL data definitions build their hierarchy with level numbers, not indentation.2
01 WS-ORDER.
05 WS-ORDER-ID PIC 9(8).
05 WS-AMOUNT PIC S9(7)V99 COMP-3.
05 WS-STATUS PIC X.
88 WS-OK VALUE '0'.
88 WS-ERROR VALUE '9'.
77 WS-COUNT PIC 9(4).
At minimum, remembering just this much is enough.
01: top-level record or group forming one unit02-49: the levels below it77: an independent elementary item88: condition-name. Attaches a name to a value of the preceding item366: forRENAMES. You will not run into it often, but it exists
What matters is not to think of 88 as a separate bool variable.
There is no separate storage area called WS-OK; rather, when WS-STATUS is '0', it can be read under the name WS-OK. That is the feel of it.
flowchart TB
accTitle: What an 88 level really is
accDescr: A diagram showing that an 88 level is not an independent boolean variable but a condition-name attached to values of the preceding item, so WS-OK is simply a name under which WS-STATUS can be read when it holds a particular value.
cn1["Base item WS-STATUS"] --> cn2{"What is the value"}
cn2 -->|"When it is 0"| cn3["True under the name WS-OK"]
cn2 -->|"When it is 9"| cn4["True under the name WS-ERROR"]
cn3 -.-> cn5["No separate storage area exists"]
Figure 5: An 88 is not a variable but a readable alias attached to a value.
One more important thing: it is the level numbers, not the whitespace, that determine the hierarchy.
The visual indentation is a useful hint, but what you should ultimately trust is the 01 / 05 / 10 / 88.2
5.2 PICTURE
PIC expresses the shape of an item.
The ones you will see most often are these.
| Notation | Rough meaning |
|---|---|
X |
Character |
9 |
Digit |
S |
Signed |
V |
Decimal point exists only logically |
X(10) |
10 characters |
9(5) |
5-digit number |
S9(7)V99 |
Signed, 7 integer digits + 2 decimal digits |
For example:
PIC X(10)→ 10 charactersPIC 9(5)V99→ 5 integer digits + 2 decimal digitsPIC S9(7)V99→ signed, 7 integer digits + 2 decimal digits
The especially important one here is V.
V holds no actual . character.
PIC 9(5)V99 is treated as “a number with 2 decimal places,” but there is no dot character in the data.
So if you interpret a file or a dump as “the string you see,” you will almost always trip.
flowchart TB
accTitle: V is a logical decimal point
accDescr: A diagram showing that the V in PIC 9(5)V99 is a logical decimal point that holds no actual dot character, so no dot is stored in the data and interpreting a file or dump as the string you see will trip you up.
vd1["PIC 9(5)V99"] --> vd2["Treated as a number with 2 decimal places"]
vd2 --> vd3["No dot character is stored in the data"]
vd3 -.-> vd4["Reading it as a visible string trips you up"]
Figure 6: The decimal point lives only in the definition, never in the data.
5.3 USAGE / DISPLAY / COMP / COMP-3
If PIC is the shape, USAGE is the representation in which the item is held.
At minimum, grasping just the following gets you a long way.45
| Notation | Rough meaning | Caution when reading |
|---|---|---|
DISPLAY |
External decimal, visible as characters | On a mainframe this may assume EBCDIC6 |
COMP / BINARY |
Binary | The visible digit count and the internal representation are different things |
COMP-3 / PACKED-DECIMAL |
Packed decimal | Looks broken if you read it as characters |
For example:
01 WS-AMOUNT-DISP PIC S9(7)V99.
01 WS-AMOUNT-BIN PIC S9(7) COMP.
01 WS-AMOUNT-PACK PIC S9(7)V99 COMP-3.
All three are “numbers,” but they hold their contents differently.
flowchart TB
accTitle: The same number can be held in different ways
accDescr: A diagram showing that even for the same signed numeric shape, DISPLAY is external decimal visible as characters, COMP is binary, and COMP-3 is packed decimal, so the USAGE decides how the contents are held.
us1["Numeric items of the same shape"] --> us2["DISPLAY (visible as characters)"]
us1 --> us3["COMP (binary)"]
us1 --> us4["COMP-3 (packed decimal)"]
us4 -.-> us5["Looks broken when read as characters"]
Figure 7: Same PIC, different USAGE, and the byte string is a different animal.
What pays off most in practice is your reflex the instant you see COMP-3.
- That is packed decimal
- Probably an amount, a tax figure, a count, or a rate
- It is supposed to look broken when viewed as text
- Eyeballing it in a CSV or UTF-8 frame of mind will bite you
Holding on to that understanding makes you much less likely to panic needlessly at how dumps and binary files appear.
flowchart TB
accTitle: Your reflex the instant you see COMP-3
accDescr: A diagram showing the reflex to build when you see COMP-3, namely that it is packed decimal, that it is probably an amount or tax figure or count or rate, and that it is supposed to look broken as text so you should not eyeball it in a CSV or UTF-8 frame of mind.
cp1["Spotted a COMP-3"] --> cp2["Recognize it as packed decimal"]
cp2 --> cp3["Suspect an amount or count field"]
cp3 --> cp4["Stay calm when text looks broken"]
Figure 8: This one reflex cuts down how often a dump makes you panic.
What a COMP-3 Byte String Actually Looks Like
Work through this by hand once and everything downstream looks different.
Packed decimal has only two rules.45
- Pack two decimal digits into each byte
- Except that the rightmost byte uses its byte for the last digit plus the sign
The sign is a 4-bit value: C is positive, D is negative, and F is unsigned.
Check it against the examples in IBM’s manual.4
| Definition | Value | Byte string |
|---|---|---|
PIC S9(4) PACKED-DECIMAL |
+1234 |
01 23 4C |
PIC S9(4) PACKED-DECIMAL |
-1234 |
01 23 4D |
PIC 9(4) PACKED-DECIMAL |
1234 |
01 23 4F |
1234 has 4 digits, so rule 2 leaves one spare digit position at the front. That is the leading 0.
Trace PIC S9(7)V99 COMP-3, which has appeared over and over in this article, the same way.
S9(7)V99is 7 integer digits + 2 decimal digits = 9 digitsVonly marks the position of the decimal point, so it consumes no bytes at all- Packing 9 digits two at a time gives 4 bytes, plus 1 byte for the last digit and the sign. 5 bytes in total
For a value of +12345.67, padding to 9 digits gives 001234567, so it comes out like this.
Value : +12345.67
9 digits : 0 0 1 2 3 4 5 6 7 plus the sign
Bytes : 00 12 34 56 7C
^ sign C = positive
For the negative value -12345.67, only the last byte changes, to 7D.
Bytes : 00 12 34 56 7D
The real question is what this looks like when you open it as text. Force 00 12 34 56 7C into one ASCII character per byte and you get:
00is NUL and12is a control character, so neither can be displayed as a character at all34is4,56isV, and7Cis|
On screen it therefore looks like “a few undisplayable characters followed by 4V|”. The sequence 12345.67 appears nowhere.
This is what “looks like mojibake but is not broken” really is. When you open a dump and meet a meaningless sequence, the first thing to suspect is whether that item’s USAGE is COMP-3.
flowchart TB
accTitle: How a COMP-3 byte string is built
accDescr: A diagram showing that packed decimal stores two decimal digits per byte and uses the rightmost byte for the last digit plus the sign, so a 9-digit number takes 5 bytes, and that opening those bytes as ASCII never shows the original sequence of digits.
pk1["Two decimal digits per byte"] --> pk2["Last byte holds the final digit and the sign"]
pk2 --> pk3["9 digits come to 5 bytes in total"]
pk3 --> pk4["Opened as ASCII the original number never appears"]
pk4 -.-> pk5["Looks like mojibake but is not broken"]
Figure 9: Learn the two packing rules and a meaningless dump turns into a readable column.
While we are here, a quick lookup table from digit count to byte count. The byte count is “divide the number of 9s by two, drop the fraction, and add one.”
| Number of 9s in the PICTURE | COMP-3 bytes |
|---|---|
| 1 | 1 |
| 2 - 3 | 2 |
| 4 - 5 | 3 |
| 6 - 7 | 4 |
| 8 - 9 | 5 |
| 10 - 11 | 6 |
| 12 - 13 | 7 |
Two digit counts share the same byte count because a spare leading digit position appears only when the number of 9s is even. That is why S9(4) becomes the 3 bytes 01 23 4C and S9(5) also fits in 3 bytes.
Without this table, matching a layout against an external file drifts one byte at a time.
One more note: DISPLAY does not necessarily mean an ASCII string.
On z/OS systems EBCDIC is the assumption, so even when digits appear as characters, the byte values can differ from ASCII '0' - '9'.6
5.4 REDEFINES / OCCURS / COPY / FILLER
These four are the places where readers get stuck.
REDEFINES
REDEFINES is a mechanism for viewing the same storage area in a different shape. It is not a copy.7
01 REC-BUF.
05 REC-TYPE PIC X.
05 REC-DATA PIC X(99).
01 HEADER-REC REDEFINES REC-BUF.
05 HDR-TYPE PIC X.
05 HDR-DATE PIC 9(8).
05 FILLER PIC X(91).
This is close to the feel of a union in C-family languages.
It often appears in the style of “distinguish one 100-byte area by record type.”
flowchart TB
accTitle: REDEFINES is a second reading of the same area
accDescr: A diagram showing that REDEFINES is not a copy but a way of viewing the same memory area in a different shape, so a single buffer can be read as a generic record or as a header record, and writing through one view changes how the other view looks.
rd1["One memory area"] --> rd2["Viewed as REC-BUF"]
rd1 --> rd3["Viewed as HEADER-REC"]
rd2 -.-> rd4["Write through one and both views change"]
rd3 -.-> rd4
Figure 10: Two definitions, but only one byte string underneath.
OCCURS
OCCURS is an array. In COBOL it tends to be called a table.
05 WS-ITEM OCCURS 12 TIMES.
10 WS-PRICE PIC 9(5).
If you further encounter OCCURS DEPENDING ON, it is a variable-length table.
In that case it can affect the positions of the items that follow, so following it with a fixed-length mindset will make you lose your footing.8
flowchart TB
accTitle: What to watch for with OCCURS DEPENDING ON
accDescr: A diagram showing that OCCURS is an array and that adding DEPENDING ON makes it a variable-length table whose value can move the positions of the items that follow, so chasing offsets with a fixed-length mindset makes you lose your footing.
oc1["Spotted an OCCURS"] --> oc2{"Is DEPENDING ON attached"}
oc2 -->|"Not attached"| oc3["Table with a fixed count"]
oc2 -->|"Attached"| oc4["Variable-length table"]
oc4 -.-> oc5["Positions of later items can move"]
Figure 11: Three extra words, DEPENDING ON, change the premise of every offset calculation.
COPY
COPY is a compile-time include.
In other words, the source you have open may not be the finished form yet.9
COPY CUSTOMER-REC.
COPY ERROR-MAP.
It is entirely normal for record definitions, shared flags, host variables for SQL, and external interfaces to be stuffed into copybooks.
When heavy use of COPY makes the source hard to read, it is faster to check whether you can get at the expanded source or a compiler listing. IBM Enterprise COBOL even has an option called MDECK for writing out the input source after library processing.10
flowchart TB
accTitle: How to read source that uses COPY
accDescr: A diagram showing that COPY is a compile-time include so the source you have open may not be the finished form, which means you open the copybooks to check, and when it is hard to read you look for the expanded source, a compiler listing, or the output of the MDECK option.
cy1["Spotted a COPY"] --> cy2["The open source may be incomplete"]
cy2 --> cy3["Open the copybook and check"]
cy2 -.->|"When it is hard to read"| cy4["Look for expanded source or a listing"]
Figure 12: The lines in front of you are not necessarily all of the program.
FILLER
FILLER is an item with no name.
But “unreferenced, therefore meaningless” is wrong.
It routinely serves as:
- Reserved space
- A compatibility gap for an old specification
- Padding to match a record length
- Slack for a
REDEFINES
FILLER merely lacks a name; it still exists as bytes. Forget this and your mapping against an external file drifts out of alignment one byte at a time.
flowchart TB
accTitle: What happens when you forget to count FILLER
accDescr: A diagram showing that FILLER merely lacks a name while still existing as bytes and serving as reserved space or record-length padding, so forgetting to count it makes the mapping against an external file drift one byte at a time.
fl1["FILLER is an item with no name"] --> fl2["It still exists as bytes"]
fl2 --> fl3{"Included in the layout calculation"}
fl3 -->|"Included"| fl4["Matches the external file"]
fl3 -->|"Forgotten"| fl5["Drifts one byte at a time"]
Figure 13: Even an item nobody references still has a job: its length.
6. The Bare Minimum of the PROCEDURE DIVISION
If the DATA DIVISION is the map, the PROCEDURE DIVISION is the route you travel.
6.1 PERFORM
PERFORM is COBOL’s basic control transfer.
Roughly speaking, it means call a piece of processing and come back.11
The forms you will see most often are these.
PERFORM INIT-PROC
PERFORM UNTIL EOF
PERFORM READ-PROC
IF NOT EOF
PERFORM EDIT-PROC
PERFORM WRITE-PROC
END-IF
END-PERFORM
PERFORM comes in two broad flavors.
- Out-of-line
PERFORM, which names a paragraph or section - Inline
PERFORM ... END-PERFORM, which writes a block in place
In older code you will also routinely see range forms like PERFORM A-100 THRU A-199.
Convenient, but adding a paragraph in the middle can silently pull that paragraph into the range, so when reading, check carefully where the range ends.
flowchart TB
accTitle: The three forms of PERFORM
accDescr: A diagram showing that PERFORM comes as an out-of-line form that calls a paragraph or section and returns, an inline form that writes a block in place, and a THRU range form found in older code where adding a paragraph in the middle can silently pull that paragraph into the range.
pf1["Spotted a PERFORM"] --> pf2["Out-of-line, calling a paragraph"]
pf1 --> pf3["Inline, written in place"]
pf1 --> pf4["THRU range form"]
pf4 -.-> pf5["Always check where the range ends"]
Figure 14: The form of the PERFORM decides which return point and which range you have to follow.
6.2 IF / EVALUATE / Scope
For conditional branching, IF is the basic tool.
Thinking of EVALUATE as roughly a switch/case is mostly correct.
What you need to watch is how scopes end.12
Code with explicit terminators such as
END-IFEND-PERFORMEND-READ
is still the readable kind.
The problem is old code. In COBOL, . acts as an implicit scope terminator and closes all the still-open statements at once.12
That means a single period changes:
- How far the
IFextends - How far the
PERFORMextends - Where the next sentence begins
Furthermore, NEXT SENTENCE is not the same as CONTINUE.
NEXT SENTENCE jumps to the point after the next period, so its destination shifts depending on where the following . happens to be.12
When reading old COBOL, “watch the periods, not the line endings” is about the right calibration.
flowchart TB
accTitle: How much a period weighs
accDescr: A diagram showing that code with explicit terminators such as END-IF is easy to read, while in old code the period acts as an implicit scope terminator that closes every still-open statement at once, so a single period changes how far an IF or a PERFORM extends.
sc1{"Are there explicit terminators"}
sc1 -->|"END-IF and friends present"| sc2["The ranges are easy to read"]
sc1 -->|"Absent, old code"| sc3["The period is the implicit terminator"]
sc3 --> sc4["One position changes the range"]
sc4 -.-> sc5["Watch the periods, not the line endings"]
Figure 15: In old code, the control flow is held by where the periods sit.
6.3 READ / WRITE / CALL
The frequent fliers in business COBOL are these.
READWRITEREWRITESTARTCALL
READ ... AT END ... in particular is the classic pattern.
READ IN-FILE
AT END
SET EOF TO TRUE
NOT AT END
PERFORM PROCESS-REC
END-READ
If there is a CALL 'SUBPGM' USING ..., control jumps to another program.
In that case, look at the callee’s LINKAGE SECTION and PROCEDURE DIVISION USING, and the shape of the handoff becomes quite visible.
flowchart TB
accTitle: How to follow a CALL you have found
accDescr: A diagram showing that a CALL transfers control to another program, so looking at the callee's LINKAGE SECTION and PROCEDURE DIVISION USING reveals the shape of the argument handoff.
ca1["Spotted a CALL"] --> ca2["Control jumps to another program"]
ca2 --> ca3["Look at the callee's LINKAGE SECTION"]
ca3 --> ca4["Read the handoff shape from USING"]
Figure 16: A call only makes sense together with the receiving end on the other side.
7. What Lives Outside COBOL
Quite often, COBOL’s world is not self-contained in the source.
- File definitions
- The execution environment
- DB connections
- The transaction environment
- Job control
all live outside it.
At minimum, grasping the following makes reading much easier.
Files and FILE STATUS
Read the FILE-CONTROL in the ENVIRONMENT DIVISION together with the FILE SECTION / FD in the DATA DIVISION. They come as a pair.13
SELECT IN-FILE ASSIGN TO ...
FILE STATUS IS WS-FS.
FD IN-FILE.
01 IN-REC.
05 ...
If there is a FILE STATUS, it receives the result code after each I/O.
When reading file-related failures or EOF handling, you cannot even begin without looking at this.14
flowchart TB
accTitle: Read the file definition as a pair
accDescr: A diagram showing that the SELECT in the FILE-CONTROL of the ENVIRONMENT DIVISION and the FD in the DATA DIVISION are read as a pair, and that when a FILE STATUS is present it receives the result code after each I/O, which is where reading file failures and EOF handling starts.
fs1["SELECT in FILE-CONTROL"] --> fs3["One file definition, taken together"]
fs2["FD in the FILE SECTION"] --> fs3
fs3 --> fs4["FILE STATUS receives the result code"]
fs4 -.-> fs5["Starting point for failures and EOF handling"]
Figure 17: The full picture of a file is written across two DIVISIONs.
EXEC SQL
If this appears, it is embedded SQL.
EXEC SQL
SELECT ...
END-EXEC.
In this case the COBOL is a “vessel for host variables,” and the actual selection criteria and update targets are on the SQL side.
So the shortcut is to read the contents of EXEC SQL as ordinary SQL.
EXEC CICS
If this appears, you are in a CICS transaction context.15
EXEC CICS
RECEIVE MAP(...)
END-EXEC.
At that instant, this stops being a plain batch-reading exercise. You need to read it together with the external context: screens, transactions, response codes, the COMMAREA, and so on.
JCL and Execution Definitions
In mainframe batch, it is not unusual for which datasets actually get allocated and in what order the jobs flow to live outside the COBOL source. When you look at the source alone and cannot tell “where is this file?”, it routinely turns out that the code is not at fault; you just have not widened your view far enough yet.
flowchart TB
accTitle: The world that sits outside the source
accDescr: A diagram showing that when EXEC SQL appears the real selection criteria live on the SQL side, when EXEC CICS appears the screen and transaction context lives outside, and in mainframe batch the dataset allocation and job order live in the JCL, so parts of the world sit outside the COBOL source.
ex1["EXEC SQL"] --> ex7["Look outside the source too"]
ex3["EXEC CICS"] --> ex7
ex5["JCL and execution definitions"] --> ex7
ex7 -.-> ex2["SQL criteria, screen context, job flow"]
Figure 18: Sometimes the confusion is not the code’s fault; your view is just too narrow.
Differences Between Compilers
This article is written with IBM systems in mind, but in practice you will also meet Micro Focus and COBOL on Linux or Windows. The skeleton is shared, so the way you read does not change, yet the points where “the same code produces a different result” are well known, and knowing them in advance keeps you out of trouble.
| What to check | z/OS family (IBM Enterprise COBOL) | Open systems (Micro Focus, Linux / Windows editions, and so on) |
|---|---|---|
| Character encoding | EBCDIC is the assumption6 | ASCII is the assumption6 |
| Reference format | Fixed format is the traditional default. Free format is also available1 | Both fixed and free exist, and which one is the default depends on the build settings1 |
| How copybooks are found | Library specification | Search paths given as compiler options |
| Dialects | — | An option exists for switching which compiler to match |
The one that bites hardest is character encoding. Even with the same PIC X(10), reading a file written on z/OS directly on Windows gives you different byte values even for digits. Most cases of “everything turned to garbage after the transfer” are this, and it is a separate problem from the COMP-3 story.
There is one more place where internal numeric representation tends to differ. In IBM Enterprise COBOL, BINARY / COMP-4 truncates at the digit count written in the PICTURE, whereas COMP-5 holds values up to the capacity of a native binary field of 2, 4, or 8 bytes, with truncation happening at the binary size instead.16 In other words, PIC S9(4) COMP and PIC S9(4) COMP-5 look alike but accept different maximum values. When COMP-5 turns up in code that exchanges values with other systems in binary form, read it as written that way on purpose.
The shortest path for checking all this against your own environment is:
- Open the build definition first (makefile, JCL, project settings). Which compiler and which options are in use become clear before you even read the source.
- Settle the reference format (fixed or free). Get this wrong and formatting in an editor will break the code.
- Settle the character encoding. EBCDIC or ASCII changes how you read a dump.
- Mark the
COMP-family items. This is where compiler differences show up almost every time.
flowchart TB
accTitle: The shortest path for checking your own environment
accDescr: A diagram showing the steps of opening the build definition first to see which compiler and options are used, settling whether the reference format is fixed or free, settling whether the character encoding is EBCDIC or ASCII, and marking the COMP-family items where compiler differences appear.
ck1["Open the build definition first"] --> ck2["Settle the reference format"]
ck2 --> ck3["Settle the character encoding"]
ck3 --> ck4["Mark the COMP-family items"]
Figure 19: Four moves before you dive into the source keep compiler differences from biting.
8. The Minimum Reading Order
When you suddenly have to read COBOL, the following order is the safe one.
- Sweep up every
COPYOpen the copybooks if you can. If not, look for a listing or the expanded source - Pick out the
01-level record definitions List the top-level items in theFILE SECTION,WORKING-STORAGE, andLINKAGE SECTION - Read the
PICs andUSAGEs Identify amounts, dates, counts, codes, flags - Search for
READ/WRITE/REWRITE/CALL/EXEC SQL/EXEC CICSGrasp the I/O and the external boundaries first - Follow only the first main path
Trace the chain of
PERFORMs from the top of thePROCEDURE DIVISION - Look at the
88s and status items The meanings of EOF, success/failure, and type codes become much easier to read - Mark every
REDEFINES/OCCURS DEPENDING ON/COMP-3They will matter later without fail, so flag them as hazardous material up front - For files, look at the
FILE STATUSThis eliminates a lot of misreadings around I/O errors
In this order, you avoid having to close-read the whole thing from the start. With COBOL, rather than trying to understand 100% from the beginning, it is far easier to nail down the three points, records, external boundaries, main path, and then go into the details.
flowchart TB
accTitle: A safe reading order
accDescr: A diagram showing a reading order that sweeps up every COPY to check the copybooks, lists the 01-level record definitions, reads the shape of items from PIC and USAGE, searches out the I/O and external boundaries, follows the chain of PERFORMs from the top of the PROCEDURE DIVISION along the main path, and marks the hazardous items.
ro1["Sweep up every COPY"] --> ro2["List the 01 levels"]
ro2 --> ro3["Read the shapes from PIC and USAGE"]
ro3 --> ro4["Search for I/O and external boundaries"]
ro4 --> ro5["Follow the main path of PERFORMs"]
ro5 -.-> ro6["Mark the hazardous items"]
Figure 20: Not a full close read, but filling in the outer moat in this order.
8.1 Exercise: Try Reading This Record Definition
Explanation alone does not stick, so here is one small record. Work out your own answers first, then look at the solutions below.
01 CUST-REC.
05 CUST-ID PIC X(8).
05 CUST-NAME PIC X(20).
05 CUST-KBN PIC X.
88 CUST-NORMAL VALUE '0'.
88 CUST-VIP VALUE '1'.
05 CUST-BALANCE PIC S9(7)V99 COMP-3.
05 CUST-HIST OCCURS 3 TIMES.
10 HIST-DATE PIC 9(8).
10 HIST-AMOUNT PIC S9(5)V99 COMP-3.
05 FILLER PIC X(4).
Questions
- How many bytes is
CUST-RECin total? - Counting from the start of the record, at which byte does
CUST-BALANCEbegin? - At which byte does the second
HIST-AMOUNTbegin? - When
CUST-KBNholds'1', which condition-name is true? - When you open this file in a text editor, which items look broken?
- Name two things this definition alone does not tell you.
Answers
-
74 bytes. The breakdown is:
Item Calculation Bytes CUST-IDX(8)8 CUST-NAMEX(20)20 CUST-KBNX1 CUST-BALANCE9-digit COMP-3 5 CUST-HIST(8 + 4)× 3 occurrences36 FILLERX(4)4 Total 74 An
88level is a condition-name, so it consumes no bytes. Counting it is a common mistake.FILLERmerely lacks a name; its 4 bytes are very much there. -
Byte 30. There are
8 + 20 + 1 = 29bytes ahead of it, so it starts right after them. -
Byte 55.
CUST-HISTstarts at byte 35 and each occurrence is 12 bytes, so the first occurrence spans bytes 35 - 46 and the second spans bytes 47 - 58. The first 8 bytes of that areHIST-DATE, soHIST-AMOUNTstarts at byte 55. -
CUST-VIP. There is no separate area calledCUST-VIP; it is simply the name under whichCUST-KBNcan be read when it holds'1'. -
CUST-BALANCEandHIST-AMOUNT. Both areCOMP-3, so reading them as characters yields a meaningless sequence.HIST-DATEisPIC 9(8)inDISPLAY, so in an ASCII environment it reads as digits, such as20260317. In an EBCDIC environment, though, the byte values differ from ASCII even when the digits look right. -
For example:
- Whether this definition itself was brought in via
COPY. Without looking at the copybook side, you cannot tell whether this is the version actually in use. - Whether the file’s character encoding is EBCDIC or ASCII. It changes how
PIC XandPIC 9 DISPLAYlook. - Whether operations ever put something other than
'0'or'1'intoCUST-KBN. Only two condition-names are defined, but that is no guarantee no other value arrives. - The attributes of the file itself (record length, fixed or variable length,
FILE STATUS). You cannot know these without looking at theENVIRONMENT DIVISIONand theFD.
- Whether this definition itself was brought in via
If you got questions 1 and 3 wrong, go back to the digit-count and byte-count table in 5.3. Misreadings of COBOL usually start right there.
9. Common Stumbling Points
Finally, here are the places where beginners get caught with very high probability.
Thinking REDEFINES is “a different variable”
It is not. It is the same storage area read in a different shape. Modify one side, and the other side’s view changes too.7
Thinking 88 is “an independent bool”
It is not.
It is just a name attached to a value of the preceding item. Behind the scenes, SET WS-OK TO TRUE stores the corresponding value into the underlying item.3
Ignoring COPY and reading only the main body
The file you have open is still only half the whole. It is entirely normal for field definitions, shared flags, and host variables to live wholesale outside it.9
Thinking MOVE is plain assignment
MOVE is not just a memcpy.
Depending on the receiving item’s type, it can involve conversion, digit alignment, zero filling, truncation, and editing or de-editing.17
Underestimating the effect of .
COBOL’s . is heavier than you imagine.
In old code with no explicit terminators, misjudging how much this period closes means misreading the control flow.12
Thinking packed decimal or EBCDIC is “mojibake”
It is not necessarily broken. Quite often it simply was never a string to begin with, or is just not ASCII.46
Assuming what follows OCCURS DEPENDING ON sits at a fixed position
The items after a variable-length table can move position depending on the value. Read it with a fixed-length mindset and all your offset calculations go wrong.8
10. Quick Reference: What to Think First
| Word you found | First thing to think |
|---|---|
01 |
Top level of a record or group. Grasp the big picture from here |
88 |
Named meaning of a flag or status code. The key to reading branches |
PIC X(...) |
Character item |
PIC 9(...) / S9(...)V... |
Numeric item. Check digit count and decimal position |
COMP |
Binary |
COMP-3 |
Packed decimal. Likely an amount or a count |
REDEFINES |
The same area being reinterpreted differently |
OCCURS |
Array / table |
OCCURS DEPENDING ON |
Variable length. Watch the positions that follow too |
FILLER |
No name, but it has length |
COPY |
You cannot see the finished form without the copybook |
PERFORM |
The skeleton of the main path |
READ / WRITE / REWRITE |
File I/O |
EXEC SQL |
DB processing |
EXEC CICS |
Transaction processing |
FILE STATUS |
I/O result code |
11. Summary
COBOL is not hard because it is old. It is just that data definitions, external files, and the execution context are tightly intertwined, which makes the initial entry point hard to see.
To restate the minimum set for reading it:
- Grasp the map via the
DIVISIONs - Read the
DATA DIVISIONfirst - Read the shape of each item via
PICandUSAGE - Mark every
COMP-3,REDEFINES,OCCURS,88, andCOPY - Follow
PERFORM,READ,WRITE, andCALL - Nail down the external boundaries via
FILE STATUS,EXEC SQL, andEXEC CICS - Do not underestimate how
.behaves
Once you can see all this, COBOL turns from “mysterious ancient magic” into “a record-processing language.” Legacy technology is not scary because the name is old; it is just that picking the wrong scale for your first look suddenly makes it hard to understand. Get the map scale right, and it reads surprisingly normally.
flowchart TB
accTitle: Get the scale right and read
accDescr: A diagram showing that grasping the map from the DIVISIONs, reading the DATA DIVISION first to pin down the shape of each item, following the flow through PERFORM and the I/O statements, and nailing down the external boundaries with FILE STATUS, EXEC SQL, and EXEC CICS sets the scale so that COBOL reads as an ordinary record-processing language.
sm1["Grasp the map from the DIVISIONs"] --> sm2["Read the shapes in the DATA DIVISION"]
sm2 --> sm3["Follow the flow via PERFORM and I/O"]
sm3 --> sm4["Nail down the external boundaries"]
sm4 -.-> sm5["Ancient magic becomes a record-processing language"]
Figure 21: The difficulty is not the age of the language but the scale you pick first.
12. References
The main sources referenced in the text. The superscript numbers in the body link straight into this list, and the arrow at the end of each entry takes you back to where it was cited.
-
IBM, “Reference format” / IBM, “Area A or Area B” / Micro Focus, “Fixed Format” ↩ ↩2 ↩3 ↩4
-
IBM, “Level-numbers” ↩ ↩2
-
IBM, “Format 2: condition-name value” ↩ ↩2
-
IBM, “Examples: numeric data and internal representation” ↩ ↩2 ↩3 ↩4
-
IBM, “PACKED-DECIMAL (COMP-3)” ↩ ↩2
-
IBM, “The EBCDIC character set” / IBM, “Handling differences in ASCII SBCS and EBCDIC SBCS characters” ↩ ↩2 ↩3 ↩4 ↩5
-
IBM, “REDEFINES clause” ↩ ↩2
-
IBM, “OCCURS DEPENDING ON clause” ↩ ↩2
-
IBM, “COPY statement” ↩ ↩2
-
IBM, “PERFORM statement” / IBM, “Procedure division structure” ↩
-
IBM, “Scope terminators” / IBM, “Coding a choice of actions” ↩ ↩2 ↩3 ↩4
-
IBM, “FILE STATUS clause” / IBM, “Using file status keys” ↩
-
IBM, “Computational items” ↩
-
IBM, “Elementary move rules” ↩
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
Safely Modifying a Legacy Business App That Has No Tests — Characterization Testing and Refactoring in Practice
To safely modify a business application with no tests, this article explains, with C# examples, the procedure for a characterization test...
When You Inherit a System With No Source Code and No Documentation — A Practical Playbook for Keeping It Running
A practical playbook for starting operations and maintenance on a business system that has no source code and no specifications. Covers p...
How Long Can You Go On Using MSMQ? — The Migration Decision for a Legacy Queue That Isn't Even "Deprecated"
MSMQ does not appear on Microsoft's official deprecated-features lists, yet System.Messaging exists only in .NET Framework and blocks mig...
Never Use a QR Code's Decoded Value As-Is — Error Correction Succeeding Does Not Guarantee the Value
QR code error correction is not a mechanism that guarantees the value is correct once correction succeeds. Using sample images and measur...
Integrating With REST APIs From PowerShell — Invoke-RestMethod in Practice
A practical guide to calling in-house and SaaS REST APIs from PowerShell: passing authentication headers, avoiding mojibake in non-ASCII ...
Related Topics
These topic pages place the article in a broader service and decision context.
Windows Technical Topics
Topic hub for KomuraSoft LLC's Windows development, investigation, and legacy-asset articles.
ActiveX Migration
Topic page for staged decisions around keeping, wrapping, or replacing COM / ActiveX / OCX assets.
Where This Topic Connects
This article connects naturally to the following service pages.
Technical Consulting & Design Review
How to read existing COBOL assets, where to start a modification, mapping external boundaries, and forming a pre-migration assessment — these topics fit well with technical consulting and design reviews.
Bug Investigation & Root Cause Analysis
Incident response right after a handover, or tracking down where inconsistencies arise in COBOL assets, is easy to approach as bug investigation and root-cause analysis.
Frequently Asked Questions
Common questions about the topic of this article.
- Where should I start when reading COBOL source code?
- Reading only the PROCEDURE DIVISION gives you half the story. Before COBOL is a language of logic it is very much a language of record definitions, so look at the DATA DIVISION first. A safe reading order is: sweep up every COPY and check the copybooks, list the 01-level record definitions, read the shape of each item from its PIC and USAGE, search for READ, WRITE, CALL, EXEC SQL, and EXEC CICS to grasp the I/O and the external boundaries, and only then follow the chain of PERFORMs from the top of the PROCEDURE DIVISION along the main path alone.
- What does PIC S9(7)V99 COMP-3 mean?
- PIC describes the shape of an item and USAGE describes the representation it is held in. S9(7)V99 is a signed number with 7 integer digits plus 2 decimal digits, but the V is a logical decimal point only: no dot character is stored in the data. COMP-3 is packed decimal, and it turns up constantly for amounts, tax figures, counts, and rates. Such a field is supposed to look broken when viewed as text, so eyeballing a dump in a CSV or UTF-8 frame of mind will trip you up.
- How should I understand COBOL's 88 levels and REDEFINES?
- An 88 is not an independent boolean variable. It is a condition-name that attaches a name to values of the preceding item. SET WS-OK TO TRUE stores the corresponding value into the underlying item behind the scenes. REDEFINES is a mechanism for viewing the same memory area in a different shape; it is not a copy, and the feel is closer to a union in the C family. Writing through one view changes how the other view looks, which is why it shows up so often as a way of distinguishing record types within a single area.
- What should I do when there are so many COPY statements that I cannot see the whole program?
- COPY is a compile-time include, so the source you have open may not be the finished form yet. It is entirely normal for record definitions, shared flags, host variables for SQL, and external interfaces to be pushed into copybooks. When it gets hard to read, the fast move is to check whether you can get at the expanded source or a compiler listing; IBM Enterprise COBOL also has an option called MDECK that writes out the input source after library processing.