Building and linking¶
This page covers the path from a .c file to a runnable CP/M .COM program and
the compiler options that most affect the result. You typically do this in your
own project directory — DCC C Compiler and ntvcm are general-purpose tools for building
CP/M / Z80 C apps anywhere, not just inside the DCC C Compiler repo. As long as the tools
are on your PATH (see Setting up the toolchain), the
commands below work from any folder that holds your .c sources.
The build script¶
The primary installed build command is dcc-ma. It compiles, optimizes,
strips the runtime, assembles, and links in one step.
With an installed package, use the common wrapper command on Windows, macOS, and Linux:
dcc-ma foo --mode fast # builds foo.c -> FOO.COM
dcc-ma foo --mode nopeep # skip the dccpeep optimizer
From a source checkout, run the implementation script directly:
./scripts/ma.sh foo --mode fast
On Windows, use the PowerShell driver. It works with the Windows PowerShell 5.1 already included with Windows, as well as PowerShell 7+:
powershell.exe -ExecutionPolicy Bypass -File .\scripts\ma.ps1 foo -Mode fast # builds foo.c -> FOO.COM
powershell.exe -ExecutionPolicy Bypass -File .\scripts\ma.ps1 foo -Mode nopeep # skip the dccpeep optimizer
This runs the compiler, the optional dccpeep peephole optimizer,
dccrtlstrip, then M80 and L80. Runtime trimming is part of the normal build
path because it keeps unused library routines out of the final .COM file. The
script resolves each tool from your PATH or from the DCC, DCCPEEP, and
DCCRTLSTRIP environment variables.
The manual pipeline (click to expand)
For manual builds or custom build systems, the full pipeline for foo.c is
shown below. dcc, dccpeep, and dccrtlstrip run on the host;
m80.com and l80.com are CP/M programs, so run them through ntvcm or
another CP/M emulator:
Define a CRLF helper using PowerShell/.NET APIs.
function Convert-ToCrlf($Path) {
$text = [IO.File]::ReadAllText($Path) -replace "`r?`n", "`r`n"
[IO.File]::WriteAllText($Path, $text)
}
Compile the C source to M80 assembly, then optionally run the peephole optimizer.
dcc -I C:\path\to\dcc -stack 512 foo.c -o FOO.MAC
dccpeep FOO.MAC _PEEPOUT.MAC
Move-Item -Force _PEEPOUT.MAC FOO.MAC
Convert the app assembly to CP/M CRLF text and assemble it with M80 under
ntvcm.
Convert-ToCrlf FOO.MAC
ntvcm m80 "=FOO.MAC" /X /O /Z /L
Copy and trim the runtime to only the blocks used by the app.
Copy-Item C:\path\to\dcc\DCCRTL.MAC DCCRTL.MAC
Convert-ToCrlf DCCRTL.MAC
dccrtlstrip -r DCCRTL.MAC -o RTLMIN.MAC FOO.MAC
Convert, assemble, and link the trimmed runtime with the app.
Convert-ToCrlf RTLMIN.MAC
ntvcm m80 "=RTLMIN.MAC" /X /O /Z
ntvcm l80 "/P:100,RTLMIN,FOO,FOO/N/E"
Define a CRLF helper: prefer unix2dos if it is installed, otherwise
use Perl (available on macOS).
to_crlf() {
if command -v unix2dos >/dev/null 2>&1; then
unix2dos "$1" >/dev/null 2>&1 || true
else
perl -0pi -e 's/\r?\n/\r\n/g' "$1"
fi
}
Compile the C source to M80 assembly, then optionally run the peephole optimizer.
dcc -I /path/to/dcc -stack 512 foo.c -o FOO.MAC
dccpeep FOO.MAC _PEEPOUT.MAC
mv _PEEPOUT.MAC FOO.MAC
Convert the app assembly to CP/M CRLF text and assemble it with M80 under
ntvcm.
to_crlf FOO.MAC
ntvcm m80 "=FOO.MAC" /X /O /Z /L
Copy and trim the runtime to only the blocks used by the app.
cp /path/to/dcc/DCCRTL.MAC DCCRTL.MAC
to_crlf DCCRTL.MAC
dccrtlstrip -r DCCRTL.MAC -o RTLMIN.MAC FOO.MAC
Convert, assemble, and link the trimmed runtime with the app.
to_crlf RTLMIN.MAC
ntvcm m80 "=RTLMIN.MAC" /X /O /Z
ntvcm l80 "/P:100,RTLMIN,FOO,FOO/N/E"
Define a CRLF helper: prefer unix2dos if it is installed, otherwise
use Perl (available on Ubuntu).
to_crlf() {
if command -v unix2dos >/dev/null 2>&1; then
unix2dos "$1" >/dev/null 2>&1 || true
else
perl -0pi -e 's/\r?\n/\r\n/g' "$1"
fi
}
Compile the C source to M80 assembly, then optionally run the peephole optimizer.
dcc -I /path/to/dcc -stack 512 foo.c -o FOO.MAC
dccpeep FOO.MAC _PEEPOUT.MAC
mv _PEEPOUT.MAC FOO.MAC
Convert the app assembly to CP/M CRLF text and assemble it with M80 under
ntvcm.
to_crlf FOO.MAC
ntvcm m80 "=FOO.MAC" /X /O /Z /L
Copy and trim the runtime to only the blocks used by the app.
cp /path/to/dcc/DCCRTL.MAC DCCRTL.MAC
to_crlf DCCRTL.MAC
dccrtlstrip -r DCCRTL.MAC -o RTLMIN.MAC FOO.MAC
Convert, assemble, and link the trimmed runtime with the app.
to_crlf RTLMIN.MAC
ntvcm m80 "=RTLMIN.MAC" /X /O /Z
ntvcm l80 "/P:100,RTLMIN,FOO,FOO/N/E"
Define a CRLF helper: prefer unix2dos if it is installed, otherwise
use Perl (available on Ubuntu).
to_crlf() {
if command -v unix2dos >/dev/null 2>&1; then
unix2dos "$1" >/dev/null 2>&1 || true
else
perl -0pi -e 's/\r?\n/\r\n/g' "$1"
fi
}
Compile the C source to M80 assembly, then optionally run the peephole optimizer.
dcc -I /path/to/dcc -stack 512 foo.c -o FOO.MAC
dccpeep FOO.MAC _PEEPOUT.MAC
mv _PEEPOUT.MAC FOO.MAC
Convert the app assembly to CP/M CRLF text and assemble it with M80 under
ntvcm.
to_crlf FOO.MAC
ntvcm m80 "=FOO.MAC" /X /O /Z /L
Copy and trim the runtime to only the blocks used by the app.
cp /path/to/dcc/DCCRTL.MAC DCCRTL.MAC
to_crlf DCCRTL.MAC
dccrtlstrip -r DCCRTL.MAC -o RTLMIN.MAC FOO.MAC
Convert, assemble, and link the trimmed runtime with the app.
to_crlf RTLMIN.MAC
ntvcm m80 "=RTLMIN.MAC" /X /O /Z
ntvcm l80 "/P:100,RTLMIN,FOO,FOO/N/E"
Define a CRLF helper using PowerShell/.NET APIs.
function Convert-ToCrlf($Path) {
$text = [IO.File]::ReadAllText($Path) -replace "`r?`n", "`r`n"
[IO.File]::WriteAllText($Path, $text)
}
Compile the C source to M80 assembly, then optionally run the peephole optimizer.
dcc -I C:\path\to\dcc -stack 512 foo.c -o FOO.MAC
dccpeep FOO.MAC _PEEPOUT.MAC
Move-Item -Force _PEEPOUT.MAC FOO.MAC
Convert the app assembly to CP/M CRLF text and assemble it with M80 under
ntvcm.
Convert-ToCrlf FOO.MAC
ntvcm m80 "=FOO.MAC" /X /O /Z /L
Copy and trim the runtime to only the blocks used by the app.
Copy-Item C:\path\to\dcc\DCCRTL.MAC DCCRTL.MAC
Convert-ToCrlf DCCRTL.MAC
dccrtlstrip -r DCCRTL.MAC -o RTLMIN.MAC FOO.MAC
Convert, assemble, and link the trimmed runtime with the app.
Convert-ToCrlf RTLMIN.MAC
ntvcm m80 "=RTLMIN.MAC" /X /O /Z
ntvcm l80 "/P:100,RTLMIN,FOO,FOO/N/E"
scripts/ma.ps1 stages m80.com and l80.com before invoking ntvcm.
For a manual build, keep those .COM files and DCCRTL.MAC in the working
directory where you run the pipeline, or adjust the paths to match your
layout. Replace /path/to/dcc (or C:\path\to\dcc) with the DCC C Compiler repo path
that contains the standard headers; if you run from the DCC C Compiler repo root, the
explicit -I is usually unnecessary.
M80 expects CP/M-style CRLF text files; LF-only files can be misread. The Unix
function shown above uses Perl or unix2dos; the Windows function uses
PowerShell/.NET APIs.
The compiler invocation¶
dcc [options] input.c [-o output.mac]
Common options:
| Option | Meaning |
|---|---|
-o file |
Write M80 assembly to file; default is out.mac, - is stdout. |
-c, -module |
Emit a separately compilable module, not a final program translation unit. |
-f, -ffloatio |
Force %f support on every printf-family call. |
-fl, -flongio |
Force 32-bit long formats on every printf-family call. |
-fno-floatio, -fno-longio |
Force the corresponding format paths off, overriding automatic detection. |
-fstack-check |
Emit a lightweight stack-overflow guard in each function prologue. |
-s bytes, -stack bytes, --stack bytes |
Reserve stack bytes; default is 512. |
-s=bytes, -stack=bytes, --stack=bytes |
Equivalent attached forms for the stack size. |
-I dir, -Idir |
Add an include search directory. |
-D name[=value], -Dname[=value] |
Predefine a macro. |
-U name, -Uname |
Undefine a preprocessor macro. |
-v, --version |
Print the compiler version and exit. |
-h, --help |
Print compiler help and exit. |
Options that affect the runtime¶
For each printf-family call with a compile-time literal format, dcc detects
%f, long, hexadecimal, and octal conversions and selects the smallest matching
runtime entry automatically. Calls with non-literal formats conservatively
include all of those conversion paths.
-f/-ffloatio— force floating-point%fsupport on everyprintf-family call, including calls whose literal format does not use it. This is normally useful only when forcing a whole-program policy; non-literal formats already use a conservative fallback.-fl/-flongio— similarly force 32-bitlongformats (%ld,%lu,%lx,%lX,%ls) on everyprintf-family call.-fno-floatio/-fno-longio— force the corresponding support off, even for a literal that uses it or a non-literal fallback. Use these size-oriented overrides only when no affected conversion can reach any call. None of these options adds floating-pointscanfinput.-s/-stack/--stack— reserve stack space (default 512; accepted range 0..32767). The heap used bymalloclives between the end of BSS and the bottom of the stack, so growing the stack shrinks the heap and vice versa. By default there are no runtime checks that stop the stack from smashing the heap.-fstack-check— opt in to a lightweight stack-overflow guard. The DCC C Compiler emits a shortcall __stchkin each function prologue (after the frame is set up) that compares the live stack pointer against the heap ceiling. If the stack has grown into the heap, the program prints?stack overflowand exits with return code0FFhinstead of silently corrupting memory. The guard costs a few bytes and one call per function, so it is off by default; turn it on while developing or for deeply recursive code. Thestacksizeutility (below) uses this guard to measure the minimum-stackreserve an app needs. This option sets the initial state for the translation unit; source can then use#pragma stack_check(on)/#pragma stack_check(off)to control guard emission in source order.-Dname[=value]— predefine a macro._DCC_=1is always defined.
Measuring the stack an app needs¶
The repo ships a stacksize utility that builds your app with -fstack-check
forced on and sweeps the -stack reserve upward until it runs without tripping
the guard, then prints the minimum and a recommended value with headroom. Run it
against an app/test name (and pass any program arguments after --):
rem simple app
scripts\stacksize.bat triangle
rem app that needs a data-file argument
scripts\stacksize.bat cobint -- e.cob
# simple app
scripts/stacksize.sh triangle
# app that needs a data-file argument
scripts/stacksize.sh cobint -- e.cob
# simple app
scripts/stacksize.sh triangle
# app that needs a data-file argument
scripts/stacksize.sh cobint -- e.cob
# simple app
scripts/stacksize.sh triangle
# app that needs a data-file argument
scripts/stacksize.sh cobint -- e.cob
rem simple app
scripts\stacksize.bat triangle
rem app that needs a data-file argument
scripts\stacksize.bat cobint -- e.cob
Both honour the same START / STEP / MAX / MODE / EMU environment
variables; see scripts/README.md
for the full reference.
Including headers¶
Include the standard headers as usual:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
Multi-module symbol names¶
M80 and L80 only keep the first 6 characters of a public (external) symbol.
DCC C Compiler emits each external C identifier as _ followed by the name, so the leading
underscore consumes one of those six characters. The practical rule for any
program built from more than one .c file is:
Every non-
staticfunction and non-staticfile-scope variable must be unique within its first 5 characters across all linked modules.
Names that only differ after the fifth character collapse to the same public
symbol. For example i_idxins, i_idxbld, and i_idxlookup all become
_I_IDX and are indistinguishable to the linker.
Anything used in only one translation unit should be declared static. A
static symbol has internal linkage, so DCC C Compiler gives it a private, generated
assembler name and the 6-character rule does not apply to it.
How a collision shows up¶
-
Within one file, DCC C Compiler catches it at compile time and stops with an error naming both symbols, for example:
global names 'i_idxins' and 'i_idxbld' are not distinguishable in M80's 6 significant character public symbols (both become '_I_IDX'); rename one -
Across different files, DCC C Compiler cannot see the clash. L80 may report
%Mult. Def. Global, or — worse — silently bind a call to the wrong definition, so the program links but misbehaves at runtime.
Fixing collisions¶
- Rename the offending identifiers so they differ within the first 5
characters (put the distinguishing letters early:
ixins,ixbld,ixlookrather than a sharedi_idx…prefix). - Or make single-file helpers
static.
Struct, union, and enum tags, typedef names, struct members, macros, enum
constants, and local variables never become public symbols, so they are exempt.
Detecting collisions¶
After a build, scan the emitted .MAC modules for external names that share a
6-character prefix:
grep -rhiE '^[[:space:]]*public ' build/*.MAC \
| awk '{print $2}' | sort -u \
| awk '{k=toupper(substr($0,1,6));
if (seen[k]) print "COLLISION " k ": " first[k] " <> " $0;
else { seen[k]=1; first[k]=$0 }}'
Any line printed is a pair you must rename or make static.
Memory layout¶
CP/M loads .COM files in one way. BSS begins immediately after the loaded
image, and the loader sets SP to the highest free byte. The heap grows on
demand between the end of BSS and the bottom of the stack. Because there is no
guard between them by default, size the stack deliberately with -stack for
programs with deep recursion or large frames — or build with -fstack-check
(above) to turn an overflow into a clean ?stack overflow exit.