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

· · COBOL, 老旧技术, 业务系统, 维护, Mainframe

交接工作、故障处理、维护供应商提供的软件包——在这些场景下,某天可能会突然收到一份 COBOL 源代码。

  • 文件名是 .cbl.cpy
  • 变量名全部是大写
  • 01057788 排列在一起
  • 出现类似 PIC S9(7)V99 COMP-3 这种介于咒语和会计软件之间的写法
  • 而且到处都是 COPY,光看打开的文件根本看不清整体结构

看到这里,脑子多少会有点发懵。

不过,用来阅读的地图并没有那么庞大。COBOL 在不同编译器和产品之间存在差异,但在阅读现有业务系统时,首先应该掌握的骨架却相当通用。本文将以 IBM 系统及典型的业务 COBOL 为背景,整理一套面向突然需要阅读源代码的人的最小知识集合

1. 先说结论(一句话)

先用一种比较粗略,但在实务中相当有用的说法来概括:

  • COBOL 与其说是逻辑语言,更应该看作是一种相当强调记录定义的语言
  • 只看 PROCEDURE DIVISION 只能理解一半,要先看 DATA DIVISION
  • PIC 表示项目的形态USAGE 表示以何种方式保存
  • COMP-3 是 packed decimal(压缩十进制),常见于金额和数量相关的场景
  • 88 与其说是另一个变量,更应该理解为给前一个项目的值起的条件名
  • REDEFINES用另一种形态查看同一块内存的机制,不是复制
  • 一旦出现 COPY,当前打开的源代码就还不是完整形态,不看 copybook 就看不清整体结构
  • 只要能追踪 PERFORMIFEVALUATEREADWRITECALL,大致就能理清流程
  • 旧版源代码是列位置具有意义的固定格式,看到的空白并不只是排版装饰1

总而言之,只要能读懂 DIVISION、PIC、USAGE、COMP-3、REDEFINES、OCCURS、88、COPY、PERFORM,迷路的概率就会大幅下降。

2. 先把 COBOL 当作「数据形态」的语言来看

如果带着 C# 或 Java 的思维去读,最初总会想去追踪 iffor 或函数调用。 但在 COBOL 中,在此之前先弄清楚「这个程序接收什么样的记录、生成什么样的记录、持有什么样的缓冲区」会更快。

典型的业务 COBOL 大致遵循下面的流程:

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

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

例如,下面就是一个骨架示例:

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

4. 不要被固定格式的外观吓到

在旧版 COBOL 中,源代码每一行的列位置本身是有意义的。如果不了解这一点就去阅读,「为什么左边有莫名其妙的空白」这个疑问永远解不开。1

固定格式大致规则如下:

  • 第 1-6 列:顺序编号
  • 第 7 列:indicator(指示符)
  • 第 8-11 列:Area A
  • 第 12-72 列:Area B

第 7 列尤其重要:

  • */:注释行
  • -:续行
  • D:debugging line(调试行)
  • *>:可以出现在行中间的注释

为了降低视觉压力,用比较粗略的方式画成图大致是这样:

1234567 8901 23456789012345678901234567890
      * 注释
       IDENTIFICATION DIVISION.
       PROGRAM-ID. SAMPLE01.

这里的空白,并不是现代意义上的「排版」,而在一定程度上属于语法本身。 在编辑器中做 tab 转换、整体左移、或随意粘贴,都可能直接破坏代码。 阅读旧版源代码时,首先要怀疑这个文件到底是 fixed format 还是 free format。如果对 fixed format 的文件套用现代格式化工具,往往会很「壮观」地出问题。

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 这个名字来读取该状态。

还有一点很重要:决定层次结构的是级别编号,而不是空白。 看上去的缩进只是参考,最终应该相信的是 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 位小数

这里特别重要的是 VV 并不对应真实存在的 . 字符PIC 9(5)V99 会被当作「带 2 位小数的数值」处理,但数据中并不会真的出现一个点字符。 因此,如果把文件或 dump 当作「看起来的字符串」来解读,基本上都会出问题。

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.

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

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

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

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

再补充一点,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 字节的区域」

OCCURS

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

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

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

COPY

COPY 是编译时的 include。 也就是说,当前打开的源代码可能还不是完整形态9

       COPY CUSTOMER-REC.
       COPY ERROR-MAP.

记录定义、公共标志、SQL 用的 host variable、外部接口被塞进 copybook 中是相当常见的情况。

如果 COPY 太多导致难以阅读,比较快的做法是确认能否查看展开后的源代码或 compiler listing。IBM Enterprise COBOL 中还提供了 MDECK 这样一个选项,用于输出经过库处理后的输入源代码。10

FILLER

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

它通常承担以下作用:

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

FILLER 只是没有名字,但作为字节数依然实际存在。 如果忽略这一点,在与外部文件做映射时,就会出现逐字节的错位。

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

6.2 IF / EVALUATE / 作用域

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

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

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

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

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

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

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

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

阅读旧版 COBOL 时,最好养成关注句号而不是行末的习惯。

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

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

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 源代码内部,这种情况相当常见。 如果单看源代码却看不出「这个文件在哪里」,问题往往不在代码本身,而是因为查看的范围还不够。

8. 最低限度的阅读顺序

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

  1. 先梳理所有 COPY 如果能打开 copybook 就打开;打不开的话,就找 listing 或展开后的源代码
  2. 收集 01 级别的记录定义FILE SECTIONWORKING-STORAGELINKAGE SECTION 的最上层内容列出来
  3. 阅读 PICUSAGE 识别金额、日期、数量、代码、标志等字段
  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,更好的做法是先抓住记录、外部边界、主路径这三点,再深入细节,这样会轻松很多。

9. 常见的卡点

最后,整理一下初学者相当容易踩坑的地方。

REDEFINES 当成「另一个变量」

这是错的。 它是用另一种形态读取同一块区域。修改其中一方,另一方看到的内容也会跟着改变。7

88 当成「独立的 bool」

这是错的。 它只是给前一个项目的值起了个名字。SET WS-OK TO TRUE 在背后其实是把对应的值写入基础项目。3

忽略 COPY,只看正文

这相当于带着一半折起来的地图就进山。 字段定义、公共标志、host variable 大量存在于文件之外,是相当普遍的情况。9

MOVE 当成单纯的赋值

MOVE 并不只是简单的 memcpy。 根据接收方的类型,可能会涉及转换、对齐位数、补零、截断、编辑与反编辑等操作。16

轻视 .(句号)的影响

COBOL 中的 . 比想象中更「重」。 在没有明确终止符的旧代码中,如果误判这个句号到底闭合到哪里,就会读错控制流程。12

把 packed decimal 或 EBCDIC 当成「乱码」

这不一定意味着数据损坏。 很多情况只是「本来就不是字符串」,或者「本来就不是 ASCII」。46

OCCURS DEPENDING ON 之后的部分当成固定位置

可变长表之后的项目,其位置可能会随值而变化。 如果带着固定长度的思路去读,偏移量计算就会全部出错。8

10. 速查表

遇到的关键字 首先应该想到的内容
01 记录或分组的最上层,从这里把握整体结构
88 标志或状态代码的含义名称,是理解分支的关键
PIC X(...) 字符项目
PIC 9(...) / S9(...)V... 数值项目,确认位数与小数点位置
COMP 二进制
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
  • 通过 PICUSAGE 理解项目的形态
  • COMP-3REDEFINESOCCURS88COPY 做标记
  • 追踪 PERFORMREADWRITECALL
  • 通过 FILE STATUSEXEC SQLEXEC CICS 把握外部边界
  • 不要轻视 .(句号)的作用

一旦看清这些,COBOL 就会从「神秘的古老魔法」变成「记录处理的语言」。 老旧技术并不是因为名字古老而可怕,而是一旦最初看错了比例尺,就会突然变得难以理解。只要比例尺对上了,其实读起来意外地平常。

12. 参考资料

正文中主要引用的资料如下。

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

面向 BtoB 企业的 Google 广告小预算运营 ── 用每月数万日元做出成果的设计与每周操作流程

本文面向希望以每月数万日元预算启动 Google 广告的 BtoB 企业,是一份实务指南。内容整理了平均每日预算与结算上限的运作原理、转化跟踪的搭建、聚焦高意向关键词、否定关键词与搜索字词报告的每周运营方法,以及小预算下的出价策略选择,均以 Google 官方资料为依据。

常见问题

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

阅读 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 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表