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.

paxdiablo
  • 4,742
  • 20
  • 47
  • 4
  • 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. – user3840170 Feb 26 '22 at 06: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 Feb 26 '22 at 08: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 Feb 26 '22 at 10:29
  • @Justme, what about the "tiny" model? Compilers that offered a "tiny" option would output a .COM file when that option was selected, or a .EXE file otherwise. I never really got in to MS-DOS programming, but weren't those two distinct file types that were loaded differently by the OS? – Solomon Slow Feb 26 '22 at 14:31
  • 2
    @Solomon did they? AFAIR, apart from some early compilers which only produced tiny-model .COM files, compilers (or rather, linkers) produced MZ executables even in tiny model, and one had to use EXE2BIN to produce a non-MZ .COM file. – Stephen Kitt Feb 26 '22 at 14:55
  • And yes, .COM files are loaded directly as memory dumps, whereas MZ executables (which can have either a .COM or .EXE extension, it doesn’t matter) are relocated etc. There are a number of Qs&As on the topic here. – Stephen Kitt Feb 26 '22 at 14:57
  • .COM executables can use memory models other than tiny too. – Stephen Kitt Feb 26 '22 at 14:59
  • Oh! OK. I vaguely remember EXE2BIN. Like I said, I never did much MS-DOS programming. I was variously employed writing code for mainframes and for esoteric graphics workstations back in the MS-DOS days. – Solomon Slow Feb 26 '22 at 15:02
  • @StephenKitt, Any executable has total control of the machine after the OS jumps to its entry point. But I was thinking (Incorrectly, apparently) that choosing "tiny" would inevitably give you a .COM file which, if it were true, would mean that, at least at load time, the OS would have to treat it differently from programs written in other models. But anyway, Stephen Kitt set me straight on that point. Apparently, simply telling the compiler to make a "tiny" program would not, in and of itself, yield a .COM file. – Solomon Slow Feb 26 '22 at 15:08
  • In early versions of DOS, COM executables were always loaded the same way, and were limited to 65,408 bytes of code. Any pre-allocated data would need to share that region, though COM executables could request additional storage from the OS after loading. When newer versions of some DOS utilities which had been COM files grew beyond the limitation of that format, Microsoft made it so that any COM file which starts with MZ will be treated as though it were an EXE file, regardless of the name. – supercat Feb 26 '22 at 18:54
  • @supercat By default, executables always get allocated the rest of the address space, even if their memory model doesn't allow them to actually use it. They can of course use the DOS memory API to reduce their allocation so that they can e.g. spawn processes. – Neil Feb 27 '22 at 00:21
  • On Stack Exchange, the intent behind closing as duplicate is to avoid fragmentation of knowledge and to allow people searching for answers to be able to find them in one place, rather than having to look in multiple places for similar information. Overall that makes answering "what is a duplicate question" a little nebulous, but it's not just strict question duplication. Your meta-arguments in this question (which usually should be in comments or on Meta, not in the question), seem like you're advocating duplication of knowledge, which doesn't really seem like the right way to go. – Makyen Feb 27 '22 at 06:46
  • From both the fact that you self-answered this and your statements surrounding the duplicate Q&A, you appear to be attempting to establish a canonical question and answer. Creating a canonical Q&A is laudable and the information you've provided here appears useful. Generally, canonical creation is done with consultation with other people interested in the affected tags. That's commonly done on the per-site Meta, and/or in a chat room devoted to the site or tag (assuming there's a room with a reasonable level of traffic). – Makyen Feb 27 '22 at 06:47
  • Part of such discussion is often the issues of what potential duplicates might exist and what existing questions might be closed as a duplicate of this question. Given that there's at least a somewhat similar question with at least some of the information here, It seems like it might still be worthwhile to have that conversation with any interested parties. [Note: I'm only peripherally involved with this SE site, so it's possible that this SE site is wanting to do things a bit different than "normal" and my statements here may be way off base.] – Makyen Feb 27 '22 at 06:47
  • @Makyen, I wasn't really proposing some canonical Q&A, the question had not been asked, so I asked it :-) I'm happy for that process to happen, just keep in mind I don't believe the question itself was similar, just that one of the answers to the question held information related to this question. – paxdiablo Feb 27 '22 at 06:56
  • And re your suggestion about putting the meta arguments in the comments or Meta, I had done that already (see comment #3). Yet someone else proposed to close as a dupe of the same question twice. When that happened, RC suggested I edit the question to state why I considered this one different enough. – paxdiablo Feb 27 '22 at 07:00
  • If you'd like to start a meta discussion, I'm happy to participate. – paxdiablo Feb 27 '22 at 07:02
  • There already is a meta discussion on our definition of “duplicate”. The other SE sites which I frequent take the stance that a Q&A is a duplicate of another if the latter’s answers answer the former’s question, not if the questions are the same (or similar). – Stephen Kitt Feb 27 '22 at 12:38
  • 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 Feb 27 '22 at 18:21
  • 1
    FWIW, I've made an answer to the meta-question with my thoughts. It appears consensus (on RC, and even before my response) is in any case that the question has to be the main point of similarity, not just the fact the information is available elsewhere. – paxdiablo Feb 27 '22 at 19:06

3 Answers3

47

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.

paxdiablo
  • 4,742
  • 20
  • 47
  • 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 Feb 26 '22 at 08: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 Feb 26 '22 at 08:38
  • 3
    The exact limit may differ a bit based on DOS version https://retrocomputing.stackexchange.com/questions/14520/how-did-large-com-files-work/14523#14523 But yes, 64 KiB minus 256 B minus a little for the initial stack is what you can depend on. – ecm Feb 26 '22 at 09:00
  • The case of a small memory model application calling a large memory model library was fine, assuming the header files were set up to properly declare the functions and data pointers. It looks as if your example is trying to point out that a large memory model application couldn't use a small memory model library, but I don't think it's getting the point across optimally. – Neil Feb 27 '22 at 00:44
  • @Neil: A large-model application could use a small-model library if any data that library would need to access was placed in the primary 64K data segment. – supercat Feb 27 '22 at 01:26
  • @supercat: Wouldn't a small model library simply do a near return when done? If that was called with a far call, that would restore the ip but not the cs, surely? Compact could use small provided ds was set correctly before it made the (near) call. – paxdiablo Feb 27 '22 at 03:26
  • 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 Feb 27 '22 at 03:31
  • With the programming languages popular in that time (C, Fortran, Pascal), was it even possible to allocate an array larger than 64K bytes? I'm guessing malloc only took a 16-bit integer as a size param. – selbie Feb 28 '22 at 05:55
  • @paxdiablo: You can "thunk" to a function that returns near by calling it near from a thunk that returns far afterwards, but this will interfere with parameters on the stack if the near function expects them to be right above the near return address. Of course you can make a more complicated thunk that takes care of moving around parts of the stack to support this use case. – ecm Feb 28 '22 at 12:07
  • @paxdiablo: I guess my normal practice was to have all of my libraries' external entry points use allcaps names and Pascal calling convention, so as to be usable from both Turbo Pascal and Turbo C, so they'd expect to be invoked by either a far call, or a combination of PUSH CS and a near call, so I never worried about the code memory model with cross-module calls, though I guess that would be an issue when using C calling convention on external calls. In any case, it was certainly possible for libraries to include their own thunks for use by 32-bit callers. – supercat Feb 28 '22 at 16:47
  • 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 Feb 28 '22 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.

Davislor
  • 8,686
  • 1
  • 28
  • 34
  • 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. – Euro Micelli Feb 26 '22 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.). – Stephen Kitt Feb 26 '22 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 Feb 26 '22 at 18:57
  • @supercat As I remember it, what really got you was that, if you wanted to be able to pass the large object (or any of its components) by reference, any function that could accept it had to take a far pointer, too. And then you would have that overhead on everything else. If you didn’t need to do that, you could keep using the Small or Medium model. – Davislor Feb 26 '22 at 19:03
  • As for the advantage of using segmented addressing, it can allow a program to fit a couple dozen 24K objects into 600K, and index through the items easily using 16-bit address arithmetic. More conventional approaches would require using 32-bit address arithmetic or avoid having any individual object straddle 64K boundaries, making it impossible to place more than two 24K objects in each 64K of storage. – supercat Feb 26 '22 at 19:05
  • 1
    @Davislor: The trick in many cases was to have variations of time-critical functions which could use different combinations of near and far pointers. A function that accepted far pointers would be usable in any context, but in many cases a function that e.g. took one near and one far pointer might perform much better than one that took two far pointers, and one that took two near pointers would likely perform better yet. – supercat Feb 26 '22 at 19:07
  • 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 Feb 26 '22 at 19:08
  • @StephenKitt The “undocumented tricks” I was thinking of things like loading a 32-bit segment, then switching to 16-bit mode in a way that left the segment valid. DPMI might deserve a mention. – Davislor Feb 26 '22 at 19:14
  • Ah right; but I don’t think any major DOS extender actually used unreal mode. I’m only aware of some demoscene productions, and Ultima VII. It’s rather difficult to get right... DPMI was introduced quite a while before DOS become obsolete (especially for games), in 1989. VCPI was older still, 1987. – Stephen Kitt Feb 26 '22 at 19:49
  • @StephenKitt I’ve revised that paragraph, but one that did was the Voodoo Memory Manager of some of the later Ultima games. – Davislor Feb 26 '22 at 19:54
  • Most of these models were also defined by Intel for their PL/M 86 compilers way back in 1980. http://www.mpsinc.com/manuals/PLM86.pdf – cup Feb 26 '22 at 20:07
  • @cup Interesting! I did not know that. Is that where they originated? – Davislor Feb 26 '22 at 21:18
  • 1
    To be specific, huge pointers used a segment increment of 0x1000, unless you were using a DOS extender, in which case it was typically 8. (16-bit Windows went as far as to export the value from the kernel so you could use it as a link-time constant, allowing the same program to work in both real and protected mode.) – Neil Feb 27 '22 at 00:35
  • @Davislor Voodoo MM was only used in Ultima VII (in its various incarnations). – Stephen Kitt Feb 27 '22 at 12:51
  • Thank you for this blast from the past. Back in the day I wrote a lot of code in PL/M to run on RMX86. Fun times. – Ron Feb 28 '22 at 16:03
  • 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 Feb 28 '22 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.

rcgldr
  • 631
  • 3
  • 6