13

Here is what I understand so far:

A program that was too large to put into main memory was broken into several overlay regions. Once you needed data from outside of the currently loaded region, something happened to swap out the overlays. But what is that something?

If I understand how virtual memory works nowadays, whenever you try to access data outside of the currently page you initiate a page fault, and an ISR handles it. This makes sense to me - a hardware interrupt triggers a function call, and then memory gets modified. But it doesn't seem like that's what happened in CP/M, is it?

10
  • 7
    in most overlay systems you have to tell the system to switch overlays using a SwitchOverlays function call Commented Jun 26, 2023 at 17:34
  • 3
    Yeah, I'd assume you'd just do it by hand. You'd probably use them for larger chunks so that you don't have to switch constantly. For instance, for a word processor, you could have the editor interface in one overlay, the spell checker in another, the printing code in a third. When the user goes to spell check, the first thing you do is swap out the editor and swap in the spell checker. Commented Jun 26, 2023 at 17:41
  • 8
    IIRC, CP/M does not even supply explicit support for overlays, so you'd have to ask "how did a particular CP/M application implement overlays?", and usually they'd do it by loading a designated memory area with records from some secondary file (often ending in .OVL etc.). WordStar did this, Turbo Pascal did, too. And there are no ISRs in CP/M.
    – dirkt
    Commented Jun 26, 2023 at 17:44
  • 5
    If your linker supports overlays, it's 'easy' and no more inefficient than is inherent in the necessary disk I/O. Assume the simple case: multiple overlays at the same level - i.e.,a tree with a root, and all other nodes after children of the root. Then a call from an overlay known to be in a different overlay results in a call into the root to switch overlays and forward the call. This can all be managed outside of the code, e.g., by providing linker commands to specify the desired structure. What CP/M might have done, I cannot say.
    – dave
    Commented Jun 26, 2023 at 18:05
  • 2
    re 'efficiency' - it's up to you. Implicit in any overlay scheme, whether 'automatic' or manual loading, is the requirement that the programmer design a sane overlay structure. You group code into overlays such that you minimize the number of overlay switches. The thing you most want to avoid is frequent calls from one overlay to another overlay that uses the same memory range. 'Common' routines should live in the resident root, not an overlay. This is why virtual memory is an improvement; the programmer no longer has to deal with quite so much detail (though attention to locality helps).
    – dave
    Commented Jun 26, 2023 at 21:24

4 Answers 4

23

In CP/M, there is no support for overlays at the operating system level. There isn't even a system call to load an overlay (comparable to INT 21h/AX=4B03h under MS-DOS). If a program uses overlays, they are implemented entirely within that program.

One CP/M program that uses overlays is ZEM (Z80 Emulator and Monitor) by P.G.Kenny. This program is written in C, which makes it a bit easier to see how the system is implemented. The various functions are grouped into four .OVL files, each assembled to load at 0x3F80. The load_ovl() function loads an overlay, and the ovl_num variable records which overlay is currently loaded.

At startup, ZEMINIT.OVL is loaded:

    run_ovl = OVL_ADDR;             /* overlay call address */

    load_ovl(INIT_OVL);             /* load and run */
    (*run_ovl)(argc, argv);         /* initialization overlay */

    version = VERSION;              /* version number */
    date = DATE;                    /* last revision date */

    command = 0;                    /* load and run FEX overlay */
    load_ovl(FEX_OVL);              /* for extra initialization */
    (*run_ovl)();
    ovl_num = N_FEX;                /* current active overlay */

Depending on which command is entered, the appropriate overlay is loaded (if it isn't already present) and then control is passed to it.

        switch (command) {          /* single letter commands */

        case 'S' :                  /* execution class commands */
        case 'T' :
        case 'U' :
        case 'G' :

            if (ovl_num != N_FEX) { /* load execution overlay */
                load_ovl(FEX_OVL);  /* if not already in memory */
                ovl_num = N_FEX;
                disk_io = TRUE;     /* flag disk i/o */
            }
            (*run_ovl)();           /* call overlay */
            break;

You mention virtual memory in your question. This doesn't apply under CP/M-80, for various reasons. The usual scenario for virtual memory is that the computer's address space is larger than physical RAM. Programs then may be bigger than physical RAM, but still fit in the computer's address space. For example, a 128k program might be loaded in a system with 64k RAM; if control passed to code at address 96k, the operating system would use mechanisms such as page faults to ensure that there was memory at 96k containing the correct section of program.

In CP/M, the scenario is slightly different: an oversize program isn't just bigger than RAM, it's also bigger than the available address space. In our example 128k program, there is no way to express "jump to code at address 96k" in the processor's instruction set. The program therefore has to be divided into separate modules each of which will fit in the available address space, and how this is done is entirely in the hands of the program itself.

(The 8080 and Z80 also lack the memory management hardware that would be necessary to implement virtual memory, and it would not be straightforward to add even using external hardware).

1
  • There are plenty of VM systems where virtual space is smaller than physical space. HD64180 is 64kB virtual, up to 1MB physical. PDP 11/70 is 64kB(i) + 64kB(d) virtual, up to 4MB physical. This allows multiple processes to be physically resident at the same time. In such a system, a large application would not use overlays; instead, it would fire off child processes, each one getting its own VM space.
    – Dave Tweed
    Commented Jun 28, 2023 at 12:08
16

The most popular language for CP/M that supported overlays was probably Turbo Pascal. Turbo Pascal's runtime libraries did all the overlay handling for you, you simply declared a function or procedure as 'overlay', which instructed the compiler to put the function into an '.OVL' file instead of the main .COM file. When such a function or procedure is called, Turbo's runtime library checks to see if the respective overlay is already loaded, if not, it locates the proper .OVL file and pulls in the code into memory space reserved during the compile process, then calls the routine. You can have multiple subroutines per overlay and multiple .OVL files per .COM file, overlays may even pull in further overlays - Turbo keeps track of what is where.

Thus, overlay handling completely relied on the compiler and its runtime libraries. One of the reasons that programs that used overlays were relatively common was (besides the limited TPA that was provided by CP/M) Turbo Pascal's extremely comfortable way of handling overlays.

So, for Turbo Pascal, the answer to this question is particularly simple: A program knows when to load an overlay because a subroutine is called that you have declared to be an OVERLAY subroutine. Everything else is handled by the runtime library.

4
  • I'd enjoy knowing what the underlying mechanism was in Turbo Pascal calling a function in an overlay. I've always imagined that functions in overlays were called via thunks, and it was the thunk responsible for loading the overlay (if needed) and then jumping to the function. Commented Jun 28, 2023 at 0:49
  • 1
    as to the underlying mechanism in turbo pascal, secondboyet.com/articles/publishedarticles/theslithytove.html
    – dlatikay
    Commented Jun 28, 2023 at 4:55
  • 1
    @dlatikay Which is apparently a description of how later versions of Turbo Pascal (6.0) for MSDOS do it. TP6.0 could make use of EMS for overlays and had quite a bit more support from the OS for memory management and memory moves, something CP/M obviously didn't have. It also gave the programmer access to the overlay system and data structures - That was not in the CP/M version which was entirely transparent to the programmer apart from marking subroutines as OVERLAY. So: Interesting article, but doesn't provide details for this question, which is about CP/M
    – tofro
    Commented Jun 28, 2023 at 8:08
  • TP 6.0 was the last version of Turbo Pascal to support overlays - 7.0 introduced support for writing protected mode programs using a DOS extender and thus overlays were considered obsolete.
    – tofro
    Commented Jun 28, 2023 at 8:14
9

In CP/M, how did a program know when to load a particular overlay?

Because it was part of the calling code/execution sequence?

A program that was too large to put into main memory was broken into several overlay regions. Once you needed data from outside of the currently loaded region, something happened to swap out the overlays. But what is that something?

Er ... a call to a routine?

I guess the main issue with understanding basic overlay usage is to expect some mysterious system component handling overlay management, except there is none.

Everything about overlays is done on application level (new speak userland) by each application on it's own. This is not only true for CP/M but as well for later DOS (*1) and several other basic systems.

Basic support, if at all (*2), was on assembler/compiler level to allow multiple subroutines to be compiled to run at the same address. Everything else was program specific. There was no standard, only language specific solutions - if at all.


At that point the question essentially leaves the RC.SE domain and becomes basic CS. But for the sake of it ... also, I already wrote the following part before realizing that I'm repeating generic CS knowledge.


Before continuing, it may help to remember what program structure is required to use overlay linking (*3):

A program having a main routine(s) and two or more subroutines (groups) which are mutually independent.

With this setup the main program can be linked to one address range, while all subroutines are linked to another address range, the overlay area, each group to the same address range. When some routine in the main program now wants to call a subroutine in an overlay it will not call it directly but by detour thru the overlay management.

To look into let's look at a simple program using a C-ish like syntax - note, this example does NOT assume that there is any language support - having one will of course take away much of those details - but we want to see all the dirty secrets :))

int Var1;

main()
{
    Var1 = 1;
    SomeSub();
    OVL_MGR(Ovl1,FuncA, ... ); - Calls Function A in Overlay 1
    OVL_MGR(Ovl2,FuncD, ... ); - Calls Function D in Overlay 2
}

SomeSub()
{
   Var1++;
}

Ovl1:FuncA(...)
{
    SomeSub();                 - Calls direct a function in the root segment
    FuncB( ... );              - Calls direct Function B in the same overlay
    OVL_MGR(Ovl1,FuncB, ... ); - Calls B thru the manager
}

Ovl1:FuncB(...)
{
    OVL_MGR(Ovl2,FuncC, ... );
}

Ovl2:FuncC(...)
{
    ...
}

Ovl2:FuncD(...)
{
    SomeSub();
    if(Var1<5)
    {
      OVL_MGR(Ovl1,FuncB, ... );
    };
}

Our overlay capable compiler/linker will now create 3 code segments:

  • Root segment, containing functions
    • main,
    • SomeSub and
    • OVL_MGR
  • First overlay segment (Ovl1), containing functions
    • FuncA and
    • FuncB
  • First overlay segment (Ovl2), containing functions
    • FuncC and
    • FuncD

Address wise the root segment is compiled to Address 0100h, while all overlays are compiled to an address after the root segment. At link time the linker will calculate this from the size of the root segment. After that it will reserve space for Variables, Stack and so on.

So for simplicity lets assume our memory layout like this:

0100h..1FFFh - Code in root segment
2000h..3FFFh - Overlay region
4000h..6CFFh - (Static) Variables
6D00h..7FFFh - Stack

No lets see how running above program may look like:

  1. When loading only the root segment is loaded into memory - at 0100h as custom for CP/M.
  2. Startup code will setup Stack and Variables and so on
  3. Main starts execution
  4. Var1 at 4000h gets set to 1
  5. SomeSub within the root segment is called using a regular call
  6. The overlay manager is called with Ovl1 and FunctA as parameter
    • actual overlay number is saved on stack (NULL as none is loaded)
    • Check if Ovl1 is loaded
    • Not loaded right now ->
      • Ovl1 gets loaded from disk to 2000h
      • Ovl1's number gets set in a global variable
    • FuncA (at address 2000h) gets called
  7. FuncA calls SomeSub within root using a standard subroutine call
  8. Same is done with FuncB, as its a regular call within the same segment
  9. FuncB now calls FuncC in overlay 2 using the overlay manager
    • actual overlay number is saved on stack (Ovl1)
    • Check if Ovl2 is loaded
    • Not loaded right now ->
      • Ovl2 gets loaded from disk to 2000h, replacing Ovl1
      • Ovl2's number gets set in a global variable
    • FuncC (also at address 2000h) gets called
  10. FuncC does it's job and returns to the overlay manager
  • Saved overlay number is pulled from Stack (Ovl1)
  • Not the same as the actual one (Ovl2) ->
    • Ovl1 gets loaded from disk to 2000h, replacing Ovl2
    • Ovl1's number gets set in a global variable
  • execution returns to the caller (FuncB)
  1. FuncB returns the standard way to FuncA
  2. FuncB is called again but using the overlay manager (maybe it was copied from some other place and not adapted *5)
  • actual overlay number is saved on stack (Ovl1)
  • New (Ovl1) is the same as the old (Ovl1), so no loading needed
  • FuncB (maybe at address 2200h) gets called
  1. FuncB does it's job (see 9..10) and returns afterwards to the overlay manager
  • Saved overlay number is pulled from Stack (Ovl1)
  • Actual (Ovl1) is the same as the old (Ovl1), so no (re) loading needed
  • execution returns to the caller (FuncA)
  1. FuncA now calls FuncD in overlay 2
  • actual overlay number is saved on stack (Ovl1) *6
  • Check if Ovl2 is loaded
  • Not loaded right now ->
    • Ovl2 gets loaded from disk to 2000h, replacing Ovl1
    • Ovl2's number gets set in a global variable
  • FuncD (also at address 2000h) gets called
  1. ... and so on ...

Well, here it gets funny, take your time and see how nice they trash each other. Perfect example how overlays should not be organized :))


*1 - Although DOS did offer a overlay loading function (Int 21h Function 4Bh Subfunction 3h), it did not offer any support past loading a single module. In fact, the overlay manager included in Microsoft languages did not use it at all (*4).

*2 - If not, one had to 'simply' write different programs and do all linking manually.

*3 - Well, there is also the method of simply exchanging programs at whole, like a text editor followed by a mail merger followed by a print formater - sometimes with a rather small root program managing the sequence. This is also sometimes called overlay, but 'chaining' might be a better description.

*4 - Link's overlay manager installs itself by default at INT 3Fh at program start and uninstalls at exit. All Overlay calls are thus coded as INT 3Fh instructions followed by a 16 bit overlay number and a 16 bit function entry address instead of the far call within a regular linked program. By using that Int as custom call instruction this will work with any calling scheme, no matter what language. But that's a different story.

*5 - When overlay support is provided by a language this usually never happens - but this is to show even such case.

*6 - Note, this is no longer NULL, as Ovl1 is still loaded. Saves possible trashing when functions in the same overlay are called further down.

2
  • That's the important point: part of the calling sequence or convention. CP/M systems didn't have any hardware to trap calls, so any call that had to transfer control to an entry point in an overlay had to detect whether it was in memory. I speak from the position of having to debug and fix somebody's broken linker to get my code working: there was an out-by-one error in the table that gave the entry address for each overlaid routine. Commented Jun 27, 2023 at 7:58
  • You could make this answer retrocomputing - though not on topic with CP/M - by going into the OS/360 link editor, which, according to Brooks, "was one of the finest overlay facilities ever built." "It allows overlay structuring to be done externally, at linkage time, without being designed into the source code" (my emphasis) (And he also said: "Yet it is also the last and finest of the dinosaurs, for it belongs to a system in which multiprogramming is the normal mode and dynamic core allocation the basic assumption.") (in The Mythical Man-Month, by Brooks, OS/360 project manager)
    – davidbak
    Commented Jun 27, 2023 at 17:26
4

I used to have disassembled dBASE II files that pretty much showed it was written in 8080 assembly language.

Its overlay structure was pretty simple. All of the overlay files were loaded starting at the same address, and at the beginning of each overlay was a jump vector (just like the CP/M BIOS). The number of jumps in the vectors varied.

No swapping. No linker. No overlay management. Just load and go.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .