Walkthrough of the COBOLworx cbl-gdb source-level debugging package.

August 31, 2020

The COBOLworx cbl-gdb package provides source-level debugging of COBOL programs compiled with the GnuCOBOL compiler. Our goal here is to provide a general understanding of how the package works, and show what it can do.

List of cbl-gdb commands

If you are looking for a summary list of the commands provided by the cbl-gdb source-level debugger, try here.

Design Goals

Our design goal was to provide an easy-to-use source-level debugging package for COBOL programs. We didn’t want to clutter up the COBOL source code with special debugging grammar; we wanted to use the free GnuCOBOL compiler; and we wanted to leverage the GNU GDB debugger for the actual debugging.

The challenge in using the GDB debugger is that it doesn’t understand COBOL, and we have chosen not to embark on the significant effort of creating a COBOL module for GDB.

We have, instead, developed a way to convince GDB that it is debugging a program written in the C Programming Language. The target executable, however, is altered so that the embedded debugging information points back to the original COBOL source code, and we have created an extension to GDB, written in Python, that manages the display of COBOL variables and their contents.

How It Is Used

Ordinarily the GnuCOBOL compiler is invoked from the command line like this:

cobc -x soh.cbl

This compiles the soh.cbl source code, and produces an executable named soh on Linux systems, and soh.exe on Windows.

When debugging information is desired, the cobc command is replaced with cobcd:

cobcd -x soh.cbl

Although GnuCOBOL has many possible command-line options, we have tried to make using the cbl-gdb compilation process as compatible as possible. If you manage to come up with a set of command-line options that works with COBC and that you think ought to work with COBCD, but doesn’t, please let us know about it. (Send such inquiries to support@cobolworx.com )

Important Limitation

Although COBC can be used to compile multiple source modules in a single invocation, and combine them into a single executable or shared library, the COBCD process can handle only one source code module at a time.

What Is It Doing?

GnuCOBOL works by translating the COBOL code into C code, which is then fed into the GCC compiler to produce an executable.

The cbl-gdb extensions work by interjecting additional steps that add COBOL variable-name identifier information into the executable, modifies the debugging information to point back to lines of the original COBOL source code, and includes an instruction to load a COBOL-aware Python script that extends GDB.

This process is explained in more detail in Appendix A.

Leveraging GDB

The GNU GDB debugger is a sophisticated and powerful debugging tool; its documentation is more than a bit daunting. But developers who have used it for actual debugging know that just a few commands are used the most.

From the command line, gdb soh means, “Start debugging the soh program.”

After GDB loads, you are presented with a (gdb) prompt. Here are the commands that are used the most when operating in pure command-line mode:

(gdb) list 1,30        # list source code lines 1 through 30
(gdb) list             # show the next ten lines
(gdb) list -           # show the previous ten lines
(gdb) break 10         # set a breakpoint at line 10
(gdb) run              # start running the program from the beginning
(gdb) frame            # show where the program is stopped, and show the
                       # next line to be executed
(gdb) backtrace        # show the stack
(gdb) continue         # continue from the point where the program is
                       # currently stopped
(gdb) continue N       # when proceeding from a breakpoint, continue
                       # until that same breakpoint is encountered
                       # N times
(gdb) finish           # continue until the current function returns
(gdb) watch <var>      # stop execution when <var> changes
(gdb) next             # Execute a single instruction, stepping OVER
                       # subroutine calls
(gdb) step             # Execute a single instruction, stepping INTO
                       # subroutine calls
(gdb) print <var>      # Show the contents of <var>
(gdb) print <var>=123  # Alter the contents of <var> to be 123

There are numerous other features, including deleting, disabling, and enabling breakpoints; displaying memory at specific locations in various formats and much more. In addition to the official GNU documentation, there are numerous “cheat sheets” and guides to be found on the internet.

GDB doesn’t require that the entire command be entered; you can use b for break, n for next, and so on. Also, when you are repeating a command – it’s often the case that you will be entering, for example, next repeatedly – you can just keep pressing the Enter key.

And, finally, although we won’t be using it here, you might want to see if $ gdb -tui soh works on your system. If it does, some users might find GDB’s Text User Interface presentation to be useful. On the other hand, it requires that NCURSES be configured properly, which is often a headache.

Enough Talk. Let’s Look at a Program

For a sample, we are going to look at the SOH program. (“SOH” means “Sack Of Hammers”, as in “Dumb as a sack of hammers.”)

        IDENTIFICATION DIVISION.
        PROGRAM-ID. SOH.

        DATA DIVISION.
        WORKING-STORAGE SECTION.
        77 HEADER PICTURE X(64) VALUE SPACE.
        77 ONE    PICTURE 9999  VALUE 0.
        77 TWO    PICTURE 9999  VALUE 0.
        77 THREE  PICTURE 9999  VALUE 0.

        PROCEDURE DIVISION.
        MOVE  "Demonstration of addition" to HEADER.
        DISPLAY HEADER.
        MOVE 1 TO ONE.
        MOVE 2 TO TWO.
        ADD ONE TO TWO GIVING THREE.
        DISPLAY ONE " plus " TWO " equals " THREE.
        END PROGRAM SOH.

For anybody new to COBOL: Every COBOL program has to have an IDENTIFICATION division. The WORKING-STORAGE section is where variable definitions go. This program defines four variables: one 64-character alphanumeric string, and three four-digit numeric variables.

The PROCEDURE division is where executable code goes. I will refrain from explaining what SOH does. (If you can’t figure it out, I suggest that you are in the wrong place.)

Let’s compile and run that program on an Ubuntu 18.04 Linux system:

$ cobcd -x soh.cbl
$ ./soh
Demonstration of addition
0001 plus 0002 equals 0003
$

No surprises there.

We compiled it with COBCD, so we are ready to use GDB to debug it. We’ll launch GDB with the -q command line option to avoid a lot of boilerplate:

$ gdb -q soh
Reading symbols from soh...
registering CPrint (Usage is "print" <COBOL identifier>") [Version 3.18]
registering CWatch (Usage is "cwatch <COBOL identifier>")
(gdb)

The feedback from GDB shows that it was able to load SOH and its debugging symbols, and that the Python debugging extension, which generated the two "registering’’ lines, was also loaded.

We can now use the GDB list command to see the program. I am going to ask to see lines 1 through 100 because I know that will show me the whole listing of this short program.

(gdb) list 1,100
1               IDENTIFICATION DIVISION.
2               PROGRAM-ID. SOH.
3
4               DATA DIVISION.
5               WORKING-STORAGE SECTION.
6               77 HEADER PICTURE X(64) VALUE SPACE.
7               77 ONE    PICTURE 9999  VALUE 0.
8               77 TWO    PICTURE 9999  VALUE 0.
9               77 THREE  PICTURE 9999  VALUE 0.
10
11              PROCEDURE DIVISION.
12              MOVE  "Demonstration of addition" to HEADER.
13              DISPLAY HEADER.
14              MOVE 1 TO ONE.
15              MOVE 2 TO TWO.
16              ADD ONE TO TWO GIVING THREE.
17              DISPLAY ONE " plus " TWO " equals " THREE.
18              END PROGRAM SOH.
(gdb)

The General Case of Setting Breakpoints and Looking at Variables

Line 12 is the first executable line, so let’s set a breakpoint there:

(gdb) b 12
Breakpoint 1 at 0x1045: file soh.cbl, line 12.
(gdb)

And then use the run command to launch the program

(gdb) r
Starting program: /home/bob/projects/soh/soh
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, SOH_ (entry=0) at soh.cbl:12
12              MOVE  "Demonstration of addition" to HEADER.
(gdb)

Just to show that we can, indeed, single-step through a COBOL program, I am going to enter the next command, and then repeatedly press Enter until the program finishes running:

(gdb) next
13              DISPLAY HEADER.
(gdb)
Demonstration of addition
14              MOVE 1 TO ONE.
(gdb)
15              MOVE 2 TO TWO.
(gdb)
16              ADD ONE TO TWO GIVING THREE.
(gdb)
17              DISPLAY ONE " plus " TWO " equals " THREE.
(gdb)
0001 plus 0002 equals 0003
[Inferior 1 (process 21007) exited normally]
(gdb)
The program is not being run.
(gdb)

Now, let’s use the run command to start the program again. It’ll stop at line 12 again (the breakpoint is still there), and then we’ll use the print command to look at the HEADER variable:

(gdb) r
Starting program: /home/bob/projects/soh/soh
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, SOH_ (entry=0) at soh.cbl:12
12              MOVE  "Demonstration of addition" to HEADER.
(gdb) print header
77 HEADER/SOH [W-S]  :  ' ' <repeats 64 times>
(gdb)

A few things to note there.

First, COBOL variable identifiers are not case-sensitive, so it doesn’t matter if you ask for “header” or “HEADER”.

Second, even though GDB shows line 12 as the instruction MOVE "Demonstration of addition" to HEADER., the print command reports that the HEADER string is 64 spaces.

This is because GDB shows you the instruction that is going to be executed when the program resumes. That instruction hasn’t happened yet.

Let’s do a next followed by another print HEADER:

(gdb) next
13              DISPLAY HEADER.
(gdb) print header
77 HEADER/SOH [W-S]  :  "Demonstration of addition", ' ' <repeats 39 times>
(gdb)

We now see that the MOVE has taken place.

You have control over the repeat-count formatting. The threshold level defaults to a count of eleven characters. You can change that to another value, making it larger or smaller. You can also set it to zero, which defeats the counting:

(gdb) print set repeat 0
(gdb) print header
77 HEADER/SOH [W-S]  :  "Demonstration of addition                                       "
(gdb)

In a program with so few variables, it can be useful to simply look at all of them with the print * command:

(gdb) p *
 1 : 77 HEADER/SOH [W-S] : "Demonstration of addition                                       "
 2 : 77 ONE/SOH [W-S] : 0000
 3 : 77 TWO/SOH [W-S] : 0000
 4 : 77 THREE/SOH [W-S] : 0000
(gdb)

The first column is an ordering number. Then comes the COBOL variable Level and name along with the program it came from. That’s followed by an indication that the variable is in WORKING-STORAGE ([W-S]), LOCAL-STORAGE ([L-S]), LINKAGE ([LNK]), or in a FILE DESCRIPTOR ([I/O]). After that is the variable’s value.

The ordering number can be used in a subsequent print command:

(gdb) p 1
77 HEADER/SOH [W-S]  :  "Demonstration of addition                                       "
(gdb)

This can be used to change the way the a variable is displayed without entering the name again. The way we most frequently use this is when we want to see a variable in the print/d “debugging” format (which is used a lot when debugging the debugger).

(gdb) p/d 1
Name:        77 HEADER/SOH [W-S]
Field:       f_8    ( 0x555555756020 )
Base:        b_8
Attr:        a_2    ( 0x555555555670 )
Offset:      0
location:    0x5555557561a0
Length:      64
AttrType:    33 (0x21) ALPHANUMERIC
AttrDigits:  0
AttrScale:   0
AttrFlags:   0x0
data: (HEX)   [ 44 65 6d 6f 6e 73 74 72 61 74 69 6f 6e 20 6f 66 20 61 64
64 69 74 69 6f 6e 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 ]
data: (ASCII) "Demonstration of addition                                       "
(gdb)

What you see here might make sense when considering that the COBOL variables are actually implemented as data structures in the C code generated by the GnuCOBOL compiler. Those interested in more information can look at the generated C code as well as the source code for GnuCOBOL’s libcob shared library.

Moving on: Using the frame command reminds us that the program is trapped just before line 13:

(gdb) frame
#0  SOH_ (entry=0) at soh.cbl:13
13              DISPLAY HEADER.

Let’s single-step ahead a couple of lines:

(gdb) next
Demonstration of addition
14              MOVE 1 TO ONE.
(gdb) next
15              MOVE 2 TO TWO.
(gdb)

We just executed line 14, MOVE 1 TO ONE, and are about to execute line 15.

Let’s check the value of ONE…

(gdb) p ONE
77 ONE/SOH [W-S]  :  0001
(gdb)

…and change it to 1,001 using the print <var>= construction:

(gdb) p ONE=1001
77 ONE/SOH [W-S]  :  1001
(gdb)

By stepping ahead another couple of lines and doing another print *, we can see the effects of changing ONE to 1,001:

(gdb) next
16              ADD ONE TO TWO GIVING THREE.
(gdb) next
17              DISPLAY ONE " plus " TWO " equals " THREE.
(gdb) p *
 1 : 77 HEADER/SOH [W-S] : "Demonstration of addition                                       "
 2 : 77 ONE/SOH [W-S] : 1001
 3 : 77 TWO/SOH [W-S] : 0002
 4 : 77 THREE/SOH [W-S] : 1003
(gdb)

Let’s take a look at GDB’s “watchpoint” capability, which we invoke with the cwatch command.

We start the program again, then set a watchpoint on the variable TWO, and then continue from there:

(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/bob/projects/soh/soh
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, SOH_ (entry=0) at soh.cbl:12
12              MOVE  "Demonstration of addition" to HEADER.
(gdb) cwatch TWO
Hardware watchpoint 2: *(char(*)[4])(0x555555756190)

(gdb) continue
Continuing.
Demonstration of addition

Hardware watchpoint 2: *(char(*)[4])(0x555555756190)

Old value = "0000"
New value = "0002"
0x000055555555513f in SOH_ (entry=0) at soh.cbl:15
15              MOVE 2 TO TWO.
(gdb)

You can see that the program halted when the value of TWO changed from “0000” to “0002”.

That should give you a pretty good idea of what you can do with the cbl-gdb package.

Slightly more advanced topics:

The SOH program has only a few variables and only a few instructions. Ordinary COBOL programs often have many different variables. Between them and their related REDEFINES, it’s not unusual for a program to have thousands of variables. When there are dozens of variables the print * display can become oppressive; with hundreds or thousands, it can be useless.

We have a feature for coping with that. The print ? command looks at a range of lines around the current trap point, and tries to find things that look like variable names, and show only those. By default, the range is set to +/- six lines. The range is adjusted using the print/r <range> switch. I am going to trap the SOH program in the middle, set the range to +/- zero lines, and we’ll see what happens:

Breakpoint 1, SOH_ (entry=0) at soh.cbl:12
12              MOVE  "Demonstration of addition" to HEADER.
(gdb) n
13              DISPLAY HEADER.
(gdb) n
Demonstration of addition
14              MOVE 1 TO ONE.
(gdb) n
15              MOVE 2 TO TWO.
(gdb) print/r
The 'p ?' range is currently +/- 6 lines
(gdb) print/r 0
The 'p ?' range has been set to +/- 0 lines
77 TWO/SOH [W-S]  :  0000
(gdb) p *
 1 : 77 HEADER/SOH [W-S] : "Demonstration of addition", ' ' <repeats 39 times>
 2 : 77 ONE/SOH [W-S] : 0001
 3 : 77 TWO/SOH [W-S] : 0000
 4 : 77 THREE/SOH [W-S] : 0000
(gdb) p ?
77 TWO/SOH [W-S]  :  0000
(gdb)

You can see how the command print/r, without a parameter, simply reported the current range setting. print/r 0 changed the range to +/- zero lines, meaning that only the current line is checked for possible variables. That explains why print ? only reported the value for TWO; TWO is the only variable referenced in line 15.

Now that you know about the /d and /r switches, you might suspect that there are others. Find them with the /h switch:

(gdb) print/h
COBOL print command usage:
'print/h' prints this message.
'print <arg>' shows <arg> using session-default formatting
'print/v<n> <arg>' displays <arg> with a one-time formatting override
'print/v0' : name and variable on one line (this is the default)
'print/v1' : just the variable
'print/v2' : two-line display with name, then variable
'print/d   : expanded information
'print/v<n>' with no arguments sets the session formatting
'print/m'  : generate output compatible with '-stack-list-variables --simple-values'
'print *'  : print all variables that are currently in context
'print ?'  : print all variables that are currently in context and within R lines of the breakpoint
'print/r N': set the range for 'print ?' to plus/minus R (the default is 6; -1 means 'all')
The default v-level can be set with the environment variable CPRINT_V=n
The default r-level can be set with the environment variable CPRINT_R=n
(gdb)

Let’s check out the /v formatting switches:

(gdb) p/v0 header
77 HEADER/SOH [W-S]  :  "Demonstration of addition", ' ' <repeats 39 times>
(gdb) p/v1 header
"Demonstration of addition", ' ' <repeats 39 times>
(gdb) p/v2 header
77 HEADER/SOH [W-S]
"Demonstration of addition", ' ' <repeats 39 times>
(gdb)

Which one is chosen is a matter of personal taste. I hardly ever change away from the default of print/v0. As the print/h screen describes, you can change the session default to v2 with print/v2

(gdb) print header
77 HEADER/SOH [W-S]  :  "Demonstration of addition", ' ' <repeats 39 times>
(gdb) print/v2
(gdb) print header
77 HEADER/SOH [W-S]
"Demonstration of addition", ' ' <repeats 39 times>
(gdb)

You can, if you prefer that GDB start with a different /v default, set that with the environment variable CPRINT_V=n

There is one switch that’s documented in print/h for completeness, but which you almost certainly won’t find a need for. print/m creates an output similar to that generated by GDB in machine-interface mode. It’s used for program-to-program communication when COBCD is combined with Microsoft’s Visual Studio Code:

(gdb) p/m *
variables=[{name="HEADER/SOH",value="\"\\\"Demonstration of addition\\\"
, ' ' <repeats 39 times>\""},{name="ONE/SOH",value="\"0001\""},{name="TW
O/SOH",value="\"0002\""},{name="THREE/SOH",value="\"0003\""}]
(gdb)

Printing From a More Complex List of Variables

Let’s switch to a different program. I am not going to bother with the source code. But it has a more complicated set of variables, and will allow more of a real-world discussion about using print.

At this point I have loaded the program, set a breakpoint at the end of it, and run it. The print * command shows that there are a lot of variables:

(gdb) p *
 1 : 01 input-record/optfde01 [I/O] : "1234560601194501L021234567900000123", ' ' <repeats 83 times>
 2 : 01 output-record/optfde01 [I/O] : "1234561945060101L021234567900000123", ' ' <repeats 83 times>
 3 : 01 file-status/optfde01 [W-S] : "."
 4 : 01 working-data/optfde01 [W-S] : "         0601194519450601Records modified: 00003"
 5 : 05 pgm-name/working-data/optfde01 [W-S] : "         "
 6 : 05 old-date/working-data/optfde01 [W-S] : 06011945
 7 : 05 old-dat1/working-data/optfde01 [W-S] : "06011945"
 8 : 10 datmo/old-dat1/working-data/optfde01 [W-S] : 06
 9 : 10 datda/old-dat1/working-data/optfde01 [W-S] : 01
10 : 10 datyr/old-dat1/working-data/optfde01 [W-S] : 1945
11 : 05 new-date/working-data/optfde01 [W-S] : 19450601
12 : 05 new-dat1/working-data/optfde01 [W-S] : "19450601"
13 : 10 datyr/new-dat1/working-data/optfde01 [W-S] : 1945
14 : 10 datmo/new-dat1/working-data/optfde01 [W-S] : 06
15 : 10 datda/new-dat1/working-data/optfde01 [W-S] : 01
16 : 05 my-counters/working-data/optfde01 [W-S] : "Records modified: 00003"
17 : 10 custmas-ctr/my-counters/working-data/optfde01 [W-S] : 00003
18 : 01 oeit/optfde01 [L-S] : "1234561945060101L021234567900000123"
19 : 05 oeit-custnum/oeit/optfde01 [L-S] : 123456
20 : 05 oeit-orddate/oeit/optfde01 [L-S] : 19450601
21 : 05 oeit-ordnum/oeit/optfde01 [L-S] : 01
22 : 05 oeit-rectype/oeit/optfde01 [L-S] : "L"
23 : 05 oeit-lineno/oeit/optfde01 [L-S] : 02
24 : 05 oeit-ponum/oeit/optfde01 [L-S] : "12345679"
25 : 05 oeit-filler/oeit/optfde01 [L-S] : "00000123"
26 : 01 oeitl/optfde01 [L-S] : '0' <repeats 16 times>, " ", '0' <repeats 18 times>
27 : 05 oeitl-custnum/oeitl/optfde01 [L-S] : 000000
28 : 05 oeitl-orddate/oeitl/optfde01 [L-S] : 00000000
29 : 05 oeitl-ordnum/oeitl/optfde01 [L-S] : 00
30 : 05 oeitl-rectype/oeitl/optfde01 [L-S] : " "
31 : 05 oeitl-lineno/oeitl/optfde01 [L-S] : 00
32 : 05 oeitl-prodnum/oeitl/optfde01 [L-S] : 00000000
33 : 05 oeitl-qty/oeitl/optfde01 [L-S] : 00000000
34 : 01 ordmast/optfde01 [L-S] : '0' <repeats 16 times>, "        "
35 : 05 oeit-custnum/ordmast/optfde01 [L-S] : 000000
36 : 05 oeit-orddate/ordmast/optfde01 [L-S] : 00000000
37 : 05 oeit-ordnum/ordmast/optfde01 [L-S] : 00
38 : 05 oeit-ponum/ordmast/optfde01 [L-S] : "        "
39 : 01 ordline/optfde01 [L-S] : '0' <repeats 18 times>
40 : 05 oeitl-lineno/ordline/optfde01 [L-S] : 00
41 : 05 oeitl-prodnum/ordline/optfde01 [L-S] : 00000000
42 : 05 oeitl-qty/ordline/optfde01 [L-S] : 00000000
(gdb

The cbl-gdb print command attempts to be as helpful as it can by not forcing you to type entire variable names. For example there are two variables that contain the letters “counter”. So, print counter results in:

(gdb) print counter
 1 : 05 my-counters/working-data/optfde01 [W-S] : "Records modified: 00003"
 2 : 10 custmas-ctr/my-counters/working-data/optfde01 [W-S] : 00003

You’ll note that those are two lines in the same working-data variable structure. But because the string “counter” was ambiguous, print showed both of them.

However, print my-counters does this:

(gdb) print my-counters
05 my-counters/working-data/optfde01 [W-S]  :  "Records modified: 00003"
(gdb)

Because there actual is only one variable with a named piece that is exactly “my-counters”, print shows just that one.

Similarly, of all the variables, only one contains the substring “ctr”:

(gdb) p ctr
10 custmas-ctr/my-counters/working-data/optfde01 [W-S]  :  00003

So, even though “ctr” doesn’t specify an entire piece of the name, it’s the only possibility, and is shown as such. And because it’s the only possibility, it can be used in a p ctr=3333 alter command:

(gdb) p ctr=3333
10 custmas-ctr/my-counters/working-data/optfde01 [W-S]  :  03333

Conclusion

You now have a general understanding of what the cbl-gdb package is doing to create a COBOL executable that can be source-level debugged by GDB.

You have also seen many of the commands available in the Python extension to GDB for the display of COBOL variables.

If you have any questions, or problems, or comments, please let us know at support@cobolworx.com.

Appendix A – the CBL-GDB compiliation process.

The GnuCOBOL compiler operates by translating the COBOL source code into the C programming language. The GCC compiler then goes through its normal behavior, which is to convert the C code to assembly language (which has a .S file extension). That assembly language is then compiled (or assembled, if you prefer) into machine language, which is packaged by the loader (along with debugging information, if any) into an executable.

(I am not unaware that entire college courses are spent expanding on what I described there so cavalierly, and experts spend entire careers implementing it.)

The COBCD script breaks down the process into its components steps, and interjects additional processing to create the debuggable executable:

  1. The COBC compiler converts the COBOL source into the intermediate .C and .S files.
  2. The COBCD-ST program scans the .C file and develops a cross-reference table matching the C variable names to the COBOL variable names.
  3. The COBCD-ST program then creates an additional .SYM.C source module that contains the symbols cross-reference and instructions to load the Python GDB extension.
  4. COBC compiles the .SYM.C module, producing a .SYM.O object file.
  5. The COBCD-SFIX program modifies the .S file, rearranging the line references so that GDB cleanly refers back to the original COBOL source.
  6. COBC assembles the modified .S file into a .O object.
  7. GCC combines the two .O files into a single executable. If you care to see this process unfold, you can set the ECHO=1 environment variable:
$ ECHO=1 cobcd -x soh.cbl
cobc -### -x soh.cbl
os_name    is  Linux
SCRIPTDIR  is  /usr/local/bin
COBC       is  cobc
COBST      is  /usr/local/bin/cobcd-st
SFIX       is  /usr/local/bin/cobcd-sfix
PYTHON     is  /usr/local/bin/cobcd.py
FILE_ROOT  is  soh
FILE_CBL   is  soh.cbl
FILE_LST   is  soh.cbl.lst
FILE_C     is  soh.c
FILE_S     is  soh.s
FILE_O     is  soh.o
FILE_SYM_C is  soh.sym.c
FILE_SYM_O is  soh.sym.o
TARGET     is  soh
cobc -S -g -fec=PROGRAM-ARG-OMITTED --fgen-c-line-directives --fgen-c-labels -T soh.cbl.lst --tlines=0 --tsymbols -x soh.cbl
/usr/local/bin/cobcd-st -q -f soh soh.cbl
cobc -c -o soh.sym.o soh.sym.c
/usr/local/bin/cobcd-sfix -q soh.s soh.s soh.c soh.cbl
cobc -c -g -o soh.o soh.s
gcc -Wl,--export-dynamic -o soh soh.sym.o soh.o -L/usr/local/lib -lcob -lm
$

And if you are interested enough, you can use the COBCDNOCLEAN=1 environment variable to tell the COBCD command not to delete the intermediate files:

$ COBCDNOCLEAN=1 cobcd -x soh.cbl
$ ls -al
total 176
drwxrwxr-x  2 bob bob  4096 2020-08-31 10:09 .
drwxrwxr-x 15 bob bob  4096 2020-08-26 09:06 ..
-rwxrwxr-x  1 bob bob 23320 2020-08-31 10:09 soh
-rw-rw-r--  1 bob bob  5837 2020-08-31 10:09 soh.c
-rw-rw-r--  1 bob bob   566 2020-08-26 09:13 soh.cbl
-rw-rw-r--  1 bob bob  1311 2020-08-31 10:09 soh.cbl.lst
-rw-rw-r--  1 bob bob  1157 2020-08-31 10:09 soh.c.h
-rw-rw-r--  1 bob bob  1451 2020-08-31 10:09 soh.c.l.h
-rw-rw-r--  1 bob bob   443 2020-08-31 10:09 soh.i
-rw-rw-r--  1 bob bob 25224 2020-08-31 10:09 soh.o
-rw-rw-r--  1 bob bob 52727 2020-08-31 10:09 soh.s
-rw-rw-r--  1 bob bob   335 2020-08-31 10:09 soh.sym.c
-rw-rw-r--  1 bob bob  1280 2020-08-31 10:09 soh.sym.o

You almost certainly don’t actually need to know all of that; all you need to do is invoke COBCD and all should be well. But at least now you know what you are ignoring.