7

The Intel 8086 CPU could address up to 1 MB of memory using segmentation, and this CPU have 4 segment registers, which are CS and SS and DS and ES.

Each segment in memory can have a maximum size of 64 KB, which means that if all 4 segment registers are used, then 256 KB of memory would be used, which leaves 768 KB of memory unused.

So how were the remaining 768 KB of memory used? was it possible to have many segments in memory of the same type, for example could we have 2 stack segments in memory, and we would modify the SS register whenever we want to use a different stack segment?

2
  • 2
    Yes, you'd modify the segment registers as needed. This also worked in higher level languages, there'd by a kind of "far pointer" which would cause segment register loads as needed (as opposed to a "near pointer", which was more efficient, but restricted to a particular segment).
    – dirkt
    Jul 27, 2019 at 16:16
  • 1
    You can use 32-bits to hold/store a pointer even though it is a 16-bit processor. Technically, you only need to 20-bits per pointer to reach the full address space, but that is more impractical as it results in more complex code for pointer dereferencing and pointer assignment; it is also impractical storing 24-bits (a round 3 bytes) per pointer, so 32-bits is what's typically done.
    – Erik Eidt
    Jul 27, 2019 at 16:31

5 Answers 5

12

Segment registers can be changed, thus allowing access to full address space.

In addition to doing this ad hoc, there are different ways to model this, called memory models:

  • small: code and data both reside in one segment, all code and data pointers are 16bit
  • medium: single data segment, multiple code segments (code pointers 32bit)
  • compact: single code segment, multiple data segments (data pointers 32bit)
  • large: multiple code and data segments (all pointers 32bit)

16bit pointers consist of offset only, and are called near pointers. 32bit pointers consist of segment and offset, and are called far pointers.

For small and medium memory models, stack and data segments were always the same. This allowed accessing local and global variables as well as heap allocated memory within same segment and thus with near pointers.

Using far pointers is slower as segment registers need to be modified each time they are used, so their use is discouraged unless explicitly needed.

11
  • 5
    Switching stacks would be basic to multi-threading, cooperative multi-tasking, and to take an example that goes way back to the early days: debuggers. Of course in the case of a debugger the other stack belongs to the target program, but the debugger still has to interact with it, briefly as a stack and then to a greater degree as data. Jul 27, 2019 at 17:38
  • 2
    P.S., I vaguely remember some programming toolchain that supported one additional memory model. Can't recall the name (of the tool, or the language, or the memory model), but the idea was to allow a single object, typically an array, to be larger than 64K. That was not possible in the ordinary "large" model because in the large model, each compilation unit had just one code segment, and just one data segment. Jul 27, 2019 at 17:43
  • 5
    @SolomonSlow yes, this would be huge model. There’s also tiny where CS=DS=SS
    – tuomas
    Jul 27, 2019 at 17:44
  • 1
    @SolomonSlow, I'd wager a guess that the processor doesn't automatically update SS if SP overflows, so using a stack larger than 64 kB would require the code to modify SS and SP so that SP can never overflow. Luckily the segments overlap, which makes it easier, but it might still be hard to know when exactly the stack needs to be checked. On each subroutine call perhaps? Or perhaps you could inspect the function call graph to know what functions can be called when SP is near overflow. Doable, probably, but sounds a bit annoying and might add overhead.
    – ilkkachu
    Jul 27, 2019 at 20:25
  • 2
    Maybe this should be a followup, but did interrupts use the same stack as the user program (I suppose they had to) and how much space needed to be reserved for this?
    – pjc50
    Jul 29, 2019 at 9:25
8

The 8086 has 20 address pins so can access 1 MB of memory.  To be clear, the 20 bit address space is linear, and the memory subsystem doesn't know about segmentation or segment registers — it just sees the 20 address pins.

The values put on the 20 address pins are generated by segment register * 16 + offset, where offset comes from a regular register or other addressing mode.

It is the programming model that is awkward since it is basically a 16-bit processor.  To access more you need to hold/store at least 20 bits per pointer, and rounding up pointer size to 32-bits is the most practical.  For pointers larger than 16-bits, pointer dereference and pointer assignment require multiple instruction sequences (e.g. dereference involves setting segment registers).

@tuomas nicely details the various programming model options.

1
  • 4
    The segment was left-shifted by four bits, not multiplied by four (which is equivalent to left-shifting by two bits).
    – user
    Jul 29, 2019 at 20:39
6

As already discussed, the 8086's memory addressing is a little... weird.

The hardware addressing was 20-bit and bytewise, which gives you the 220 addressable bytes (1 MiB).

To the software side, this was exposed as a segment (later, when the 80286 introduced protected mode, renamed a selector, but by default in real mode selectors were set up to work identically to segments) and an offset.

The physical address was generated by taking the segment, left-shifted by four bits (not multiplied by four, as another answer claims), and then adding the offset, then taking the lowest (most significant) 20 bits of the result.

It follows that any given physical memory address can be accessed via a number of different segment:offset pairs. There is no difference to the CPU-RAM interface between, say, F000:FFF0 and FFFF:0. (We can see this as F000<<4 becomes F0000, add FFF0 gives FFFF0; likewise, FFFF<<4 is FFFF0, add 0 gives FFFF0.)

As a consequence, it also follows that segments can overlap. In the above example, segments F000h and FFFFh overlap at address FFFF0h. In general, every segment selects a sliding 64 KiB window of the 1 MiB address space, shifted 16 bytes for each segment.

Memory models are a distraction, here; they mostly influence which segment value is the default for a particular type of operation, and which type of addressing is normally used.

To access a particular physical address, generally speaking, an appropriate segment register is loaded with the correct segment, and then a normal segment:offset access is performed via that segment register. Some instructions (such as jumps) can or even require the programmer to bypass the segment load operation and specify the segment directly in the instruction; other instructions can use only a specific segment register.

Since loading a segment register is a relatively invasive operation (as it changes global CPU state which you'll potentially need to reset before doing something else), it's something you'd try to avoid. By carefully choosing how you're addressing memory, many segment changes could be avoided, or restricted to the extra segment register, ES.

The CPU doesn't care about the type of segment (on the 8086/8088, there is nothing to mark a stack segment as different from a code or data segment; features like NX, or read-only segments, came later), but some operations have restrictions on which segment registers they can be used with. For example, you cannot perform an intersegment jump instruction into code pointed at by DS, but that's a restriction of the jump instruction, not the CPU. If for some strange reason you really want to jump to DS:IP+4, you just need to put that address in a place where a jump instruction can use it; there is no need to do anything to the memory itself.

Back in the day, the ability of the 8086/8088 to provide these different windows into the address space was sold as a feature by Intel, proposing that it would ease the development of multitasking or multiuser environments. For example, from the iAPX 86/88 user's manual, page 2-11:

The segmented structure of the 8086/8088 memory space supports modular software design by discouraging huge, monolithic programs. The segments also can be used to advantage in many programming situations. Take, for example, the case of an editor for several on-line terminals. A 64k text buffer (probably an extra segment) could be assigned to each terminal. A single program could maintain all the buffers by simply changing register ES to point to the buffer of the terminal requiring service.

6

Before "protected memory," all bytes were basically the same as far as the CPU was concerned. The segment registers are a lot like "bank selectors" that you'd see in other segmented memory schemes, except that they overlapped every 16 bytes. So, if you needed to use 128KB of RAM for a game data file, you would just change the DS register between two or more values to choose which 64k chunk of the 128k file you were reading/writing. Of course, developers had to be careful to either use MS-DOS or BIOS routines to make sure they didn't accidentally overwrite the Interrupt Vector Table, etc, but aside from that, the CPU and memory controller didn't actually enforce many sanity checks directly.

For example, MS-DOS's "COM" format was just raw machine code without any headers. Before execution, CS, DS, and SS were all set to the same value. However, a developer could write code to move SS up 64k, giving access to a full-sized stack, and then move DS around as they like to load and save memory as they'd like. The EXE format had additional headers before the machine code, allowing CS, DS, and SS to be different initially. From there, if you were using a higher level language (C was common back then), the langage's memory manager could change CS and SS as necessary to access more than 64k. The only limit is that you could only access 64k of memory per register at one time, without changing the segment register.

2

8086 assemblers and compilers supported different memory models where a program could have one or more code segments combined with one or more data segments. For example, a small model program would have one code segment (cs) and one data segment (ds/es). A large model program would have multiple code segments (cs, using a far jump), multiple data segments (with the segment in one of ds or es). The number (one/multiple) of stack segments would generally match the number of data segments (one/multiple).

You must log in to answer this question.

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