[New to Gemini? Have a look at my Gemini FAQ.]
This article was bi-posted to Gemini and the Web; Gemini version is here: gemini://gemini.circumlunar.space/users/kraileth/neunix/2025/gentle_introduction_cpm.gmi
This article is just what the headline promises: an introduction to the CP/M operating system. No previous knowledge of 1970s and early ’80s operating systems is required. However, some familiarity with Linux or a BSD-style operating system is assumed, as the setup process suggested here involves using a package manager and command-line tools. But why explore CP/M in the 2020s? There are (at least) two good reasons: 1) historical education 2) gaining a better understanding of how computers actually work.
Last year I wrote two articles about CP/M after having taken a first look at it:
A journey into the 8-Bit microcomputing past: Exploring the CP/M operating system – part 1
A journey into the 8-Bit microcomputing past: Exploring the CP/M operating system – part 2
These were written with a focus on the first reason; I had (partially) read the manuals and tried out a few commands in an emulator (as well as done a little bit of research). I wrote an outsider’s look at CP/M and covered the various versions that were released and some of their notable features.
This article is different. It’s for readers who want to get started with CP/M themselves. Expect a practical introduction to get familiar enough with the platform to be able to explore a wealth of historic software, often enough ground-breaking and influential.
Getting Ready (Installing an Emulator)
Last time I had tried out a couple of Z80 emulators and found Udo Munk’s z80pack to be the one I liked best. It’s not widely packaged; only FreeBSD includes it, but using the package requires some setup. Other options like YAZE exist, but it’s more work to get the original CP/M working on them whereas z80pack comes with disk images of various CP/M versions.
Of course you can compile the emulator and tools yourself. But to simplify the setup process, I’ve created a port for Ravenports, a universal package system for POSIX operating systems. Via Ravenports, the emulator and tools I use here will soon be available as binary packages for the following operating systems (in the future additional platforms might get added):
DragonFly BSD
FreeBSD
Linux (glibc-based distributions)
MidnightBSD
NetBSD
If you’re on one of those systems, you can download, inspect and run this script if you want to go that route. It will bootstrap a secondary package manager on your system, which you can use to install additional software on your machine. Those programs live in a separate portion of the filesystem (i. e. below /raven), which means that they won’t interfere with the native package manager of your platform.
The package manager is called ‘rvn’. It supports subpackages and so-called variants which is why the package names look a little complicated at first glance. The z80pack port has no variants, hence only the standard (“std”) is available, but the port is split into three subpackages: “docs”, “images” and “primary”. The first contains documentation, the second one provides the CP/M disk images and the last one is the actual emulator. A special subpackage called “set” can always be used to install all available subpackages of a project.
Tilde characters separate the fields of the package name. At the time of this writing, the complete package is ‘z80pack~set~std~1.38’ (base name, subpackage, variant, version). Package names can be shortened as long as they are unambiguous. So to install the complete package, you can run this:
This will pull in ‘z80pack~docs~std~1.38’, ‘z80pack~images~std~1.38’ and ‘z80pack~primary~std~1.38’ as well as the required dependencies.
You will have to add /raven/bin to your PATH environment variable to be able to use it. Depending on your shell of choice use setenv or export. Most people will want to do this:
When you installed the package, the installation message hints at a utility script that I wrote for the port. Just execute ‘runcpm’ and it will display a little help text to let you know what it does and what other command names it’s available under.
The help message from the utility script
Running any of these requires the /raven/bin to be in the PATH variable, otherwise they won’t find the actual emulator binary. Of course you can also modify /raven/bin/runcpm accordingly if you prefer that.
Running CP/M
If you’ve installed the emulator and have your PATH configured, you’re ready to go. You just pick a version from the list that ‘runcpm’ told you about and start the emulator. But which one to try? You can of course try out any of them, but I highly recommend to start with version 2.2 for a couple of reasons. Versions 1.x work but are pretty limited in terms of command-line editing and things like that. They are fascinating relics from an age before monitors were common and when output was usually printed on paper. You can explore them later. Version 3 is more complex (by CP/M standards) and might confuse you. CP/M 2.x is basically “classic” CP/M, a solid but simplistic OS that’s straight-forward to get into. That’s also the version used in this article, so if you want to follow along, just go with 2.2 for now.
Now that we’ve chosen a version, let’s start the emulator. So, as your user just issue
and that will turn your terminal into an emulated Z80 system with CP/M 2.2!
Emulated CP/M 2.2 booted up and ready
On modern systems, the boot process is too short to notice and the system is up instantly. CP/M displays ‘A>’ to let you know that it’s ready to take commands from you. The CCP (Console Command Processor) is the core component of CP/M that handles user interaction like prompting you and executing commands. Think of it as your shell.
In their simplest form, commands are just simple words or abbreviations which you type after the prompt symbol and then hit ENTER to execute them. Let’s issue our first command. Try this:
What did it do? Right, all the previous messages are gone. And that’s no wonder: CLS is short for “CLear Screen”. The CCP is case insensitive; it doesn’t care if you input “cls”, “CLS” or “cLs” – that’s all the same thing.
You can always give empty command lines, if you want a bit of screen space between some output and the next. So pressing ENTER without first typing a command is perfectly acceptable. But let’s try a different command:
Quitting the emulator
As you might have guessed from the name, this command is used to quit the emulator and returns you to your standard *nix terminal. It’s not the most interesting command but definitely one you will want to know about. It was added to stop z80-sim and is not part of the original CP/M.
Filename Basics
With these basics done, we can finally take a look at something more useful. Try this command:
Output of the DIR command
DIR is short for “DIRectory” and it lists all the files recorded in the – you guessed it – directory (i. e. all the files present on the drive). CP/M’s filesystem is flat, which means that there’s no folders or subdirectories. All the files on one drive are together in one place. Files are referenced by what CP/M calls a file specification. These consist of up to four pieces of information (on 2.x).
Okay, let’s take a look at the output. We’ll ignore the “A:” for the moment. File naming follows a schema known as 8.3. This means that a filename can be 8 characters long, then a dot follows and after that it can have a type of up to three characters. Note: Terminology evolved over time. In older CP/M versions, these were known as the primary and secondary names. Later, during DOS times, they were referred to as the filename and the extension.
It’s best to think of the whole thing as the actual name of the file, i. e. the up to 8 characters, the dot, and the type. Only the first character is strictly required, though, so “A” is in fact a perfectly valid filename. Internally, CP/M will fill up the remaining characters with blanks, so this file is represented as an A, 7 spaces, a dot and another 3 spaces. Filenames, just like CCP input, are case-insensitive, too.
Other than being mindful of the maximum length, try to stick to letters and numbers for names. Many special characters are allowed, but some are reserved and must not be used. But you cannot just memorize these once: different versions of CP/M reserved a different set of special characters! The full list for CP/M 3 is this: . , ; : = ? * [ ] | ( ) / \ ! & $ + –
Some of these are not reserved in earlier versions, but again, don’t get fancy and you’ll stay out of trouble. Other than that, you need to be aware that the part before the dot is up to you entirely whereas the type is meant to hint at what kind of file this is.
Now that we understand the filename schema, let’s look at the types of files as the list provided by DIR has them. For example there’s “DUMP.COM” (DIR doesn’t display the dot for some reason), “STAT.COM”, and so on. These are both COM files, which is short for “command” files – and it means that these are executable commands (programs).
The other types that you can see here are UTL and HLP. The former are two “util” files; these are special programs that cannot run on their own but can be loaded by CP/M’s debugger program. The other is short for “help” and was chosen because this file contains the help text for the WM program. DIR uses colons to delimit one column of files from the others.
Now that you know that the COM files are executable commands, DIR basically gave you a list of what programs are available for you to run. But wait a moment! There’s CLS.COM and BYE.COM – but where’s DIR.COM? Good catch. There’s actually two kinds of commands: those like CLS, which exists as COM file on a drive, and the others like DIR. The first kind are called transient commands, the others are built-ins like your Unix shell’s ‘echo’ command. DIR and a few others are part of the CCP and do not exist as separate programs. (Well, in CP/M 3 DIR.COM does exist, even though the command is a built-in, too! That’s because the transient offers additional functionality over the standard command. But like I said before, CP/M 3 is a little different.)
File Specification Basics
So far we have only executed programs which serve a pretty simple purpose and can thus work on their own. Time to take the next step. Let’s execute another program:
The result is this error message:
DUMP is a tool to get a hex dump of a binary file and with the error message it is telling us that it needs an input file but couldn’t find it (in this case because we didn’t specify one!). So we need to give this program a file to operate on. Let’s have the program display a hex representation of itself. We can do that like this:
This time the result looks much more interesting (see screenshot).
Output of the DUMP command on itself
While admittedly the output is not terribly useful for a user without a programming background, this is still an important achievement. We’ve not just executed another program, we’ve executed it on a specific file. In our little command line, DUMP is the program name like always. However after that (delimited by a space character), we’ve given it the so-called file specification (or filespec for short) to let the program know which file we want a hexadecimal representation of.
Now we will take a look at another command, TYPE. Don’t look for TYPE.COM, it’s another built-in. This is a simple command for displaying the contents of files (i. e. “typing it out” to the console). If we run it without a filespec, this happens:
Unlike the more verbose DUMP, this program is fairly minimal in letting you know that you screwed up. The question mark tells you “nope, doesn’t work that way!” and it’s on you to figure out what the problem is. That is, you have to know that TYPE requires a filespec to be able to type out the file’s contents, of course.
But why is it so minimal? Well, text strings are wasteful. The long error message that DUMP provides could have been used for several program instructions. And remember, that we are in an emulated environment where the machine has 64 kilobytes (!!) of memory, which is not a lot. To make matters worse, CP/M could run on machines with as little as 16k of RAM (versions 1.x at least). Since TYPE is a built-in and the whole CCP has to stay in memory all the time, putting anything in there that’s not strictly required, means stealing from the precious memory which would then no longer be available for other programs. Always keep in mind the extremely constrained environment that people had to make do with back in the day, and you’ll understand most of the design decisions that seem rather weird from today’s perspective.
But since we figured out what we did wrong, let’s try again:
This results in the following output:
Most of the file is unreadable garbage and some of it even consists of unprintable characters. This is why you normally use TYPE only for plain-text files and view binary files with DUMP. We can see two text strings here, though, one of which is the error message that we’ve already encountered. Given how short the program is, you can see pretty well how wasteful those text strings are!
Some commands take more than one filespec. For example REN (“REName”), another built-in. It allows you to change the name of an existing file to another. One CP/M quirk that you have to be aware of, is that it borrowed the notation from a line of DEC operating systems. It doesn’t copy / rename file 1 TO file 2 as you’re probably used to. Source and destination are inverted, so you copy / rename file 1 FROM file 2.
Let’s see what REN does when you give it only one filespec:
The command took the information that it had, tried to rename the file to itself – and couldn’t do that because, of course, that file’s name is already taken. Here’s how to rename the file to RENAME.TST (new name) from the file WM.HLP (old name):
The equals character is required as the separator of the two filespecs. This command line doesn’t output any error, which in this case means that it succeeded. Feel free to check it with DIR, before we revert what we just did:
More on Filespecs
Some commands like DIR can work both with and without a filespec. We’ve only done the latter so far, so it’s time to give the other option a try:
This will make DIR only display the file that we asked for instead of the whole list. What is this form of DIR good for? Just to check whether a specific file exists? No, there’s a much better use for it. But to understand that, we need to know a bit more about file specifications.
So far we have only used what CP/M calls unambiguous filespecs aka. unambiguous file references aka. unambiguous filenames (ufn), which refer to one particular file only. That means we’ve always used exact names with our commands so far. CP/M supports two kinds of wildcards, though, the question mark and the asterisk. You can use these to construct ambiguous filespecs aka. ambiguous file references aka. ambiguous filenames (afn) which can potentially match several files.
A question mark means any character. For example we could modify the previous command line slightly like this:
The output is the same, because DUMP.COM is the only file in our directory that matched the afs. But let’s assume there were also files such as DUMP.BOM, DUMP.LOM and so on – in that case the afs would match them, too, and DIR would display all of them. You can use multiple wildcards in a filespec, so for example DUM?.C?M would still match our DUMP.COM file but also other possible files like DUMB.CIM and so on.
The asterisk is even more powerful; it doesn’t match a single character at that position, but translates to anything. For example *.COM means “any filename as long as the file has a type of COM”. You can use this to have DIR list all the available commands only and filter out any other files:
This will produce a list where our two UTL files and the HLP file are missing. You can also use something like A*.* to list all the files that begin with the letter A.
Combining these wildcards, you can do some pretty advanced but useful name matching. Think for a moment about what this example matches: ??G*.C* – it matches all files which have a filename of three or more characters where the third one is a G and which has a filetype that starts with C.
By the way, when you run DIR without any filespec, that’s the same as if you run DIR *.* – for the DIR command the universal filespec is the default.
Alright! Now for the last thing that you need to know about filespecs: they can consist of a third component in addition to the filename and type. Run these two commands and compare the output:
Hey, there’s the a: finally. And yes, the output is identical. Try another one, before we talk about what this does:
It’s exactly the same again! Okay, let’s take one step back and take a look at the prompt that we see all the time: A>. With the greater than character, the CCP tells you that it’s ready to let you input a command. But what’s the A all about? It refers to what CP/M calls the logged-on disk. It let’s you know that disk drive A is the one it will assume commands refer to unless told otherwise.
And that’s what we did with the A: – we requested DIR to list the files on drive A. Since that’s implicitly assumed when we don’t state it, it didn’t make any difference. And the universal filespec (*.*) is the implicit default for DIR, so in our case all of these were identical.
So let’s try out accessing a different disk for the first time, shall we? CP/M 2.2 as it is provided by z80pack consists of two disks, so we have a drive B, too. How about taking a look at which additional programs are on there? We can do that like this:
Here’s the output:
As you can see, for convenience, there’s CLS.COM and BYE.COM on there, too, but also some additional programs that we haven’t seen, yet.
Drives
Since we’re on the matter, anyways, let’s talk about drives next. A useful command that we haven’t used, yet, is STAT (from STATus). You can use it to find out how much space remains on a certain disk. Let’s check that for both drives:
Seems like drive A is somewhat short on space while on B there’s still a lot of room for additional files. If we want to examine the programs on drive B, for example R.COM, we can do this:
However it’s a bit annoying to always have to use the full filespec including the drive, right? And that’s why the logged-on drive can be changed. We want to do some work mostly on drive B next, so let’s do that. It’s as easy as this:
This will change the prompt to B> to let you know that now drive B is the default disk. If you for example run DIR without a filespec now, you’ll get a list of files on that drive until you change back. Let’s try to get a hex dump of one of the other programs on this drive:
Huh? What’s this? Well, the CCP let’s you know that it has no idea what you’re talking about. Remember that unlike DIR the DUMP command is a transient. It’s on drive A and it was readily available so far because that drive was the logged-on default. Now we’re on drive B and there is no DUMP.COM there! So to get the hex dump that we were looking for, we can do this:
That works! But while we don’t have to include B: for the filespec anymore, now we have to include A: to run the command… So we have merely traded one little headache for another. But there’s a solution to this, of course! Let’s take a look at STAT again. It is not only able to tell you about the remaining space on a disk, it can also give you information about a file. Let’s use it to take a look at DUMP.COM on drive A:
Okay, looks like the file takes up 3 records in the filesystem which is equivalent to a size of 1k. That’s a fairly small program and we have more than enough space to simply copy it over to drive B. That’s what we will use PIP (from “Peripheral Interchange Program”) for. Remember the syntax of REN? For PIP it’s similar and unlike REN it can actually copy files rather than renaming them and supports doing so across different devices as well. Here’s how we copy over DUMP.COM from drive A:
Think about this command line for a second. Do you see which part of a filespec is unnecessary? Exactly, since we have drive B logged-on, we could also have used A:PIP DUMP.COM=A:DUMP.COM instead for the same result. Check with DIR whether the file was copied over if you wish. Now we can simply run the program from the current disk which is much more convenient:
Great! Now let’s assume we’re done with exploring programs with DUMP and are eventually running out of space. We need to clean up now and then. Removing files is what the ERA (from “ERAse”) command is good at. To get rid of our additional DUMP.COM on the current drive, we can issue the following commands:
That’s 1k of space reclaimed. It may not sound like much, but as we all know, even small files do add up. Oh, and you cannot only run out of space on a drive. You can also run out directory entries! CP/M 2.2 supports up to 64 files on any drive, which is a lot of files, but at the same time not an exceptionally high limit, either.
Control Characters
Let’s change the logged-on drive back to A now:
Next we’re going to use STAT again but on its own rather than on a drive or a file:
This is pretty useful for getting a quick overview. Keep in mind, though, that it will only display information about drives that you have accessed in your current session! If you use STAT the next time after you just started the emulator, it won’t know about drive B, yet. Now let’s do something stupid and try to list files on a non-existing drive:
BDOS (or Basic Disk Operating System) is the OS component responsible for disks and filesystems. And it rightfully complains that there’s an error on drive C. You cannot simply acknowledge the error or something; if you press ENTER, the error is simply repeated. The system is in a state from which it cannot recover.
What you have to do in this case is sending the ^C control character (press CTRL-C to produce it). This will make CP/M perform a warm start and you get the CCP prompt back. Never try to change the logged-on drive to a none-existing one, though! In that case a warm start is not enough and you will have to kill the emulator from your host system. Historically warm starts were also required if you physically changed the diskette in a drive.
There’s a couple of other control sequences that are useful to know about. For example if you typed a longer command line and change your mind (or mistyped something right at the beginning), it is useful to press CTRL-U which invalidates the current command line. You can simply press ENTER afterwards and the CCP will ignore what you typed. CP/M 2.x supports a more useful control character, though, CTRL-X, which will simply erase the current command line, allowing you to try again right away.
If you are for example using TYPE to display a longer text file, the contents will rush by on the screen. In case you’re interested not in the end of the file but in some section in the middle, you’re supposed to press CTRL-S to suspend further printing until another key is pressed. This may have worked back in the day (and you can probably still use it if you configure the emulator to run at a slower speed), but it’s not a particularly great mechanic for today.
CTRL-Z means end-of-input. It’s not used on the command line but some programs like the editor ED make use of it.
There’s a few more, but the last one that I want to cover is CTRL-E. Sending this control character causes a carriage return without executing the command line. This is useful if you have to enter a very long command line which won’t fit on a single line. Now this might surprise you since so far all of our command lines have been rather short. But they don’t necessarily have to be! For example, PIP can be used to concatenate multiple files into one. If you’ve got a lot of long filenames, the resulting command line might run over the terminal width.
Other Things to be Aware of
We’ve already covered a bit of ground here and you should have a good idea of basic CP/M usage that you can build upon. But while the known unknowns can be annoying, it’s usually the unknown unknowns that actually bite you. So let’s at least convert some of the latter to the former, shall we?
CP/M 2.x supports 16 so-called user areas, which can be used to organize files, so in a way the previously made statement about the filesystem being flat with all files in one location is not entirely correct. It’s good to know that they exist, but by default only user area 0 is being used and that’s what you may want to stick with.
What is typed after a command name is called a command trail in CP/M lingo. We’ve only used filespecs here, but there’s another thing: parameters. These don’t refer to files but modify the behavior of the command. Unfortunately, they are not standardized! For example, STAT uses dollar notation and PIP expects parameters in square brackets. Here’s a few examples:
This instructs STAT to set the read-only flag for the file instead of displaying file information. STAT also accepts a few special keywords, too, like DSK: which will make it display detailed disk information (see screenshot).
Output of the STAT command displaying disk information
The trouble with PIP is that it can only copy things FROM a different user area and not TO one. This means that when you switch the user area, you need to have PIP available there to copy something else over. But how do you do that without PIP? Well, first you need to load the program into memory. The debugger DDT can do that for us, but then instead of actually debugging it, we’re leaving the application by issuing “G0” at its dash prompt.
PIP is now loaded into RAM, and DDT was nice enough to tell us that the next free memory address is 1E00, which in turn means that PIP occupies 1C memory pages. Converted to decimal, that’s 29 pages. Knowing that, we can change to another user area, e. g. number 8 using the USER build-in:
As you can see, it’s empty. Now we can use the build-in command SAVE to write the contents of the 29 memory pages into a file:
Here’s the result: we have PIP! Which means we can use it to copy over another file to this user area, using the G parameter for PIP with user area number 0 from where we want to copy it:
Oops. Or rather, we can’t. Got any guesses why that is?
Of course! The STAT program we wanted to copy over is 5k in size, but the drive has only 3k left…
Having seen these limitations, you probably understand why I suggest not bothering with user areas as you’re getting started with CP/M. I wanted to at least touch on them, though, so you don’t get the feeling that you’re missing out!
Another thing that you should know exists are devices. CP/M reserves some three-letter abbreviations for device names. If you ever wondered, why to this day you cannot create a file called CON in Windows, that’s because it’s CP/M’s reserved device name for the console. PUN is for a paper punch, RDR is a paper tape reader, LST is for a printer and so on. You could use STAT to switch input or output to different devices and PIP supports them, too. This allowed you to read files from the RDR or print a file by copying it to LST.
With this information you should be good to generally find your way around in CP/M. You cannot really do much with it, yet, but we’ll take care of that another time. Tune in again if you like.
What’s next?
I haven’t decided whether I’ll write another CP/M article next (it would make sense, though) or if it will be something else. In autumn, I definitely want to get back to my CBSD series, though!