开发者

Programs larger than 64k when using real segmented mode with x86 assembly and DOS(16-bit)

I wanted to know how you need to handle assembly programs that are larger than 64k when you're doing 16-bit (assembly)programming in real segmented mode for DOS. In the book that I'm following("Asse开发者_JS百科mbly Language Step by Step" by Jeff Duntemann) the author mentions something about using more than one code segment(but unfortunately doesn't go into the details). I just wanted to know how to do this. I know this memory model is obsolete now, but I just, out of curiosity, wanted to know how you would accomplish this.


Well, you just write upper word of your working address to DS, and you can use it via DS:DI. This way you can use more up to 300-500kb of ram.


Using more than 64kb memory isn't, by itself, terribly complicated (except if you need to deal with data structures crossing segment boundaries - that can be "fun"). If all you need is dynamic memory, simply use the 16:16 (seg:ofs) FAR pointers.

You can keep DS pointed to your main data segment and use ES (or, if executing on a 80386 or newer, even in real mode) FS or GS data segments to hold the segment part of your far pointers. You can use whatever general-purpose register you want for the offset part, DS:SI and ES:DI are special only when dealing with the string instructions (lods/movs/stos). Also, keep in mind that BP and SP default to SS segment if you don't use an explicit segment override!

If you need more than 64kb of static program data (wow!), you need an assembler with segment/section support, and you'll most likely also need to assemble to an object format and before linking to .exe. How to handle the static data far pointers is going to depend on your assembler of choice, but there will likely be some keyword for referencing the segment part of a variable.


The segment register offsets the base of the memory access. The address in 16-bit mode is calculated as:

address = ((twenty_bit_t)segment << 4) + offset

where (the imaginary) twenty_bit_t is a type that is at least 20 bits.

The address space in 16-bit real mode is 1MB, or 20 bits. The segment allows you to influence those top four bits, and does it at a granularity of 16 bytes (often called a "paragraph" in those days). Adding one to the segment value puts your pointer 16 bytes ahead in memory.

Your offset is limited to a 16-bit value, so to access beyond that you had to use "far pointers". A far pointer is 32-bits long. But not really 32-bits, the high 16 bits is really just the segment, which overlaps the offset.

Far pointers are significantly more expensive than normal pointers because (with typical compilers of the time) every time it dereferences a different pointer it had to load the segment register. Often the compiler would just go ahead and load the segment registers every time.

There were several "models" (as we called them then). It was basically a matrix of all combinations of near/far code pointers, near/far data pointers, (and IIRC a way to have > 64KB of globals).

Compilers provided fine-grained control over whether each pointer is far or near. It was an optimization to use near code and near data for heavily accessed code/data, and put extra extension directives to declare specific pointers or functions as far where needed.

Far calls and returns were not as severe of an impact as far data though, since pointers are dereferenced much more often than functions are called.

x86 CPUs have special instructions (and prefixes) to deal with far calls and returns and far pointers. Even 32-bit OSs need to at least initialize the segment registers. Some x86 system instructions require use of segment registers. However, in 32-bit mode, the value of the segment register has an entirely different meaning from everything I've described above.

Linking and loading

Object files have blocks of code and data, each one destined to go into a specific segment (by name). Each segment is also flagged as code or data etc. The linker figures out what is needed, figures out how big each segment is, figures out an address for everything, and remembers the locations of any pointers that are within the executable. Far call operands and initialized far pointers will be computed as though the load address of the executable is zero, and a relocation entry is emitted for each one.

DOS handled memory dynamically, so your program would be loaded at an unpredictable address. To deal with this, DOS executables have "relocations", which are a list of pointers to code and initialized data. When loading, DOS goes through that list and adds the base segment of the load address to the value pointed to by the relocation entry. For example, a code relocation would point right to the bytes that represent the segment in a call instruction. This fixed up any initialized global far pointers and any far call instruction operands.

(Starting with the 286 the address calculation actually became almost 21 bit - with the segment register at FFFF + offset FFFF, the address would be 10FFEF - (1 MB + 64 KB - 16 B). HIMEM.SYS actually has an API call to allocate the high memory, all or nothing. Typically people let DOS have it by putting DOS=HIGH in their config.sys.)


That's easy. You have several code segments, probably spread among several overlay files (typically .OVL .OVR or .BIN). You load the overlay(s) you need, and jump to them with a far jump/call, which modifies CS:IP.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜