引用本文(DOI: 10.5281/zenodo.21615498)
本文保存于 Zenodo。以下同时提供始终指向最新版本的 DOI,以及固定于您正在阅读版本的 DOI。
小村 豪(2026)。《阅读 COBOL 源代码前应该掌握的最小知识》。小村软件有限公司。https://doi.org/10.5281/zenodo.21615498 https://comcomponent.com/zh-CN/blog/2026/03/17/001-cobol-minimum-reading-guide/
- DOI(最新版本)
- 10.5281/zenodo.21615498
- DOI(此版本)
- 10.5281/zenodo.22282019
交接工作、故障处理、维护供应商提供的软件包——在这些场景下,某天可能会突然收到一份 COBOL 源代码。
- 文件名是
.cbl或.cpy - 变量名全部是大写
01、05、77、88排列在一起- 出现类似
PIC S9(7)V99 COMP-3这种介于咒语和会计软件之间的写法 - 而且到处都是
COPY,光看打开的文件根本看不清整体结构
看到这里,大多数人都会先停下手来。
不过,用来阅读的地图并没有那么庞大。COBOL 在不同编译器实现和产品之间存在差异,但在阅读既有业务系统时,首先应该掌握的骨架却相当通用。本文将以 IBM 系统及典型的业务 COBOL 为背景,整理一套面向突然需要阅读源代码的人的最小知识集合。
flowchart TB
accTitle: 停下手来的原因与地图的大小
accDescr: 全部大写的变量名、级别编号、PIC S9(7)V99 COMP-3 这类写法,以及到处都是 COPY 导致看不清整体结构,都会让人停下手来;但阅读既有业务系统所需先掌握的骨架相当通用,地图并没有那么庞大。
iv1["被陌生的写法卡住"] --> iv2["但骨架跨编译器实现是通用的"]
iv2 --> iv3["先只拿到最小知识集合这张地图"]
iv1 -.-> iv4["到处都是COPY,看不清整体"]
图1:看起来像咒语的写法,其实归结为少数几个通用概念。
1. 先说结论(一句话)
先用一种比较粗略,但在实务中相当有用的说法来概括:
- COBOL 与其说是逻辑语言,更应该看作是一种相当强调记录定义的语言
- 只看
PROCEDURE DIVISION只能理解一半,要先看DATA DIVISION PIC表示项目的形态,USAGE表示以何种方式保存COMP-3是 packed decimal(压缩十进制),常见于金额和数量相关的场景88与其说是另一个变量,更应该理解为给前一个项目的值起的条件名REDEFINES是用另一种形态查看同一块内存的机制,不是复制- 一旦出现
COPY,当前打开的源代码就还不是完整形态,不看 copybook 就看不清整体结构 - 只要能追踪
PERFORM、IF、EVALUATE、READ、WRITE、CALL,大致就能理清流程 - 旧版源代码是列位置具有意义的固定格式,看到的空白并不只是排版装饰1
总而言之,只要能读懂 DIVISION、PIC、USAGE、COMP-3、REDEFINES、OCCURS、88、COPY、PERFORM,迷路的概率就会大幅下降。
图中实线表示始终成立的关系,虚线表示带条件的关系(成立条件写在详情页各关系的说明中)。关系的完整列表(共 21 条,附依据与确信度)以及主要概念的定义,汇总在知识地图详情页(日文)。数据:JSON-LD / Turtle
2. 先把 COBOL 当作“数据形态”的语言来看
如果带着 C# 或 Java 的思维去读,最初总会想去追踪 if、for 或函数调用。
但在 COBOL 中,在此之前先弄清楚“这个程序接收什么样的记录、生成什么样的记录、持有什么样的缓冲区”会更快。
典型的业务 COBOL 大致遵循下面的流程:
- 从文件或数据库中读取记录
- 存入
WORKING-STORAGE上的项目 - 进行条件分支
- 转换到另一种记录格式
- 写出结果
也就是说,布局往往比算法更先浮现出来。
flowchart TB
accTitle: 典型业务COBOL的流程
accDescr: 从文件或数据库读取记录,存入 WORKING-STORAGE 上的项目,进行条件分支,转换到另一种记录格式后写出,这就是典型业务 COBOL 的流程。
f1["读取记录"] --> f2["存入WORKING-STORAGE"]
f2 --> f3["进行条件分支"]
f3 --> f4["转换到另一种记录"]
f4 --> f5["写出结果"]
图2:主角是记录的流动,算法只是夹在其间。
例如,下面就是一个骨架示例:
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.
阅读这段代码时,最先应该关注的不是 PERFORM,而是 SALE-AMOUNT 的类型和 EOF 的含义。
按这个顺序去读,COBOL 一下子就安静下来了。
3. 先看清 4 个 DIVISION
COBOL 源代码首先大致分为 4 个 DIVISION。
| DIVISION | 首先要看的内容 |
|---|---|
IDENTIFICATION DIVISION |
程序名、旧注释、来源信息 |
ENVIRONMENT DIVISION |
文件、外部资源、输入输出前提 |
DATA DIVISION |
记录定义、工作区、参数 |
PROCEDURE DIVISION |
实际的处理流程 |
其中特别重要的是以下几项:
FILE SECTION存放输入输出文件的记录定义WORKING-STORAGE SECTION存放日常使用的变量、标志、计数器、工作缓冲区LOCAL-STORAGE SECTION有时存放每次调用都会重新初始化的区域LINKAGE SECTION有时存放从外部传入的参数,或子程序的接收接口
如果看到 LINKAGE SECTION 和 PROCEDURE DIVISION USING ...,很可能说明这个程序不是独立完整的,而是依赖外部传入的数据来运行。
flowchart TB
accTitle: LINKAGE SECTION 所暗示的事情
accDescr: 如果看到 LINKAGE SECTION 和 PROCEDURE DIVISION USING,那么这个程序很可能不是独立完整的,而是依赖外部传入的数据来运行。
lk1["存在LINKAGE SECTION"] --> lk3["很可能依赖外部数据运行"]
lk2["存在PROCEDURE DIVISION USING"] --> lk3
lk3 -.-> lk4["不是独立完整的程序"]
图3:一旦看到接收接口的定义,就要以调用方存在为前提来阅读。
4. 不要被固定格式的外观吓到
在旧版 COBOL 中,源代码每一行的列位置本身是有意义的。如果不了解这一点就去阅读,“为什么左边有莫名其妙的空白”这个疑问永远解不开。1
固定格式大致规则如下:
- 第 1 - 6 列:顺序编号
- 第 7 列:indicator(指示符)
- 第 8 - 11 列:Area A
- 第 12 - 72 列:Area B
第 7 列尤其重要:
*或/:注释行-:续行D:debugging line(调试行)*>:可以出现在行中间的注释
把列与内容的对应关系配上刻度来看,就是下面这样。第 1 行是十位刻度,第 2 行是个位刻度。
1 2 3 4 5 6 7 8
12345678901234567890123456789012345678901234567890123456789012345678901234567890
SSSSSSIAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB........
S= 顺序编号(第 1 - 6 列)I= indicator(第 7 列)A= Area A(第 8 - 11 列)B= Area B(第 12 - 72 列).= 第 73 列及之后。有些编译器实现会把它当作识别栏使用,但它不参与程序的语义
套用到真实源代码上,就是下面这样。
000100* 本行第 7 列是 *,因此是注释行
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'.
阅读要点有 4 个。
- 第 1 - 6 列是顺序编号。上例中标成
000100的就是它,与程序的行为无关。也可能是空白。 - 第 7 列是 indicator。
*表示注释,-表示续行。上例的000900行就是续行,它接着上一行的字符串字面量继续写。 - 第 8 - 11 列是 Area A。
DIVISION、SECTION、段落名、FD,以及01和77这样的级别编号都要从这里开始写。上例的IDENTIFICATION DIVISION.和01 WS-ORDER.就是从 Area A 起始的。 - 第 12 - 72 列是 Area B。普通语句,以及
05这类下级层次写在这里。上例的05 WS-ORDER-ID就是从 Area B 起始的。
这里的空白,并不是现代意义上的“排版”,而在一定程度上属于语法本身。 在编辑器中做 tab 转换、整体左移、或随意粘贴,都会实实在在地把代码弄坏。 阅读旧版源代码时,首先要怀疑这个文件到底是 fixed format 还是 free format。如果对 fixed 格式的源代码套用现代的自动格式化,Area A 与 Area B 的边界就会被打乱,编译也就通不过了。
flowchart TB
accTitle: 动固定格式源代码之前
accDescr: 面对旧版源代码,先确认是 fixed format 还是 free format;对 fixed 格式的源代码套用现代自动格式化或 tab 转换,会打乱 Area A 与 Area B 的边界,导致编译通不过。
fx1["打开了旧版源代码"] --> fx2{"是fixed还是free"}
fx2 -->|"fixed格式"| fx3["列位置本身就是语法"]
fx3 -.-> fx4["自动格式化或tab转换会弄坏它"]
fx2 -->|"free格式"| fx5["列的约束比较宽松"]
图4:先确定文件的格式,再谈格式化。
5. DATA DIVISION 的最低限度知识
5.1 级别编号
COBOL 的数据定义不是靠缩进,而是靠级别编号(level number)来构建层次结构。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).
只要记住以下几点,基本就足够了:
01:一整个最上层记录,也就是分组02-49:其下的层级77:独立的单一项目88:condition-name,给前一个项目的值起名字366:用于RENAMES,出现频率不高,但确实存在
需要注意的是,不要把 88 当成一个独立的 bool 变量。
并不是存在一个单独叫 WS-OK 的区域,而是当 WS-STATUS 的值为 '0' 时,可以用 WS-OK 这个名字来读取它。
flowchart TB
accTitle: 88级别的真面目
accDescr: 88 级别不是独立的 bool 变量,而是给前一个项目的值起的条件名;只有当 WS-STATUS 取到特定值时,才能用 WS-OK 这个名字读到它。
cn1["基础项目 WS-STATUS"] --> cn2{"值是什么"}
cn2 -->|"为0时"| cn3["以WS-OK这个名字为真"]
cn2 -->|"为9时"| cn4["以WS-ERROR这个名字为真"]
cn3 -.-> cn5["并不存在另一块区域"]
图5:88 不是变量,而是给某个值起的、便于阅读的别名。
还有一点很重要:决定层次结构的是级别编号,而不是空白。
看上去的缩进只是参考,最终应该相信的是 01 / 05 / 10 / 88 这些编号本身。2
5.2 PICTURE
PIC 表示该项目的形态。
最常见的写法大致如下:
| 记法 | 大致含义 |
|---|---|
X |
字符 |
9 |
数字 |
S |
带符号 |
V |
小数点只存在于逻辑层面 |
X(10) |
10 个字符 |
9(5) |
5 位数值 |
S9(7)V99 |
带符号,整数 7 位 + 小数 2 位 |
举例来说:
PIC X(10)→ 10 个字符PIC 9(5)V99→ 5 位整数 + 2 位小数PIC S9(7)V99→ 带符号 7 位整数 + 2 位小数
就是这样。
这里特别重要的是 V。
V 并不对应真实存在的 . 字符。
PIC 9(5)V99 会被当作“带 2 位小数的数值”处理,但数据中并不会真的出现一个点字符。
因此,如果把文件或 dump 当作“看起来的字符串”来解读,基本上都会出问题。
flowchart TB
accTitle: V是逻辑上的小数点
accDescr: PIC 9(5)V99 中的 V 是逻辑上的小数点,并不对应真实的点字符,数据中不会出现点,因此把文件或 dump 当作看起来的字符串来解读就会出问题。
vd1["PIC 9(5)V99"] --> vd2["按带2位小数的数值处理"]
vd2 --> vd3["数据中不会出现点字符"]
vd3 -.-> vd4["当作可见字符串解读就会出问题"]
图6:小数点只存在于定义之中,不存在于数据之中。
5.3 USAGE / DISPLAY / COMP / COMP-3
如果说 PIC 是形态,那么 USAGE 就是以何种方式保存。
最低限度只需要掌握以下几点,基本就能读懂大部分内容。45
| 记法 | 大致含义 | 阅读时的注意事项 |
|---|---|---|
DISPLAY |
以字符形式呈现的外部十进制 | 在 mainframe 上有时以 EBCDIC 为前提6 |
COMP / BINARY |
二进制数 | 看到的位数与内部表示并不相同 |
COMP-3 / PACKED-DECIMAL |
packed decimal(压缩十进制) | 当作文本读取会显得像是乱码 |
举例来说:
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.
这三者虽然都是“数值”,但内部保存方式各不相同。
flowchart TB
accTitle: 同样的数值,保存方式却不同
accDescr: 同样是带符号数值,DISPLAY 是以字符形式呈现的外部十进制,COMP 是二进制数,COMP-3 是 packed decimal,USAGE 不同,内部保存方式就不同。
us1["形态相同的数值项目"] --> us2["DISPLAY〔以字符形式呈现〕"]
us1 --> us3["COMP〔二进制数〕"]
us1 --> us4["COMP-3〔packed decimal〕"]
us4 -.-> us5["当作文本读取会显得像乱码"]
图7:PIC 相同,只要 USAGE 变了,字节序列就是另一回事。
在实务中最关键的,是看到 COMP-3 时应该做出的反应:
- 它是 packed decimal
- 很可能是金额、税额、数量、费率相关的字段
- 当作文本查看,看起来像乱码本就正常
- 带着 CSV 或 UTF-8 的心态去看,就会踩坑
有了这种理解,在查看 dump 或二进制文件时,就不会毫无必要地感到恐慌。
flowchart TB
accTitle: 看到COMP-3时该有的反应
accDescr: 看到 COMP-3 就知道它是 packed decimal,很可能是金额、税额、数量、费率类项目,当作文本查看显得像乱码本就正常,因此不要带着 CSV 或 UTF-8 的心态去看。
cp1["发现了COMP-3"] --> cp2["认出它是packed decimal"]
cp2 --> cp3["怀疑它是金额或数量类项目"]
cp3 --> cp4["文本里看起来是乱码也不慌"]
图8:只要有这一个条件反射,在 dump 面前发慌的次数就会减少。
COMP-3 实际会变成什么样的字节序列
这一段只要动手推一次,之后看到的东西就不一样了。
- 一个字节里塞两位十进制数字
- 但最右边那个字节例外,它用低位一位数字加符号来占满一个字节
符号用 4 位的值表示,C 表示正,D 表示负,F 表示无符号。
用 IBM 手册中给出的例子来确认。4
| 定义 | 值 | 字节序列 |
|---|---|---|
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 只有 4 位,因为规则 2 的关系,最前面会空出一位。那个位置就是开头的 0。
照同样的办法,来推一下本文反复出现的 PIC S9(7)V99 COMP-3。
S9(7)V99是整数 7 位 + 小数 2 位 = 9 位V只表示小数点的位置,因此一个字节也不占用- 9 位按两位一组塞进去是 4 个字节,剩下的一位加符号再占 1 个字节。合计 5 个字节
如果值是 +12345.67,补齐到 9 位就是 001234567,于是结果如下。
值 : +12345.67
9 位表示 : 0 0 1 2 3 4 5 6 7 与符号
字节 : 00 12 34 56 7C
^ 符号 C = 正
如果是负值 -12345.67,只是最后变成 7D 而已。
字节 : 00 12 34 56 7D
把它当作文本打开会看到什么才是重点。硬把 00 12 34 56 7C 按一个字节对应一个字符去套 ASCII,结果是:
00是 NUL,12是控制字符,本来就无法作为字符显示34是4,56是V,7C是|
也就是说,屏幕上看起来像是“几个无法显示的字符之后跟着 4V|”。12345.67 这个排列在任何地方都不会出现。
这就是“看起来像乱码,其实并没有坏掉”的真相。打开 dump 遇到莫名其妙的排列时,首先要怀疑这个项目的 USAGE 是不是 COMP-3。
flowchart TB
accTitle: COMP-3的字节序列是怎么造出来的
accDescr: packed decimal 把十进制数字按两位一组塞进一个字节,只有最右边的字节用低位一位数字加符号,因此 9 位数值占 5 个字节;即使按 ASCII 打开,原来的数值排列也不会出现在任何地方。
pk1["十进制数字两位一组塞进一字节"] --> pk2["最后一字节是低位一位加符号"]
pk2 --> pk3["9位合计5个字节"]
pk3 --> pk4["按ASCII打开看不到原数值"]
pk4 -.-> pk5["看起来像乱码,其实没坏"]
图9:只要知道这两条塞法规则,看不懂的 dump 就变成了可读的字节列。
顺便放一张从位数推字节数的速查表。字节数用“9 的个数除以 2 向下取整,再加 1”就能算出来。
| PICTURE 中 9 的个数 | COMP-3 的字节数 |
|---|---|
| 1 | 1 |
| 2 - 3 | 2 |
| 4 - 5 | 3 |
| 6 - 7 | 4 |
| 8 - 9 | 5 |
| 10 - 11 | 6 |
| 12 - 13 | 7 |
同一个字节数下会并排出现两个位数,是因为只有 9 的个数为偶数时,最前面才会空出一位。S9(4) 会变成 01 23 4C 这 3 个字节,而 S9(5) 同样只用 3 个字节,原因就在这里。
在和外部文件做布局比对时,没有这张表就会一个字节一个字节地错下去。
再补充一点,DISPLAY 并不代表一定是 ASCII 字符串。
在 z/OS 系统中通常以 EBCDIC 为前提,因此即使数字看起来是以字符形式呈现,其字节值也可能与 ASCII 的 '0' - '9' 不同。6
5.4 REDEFINES / OCCURS / COPY / FILLER
这四个是阅读时最容易卡住的地方。
REDEFINES
REDEFINES 是用另一种形态查看同一块区域的机制,不是复制。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).
这与 C 语言中的 union 概念比较接近。
常见的写法类似于:“用不同的记录类型去区分同一块 100 字节的区域”。
flowchart TB
accTitle: REDEFINES是对同一块区域的另一种解释
accDescr: REDEFINES 不是复制,而是用另一种形态查看同一块内存区域;同一个缓冲区既能按通用记录读,也能按头部记录读,改写其中一方,另一方看到的内容也会随之改变。
rd1["同一块内存区域"] --> rd2["按REC-BUF来看"]
rd1 --> rd3["按HEADER-REC来看"]
rd2 -.-> rd4["写了一方,两边看到的都会变"]
rd3 -.-> rd4
图10:定义有两份,实体的字节序列却只有一份。
OCCURS
OCCURS 相当于数组,在 COBOL 中常被称为 table。
05 WS-ITEM OCCURS 12 TIMES.
10 WS-PRICE PIC 9(5).
如果进一步出现 OCCURS DEPENDING ON,就说明这是一个可变长表。
这种情况下,后续项目的位置也可能受到影响,如果按固定长度的思路去追踪,就很容易踩空。8
flowchart TB
accTitle: OCCURS DEPENDING ON 的注意事项
accDescr: OCCURS 是数组,一旦带上 DEPENDING ON 就成了可变长表,后续项目的位置也可能随值而动,因此按固定长度的思路去算偏移量就会踩空。
oc1["发现了OCCURS"] --> oc2{"是否带DEPENDING ON"}
oc2 -->|"没有带"| oc3["固定次数的表"]
oc2 -->|"带了"| oc4["可变长表"]
oc4 -.-> oc5["后续项目的位置也可能移动"]
图11:DEPENDING ON 这三个词一出现,偏移量计算的前提就变了。
COPY
COPY 是编译时的 include。
也就是说,当前打开的源代码可能还不是完整形态。9
COPY CUSTOMER-REC.
COPY ERROR-MAP.
记录定义、公共标志、SQL 用的 host variable、外部接口被塞进 copybook 中是相当常见的情况。
如果 COPY 太多导致难以阅读,比较快的做法是确认能否查看展开后的源代码或 compiler listing。IBM Enterprise COBOL 中还提供了 MDECK 这样一个选项,用于输出经过库处理后的输入源代码。10
flowchart TB
accTitle: 带COPY的源代码怎么读
accDescr: COPY 是编译时的 include,打开的源代码可能还不是完整形态,因此要打开 copybook 确认;如果难以阅读,就去找展开后的源代码、compiler listing 或 MDECK 选项的输出。
cy1["发现了COPY"] --> cy2["打开的源代码可能不完整"]
cy2 --> cy3["打开copybook确认"]
cy2 -.->|"难以阅读时"| cy4["去找展开后的源代码或listing"]
图12:眼前看到的行数,未必就是这个程序的全部。
FILLER
FILLER 是没有名字的项目。
但这并不意味着“不被引用就没有意义”。
它通常承担以下作用:
- 预留区域
- 与旧规格兼容用的空位
- 用于对齐记录长度
REDEFINES所需的填充空间
FILLER 只是没有名字,但作为字节数依然实际存在。 如果忘了这一点,在与外部文件做映射时,整个世界就会一个字节一个字节地错开。
flowchart TB
accTitle: 漏数FILLER会发生什么
accDescr: FILLER 只是没有名字,作为字节数依然存在,还承担预留区域和对齐记录长度的作用,因此漏数它就会让与外部文件的映射一个字节一个字节地错开。
fl1["FILLER是没有名字的项目"] --> fl2["作为字节数依然存在"]
fl2 --> fl3{"布局计算中算进去了吗"}
fl3 -->|"算进去了"| fl4["与外部文件对得上"]
fl3 -->|"忘记了"| fl5["一个字节一个字节地错开"]
图13:不被引用的项目,也承担着“长度”这个职责。
6. PROCEDURE DIVISION 的最低限度知识
如果说 DATA DIVISION 是地图,那么 PROCEDURE DIVISION 就是移动路径。
6.1 PERFORM
PERFORM 是 COBOL 最基本的控制转移方式。
简单来说,就是调用处理后再返回。11
常见的写法如下:
PERFORM INIT-PROC
PERFORM UNTIL EOF
PERFORM READ-PROC
IF NOT EOF
PERFORM EDIT-PROC
PERFORM WRITE-PROC
END-IF
END-PERFORM
PERFORM 大致分为两种:
- 指定段落或节的 out-of-line
PERFORM - 直接在原地书写代码块的 inline
PERFORM ... END-PERFORM
在更旧的代码中,还经常会看到 PERFORM A-100 THRU A-199 这种范围指定的写法。
这种写法很方便,但如果中途新增段落,很容易引发连带执行的问题,所以阅读时要仔细确认范围的终点。
flowchart TB
accTitle: PERFORM的三种形态
accDescr: PERFORM 既有指定段落或节、调用后再返回的 out-of-line 形式,也有直接在原地写代码块的 inline 形式,旧代码里还有用 THRU 指定范围的写法,而范围指定在中途新增段落时很容易引发连带执行的问题。
pf1["发现了PERFORM"] --> pf2["调用段落的out-of-line"]
pf1 --> pf3["就地书写的inline"]
pf1 --> pf4["THRU的范围指定"]
pf4 -.-> pf5["务必确认范围的终点"]
图14:PERFORM 是哪种形态,决定了要追的返回点和范围。
6.2 IF / EVALUATE / 作用域
条件分支的基础是 IF。
EVALUATE 可以理解为类似 switch/case 的结构,这种理解基本没问题。
需要注意的是作用域的结束方式。12
END-IFEND-PERFORMEND-READ
这类带有明确终止符的代码还比较容易阅读。
问题在于旧代码。在 COBOL 中,. 会作为隐含的作用域终止符(scope terminator),一次性结束所有尚未闭合的语句。12
也就是说,仅凭一个句号,就会改变:
IF的范围到哪里结束PERFORM的范围到哪里结束- 从哪里跳转到下一个 sentence
此外,NEXT SENTENCE 与 CONTINUE 并不相同。
NEXT SENTENCE 会跳转到下一个句号之后,因此跳转位置会随后续 . 的位置而改变。12
阅读旧版 COBOL 时,把关注点放在句号而不是行末,正好合适。
flowchart TB
accTitle: 句号的分量
accDescr: 带 END-IF 这类明确终止符的代码容易阅读,但旧代码里句号会作为隐含的作用域终止符,一次性结束所有尚未闭合的语句,因此一个句号就能改变 IF 和 PERFORM 的范围。
sc1{"有没有明确的终止符"}
sc1 -->|"有END-IF等"| sc2["范围容易读出来"]
sc1 -->|"没有的旧代码"| sc3["句号是隐含的终止符"]
sc3 --> sc4["一个句号的位置就改变范围"]
sc4 -.-> sc5["盯句号,而不是盯行末"]
图15:旧代码的控制流程,掌握在句号的位置手里。
6.3 READ / WRITE / CALL
在业务 COBOL 中,以下这些是高频出现的语句:
READWRITEREWRITESTARTCALL
其中 READ ... AT END ... 是最典型的写法。
READ IN-FILE
AT END
SET EOF TO TRUE
NOT AT END
PERFORM PROCESS-REC
END-READ
如果出现 CALL 'SUBPGM' USING ...,就意味着跳转到了另一个程序。
这时,查看被调用程序的 LINKAGE SECTION 和 PROCEDURE DIVISION USING,就能相当清楚地看出参数传递的方式。
flowchart TB
accTitle: 发现CALL之后怎么追
accDescr: 有 CALL 就意味着跳到另一个程序,去看被调用方的 LINKAGE SECTION 和 PROCEDURE DIVISION USING,就能看清参数传递的方式。
ca1["发现了CALL"] --> ca2["跳转到另一个程序"]
ca2 --> ca3["查看被调用方的LINKAGE SECTION"]
ca3 --> ca4["用USING掌握参数传递的方式"]
图16:调用的含义,要和被调用方的接收接口一起读。
7. COBOL 之外的世界
COBOL 往往并不是仅凭源代码就能自成一体的世界。
- 文件定义
- 运行环境
- 数据库连接
- 事务环境
- job 控制
这些内容都分散在源代码之外。
至少掌握以下几点,会让阅读变得更顺畅。
文件与 FILE STATUS
ENVIRONMENT DIVISION 中的 FILE-CONTROL,与 DATA DIVISION 中的 FILE SECTION / FD,应该配套一起阅读。13
SELECT IN-FILE ASSIGN TO ...
FILE STATUS IS WS-FS.
FD IN-FILE.
01 IN-REC.
05 ...
如果存在 FILE STATUS,其中会保存每次 I/O 操作后的结果代码。
在阅读与文件相关的故障或 EOF 判定时,不看这个字段几乎无从下手。14
flowchart TB
accTitle: 文件定义要配套阅读
accDescr: ENVIRONMENT DIVISION 中 FILE-CONTROL 的 SELECT 与 DATA DIVISION 中的 FD 要配套阅读,如果有 FILE STATUS,每次输入输出后的结果代码都会存进去,因此文件类故障和 EOF 判定都从这里读起。
fs1["FILE-CONTROL的SELECT"] --> fs3["合起来才是一个文件定义"]
fs2["FILE SECTION的FD"] --> fs3
fs3 --> fs4["FILE STATUS里放着结果代码"]
fs4 -.-> fs5["解读故障与EOF判定的起点"]
图17:文件的样貌,是分写在两个 DIVISION 里的。
EXEC SQL
如果出现这个语句,说明用到了嵌入式 SQL。
EXEC SQL
SELECT ...
END-EXEC.
在这种情况下,COBOL 更像是“host variable 的容器”,真正的查询条件和更新对象都在 SQL 一侧。
因此,把 EXEC SQL 内部内容当作普通 SQL 来阅读是比较快的路径。
EXEC CICS
如果出现这个语句,说明进入了 CICS 的事务上下文。15
EXEC CICS
RECEIVE MAP(...)
END-EXEC.
到这一步,就不再是单纯的批处理阅读了。 需要连同画面、事务、响应码、COMMAREA 等外部上下文一起理解。
JCL 或运行定义
在 mainframe batch 中,实际分配的是哪个数据集,以及job 按什么顺序执行,不在 COBOL 源代码里的情况相当常见。 如果单看源代码却看不出“这个文件在哪里”,问题往往不在代码本身,而只是因为查看的范围还不够。
flowchart TB
accTitle: 源代码之外的世界
accDescr: 出现 EXEC SQL 时真正的查询条件在 SQL 一侧,出现 EXEC CICS 时要看画面和事务等外部上下文,mainframe batch 中数据集的分配和 job 的执行顺序则在 JCL 里,世界被分在 COBOL 源代码之外。
ex1["EXEC SQL"] --> ex7["也要看源代码之外"]
ex3["EXEC CICS"] --> ex7
ex5["JCL或运行定义"] --> ex7
ex7 -.-> ex2["SQL的条件、画面上下文、job的流程"]
图18:看不懂未必是代码的错,有时只是查看的范围太窄。
编译器实现的差异
本文是以 IBM 系统为背景写的,但在实务中也会遇到 Micro Focus,或者 Linux / Windows 上的 COBOL。骨架是通用的,读法不会变,但“写法相同、结果却不同”的地方是固定的那几处,事先掌握就不会出事。
| 关注点 | z/OS 系(IBM Enterprise COBOL) | 开放系统(Micro Focus、Linux / Windows 版等) |
|---|---|---|
| 字符编码 | 以 EBCDIC 为前提6 | 以 ASCII 为前提6 |
| 参照格式 | fixed 格式是传统上的默认值,也可以选 free 格式1 | fixed 和 free 两种都有,默认是哪一种取决于构建配置1 |
| copybook 的查找方式 | 通过库指定 | 通过编译器选项指定搜索路径 |
| 方言 | — | 有用于切换“对齐到哪种编译器实现”的选项 |
其中影响最大的是字符编码。同样是 PIC X(10),在 z/OS 上写出来的文件直接拿到 Windows 一侧去读,连数字的字节值都不一样。“一搬过来就全变成乱码”多半是这个原因,与 COMP-3 是两回事。
还有一处也容易出现差异,就是数值的内部表示。在 IBM Enterprise COBOL 中,BINARY / COMP-4 会按 PICTURE 中写的位数发生截断,而 COMP-5 则按 2 / 4 / 8 字节这样的原生二进制容量来保存值,截断也发生在二进制大小那一侧。16 也就是说,PIC S9(4) COMP 和 PIC S9(4) COMP-5 看起来一样,能容纳的值的上限却不同。如果在与其他系统直接以二进制交换数值的代码里看到 COMP-5,请把它当作有意这么写来阅读。
对照自己的环境时,最短的步骤如下。
- 先打开构建定义(makefile、JCL、项目配置)。 用的是哪种编译器实现、加了哪些选项,这些比源代码更先能弄清楚。
- 确定参照格式(fixed / free)。 在这一点搞错的情况下用编辑器格式化,代码就会坏掉。
- 确定字符编码。 是 EBCDIC 还是 ASCII,会改变 dump 的读法。
- 给
COMP系列的项目做标记。 编译器实现之间的差异基本都出在这里。
flowchart TB
accTitle: 对照自己环境的最短步骤
accDescr: 先打开构建定义确认是哪种编译器实现和哪些选项,再确定参照格式是 fixed 还是 free,接着确定字符编码是 EBCDIC 还是 ASCII,最后给容易出现实现差异的 COMP 系列项目做标记。
ck1["先打开构建定义"] --> ck2["确定参照格式"]
ck2 --> ck3["确定字符编码"]
ck3 --> ck4["给COMP系列项目做标记"]
图19:读源代码之前的这四步,能挡住编译器实现差异带来的问题。
8. 最低限度的阅读顺序
突然需要阅读 COBOL 时,按下面这个顺序会比较安全:
- 先梳理所有
COPY如果能打开 copybook 就打开;打不开的话,就找 listing 或展开后的源代码 - 收集
01级别的记录定义 把FILE SECTION、WORKING-STORAGE、LINKAGE SECTION的最上层内容列出来 - 阅读
PIC和USAGE识别金额、日期、数量、代码、标志等字段 - 搜索
READ/WRITE/REWRITE/CALL/EXEC SQL/EXEC CICS先掌握输入输出和外部边界 - 只追踪最初的主路径
从
PROCEDURE DIVISION开头沿着PERFORM链走一遍 - 查看
88和状态项目 这样能更容易理解 EOF、正常/异常、类型代码的含义 - 给
REDEFINES/OCCURS DEPENDING ON/COMP-3做标记 这些之后一定会用到,提前作为“危险项”标出来 - 如果涉及文件,查看
FILE STATUS能大幅减少对 I/O 错误的误读
按这个顺序,就不需要一开始就逐字精读全文。 与其一开始就追求 100% 理解 COBOL,更好的做法是先抓住记录、外部边界、主路径这三点,再深入细节,这样会轻松很多。
flowchart TB
accTitle: 安全的阅读顺序
accDescr: 先梳理 COPY 并确认 copybook,列出 01 级别的记录定义,用 PIC 和 USAGE 读懂项目的形态,再靠搜索掌握输入输出和外部边界,然后从 PROCEDURE DIVISION 开头沿 PERFORM 链追主路径,最后给危险项做标记。
ro1["梳理所有COPY"] --> ro2["列出01级别的记录定义"]
ro2 --> ro3["用PIC和USAGE读懂形态"]
ro3 --> ro4["搜索输入输出与外部边界"]
ro4 --> ro5["追踪主路径的PERFORM链"]
ro5 -.-> ro6["给危险项先做好标记"]
图20:不是通读全文,而是按这个顺序从外围一层层填进去。
8.1 练习:读一读这个记录定义
光讲读法不容易记住,所以这里放一个小记录。请先自己给出答案,再看下面的解答。
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).
问题
CUST-REC整体是多少字节?CUST-BALANCE从记录开头数起,是从第几个字节开始的?- 第 2 条
HIST-AMOUNT从开头数起,是从第几个字节开始的? - 当
CUST-KBN中放着'1'时,为真的条件名是哪一个? - 用文本编辑器打开这个文件时,看起来会坏掉的是哪些项目?
- 请举出两个仅凭这份定义无法知道的事情。
解答
-
是 74 字节。明细如下。
项目 计算 字节数 CUST-IDX(8)8 CUST-NAMEX(20)20 CUST-KBNX1 CUST-BALANCE9 位的 COMP-3 5 CUST-HIST(8 + 4)× 3 次36 FILLERX(4)4 合计 74 88级别是条件名,因此不占用字节。把它也数进去是很常见的错误。FILLER只是没有名字,那 4 个字节实实在在地存在。 -
从第 30 个字节开始。前面有
8 + 20 + 1 = 29个字节,所以它从下一个字节开始。 -
从第 55 个字节开始。
CUST-HIST从第 35 个字节开始,每条 12 字节,因此第 1 条是第 35 - 46 字节,第 2 条是第 47 - 58 字节。其中开头 8 个字节是HIST-DATE,所以HIST-AMOUNT从第 55 个字节开始。 -
是
CUST-VIP。并不是另外存在一块叫CUST-VIP的区域,而只是当CUST-KBN为'1'时,可以用这个名字读到它。 -
是
CUST-BALANCE和HIST-AMOUNT。两者都是COMP-3,当作字符读取就会得到没有意义的排列。HIST-DATE是PIC 9(8)的DISPLAY,在 ASCII 环境下可以像20260317那样按数字读出来。不过在 EBCDIC 环境下,即使看起来是数字,字节值也与 ASCII 不同。 -
例如下面这些。
- 这份定义本身是不是通过
COPY引入的。 不看 copybook 那一侧,就不知道它是不是实际在用的版本。 - 文件的字符编码是 EBCDIC 还是 ASCII。 这会改变
PIC X和PIC 9 DISPLAY的呈现方式。 - 运维上
CUST-KBN是否会出现'0'和'1'以外的值。 条件名只定义了两个,但这并不保证不会出现其他值。 - 文件本身的属性(记录长度、是否可变长、
FILE STATUS)。 这些不看ENVIRONMENT DIVISION和FD就无从得知。
- 这份定义本身是不是通过
如果问题 1 和 3 答错了,请回到 5.3 的位数与字节数对照表。COBOL 的误读,大多就是从这里开始的。
9. 常见的卡点
最后,整理一下初学者相当容易踩坑的地方。
把 REDEFINES 当成“另一个变量”
这是错的。 它是用另一种形态读取同一块区域。修改其中一方,另一方看到的内容也会跟着改变。7
把 88 当成“独立的 bool”
这是错的。
它只是给前一个项目的值起了个名字。SET WS-OK TO TRUE 在背后其实是把对应的值写入基础项目。3
忽略 COPY,只看正文
打开的这个文件,只是整体的一半。 字段定义、公共标志、host variable 大量存在于文件之外,是相当普遍的情况。9
把 MOVE 当成单纯的赋值
MOVE 并不只是简单的 memcpy。
根据接收方的类型,可能会涉及转换、对齐位数、补零、截断、编辑与反编辑等操作。17
轻视 . 的影响
COBOL 中的 . 比想象中更“重”。
在没有明确终止符的旧代码中,如果误判这个句号到底闭合到哪里,就会读错控制流程。12
把 packed decimal 或 EBCDIC 当成“乱码”
这不一定意味着数据损坏。 很多情况只是“本来就不是字符串”,或者“本来就不是 ASCII”。46
把 OCCURS DEPENDING ON 之后的部分当成固定位置
可变长表之后的项目,其位置可能会随值而变化。 如果带着固定长度的思路去读,偏移量计算就会全部出错。8
10. 优先查看的速查表
| 遇到的关键字 | 首先应该想到的内容 |
|---|---|
01 |
记录或分组的最上层,从这里把握整体结构 |
88 |
标志或状态代码的含义名称,是理解分支的关键 |
PIC X(...) |
字符项目 |
PIC 9(...) / S9(...)V... |
数值项目,确认位数与小数点位置 |
COMP |
binary |
COMP-3 |
packed decimal,很可能是金额或数量相关 |
REDEFINES |
用另一种解释方式查看同一块区域 |
OCCURS |
数组 / table |
OCCURS DEPENDING ON |
可变长,需要留意后续位置 |
FILLER |
没有名字,但有长度 |
COPY |
不看 copybook 就看不到完整形态 |
PERFORM |
主路径的骨架 |
READ / WRITE / REWRITE |
文件 I/O |
EXEC SQL |
数据库处理 |
EXEC CICS |
事务处理 |
FILE STATUS |
I/O 的结果代码 |
11. 总结
COBOL 之所以难懂,并不是因为它老旧。 而是因为数据定义、外部文件、运行上下文紧密结合在一起,导致最初的入口不容易被看清。
再总结一次阅读用的最小知识集合:
- 用
DIVISION把握整体地图 - 先阅读
DATA DIVISION - 通过
PIC和USAGE理解项目的形态 - 给
COMP-3、REDEFINES、OCCURS、88、COPY做标记 - 追踪
PERFORM、READ、WRITE、CALL - 通过
FILE STATUS、EXEC SQL、EXEC CICS把握外部边界 - 不要轻视
.的作用
一旦看清这些,COBOL 就会从“神秘的古老魔法”变成“记录处理的语言”。 老旧技术并不是因为名字古老而可怕,而是一旦最初的比例尺选错了,就会突然变得难以理解。只要地图的比例尺对上了,其实读起来意外地平常。
flowchart TB
accTitle: 对准比例尺来读
accDescr: 用 DIVISION 把握地图,先读 DATA DIVISION 掌握项目的形态,再用 PERFORM 和 READ 等追踪流程,最后用 FILE STATUS、EXEC SQL、EXEC CICS 把握外部边界,按这个顺序对准比例尺,COBOL 就能当作记录处理的语言正常阅读。
sm1["用DIVISION把握地图"] --> sm2["用DATA DIVISION读懂形态"]
sm2 --> sm3["用PERFORM和输入输出追踪流程"]
sm3 --> sm4["把握外部边界"]
sm4 -.-> sm5["古老魔法变成记录处理的语言"]
图21:难懂的真相不是语言老旧,而是最初选错了比例尺。
12. 参考资料
正文中主要引用的资料如下。正文里的上标数字就是指向这份列表的链接,每一项末尾的箭头可以返回原来的位置。
-
IBM,“Reference format” / IBM,“Area A or Area B” / Micro Focus,“Fixed Format” ↩ ↩2 ↩3 ↩4
-
IBM,“Level-numbers” ↩ ↩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 子句” ↩ ↩2
-
IBM,“OCCURS DEPENDING ON clause” ↩ ↩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,“编写在 CICS 下运行的 COBOL 程序” ↩
-
IBM,“Computational items” ↩
-
IBM,“Elementary move rules” ↩
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
不要直接使用 QR 码的读取值——纠错通过也不保证值是正确的
QR 码的纠错并不是一种只要纠错通过就保证值正确的机制。本文基于样本图像和两种解码器的实测,讲解污损落点不同会被读成另一个值的原因,以及业务系统一侧必须做的验证。
用 PowerShell 与 REST API 集成 ── Invoke-RestMethod 的实务
本文整理从 PowerShell 调用内部系统 API 与 SaaS REST API 的实务做法,涵盖认证请求头的传递方式、日语 JSON 乱码的应对、4xx/5xx 错误处理、429 重试、分页,直至代理与 TLS 的陷阱。
业务系统的编码设计 ── 商品编码・客户编码的确定方法与校验位
确定商品编码・客户编码等业务系统编码体系的实践指南。整理了有意义编码与无意义流水号的判断表、JAN・Luhn等校验位算法及C#实现、Excel开头零丢失的应对方法,直至位数溢出与迁移。
故障处理不止于恢复 ── 写给小型开发团队的 Postmortem(再发防止)实践模板
把故障处理停留在「修复、道歉,就此结束」,同样的故障就会反复发生。本文把 blameless postmortem 翻译给小型团队使用,整理出可在 1 小时内写完的模板、再发防止策略的强度判断表,以及实施的分级(triage)标准。
ADR(Architecture Decision Record)入门 —— 在小规模开发中,用最简方法留住「为什么这样设计」
代码不会告诉你「为什么这样做」。本文讲解如何用 ADR(Architecture Decision Record),以「1 个决定 = 1 个 Markdown 文件」的方式记录设计判断的理由,并附带模板、写与不写的判断表,以及实际案例。
相关主题
与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。
Windows 技术主题
汇整 KomuraSoft LLC 关于 Windows 开发、故障调查与既有资产活用文章的主题中心。
与本主题相关的服务
本文与以下服务页面相关联,欢迎从最接近的入口查看。
技术咨询 & 设计评审
从解读既有 COBOL 资产、找到改造的切入口、掌握外部边界,一直到迁移前的评估梳理,这个主题很适合技术咨询与设计评审。
故障调查 & 根本原因分析
刚接手就要处理故障,或者要追查 COBOL 资产在哪里出现了不一致,这类工作按缺陷调查与原因分析来推进会比较顺利。
常见问题
汇总了咨询这一主题时常见的问题。
- 阅读 COBOL 源代码应该从哪里开始?
- 只看 PROCEDURE DIVISION 只能理解一半。COBOL 与其说是逻辑语言,更应该看作是一种相当强调记录定义的语言,所以要先看 DATA DIVISION。比较安全的阅读顺序是:先梳理所有 COPY,确认 copybook 内容;列出 01 级别的记录定义;通过 PIC 和 USAGE 读懂各项目的形态;搜索 READ、WRITE、CALL、EXEC SQL、EXEC CICS 来掌握输入输出和外部边界;最后从 PROCEDURE DIVISION 开头沿着 PERFORM 链只追踪主路径。
- PIC S9(7)V99 COMP-3 是什么意思?
- PIC 表示项目的形态,USAGE 表示以何种方式保存。S9(7)V99 表示带符号、整数 7 位 + 小数 2 位的数值,其中 V 只是逻辑上的小数点,数据中并不会真的存在一个点字符。COMP-3 是 packed decimal(压缩十进制),常见于金额、税额、数量、费率类项目。如果把它当作文本查看,看起来会像是乱码,这本来就是正常现象,如果带着 CSV 或 UTF-8 的心态去看 dump,就会踩坑。
- COBOL 的 88 级别和 REDEFINES 应该如何理解?
- 88 并不是独立的 bool 变量,而是给前一个项目的某个值起的名字,称为条件名(condition-name)。SET WS-OK TO TRUE 在背后其实是把对应的值写入基础项目。REDEFINES 是用另一种形态查看同一块内存区域的机制,不是复制,更接近 C 语言中的 union 概念。修改其中一方,另一方看到的内容也会随之改变,这种写法常用于按记录类型区分同一块区域的场景。
- COPY 语句太多导致看不清整体结构时该怎么办?
- COPY 是编译时的 include,因此当前打开的源代码可能还不是完整形态。记录定义、公共标志、SQL 用的 host variable、外部接口被塞进 copybook 中是相当常见的情况。如果不容易阅读,比较快的做法是确认能否查看展开后的源代码或 compiler listing。IBM Enterprise COBOL 中也提供了 MDECK 这个选项,用于输出经过库处理后的输入源代码。