|
|
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Typical Program Memory Layout |
|||
| Description | Minimum Required Access | Requires Non-Volatile Storage? | Run Time Behavior |
|---|---|---|---|
| Code | Execute-Only | Yes | Fixed/Static |
| Constants | Read-Only | Yes | Fixed/Static |
| Initialized Data | Read/Write | Yes | Fixed/Static |
| Uninitialized Data | Read/Write | No | Fixed/Static |
| Heap | Read/Write | No | Variable/Grows Upwards |
| Stack | Read/Write | No | Variable/Grows Downwards |
"Non-volatile" indicates whether or not there needs to be some form of non-volatile storage. If yes, this can be on disk or in some hardware ROM or flash memory. But it needs to be saved somwhere.
Also, keep in mind that both heap and stack are both part of a fixed
block of memory, with the stack starting at the end of it and growing down and the heap
starting at the beginning of it and growing up. The remaining "unused" memory in
between the active heap and the active stack, like a double-ended candle, burns at both
ends.
The code section is much like the constant data section, except that it
is for program instructions. (Code is nothing more than just constant data, really.) The
size of the code space is usually set at link time and does not need to change during
execution.
The constant data section, as I use the term above, is meant for data which cannot be
modified during execution. This region is different from initialized data because
constants do not ever require write-access during run-time. Examples of this kind of data
are PI and sine tables, both used in trigonometry. For some systems, these constants might
be co-located with code, if the code space permits reading-as-data. For others, the
constants will be treated just like initialized data, except that none of the code will
attempt to change any of it.
The initialized data section is for a region of read/write memory, where the initial
values must be preset to specific values before the program is allowed to start running.
However, the initialized data area requires read/write access at run time because they can
(and probably will) be changed while the program runs. An example of this kind of data
might be the seed for a random number generator, which is initialized to some default
value before the program starts running. Or some default values that may be later changed
by the operator/user of the program.
The unitialized data section is for data which does not require initial values. This means
that the data could start out randomized and that this would not impact the correct
behavior of the program. An example here may be a common, shared variable which is
initialized from some command line parameter value after the program starts running.
The heap section isn't static at run time. Normally, this section is set up having zero
size to start and then grows and shrinks during execution. This is the area used by
routines like C's malloc(), for example. In assembly programs, heap is not as often used
as it is in C, but the idea is still valid for assembly programs and may be useful. Either
way, a simple design for the heap has it growing upwards and away from the last memory
location required by all the collected static regions, towards the stack. Heap space, in
some languages or systems, uses what is called "garbage collection" methods in
order to compact it when memory is freed from the middle.
The stack section also isn't static at run time. Normally, this section also starts out
with zero size and grows and shrinks during execution. It usually grows downward and away
from the last possible memory location for the program and towards the changing end of the
heap section. The result of the operation of the stack and heap is that there is an
invisible area of read-write memory, a "no man's land" so to speak, between the
heap and the stack; an area which often shrinks like a candle burning at both ends. If,
during execution, the heap grows into the stack (or visa versa) then the program will
probably fail to operate correctly. Some operating systems are able to automatically
detect this event and correct for it by adding some more memory into the gap.
Most conventional programming languages and their linkers are designed with the above
run-time model in mind, with the above mentioned sections arranged in that order in
memory. For example, no matter how many source code files you have, no matter how many
different fragments of code you write, it's the linker's job to somehow collect all those
various fragments and place them into one, big, common code section so that all the code
is in a single convenient place. In addition, the linker should try and collect up all of
your constants and literal text strings together into another common constant section and,
if appropriate, place it just after the code section. Since both represent fixed,
immutable constant bytes, that plan makes sense. Then, it makes sense to find all your
initialized variables, those needing to have a starting value when the program begins,
into yet another common section and to place that one just after the other two. With all
those collected together, that's all that is actually needed to be saved in some form of
non-volatile storage, like flash or a floppy or hard disk. These are the only parts of
your program that the operating system can't compute or generate on its own. The
uninitialized variables, heap, and stack are easily built at run-time without any
additional information.
Imagine what would happen if these things weren't organized in this way. Bits of unitialized variables mixed in with initialized ones, constants salted into random locations in uninitialized memory space, etc. The entire mixed up mess would need to be kept in non-volatile storage, perhaps excepting the heap and stack, with a potentially serious waste of good non-volatile memory holding unnecessary values for uninitialized variable memory that is going to be initialized later, once the program starts up, anyway. It really does help for the linker to organize things according to these uses.
This scheme also allows rather simple program loaders and protection schemes, since only the first three sections need to be saved on disk and those can be quickly and directly loaded into the first part of the allocated memory for the program. Also, as another example, an operating system with appropriate hardware support can arrange things so that the first two sections are kept in memory marked as read-only and the last four sections in read-write memory. Some types of hardware only support a single division of memory in this way and organizing the program like this helps it to better fit inexpensive hardware designs. Some operating systems, like DOS, manage only the read/write, dynamic RAM available directly to their CPU, so all of the memory is read-write. That's fine. It allows the code and constants to be written, but if you don't try to do that there is no problem.
In operating systems that can share memory sections between instances of running programs (for example, when you run two or three separate copies of Microsoft Word), it makes it much nicer if the common code and common constants are collected up because that permits those memory sections to simply be mapped into the local instance memory of each process without having to reload or allocate separate memory for each. If these fragments were all mixed up with other types of memory, then the operating system would have no choice but to allocate the entire program memory space for each instance, without any sharing.
In von Neumann architectures, like the DOS-based PC, the operating
system stores only the code, constant, and initialized data sections (sometimes just
called CODE, CONST, and INIT) on a disk, in a specially formatted, executable file -- the
.COM and .EXE file. When the program is to be started, the operating system allocates
enough volatile RAM from its own volatile RAM memory heap to hold the entire program with
all the segments and then loads just those three on-disk portions from the file into the
associated segments in RAM. It then starts the program. There is no need to load the other
sections -- the stack and heap are empty, anyway, and the uninitialized data doesn't need
to be initialized from data on the disk. Some languages, like C, do guarantee that the
unitialized data area will be initialized to a semantic zero, though -- but this is
handled by the C start-up code found in the library that all C programs link with.
In Harvard architectures, it's somewhat more difficult to arrange some of these details,
as any operating system trying to load the program must have access to the CODE segment as
if it were a writable data segment. This is one reason why many Harvard style CPUs support
a special type of "code memory treated as data" instruction, just for that
purpose.
In protected mode environments, such as what the 80286+ can provide, where code segments
are not even writable when they are configured as code memory, the operating system is
often forced to forge additional, writable data selectors as aliases to the code
area of the program it is loading and use those instead.
When you write your assembly code for your program, it's helpful to consider which one of the six basic types of memory each part of your program requires. The assembler pre-defines some directives for just these different purposes, too. For example, the .code directive specifies the first type; the .const directive specifies the second type; the .data directive specifies the third type; the .data? directive specifies the fourth type; and the .stack directive specifies the sixth type (the ML assembler directives don't have a special one just for the fifth type.) Note this fact and place your code and data where it belongs.
I haven't discussed the meaning of "Align" or "Combine" or "Class" and haven't said much, if anything, about the group called DGROUP. So, I'd recommend reading some of the documentation noted in my PC Docs web page -- these include some PDF versions of the Microsoft MASM manuals as well as a Microsoft web page containing the technical documentation. Also, Randy Hyde's excellent tutorial, as well. There is a lot of excellent sources and my discussions are only a tiny drop, by comparison.
But here's a table borrowed from Appendix E of the Microsoft MASM Programmer's Guide. It may be helpful. Take note of the different directive types in each model and relate these back to my comments about operating systems, generally, noted earlier on this page. This may help guide you in properly using these directives.
Default Segments and Types for Standard Memory Models |
||||||
| Model | Directive | Name | Align | Combine | Class | Group |
|---|---|---|---|---|---|---|
| Tiny | .CODE | _TEXT | WORD | PUBLIC | 'CODE' | DGROUP |
| .FARDATA | FAR_DATA | PARA | PRIVATE | 'FAR_DATA' | ||
| .FARDATA? | FAR_BSS | PARA | PRIVATE | 'FAR_BSS' | ||
| .DATA | _DATA | WORD | PUBLIC | 'DATA' | DGROUP | |
| .CONST | CONST | WORD | PUBLIC | 'CONST' | DGROUP | |
| .DATA? | _BSS | WORD | PUBLIC | 'BSS' | DGROUP | |
| Small | .CODE | _TEXT | WORD | PUBLIC | 'CODE' | |
| .FARDATA | FAR_DATA | PARA | PRIVATE | 'FAR_DATA' | ||
| .FARDATA? | FAR_BSS | PARA | PRIVATE | 'FAR_BSS' | ||
| .DATA | _DATA | WORD | PUBLIC | 'DATA' | DGROUP | |
| .CONST | CONST | WORD | PUBLIC | 'CONST' | DGROUP | |
| .DATA? | _BSS | WORD | PUBLIC | 'BSS' | DGROUP | |
| .STACK | STACK | PARA | STACK | 'STACK' | DGROUP | |
| Medium | .CODE | name_TEXT | WORD | PUBLIC | 'CODE' | |
| .FARDATA | FAR_DATA | PARA | PRIVATE | 'FAR_DATA' | ||
| .FARDATA? | FAR_BSS | PARA | PRIVATE | 'FAR_BSS' | ||
| .DATA | _DATA | WORD | PUBLIC | 'DATA' | DGROUP | |
| .CONST | CONST | WORD | PUBLIC | 'CONST' | DGROUP | |
| .DATA? | _BSS | WORD | PUBLIC | 'BSS' | DGROUP | |
| .STACK | STACK | PARA | STACK | 'STACK' | DGROUP | |
| Compact | .CODE | _TEXT | WORD | PUBLIC | 'CODE' | |
| .FARDATA | FAR_DATA | PARA | PRIVATE | 'FAR_DATA' | ||
| .FARDATA? | FAR_BSS | PARA | PRIVATE | 'FAR_BSS' | ||
| .DATA | _DATA | WORD | PUBLIC | 'DATA' | DGROUP | |
| .CONST | CONST | WORD | PUBLIC | 'CONST' | DGROUP | |
| .DATA? | _BSS | WORD | PUBLIC | 'BSS' | DGROUP | |
| .STACK | STACK | PARA | STACK | 'STACK' | DGROUP | |
| Large -or- Huge | .CODE | name_TEXT | WORD | PUBLIC | 'CODE' | |
| .FARDATA | FAR_DATA | PARA | PRIVATE | 'FAR_DATA' | ||
| .FARDATA? | FAR_BSS | PARA | PRIVATE | 'FAR_BSS' | ||
| .DATA | _DATA | WORD | PUBLIC | 'DATA' | DGROUP | |
| .CONST | CONST | WORD | PUBLIC | 'CONST' | DGROUP | |
| .DATA? | _BSS | WORD | PUBLIC | 'BSS' | DGROUP | |
| .STACK | STACK | PARA | STACK | 'STACK' | DGROUP | |
| Flat | .CODE | _TEXT | DWORD | PUBLIC | 'CODE' | |
| .FARDATA | _DATA | DWORD | PUBLIC | 'DATA' | ||
| .FARDATA? | _BSS | DWORD | PUBLIC | 'BSS' | ||
| .DATA | _DATA | DWORD | PUBLIC | 'DATA' | ||
| .CONST | CONST | DWORD | PUBLIC | 'CONST' | ||
| .DATA? | _BSS | DWORD | PUBLIC | 'BSS' | ||
| .STACK | STACK | DWORD | PUBLIC | 'STACK' | ||
Last updated: Monday, July 12, 2004 01:06