23

I've heard the phrase "memory model" used in relation to MS-DOS programming (and early Windows), with terms such as "small" and "compact".

But what were the actual definitions of these memory models?


Please note: I have been made aware of the fact these memory models are discussed in an answer to a different question but it is a very different question, involving only one aspect of the tiny model (which it calls "real mode flat model").

These differences were why I did not find this information on an initial search of RC. The question (which has now been proposed twice as a reason to close) can be found in the first comment to this question, if you wish to check it out and make your own decision.

Since the intent of the close-as-duplicate rule is to prevent duplicate questions, I believe this is still very much a valid question. I don't believe anyone here would think that searchers should have to go searching through all the answers to tangentially-related questions to find the information they need :-) That would make RC a far less useful site in my opinion.

21
  • 4
    Does this answer your question? How does the ‘real mode flat model’ work?
    – Raffzahn
    Commented Feb 26, 2022 at 4:43
  • 4
    The information is there, but it’s hardly discoverable. I’d say it makes sense to have a question that is explicitly about memory models in general. Commented Feb 26, 2022 at 6:19
  • 2
    That's actually why I asked the question. The close reason for a dupe is "the question has been asked before and has an answer". The dupe that was proposed, although it had an answer that contained this info at the bottom, was not really asking anything about anything other than tiny model (what it called real mode flat model).
    – paxdiablo
    Commented Feb 26, 2022 at 8:35
  • 6
    Can you be more specific? It's not the DOS itself that has any memory models, it's for example the C compiler that provides you with different memory models depending on how large program you want to make. In the end DOS just loads your executable to run, and it runs on the x86 CPU, your program can do anything it wants with the memory.
    – Justme
    Commented Feb 26, 2022 at 10:29
  • 3
    @Stephen, surely there must be some similarity required in the question. If an answer to Does Java have undefined behaviour? states that incrementing a signed int in C is one example of UB, it shouldn't preclude someone asking Why does my C variable get weird values when it gets too big?. I don't think anyone is going to think the first question will relate to the second, so they won't even look at the answers. But, in any case, thanks for finding the meta question, I'll move further comment over to there.
    – paxdiablo
    Commented Feb 27, 2022 at 18:21

3 Answers 3

46

The memory models all had to do with how much code and/or data your program was using. First some background.

The 8086(1) was based on earlier Intel chips where their address space was strictly 64K and you had access to all of that for both code and data, by using a 16-bit address.

However, with the 8086 allowing for more memory, they used an rather ingenious solution where special segment registers would choose the base of the memory you were allowed to use and you could then address the 64K at and beyond that point. This base could be a different value for code and data (and stack, for that matter).

The translation to turn segment register S and address A into a physical address P was P = S * 16 + A or, put another way:

  SSSS0
+  AAAA
  -----
  PPPPP

So a segment could start of any physical address that was a multiple of sixteen and this allowed a great deal of flexibility where you could place your code depending on how much space it needed. Multiple programs could exist in memory at the same time but, since there was no memory protection, you had to be careful.

The segment registers CS and DS decided where code and data addresses were in the physical address space (SS was for the stack and ES was an extra segment register).

These earlier Intel chips (the ones without segment registers, and limited to 64K) could be thought of as actually having all those segment registers but always set to zero. This would mean all code, data and stack all reside in the first 64K.

So, old programs that expected to contain all data, code, and stack in a single 64K chunk would hopefully be easily translatable for the newer chips and by just setting CS, DS, and SS to the same value when running them, would work fine. They would access only their 64K space since they had no knowledge of being able to change segment registers.

However, new programs could take advantage of this knowledge to allow for more than 64K of code and/or data, simply by changing segment registers at will.

You could access more than 64K of data by fiddling with DS or using special instructions that used ES instead. You could jump to code outside of your current CS segment by using a far call rather than a regular (near) call instruction.

As an aside, this scheme lasted well into the protected mode era, even after the simple calculation of a physical address was replaced with selectors that used tables to figure out physical addresses and also allowed limits on how much data you could access starting from that address (e.g., possibly less than 64K).

Having said all that, we turn now to the memory models. I'm not sure that all of these were "official" memory models (from Intel or MS-DOS) but they were in use by various products.

  • Tiny: This was effectively the same as the pre-8086 scheme, allowing 64K for code, data, and stack. All of CS, DS, and SS were set to the same value. This was the memory model that COM files used (when started - they could of course change segment registers after that should they so desire). EXE files could use tiny models as well but also had the following models allowed to them.

  • Small: Similar to tiny in that CS and DS would never change but they would be different values. This allowed for 64K code and a separate 64K data. In this case (and others below), you could have had SS either the same or different to DS, depending on your needs.

  • Compact: A single unchanging CS was still used but DS was allowed to change. Hence code was limited to 64K but data could be substantially more.

  • Medium: The "opposite" of compact, this used far calls so that CS could change but DS stayed at one value. Allowed for more than 64K of code but limited data to 64K.

  • Large: Used far calls and multiple DS values, allowing for more than 64K of both code and data.

  • Huge: Large, but with a small twist. Even though the large and compact models gave you more than 64K of data, each individual data item tended to be limited to 64K (within a single data segment). What the huge model added was the ability for a single data item to exceed 64K by using some form of "trickery" implemented in software.

Now you could combine memory models if, for example, you wanted to be mostly small model but wanted one function to far-callable. In that case, you would have to somehow notify your compiler that this function was a far-call one, such as using FAR in your code to mark it so (it would then adjust calls to it to be far calls).

You would also have to be very careful as the runtime libraries that were added to your code were usually selected for a specific memory model. So a small model program is not going to be very amenable to having the library functions called where the CS is different to its own.

The problem there is not so much the call since your far-callable code could far-call to the library. But the library itself will do a near return, not a far return. That wouldn't tend to end well :-)


(1) And the 8088, which was functionally the same as an 8086 but with an 8-bit data bus.

11
  • 2
    "This was the memory model that COM files used." Actually flat-format executables just started out with cs = ds = ss, but they could set up other segmentation schemes on their own. MZ format executables just had other possible values for initial cs and ss plus segment relocations built into their header to ease use of different segments. All of that can be done by flat executables manually. (The only feature not generally allowed by flat-format .COM executables is files larger than about 64 KiB.)
    – ecm
    Commented Feb 26, 2022 at 8:34
  • 3
    That's a good point, @ecm: I've clarified that to state that was how they were started, so there's no confusion they were locked into that condition. Thanks. I assume, BTW, you said "about 64K" because they were loaded at 0x0100, yes? Hence the file could only be 64K-256?
    – paxdiablo
    Commented Feb 26, 2022 at 8:38
  • 3
    The exact limit may differ a bit based on DOS version retrocomputing.stackexchange.com/questions/14520/… But yes, 64 KiB minus 256 B minus a little for the initial stack is what you can depend on.
    – ecm
    Commented Feb 26, 2022 at 9:00
  • 1
    For large to small (or large to compact), you would have to put a near call XXXX, far ret somewhere in the target segment and then far call to that. The far call would get you into the correct segment so that the near call (and the near ret that came back) would work, then the far ret would return you to the different code segment. Or am I missing something?
    – paxdiablo
    Commented Feb 27, 2022 at 3:31
  • 2
    @paxdiablo: In terms of performance, the difference between near and far data totally dwarfed the difference between near and far code. One thing I wish compilers had supported would have been a means of putting data into a code segment that could be accessed via CS prefix without having to use up space in a data segment, since some tasks using translation tables could have really benefited from such a design [e.g. a function to read data from segment #1, translate it using a table in segment #2, and write it out to the display (segment #3).
    – supercat
    Commented Feb 28, 2022 at 16:54
21

The memory models were defined by compilers for high-level languages, and were reasonably standard between Microsoft, Borland and Watcom. The Small, Medium, Compact and Large models appear to have originated with an Intel compiler from 1980.

First, a brief explanation of how the 8086 architecture worked. It was a 16-bit CPU that could only address memory in chunks called segments. Each segment was 65,536 bytes in size, because that was the number of bytes 16 bits could address. A program could use four segment registers at a time, SS (stack), CS (code), DS (data), and ES (extra). A 16-bit pointer within one of these segments was a near pointer. Originally, these segments could start at any 16-byte “paragraph” of the one-megabyte “conventional memory,” so a far pointer needed 32 bits to hold a 20-bit addres. Later machines added the ability to switch between segments of “expanded” or “extended” memory, to protect memory as not writable or not executable, as well as adding two more segment registers, FS (doesn’t stand for) and GS (anything).

I wrote a long answer a while back about the reasons Intel made this choice. It made sense at the time, but only because the engineers believed that Intel would someday be able to break backward compatibility with it and move on.

Memory models defined whether a program would assume all its code was in a single segment, all its data, both or neither. This determined whether the program could assume any arbitrary function it called was in the same code segment, or any data it accessed was already in its data segment, and therefore whether it needed extra memory to store the segment and extra code to update the segment register. Assembly language didn’t really need a formal memory model, as the programmer could always decide whether to write a near or a far instruction. High-level languages, though, needed to make a trade-off between using the smaller near pointers, which were more efficient, and the wider far pointers, which could support more than 64K of code or data. (Similarly, programmers today sometimes write 32-bit code on a 64-bit machine because 32-bit pointers use less memory.) The terminology that became standard was:

  • The Small memory model had one segment for all the code, and another for all the data. All pointers were, by default, near pointers.
  • The Compact model had no more than 64K of code, so all jumps and calls could be near, but could deal with more than 64K of data. In particular, it could give the stack its own segment, and be at less risk of a stack overflow. Jumps, calls and function pointers in languages that had them were all near, but pointers to data were far by default. This was probably the most commonly-used model on MS-DOS.
  • The Medium model had no more than 64K of data, and more than 64K but less than 640K of code. (MS-DOS was not able to load code above address 0xA0000, at least not the normal way, because that was where IBM had decided to put the video memory on the original PC.)
  • The Large model used far pointers by default, and could support more than 64K of both code and data.

Importantly, although the Compact or Large models supported more than 64K of data, no individual array, structure or object was allowed to be larger than 64K. Each such object needed to fit within a single segment. (This is also why C and C++ still do not allow you to compare or subtract two pointers from separate objects. This would break on an architecture that uses segments.)

A program using a larger model might still be able to use near pointers locally, or place a family of functions into the same segment group where they could call each other with near calls. One with a smaller memory model might have a few far functions outside the main code segment, or only a few pieces of far data, and fit the rest under the 64K limit.

There were a few other memory models as well.

  • Borland Turbo C, and a few other compilers, supported a Tiny memory model, where all code and data fit into a single 64K segment.

This existed for historical reasons. Intel had based its 8086 on an earlier CPU, the Intel 8080. The 8080 only supported 64K of memory and 16-bit addresses, without segments. There were a lot of programs written for it, and in particular, the circumstances of MS-DOS’ creation (a fascinating story which anyone reading this far down the page on a retrocomputing site already has heard some version of) meant that MS-DOS 1.0 supported a .COM format for executables based on CP/M for the 8080. The primary use of the Tiny model was that a program that used it could be compiled to a smaller .COM executable, rather than the .EXE format.

Later on, compilers added a sixth model.

  • The Huge model used far pointers for code and data, but treated data as a flat address space. The difference between Large and Huge was that, if pointer arithmetic on a far data pointer overflowed the bottom 16 bits, they would wrap around. If pointer arithmetic on a huge data pointer overflowed the bottom 16 bits, it would increment the segment.

This had the minor benefit that two data pointers were aliases of each other if and only if they were encoded with the same bits, and the much more important advantage that arrays and structures were now allowed to be more than 64K in size.

Finally, some MS-DOS programs (most but not all of them, games) in the ’90s began using DOS extenders. Many of these used undocumented tricks to let a DOS program use a flat, 32-bit memory space. Toward the end of DOS’ lifespan, these became standardized as DPMI and other interfaces.

16
  • 6
    Excellent pointing out that “memory models” were largely a high-level language concept, not something defined by the CPU. One more point: “why bother? Why not just be able to access all the memory and be done with it (‘large’ and ‘huge’ models)?” Because operations and calls on FAR pointers are more expensive (slower, longer) than operations on NEAR pointers. And the difference could be significant. In the Huge model, even array indexing became expensive. As a programmer you wanted to compile your program under the cheapest/fastest memory model that got the job done. Commented Feb 26, 2022 at 17:57
  • 4
    The first users of DOS extenders weren’t games, but programs such as 1-2-3, AutoCAD, 3D Studio... And when 32-bit extenders became popular, the interfaces were documented (DPMI etc.). Commented Feb 26, 2022 at 18:54
  • 2
    The speed penalty for using far pointers was significant, but if a program used mostly near pointers, using far pointers for a few large objects wouldn't be too severe a cost. Using huge pointers, however, would impose a really massive performance hit. Figure large pointers would typically impose a 2:1 performance penalty on code using them, while huge pointers would likely impose an order of magnitude cost beyond that.
    – supercat
    Commented Feb 26, 2022 at 18:57
  • 2
    @Davislor: A useful paradigm was to think of "far" memory as though it were a storage device; rather than trying to work with things in far memory directly, code should read them into "near" storage, manipulate them, and write them back.
    – supercat
    Commented Feb 26, 2022 at 19:08
  • 2
    @EuroMicelli: Besides the speed penalty, there was also the issue that far pointers themselves took up twice as much memory as near pointers (4 bytes vs. 2 bytes). This was a big deal in the days when microcomputer RAM was measured in KB instead of GB.
    – dan04
    Commented Feb 28, 2022 at 20:34
3

For early versions of Windows, there were 3 more memory models. 286 protected mode, up to 16 MB of memory, where segment registers became selectors. These were called huge pointers. The selectors were incremented by 8 (instead of 4096), to advance to the next 64K bytes of data. GlobalAlloc() was use to allocate "huge" data blocks and return a "huge" pointer.

For 386, there was Winmem32, a 32 bit flat memory model, but Watcom compilers were the only ones to support this as a memory model, while Microsoft included an example assembly snippet to use this feature. This made Watcom compilers popular for a while, but it didn't last long, because Window 95 and Windows NT were released not much later.

For 386, there was also Win32S, which used a portion of the Windows NT API.

You must log in to answer this question.

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