|
One of the earlier machines I programmed in assembly was the Hewlett-Packard 2116 processor. It was part of the HP 2000F timesharing computer system I had access to. In this machine, there was no concept of a stack inherently understood by the CPU. When a subroutine was called, for example, the first instruction of that routine was arbitrarily destroyed by the CPU in order to place the return address there. Then the CPU continued with the next instruction of the routine. Leaving the routine meant jumping indirectly through the first location of the subroutine. This was a recipe for semantic limitations. Subroutines couldn't call themselves recursively, nor could they call another routine which might call them back. Pre-emption was also a big problem because, if the CPU switched to another task and that task needed to call an interrupted subroutine, disaster would result. These days, it's easy for programmers to not recall (or even know about) those days before such important concepts like the stack were developed and refined and then implemented. Most CPUs today support a stack concept in their hardware -- it's just that useful. But there was a time when these things were hard-learned by experience and imagined only through arcane theory. The Operating System Point of ViewWhile your program lives in its own world, the operating system or operating environment (to use a more equivocal term) has a somewhat different view of things, including your program. The O/S has the responsibility of managing memory and loading your program somewhere in it. It may also need to protect itself from your programs and your program from other programs, as well. It also needs to perform relatively quickly. The O/S also provides various common services that programs need and may have to virtualize resources on the computer (make it look like your program is the only program using them, even though there are actually a dozen other programs using it, too.) Operating system designers, over the years, have arrived at a rather useful, simplified view about what a program or task should look like when it is located in memory. This simplified view is quite broadly useful and, although there are myriad variations on myriad computers to deal with myriad issues, continues to show its value over and over again as a clarifying concept. As I've already suggested earlier, a program is often the combined product of several translation units or modules or source files, linked together in a useful fashion. Whether a program is written in assembly language, C, Pascal, other languages, or some combination of them, the memory layout for the program usually follows a standardized template. Although linkers support many complex details to support building correct programs from separately compiled modules, the final, linked product tends to break down into six distinct sections. All of the code is collected together, constants are collected together, static data which needs to be initialized is collected together, and static data which does not need any initialization is also collected. Automatic data is placed on the 'stack' and heap space grows from the end of the static data (initialized, uninitialized, and constant) up towards the stack while the stack grows downwards. These six distinct areas are placed into consecutive areas of memory, ordered in a precise way, like this:
"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. 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. SummaryWhen 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.
Last updated: Monday, July 12, 2004 01:06
|