CMPSC
311,
Introduction to Systems Programming
Process Address Space
Reading
- CS:APP
- This looks like way too much reading, but some of it is
repetitious, or goes into details that aren't concerned so
much with
where things are in memory.
- You probably should start with Sec. 7.4, for a general
overview.
- Sec. 1.2, Programs Are Translated ...
- Sec. 1.7.3, Virtual Memory (esp. Fig. 1.13)
- Sec. 3.2, Program Encodings, to the end of p. 161
- Sec. 3.7, Procedures (concentrate on management of the
stack,
in Sec. 3.7.1, 3.7.4, 3.7.5)
- Sec. 3.12, Out-of-Bounds Memory References and Buffer
Overflow
- A lesson from the "Defence Against the Dark Arts"
professor.
- Ch. 7, intro, Sections 1 through 4, 8 through 10, and 13.
- Sec. 8.2.3, Private Address Space
- Sec. 8.4.5, Loading and Running Programs
- Ch. 9, intro, pp. 776-777
- Sec. 9.1, 9.2, 9.7.2 (esp. Fig. 9.26, which you have now
seen
many times)
- Sec. 9.8.3, The
execve Function Revisited
- Sec. 9.8.4, User-Level Memory Mapping with the
mmap
Function
- APUE
- Sec.
7.6, Memory Layout of a C Program
- Sec. 7.7, Shared Libraries
- Sec.
7.8, Memory Allocation
- Similar material is
found
in Patterson & Hennessy, Computer
Organization
and Design, 4th edition, Sec. 2.12, Translating and
Starting a Program. (This is the textbook for CMPEN
331/431.)
- Solaris man page for
elfdump
The address space of a
process consists of the memory locations that can be referenced by
the
process or its
threads.
- The Posix Standard defines a process
as "An address space with one or more threads executing within
that
address space, and the required system resources for those
threads".
- And, it defines a thread
as "A single flow of control within a process".
- broad view - the address space of a process is all memory
locations at addresses 0 through N, for some value of N that
depends on
the processor design, the memory design, the operating system
design,
the compiler design, and the way in which the program was
compiled
- this does not depend on the program!
- locations at addresses larger than N cannot be accessed -
they
do not exist in the process address space
- narrow view - same, but limited to the locations that are
actually accessed by the program
- which verb tense should we use? are accessed, were
accessed, will be accessed, could be accessed
- some locations at addresses less than or equal to N cannot
not
be accessed - they do not exist in the process address space,
or do not have proper permissions for the attempted access
The addresses you see in any program will fall into four general
ranges
of addresses, known as segments,
- text segment - the compiled program, which does not change
- at
fixed
locations
determined by the compiler, linker and loader
- data segment - objects with static storage duration
- at fixed locations determined by the compiler,
linker and loader
- heap segment - objects with dynamic storage duration
- at variable locations determined at runtime by the
memory
allocator
that came with the programming language
- explicit allocation
- stack segment - objects with automatic storage duration
- at variable locations determined at runtime by
program
activity
such as function calls
- implicit allocation
There are other segments that you will learn about
later.
The text segment
- The text segment holds program instructions in the machine
language.
- The program instructions are taken from the executable object file, and
placed
at fixed addresses determined by the compiler, linker and
loader.
- The compiler generates relocatable
object
files (
.o files) containing collections of
instructions using relative addresses (relative to the start of
each
function or data object).
- Actually, it's the compiler followed by the assembler.
- Relocatable object files don't have enough information to be
executable object files.
- The linker modifies multiple collections of relative addresses
to
become one collection of relative addresses (relative to the
start of
the whole program) stored as a library
object
file or as an executable object file.
- Actually, there could be more than one, but there won't be
many, and they will all look like the text and data segments.
- Library object files don't have enough information to be
executable object files. For example, no
main().
- The operating system's loader reads the executable object file
and determines where to place the start of the program in main
memory
(probably the same for all programs, which simplifies the
design).
- After loading, the text area should be marked read-only so the
program cannot
modify its own instructions.
The data segment
- The
data
segment holds global variables, static local variables and
various
constants.
- The locations of these objects do not change. [Recall,
we
are
using the definition of object from C, not from C++ or Java.]
- Data associated with the program instructions can be allocated
at
compile time (in this section) or at runtime (in the stack or
heap
sections).
- Actually, it's the compiler followed by the assembler that
determines compile time.
- The loader reads the executable object file
and determines where to place the start of the data in main
memory
(probably just above the text segment, which simplifies the
design).
- Separate read-write and read-only data sections are usually
provided.
- Explicit initialization (for example, by global
int n =
17;)
needs
explicit
information
in the object file.
- address, size, value, at least
- Implicit initialization (for example, by global
int n;
which defaults to 0) needs no explicit information in the object
file,
other than address and size. The loader needs to know
about this
section, which is traditionally called the bss section (bss =
block
started by symbol).
Shared libraries
- Precompiled library functions could be linked into the
executable
file when it is first created (static linking), but that's not
an
efficient use of main memory or disk space.
- These are library archive
files (
.a files).
- Instead, only a "stub function" is linked in, and the stub is
replaced at runtime by the real function (dynamic linking).
- Here, runtime means either "when the program is loaded" or
"when the function is first called."
- In fact, the OS loads only one copy of the library functions,
and
it is shared by all processes that need it.
- These are shared object
files
(
.so files).
- Data associated with the shared library could also follow
static
linking or dynamic linking.
- Solaris no longer implements static linking, only dynamic
linking.
The stack segment
- The stack segment holds function
arguments,
saved registers, return addresses, local variables and temporary
values
allocated when a function is called and executes.
- The details depend on the Instruction Set Architecture, and
on
software conventions established by the operating system and
compiler
writers.
- Function parameters in C are treated as local variables,
initialized from the function arguments.
- Some optimizations are possible.
- The first few function arguments would usually go into
registers, if they fit. Space for all the arguments is
still
reserved on the stack.
- The return value would usually go into a register, if it
fits,
otherwise on the stack.
- The stack is
initialized
by the system-supplied startup function, which calls main().
- The
size of the stack segment can increase if more space is needed,
until
it
bumps into another segment or increases beyond a system-imposed
limit.
The heap segment
- The heap segment holds dynamically allocated objects such
as
those created with malloc() in C or new in
C++.
- The size of the heap segment can increase if more space is
needed,
until
it bumps into another segment.
- Don't confuse this with the generic data structure called a
heap,
which is a special kind of partially sorted sequence.
Memory-mapped segments
- The
mmap() function will allocate a new section,
which can be deallocated by munmap().
- The contents of this section can be uninitialized, or
initialized
from the contents of a file.
- Some implementations of
malloc() use mmap()
to handle very large requests.
The kernel area
- code and data reserved exclusively for use by the operating
system
Exercises.
- Suppose you apply the unary
& operator to a
local variable. What impact does that have on the storage
for the
variable, and on the use of that storage?
- Why would it be desirable for every address returned by
malloc()
to be a multiple of 8 or 16?
- Why would it be a bad idea for the location of the heap
segment
to move?
- It's a contiguous sequence of bytes, so all we need to know
is
its starting address and size. Then we find some other
memory
location of the same size, copy all the bytes, and use the
original
location for some other purpose.
- What could go wrong?
A map of the process address space should show at least
- the runtime stack, including the stack base and the direction
of
stack
growth
- temporarily allocated objects, with addresses determined at
runtime
- allocation and deallocation is implicit by the rules of
parameter-passing and local variables
- the dynamic data area (the heap), including the heap base and
direction of growth
- temporarily allocated objects, with addresses determined at
runtime
- allocation and deallocation is explicit by program action
- the static data area, including the initialized and
uninitialized
data
segments
- permanently allocated objects, at fixed addresses determined
by
the compiler, linker and loader
- location and extent of the text area
- location and extent of the kernel area
There are two ways to gather information for this map - look at the
output of programs like elfdump, which read an
executable
file, or query the program as it is running. You can also get
this information from an interactive debugger, but that's for
later. Comparing 32-bit and 64-bit address spaces is also a
good
idea (this is relatively easy on Solaris), and comparing different
operating systems would also help.
The map of the stack should
include
main()'s
function arguments and local variables, but since these locations
are
subject
to change we shouldn't try to be too specific. Obviously, once
the
program is compiled and loaded, its location would not change, but
we'll
see later that more parts can be added to the program as it runs.
So far, this is a
logical or notional picture of how a program looks. The
goal here is really to see how the particular implementation
of Solaris actually arranges memory, and whether the 32-bit and
64-bit
versions differ in any organizational way. Linux arranges
memory
in the same general way, but the addresses are different.
Here is some of what we need to test, to see if the "facts on the
ground" agree with our hypotheses and the textbook examples.
- main() is
in the text segment.
- The global variable environ
is in the data segment.
- The arguments to main() (argc, argv,
envp)
are on the stack.
- The arrays pointed to by argv, environ
and envp are in the data area, in the heap or on the
stack,
and we should determine which.
- The strings pointed to by argv[i], environ[i]
and envp[i] again are either in the data area, in the
heap or
on the stack, and we should determine which.
- etc.
The first round of tests uses Solaris on a Sparc processor in 32-bit
mode.
Here is an example of elfdump's
output, from a simple
program. The program itself produces no output.
Look for mention of the program's variables in the elfdump
output using the grep command, and sort the results by
address:
% grep var demo.0.elfdump |
grep
0x | sort -k 2r
[20] 0x00020e48
0x00000004 OBJT GLOB D 0
.bss
global_var_2
[63] 0x00020e48
0x00000004
OBJT GLOB D 0
.bss
global_var_2
[15] 0x00020e44
0x00000004
OBJT GLOB D 0
.bss
global_var_0
[58] 0x00020e44
0x00000004
OBJT GLOB D 0
.bss
global_var_0
[18] 0x00020e38
0x00000004
OBJT GLOB D 0
.data
global_var_1
[61] 0x00020e38
0x00000004
OBJT GLOB D 0
.data
global_var_1
[22] 0x00010cc0
0x00000004
OBJT GLOB D 0
.rodata global_var_3
[65] 0x00010cc0
0x00000004
OBJT GLOB D 0
.rodata global_var_3
The second grep is used to filter out some symbol
table
information that has no address information.
.bss is the traditional name for the segment
containing implicitly initialized data, which is set to 0 (bss =
block
started by symbol)
.data is the name of the static data area, with
read-write permission
.rodata is the name of the static data area, with
read-only permission
Note that the local variables declared in main() do
not
appear in the elfdump output.
Now we will use some macros to help print addresses of variables at
runtime.
We used the utility env with addresses.[34].c
to control the environment
strings in a new process, otherwise there is too much output.
The programs can be compiled (on Solaris) using 32-bit addresses or
64-bit
addresses, in C89 or C99, and with Sun's compiler or the GNU
compiler.
The compiler commands would be like
cc -m32 -o
prog_c89_32
-v prog.c
c99 -m32 -o
prog_c99_32
-v prog.c
cc
-m64
-o prog_c89_64 -v prog.c
c99 -m64
-o prog_c99_64 -v prog.c
gcc
-m32 -o prog_c89_32 -Wall -Wextra prog.c
gcc -std=c99 -m32
-o
prog_c99_32 -Wall -Wextra prog.c
gcc
-m64 -o prog_c89_64 -Wall -Wextra prog.c
gcc -std=c99 -m64
-o
prog_c99_64 -Wall -Wextra prog.c
On Linux and Mac OS X, use the GCC compiler that is normally
installed.
Here are the previous examples, but now with 64-bit addresses.
Last revised, 4 Feb. 2013