阅读 COBOL 源代码前应该掌握的最小知识

· 更新日期: · · COBOL, 老旧技术, 业务系统, 维护, Mainframe

更新记录(2 条,最后更新 2026年09月03日)

本文的修改记录。已保存的更新前版本,可通过带有 DOI 的永久链接阅读。

本文此前是日文原文的节译,缺少大量章节、表格、Mermaid 图、图题、脚注与 FAQ。现已改写为日文原文的完整译文,技术主张与日文版一致,并补上了此前缺失的图与表,同时统一了全篇的术语译法。 查看更新前的版本 (DOI: 10.5281/zenodo.22276862)
补充了日文原文中已有的咨询引导(consultation_services)。正文内容没有改动。 查看更新前的版本 (DOI: 10.5281/zenodo.21615499)
首次发布
引用本文(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 为背景,整理一套面向突然需要阅读源代码的人的最小知识集合。

停下手来的原因与地图的大小全部大写的变量名、级别编号、PIC S9(7)V99 COMP-3 这类写法,以及到处都是 COPY 导致看不清整体结构,都会让人停下手来;但阅读既有业务系统所需先掌握的骨架相当通用,地图并没有那么庞大。被陌生的写法卡住但骨架跨编译器实现是通用的先只拿到最小知识集合这张地图到处都是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 大致遵循下面的流程:

  1. 从文件或数据库中读取记录
  2. 存入 WORKING-STORAGE 上的项目
  3. 进行条件分支
  4. 转换到另一种记录格式
  5. 写出结果

也就是说,布局往往比算法更先浮现出来。

典型业务COBOL的流程从文件或数据库读取记录,存入 WORKING-STORAGE 上的项目,进行条件分支,转换到另一种记录格式后写出,这就是典型业务 COBOL 的流程。读取记录存入WORKING-STORAGE进行条件分支转换到另一种记录写出结果

图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 ...,很可能说明这个程序不是独立完整的,而是依赖外部传入的数据来运行。

LINKAGE SECTION 所暗示的事情如果看到 LINKAGE SECTION 和 PROCEDURE DIVISION USING,那么这个程序很可能不是独立完整的,而是依赖外部传入的数据来运行。存在LINKAGE SECTION很可能依赖外部数据运行存在PROCEDURE DIVISION USING不是独立完整的程序

图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 的边界就会被打乱,编译也就通不过了。

动固定格式源代码之前面对旧版源代码,先确认是 fixed format 还是 free format;对 fixed 格式的源代码套用现代自动格式化或 tab 转换,会打乱 Area A 与 Area B 的边界,导致编译通不过。fixed格式free格式打开了旧版源代码是fixed还是free列位置本身就是语法自动格式化或tab转换会弄坏它列的约束比较宽松

图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,给前一个项目的值起名字3
  • 66:用于 RENAMES,出现频率不高,但确实存在

需要注意的是,不要把 88 当成一个独立的 bool 变量。 并不是存在一个单独叫 WS-OK 的区域,而是当 WS-STATUS 的值为 '0' 时,可以用 WS-OK 这个名字来读取它。

88级别的真面目88 级别不是独立的 bool 变量,而是给前一个项目的值起的条件名;只有当 WS-STATUS 取到特定值时,才能用 WS-OK 这个名字读到它。为0时为9时基础项目 WS-STATUS值是什么以WS-OK这个名字为真以WS-ERROR这个名字为真并不存在另一块区域

图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 当作“看起来的字符串”来解读,基本上都会出问题。

V是逻辑上的小数点PIC 9(5)V99 中的 V 是逻辑上的小数点,并不对应真实的点字符,数据中不会出现点,因此把文件或 dump 当作看起来的字符串来解读就会出问题。PIC 9(5)V99按带2位小数的数值处理数据中不会出现点字符当作可见字符串解读就会出问题

图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.

这三者虽然都是“数值”,但内部保存方式各不相同。

同样的数值,保存方式却不同同样是带符号数值,DISPLAY 是以字符形式呈现的外部十进制,COMP 是二进制数,COMP-3 是 packed decimal,USAGE 不同,内部保存方式就不同。形态相同的数值项目DISPLAY〔以字符形式呈现〕COMP〔二进制数〕COMP-3〔packed decimal〕当作文本读取会显得像乱码

图7:PIC 相同,只要 USAGE 变了,字节序列就是另一回事。

在实务中最关键的,是看到 COMP-3 时应该做出的反应:

  • 它是 packed decimal
  • 很可能是金额、税额、数量、费率相关的字段
  • 当作文本查看,看起来像乱码本就正常
  • 带着 CSV 或 UTF-8 的心态去看,就会踩坑

有了这种理解,在查看 dump 或二进制文件时,就不会毫无必要地感到恐慌。

看到COMP-3时该有的反应看到 COMP-3 就知道它是 packed decimal,很可能是金额、税额、数量、费率类项目,当作文本查看显得像乱码本就正常,因此不要带着 CSV 或 UTF-8 的心态去看。发现了COMP-3认出它是packed decimal怀疑它是金额或数量类项目文本里看起来是乱码也不慌

图8:只要有这一个条件反射,在 dump 面前发慌的次数就会减少。

COMP-3 实际会变成什么样的字节序列

这一段只要动手推一次,之后看到的东西就不一样了。

packed decimal 的规则只有两条。45

  1. 一个字节里塞两位十进制数字
  2. 但最右边那个字节例外,它用低位一位数字加符号来占满一个字节

符号用 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。

COMP-3的字节序列是怎么造出来的packed decimal 把十进制数字按两位一组塞进一个字节,只有最右边的字节用低位一位数字加符号,因此 9 位数值占 5 个字节;即使按 ASCII 打开,原来的数值排列也不会出现在任何地方。十进制数字两位一组塞进一字节最后一字节是低位一位加符号9位合计5个字节按ASCII打开看不到原数值看起来像乱码,其实没坏

图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 字节的区域”。

REDEFINES是对同一块区域的另一种解释REDEFINES 不是复制,而是用另一种形态查看同一块内存区域;同一个缓冲区既能按通用记录读,也能按头部记录读,改写其中一方,另一方看到的内容也会随之改变。同一块内存区域按REC-BUF来看按HEADER-REC来看写了一方,两边看到的都会变

图10:定义有两份,实体的字节序列却只有一份。

OCCURS

OCCURS 相当于数组,在 COBOL 中常被称为 table。

       05  WS-ITEM OCCURS 12 TIMES.
           10  WS-PRICE    PIC 9(5).

如果进一步出现 OCCURS DEPENDING ON,就说明这是一个可变长表。 这种情况下,后续项目的位置也可能受到影响,如果按固定长度的思路去追踪,就很容易踩空。8

OCCURS DEPENDING ON 的注意事项OCCURS 是数组,一旦带上 DEPENDING ON 就成了可变长表,后续项目的位置也可能随值而动,因此按固定长度的思路去算偏移量就会踩空。没有带带了发现了OCCURS是否带DEPENDING ON固定次数的表可变长表后续项目的位置也可能移动

图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

带COPY的源代码怎么读COPY 是编译时的 include,打开的源代码可能还不是完整形态,因此要打开 copybook 确认;如果难以阅读,就去找展开后的源代码、compiler listing 或 MDECK 选项的输出。难以阅读时发现了COPY打开的源代码可能不完整打开copybook确认去找展开后的源代码或listing

图12:眼前看到的行数,未必就是这个程序的全部。

FILLER

FILLER 是没有名字的项目。 但这并不意味着“不被引用就没有意义”。

它通常承担以下作用:

  • 预留区域
  • 与旧规格兼容用的空位
  • 用于对齐记录长度
  • REDEFINES 所需的填充空间

FILLER 只是没有名字,但作为字节数依然实际存在。 如果忘了这一点,在与外部文件做映射时,整个世界就会一个字节一个字节地错开。

漏数FILLER会发生什么FILLER 只是没有名字,作为字节数依然存在,还承担预留区域和对齐记录长度的作用,因此漏数它就会让与外部文件的映射一个字节一个字节地错开。算进去了忘记了FILLER是没有名字的项目作为字节数依然存在布局计算中算进去了吗与外部文件对得上一个字节一个字节地错开

图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 这种范围指定的写法。 这种写法很方便,但如果中途新增段落,很容易引发连带执行的问题,所以阅读时要仔细确认范围的终点。

PERFORM的三种形态PERFORM 既有指定段落或节、调用后再返回的 out-of-line 形式,也有直接在原地写代码块的 inline 形式,旧代码里还有用 THRU 指定范围的写法,而范围指定在中途新增段落时很容易引发连带执行的问题。发现了PERFORM调用段落的out-of-line就地书写的inlineTHRU的范围指定务必确认范围的终点

图14:PERFORM 是哪种形态,决定了要追的返回点和范围。

6.2 IF / EVALUATE / 作用域

条件分支的基础是 IF。 EVALUATE 可以理解为类似 switch/case 的结构,这种理解基本没问题。

需要注意的是作用域的结束方式。12

  • END-IF
  • END-PERFORM
  • END-READ

这类带有明确终止符的代码还比较容易阅读。

问题在于旧代码。在 COBOL 中,. 会作为隐含的作用域终止符(scope terminator),一次性结束所有尚未闭合的语句。12

也就是说,仅凭一个句号,就会改变:

  • IF 的范围到哪里结束
  • PERFORM 的范围到哪里结束
  • 从哪里跳转到下一个 sentence

此外,NEXT SENTENCE 与 CONTINUE 并不相同。 NEXT SENTENCE 会跳转到下一个句号之后,因此跳转位置会随后续 . 的位置而改变。12

阅读旧版 COBOL 时,把关注点放在句号而不是行末,正好合适。

句号的分量带 END-IF 这类明确终止符的代码容易阅读,但旧代码里句号会作为隐含的作用域终止符,一次性结束所有尚未闭合的语句,因此一个句号就能改变 IF 和 PERFORM 的范围。有END-IF等没有的旧代码有没有明确的终止符范围容易读出来句号是隐含的终止符一个句号的位置就改变范围盯句号,而不是盯行末

图15:旧代码的控制流程,掌握在句号的位置手里。

6.3 READ / WRITE / CALL

在业务 COBOL 中,以下这些是高频出现的语句:

  • READ
  • WRITE
  • REWRITE
  • START
  • CALL

其中 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,就能相当清楚地看出参数传递的方式。

发现CALL之后怎么追有 CALL 就意味着跳到另一个程序,去看被调用方的 LINKAGE SECTION 和 PROCEDURE DIVISION USING,就能看清参数传递的方式。发现了CALL跳转到另一个程序查看被调用方的LINKAGE SECTION用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

文件定义要配套阅读ENVIRONMENT DIVISION 中 FILE-CONTROL 的 SELECT 与 DATA DIVISION 中的 FD 要配套阅读,如果有 FILE STATUS,每次输入输出后的结果代码都会存进去,因此文件类故障和 EOF 判定都从这里读起。FILE-CONTROL的SELECT合起来才是一个文件定义FILE SECTION的FDFILE STATUS里放着结果代码解读故障与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 源代码里的情况相当常见。 如果单看源代码却看不出“这个文件在哪里”,问题往往不在代码本身,而只是因为查看的范围还不够。

源代码之外的世界出现 EXEC SQL 时真正的查询条件在 SQL 一侧,出现 EXEC CICS 时要看画面和事务等外部上下文,mainframe batch 中数据集的分配和 job 的执行顺序则在 JCL 里,世界被分在 COBOL 源代码之外。EXEC SQL也要看源代码之外EXEC CICSJCL或运行定义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,请把它当作有意这么写来阅读。

对照自己的环境时,最短的步骤如下。

  1. 先打开构建定义(makefile、JCL、项目配置)。 用的是哪种编译器实现、加了哪些选项,这些比源代码更先能弄清楚。
  2. 确定参照格式(fixed / free)。 在这一点搞错的情况下用编辑器格式化,代码就会坏掉。
  3. 确定字符编码。 是 EBCDIC 还是 ASCII,会改变 dump 的读法。
  4. 给 COMP 系列的项目做标记。 编译器实现之间的差异基本都出在这里。
对照自己环境的最短步骤先打开构建定义确认是哪种编译器实现和哪些选项,再确定参照格式是 fixed 还是 free,接着确定字符编码是 EBCDIC 还是 ASCII,最后给容易出现实现差异的 COMP 系列项目做标记。先打开构建定义确定参照格式确定字符编码给COMP系列项目做标记

图19:读源代码之前的这四步,能挡住编译器实现差异带来的问题。

8. 最低限度的阅读顺序

突然需要阅读 COBOL 时,按下面这个顺序会比较安全:

  1. 先梳理所有 COPY 如果能打开 copybook 就打开;打不开的话,就找 listing 或展开后的源代码
  2. 收集 01 级别的记录定义 把 FILE SECTION、WORKING-STORAGE、LINKAGE SECTION 的最上层内容列出来
  3. 阅读 PIC 和 USAGE 识别金额、日期、数量、代码、标志等字段
  4. 搜索 READ / WRITE / REWRITE / CALL / EXEC SQL / EXEC CICS 先掌握输入输出和外部边界
  5. 只追踪最初的主路径 从 PROCEDURE DIVISION 开头沿着 PERFORM 链走一遍
  6. 查看 88 和状态项目 这样能更容易理解 EOF、正常/异常、类型代码的含义
  7. 给 REDEFINES / OCCURS DEPENDING ON / COMP-3 做标记 这些之后一定会用到,提前作为“危险项”标出来
  8. 如果涉及文件,查看 FILE STATUS 能大幅减少对 I/O 错误的误读

按这个顺序,就不需要一开始就逐字精读全文。 与其一开始就追求 100% 理解 COBOL,更好的做法是先抓住记录、外部边界、主路径这三点,再深入细节,这样会轻松很多。

安全的阅读顺序先梳理 COPY 并确认 copybook,列出 01 级别的记录定义,用 PIC 和 USAGE 读懂项目的形态,再靠搜索掌握输入输出和外部边界,然后从 PROCEDURE DIVISION 开头沿 PERFORM 链追主路径,最后给危险项做标记。梳理所有COPY列出01级别的记录定义用PIC和USAGE读懂形态搜索输入输出与外部边界追踪主路径的PERFORM链给危险项先做好标记

图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).

问题

  1. CUST-REC 整体是多少字节?
  2. CUST-BALANCE 从记录开头数起,是从第几个字节开始的?
  3. 第 2 条 HIST-AMOUNT 从开头数起,是从第几个字节开始的?
  4. 当 CUST-KBN 中放着 '1' 时,为真的条件名是哪一个?
  5. 用文本编辑器打开这个文件时,看起来会坏掉的是哪些项目?
  6. 请举出两个仅凭这份定义无法知道的事情。

解答

  1. 是 74 字节。明细如下。

    项目 计算 字节数
    CUST-ID X(8) 8
    CUST-NAME X(20) 20
    CUST-KBN X 1
    CUST-BALANCE 9 位的 COMP-3 5
    CUST-HIST (8 + 4) × 3 次 36
    FILLER X(4) 4
    合计   74

    88 级别是条件名,因此不占用字节。把它也数进去是很常见的错误。FILLER 只是没有名字,那 4 个字节实实在在地存在。

  2. 从第 30 个字节开始。前面有 8 + 20 + 1 = 29 个字节,所以它从下一个字节开始。

  3. 从第 55 个字节开始。CUST-HIST 从第 35 个字节开始,每条 12 字节,因此第 1 条是第 35 - 46 字节,第 2 条是第 47 - 58 字节。其中开头 8 个字节是 HIST-DATE,所以 HIST-AMOUNT 从第 55 个字节开始。

  4. 是 CUST-VIP。并不是另外存在一块叫 CUST-VIP 的区域,而只是当 CUST-KBN 为 '1' 时,可以用这个名字读到它。

  5. 是 CUST-BALANCE 和 HIST-AMOUNT。两者都是 COMP-3,当作字符读取就会得到没有意义的排列。HIST-DATE 是 PIC 9(8) 的 DISPLAY,在 ASCII 环境下可以像 20260317 那样按数字读出来。不过在 EBCDIC 环境下,即使看起来是数字,字节值也与 ASCII 不同。

  6. 例如下面这些。

    • 这份定义本身是不是通过 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 就会从“神秘的古老魔法”变成“记录处理的语言”。 老旧技术并不是因为名字古老而可怕,而是一旦最初的比例尺选错了,就会突然变得难以理解。只要地图的比例尺对上了,其实读起来意外地平常。

对准比例尺来读用 DIVISION 把握地图,先读 DATA DIVISION 掌握项目的形态,再用 PERFORM 和 READ 等追踪流程,最后用 FILE STATUS、EXEC SQL、EXEC CICS 把握外部边界,按这个顺序对准比例尺,COBOL 就能当作记录处理的语言正常阅读。用DIVISION把握地图用DATA DIVISION读懂形态用PERFORM和输入输出追踪流程把握外部边界古老魔法变成记录处理的语言

图21:难懂的真相不是语言老旧,而是最初选错了比例尺。

12. 参考资料

正文中主要引用的资料如下。正文里的上标数字就是指向这份列表的链接,每一项末尾的箭头可以返回原来的位置。

共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。

与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。

本文与以下服务页面相关联,欢迎从最接近的入口查看。

常见问题

汇总了咨询这一主题时常见的问题。

阅读 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 这个选项,用于输出经过库处理后的输入源代码。

作者简介

本文作者的个人简介页面。

Go Komura

小村软件有限公司 代表

以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表