阅读 COBOL 源代码前应该掌握的最小知识
· 小村 豪 · COBOL, 老旧技术, 业务系统, 维护, Mainframe
交接工作、故障处理、维护供应商提供的软件包——在这些场景下,某天可能会突然收到一份 COBOL 源代码。
- 文件名是
.cbl或.cpy - 变量名全部是大写
01、05、77、88排列在一起- 出现类似
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 就看不清整体结构 - 只要能追踪
PERFORM、IF、EVALUATE、READ、WRITE、CALL,大致就能理清流程 - 旧版源代码是列位置具有意义的固定格式,看到的空白并不只是排版装饰1
总而言之,只要能读懂 DIVISION、PIC、USAGE、COMP-3、REDEFINES、OCCURS、88、COPY、PERFORM,迷路的概率就会大幅下降。
2. 先把 COBOL 当作「数据形态」的语言来看
如果带着 C# 或 Java 的思维去读,最初总会想去追踪 if、for 或函数调用。
但在 COBOL 中,在此之前先弄清楚「这个程序接收什么样的记录、生成什么样的记录、持有什么样的缓冲区」会更快。
典型的业务 COBOL 大致遵循下面的流程:
- 从文件或数据库中读取记录
- 存入
WORKING-STORAGE上的项目 - 进行条件分支
- 转换到另一种记录格式
- 写出结果
也就是说,布局往往比算法更先浮现出来。
例如,下面就是一个骨架示例:
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 ...,很可能说明这个程序不是独立完整的,而是依赖外部传入的数据来运行。
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,给前一个项目的值起名字366:用于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 位小数
这里特别重要的是 V。
V 并不对应真实存在的 . 字符。
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 / 作用域
条件分支的基础是 IF。
EVALUATE 可以理解为类似 switch/case 的结构,这种理解基本没问题。
需要注意的是作用域的结束方式。12
END-IFEND-PERFORMEND-READ
这类带有明确终止符的代码还比较容易阅读。
问题在于旧代码。在 COBOL 中,.(句号)会作为隐含的作用域终止符,一次性结束所有尚未闭合的语句。12
也就是说,仅凭一个句号,就会改变:
IF的范围到哪里结束PERFORM的范围到哪里结束- 从哪里跳转到下一个 sentence
此外,NEXT SENTENCE 与 CONTINUE 并不相同。
NEXT SENTENCE 会跳转到下一个句号之后,因此跳转位置会随后续 . 的位置而改变。12
阅读旧版 COBOL 时,最好养成关注句号而不是行末的习惯。
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,就能相当清楚地看出参数传递的方式。
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 时,按下面这个顺序会比较安全:
- 先梳理所有
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,更好的做法是先抓住记录、外部边界、主路径这三点,再深入细节,这样会轻松很多。
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 - 通过
PIC和USAGE理解项目的形态 - 给
COMP-3、REDEFINES、OCCURS、88、COPY做标记 - 追踪
PERFORM、READ、WRITE、CALL - 通过
FILE STATUS、EXEC SQL、EXEC CICS把握外部边界 - 不要轻视
.(句号)的作用
一旦看清这些,COBOL 就会从「神秘的古老魔法」变成「记录处理的语言」。 老旧技术并不是因为名字古老而可怕,而是一旦最初看错了比例尺,就会突然变得难以理解。只要比例尺对上了,其实读起来意外地平常。
12. 参考资料
正文中主要引用的资料如下。
-
IBM,”Reference format” / IBM,”Area A or Area B” / Micro Focus,”Fixed Format” ↩ ↩2
-
IBM,”Level-numbers” ↩ ↩2
-
IBM,”Examples: numeric data and internal representation” ↩ ↩2
-
IBM,”PACKED-DECIMAL (COMP-3)” ↩
-
IBM,”The EBCDIC character set” / IBM,”Handling differences in ASCII SBCS and EBCDIC SBCS characters” ↩ ↩2 ↩3
-
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,”Elementary move rules” ↩
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
委托开发 Windows 应用程序前该梳理的事项
在委托外包开发 Windows 应用程序之前,梳理现有软件改造、设备联动、COM/ActiveX、发布与更新、维护等需要注意的要点。
什么是 Reg-Free COM——无需注册使用 COM 的机制
整理 Reg-Free COM 的基础知识、激活上下文(activation context)与清单(manifest)的作用、优点、局限,以及在实务中判断是否采用的思路。
用 PerfView 与 dotnet-trace 定位“变慢”的原因 ── .NET 性能排查实务入门
当业务应用出现“变慢”“CPU 占满”“偶尔卡死”时,该用哪个工具看什么?本文梳理 PerfView 与 dotnet-trace 的分工、CPU 采样的解读方法(inclusive/exclusive)、用 ThreadTime 排查阻塞时间,以及与 EventSourc...
面向 BtoB 企业的 Google 广告小预算运营 ── 用每月数万日元做出成果的设计与每周操作流程
本文面向希望以每月数万日元预算启动 Google 广告的 BtoB 企业,是一份实务指南。内容整理了平均每日预算与结算上限的运作原理、转化跟踪的搭建、聚焦高意向关键词、否定关键词与搜索字词报告的每周运营方法,以及小预算下的出价策略选择,均以 Google 官方资料为依据。
打造能被地区名搜索到的网站 ── 中小企业的本地SEO实战指南(地区页面与Google商家资料)
用「地区名称+行业」搜索却找不到自家公司的中小企业,适合看这篇。内容涵盖修正本地SEO的先后顺序:整理Google商家资料、统一NAP信息、设计地区页面、网站端的基础建设,以及在Search Console上衡量效果,全部以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 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。