Process Environment

Excerpted from Stevens and Rago, chapter 7.

main Function

A C program starts execution with a function called main. The prototype for the main function is

     int main(int argc, char *argv[]);
where argc is the number of command-line arguments and argv is an array of pointers to the arguments.

When a C program is executed by the kernel — by one of the exec functions — a special start-up routine is called before the main function is called. The executable program file specifies this routine as the starting address for the program; this is set up by the link editor when it is invoked by the C compiler. This start-up routine takes values from the kernel — the command line arguments and the environment — and sets things up so that the main function is called.

This start-up routine is written so that if main returns, the exit function is called. If the start-up routine were coded in C (it is often coded in assembler) the call to main could look like

    exec(main(argc,argv));

Process Termination

There are eight ways for a process to terminate. Normal termination occurs in five ways:

  1. Return from main
  2. Calling exit
  3. Calling _exit or _Exit
  4. Return of the last thread from its start routine
  5. Calling pthread_exit from the last thread

Abnormal termination occurs in three ways:

  1. Calling abort
  2. Receipt of a signal
  3. Response of the last thread to a cancellation request

Exit Functions

Three functions terminate a program normally: _exit and _Exit, which return to the kernel immediately, and exit, which performs certain cleanup processing and then returns to the kernel.


#include <stdlib.h>

void exit(int status); 
void _Exit(int status);

#include <unistd.h> 

void _exit(int status); 

The reason for the different headers is that exit and _Exit are specified by ISO C whereas _exit is specified by POSIX.1.

Historically, the exit function has always performed a clean shutdown of the standard I/O library: the fclose function is called for all open streams. This causes all buffered output data to be flushed (written to the file).

All three exit functions expect a single integer argument, which we call the exit status. Most UNIX System shells provide a way to examine the exit status of a process. If (a) any of these functions is called without an exit status, (b) main does a return without a return value, or (c) the main function is not declared to return an integer, the exit status of the process is undefined. However, if the return type of main is an integer and main "falls off the end" (an implicit return), the exit status of the process is 0.

Returning an integer value from the main function is equivalent to calling exit with the same value. Thus

	exit(0);
is the same as
 
	return 0;
from the main function.

atexit function


#include <stdlib.h>

int atexit{void (*func) (void));

With ISO C, a process can register up to 32 functions that are automatically called by exit. These are called exit handlers and are registered by calling the atexit function.

The declaration of atexit says that we pass the address of a function as the argument to atexit. When this function is called, it is not passed any arguments and is not expected to return a value. The exit function calls these functions in reverse order of registration. Each function is called as many times as it was registered. exit first calls the exit handlers and then closes (via fclose) all open streams.

Summary

Figure 1 summarizes how a C program is started and the various ways it can terminate.


Figure 1 How a C program is started and how it terminates

Note that the only way a program is executed by the kernel is when one of the exec functions is called. The only way a process voluntarily terminates is when _exit or _Exit is called, either explicitly or implicitly (by calling exit). A process can also be involuntarily terminated by a signal (not shown in Figure 1).

The following program demonstrates different termination routines and the atexit function.

Memory Layout of a C Program

Historically, a C program has been composed of the following pieces:


Figure 2. Typical memory arrangement

Figure 2 shows the typical arrangement of these segments. This is a logical picture of how a program looks; there is no requirement that a given implementation arrange its memory in this fashion. Nevertheless, this gives us a typical arrangement to describe. With Linux on an Intel x86 processor, the text segment starts at location 0x08048000, and the bottom of the stack starts just below 0xC0000000. (The stack grows from higher-numbered addresses to lower-numbered addresses on this particular architecture.) The unused virtual address space between the top of the heap and the top of the stack is large.

Note from Figure 2 that the contents of the uninitialized data segment are not stored in the program file on disk. This is because the kernel sets it to 0 before the program starts running. The only portions of the program that need to be saved in the program file are the text segment and the initialized data.

The size(l) command reports the sizes (in bytes) of segments. For example:

$ size /usr/bin/cc /bin/sh 
   text   data    bss    dec    hex  filename
  79606   1536    916  82058  1408a  /usr/bin/cc
 619234  21120  18260 658614  aOcb6  /bin/sh
The fourth and fifth columns are the total of the three sizes, displayed in decimal and hexadecimal, respectively.

Shared Libraries

Shared libraries remove the common library routines from the executable file, instead maintaining a single copy of the library routine somewhere in memory that all processes reference. This reduces the size of each executable file but may add some runtime overhead, either when the program executed or the first time each shared library function is called. Another advantage of shared libraries is that library functions can be replaced with new versions without having to relink edit every program that uses the library. (This assumes that the number and type of arguments haven't changed.)

Different systems provide different ways for a program to say that it wants to use or not use the shared libraries. Options for the cc(l) and ld(l) commands are typical. As an example of the size differences, the following executable file — the classic hello.c program — was first created without shared libraries:

$ gcc -static -o hello hello.c  prevent gcc from using shared libraries
$ ls -l hello
-rwxr-xr-x  1 loomis users 465705 2006-09-14 22:46 hello
$ size hello
   text    data     bss     dec     hex filename
 399390    3368    4732  407490   637c2 hello

If we compile this program to use shared libraries, the text and data sizes of the executable file are greatly decreased:

$ gcc -o hello hello.c gcc defaults to use shared libaries
$ ls -l hello
-rwxr-xr-x  1 loomis users 7023 2006-09-14 22:47 hello
$ size hello
   text    data     bss     dec     hex filename
   1013     260       4    1277     4fd hello

Memory Allocation

ISO C specifies three functions for memory allocation:


#include <stdlib.h>
 
void *malloc(size_t size); 
void *calloc (size_t nobj, size_t size);
void *realloc (void *ptr, size_t newsize);

void free(void *ptr);

All three allocation routines return a non-null pointer if OK and NULL on error

The pointer returned by the three allocation functions is guaranteed to be suitably aligned so that it can be used for any data object. For example, if the most restrictive alignment requirement on a particular system requires that doubles must start at memory locations that are multiples of 8, then all pointers returned by these three functions would be so aligned.

Because the three alloc functions return a generic void * pointer, if we #include <stdlib.h> (to obtain the function prototypes), we do not explicitly have to cast the pointer returned by these functions when we assign it to a pointer of a different type.

The function free causes the space pointed to by ptr to be deallocated. This freed space is usually put into a pool of available memory and can be allocated in a later call to one of the three alloc functions.

The realloc function lets us increase or decrease the size of a previously allocated area. (The most common usage is to increase an area.) For example, if we allocate room for 512 elements in an array that we fill in at runtime but find that we need room for more than 512 elements, we can call realloc. If there is room beyond the end of the existing region for the requested space, then realloc doesn't have to move anything; it simply allocates the additional area at the end and returns the same pointer that we passed it. But if there isn't room at the end of the existing region, realloc allocates another area that is large enough, copies the existing 512-element array to the new area, frees the old area, and returns the pointer to the new area. Because the area may move we shouldn't have any pointers into this area. sizes.. .

Note that the final argument to realloc is the new size of the region, not the difference between the old and new sizes. As a special case, if ptr is a null pointer, realloc behaves like malloc and allocates a region of the specified newsize.

The allocation routines are usually implemented with the sbrk(2) system call. The system call expands (or contracts) the heap of the process.

Although sbrk can expand or contract the memory of a process, most versions of malloc and free never decrease their memory size. The space that we freed is available for a later allocation, but the freed space is not usually returned to the kernel; that space is kept in the malloc pool.

It is important to realize that most implementations allocate a little more space than is requested and use the additional space for record keeping — the size of the allocated block, a pointer to the next allocated block, and the like. This means that writing past the end of an allocated area could overwrite this record-keeping information in a later block. These types of errors are often catastrophic, but difficult to find, because the error may not show up until much later. Also, it is possible to overwrite this record keeping by writing before the start of the allocated area.

Writing past the end or before the beginning of a dynamically-allocated buffer can corrupt more than internal record-keeping information. The memory before and after a dynamically-allocated buffer can potentially be used for other dynamically-allocated objects. These objects can be unrelated to the code corrupting them, making it even more difficult to find the source of the corruption.

Other possible errors that can be fatal are freeing a block that was already freed and calling free with a pointer that was not obtained from one of the three alloc functions. If a process calls malloc, but forgets to call free, its memory usage continually increase; this is called leakage. By not calling free to return unused space, the size of a process s address space slowly increases until no free space is left. During this time, performance can degrade from excess paging overhead.

Because memory allocation errors are difficult to track down, some systems provide versions of these functions that do additional error checking every time one of the three alloc functions or free is called. These versions of the functions are often specified by including a special library for the link editor. There are also publicly available sources that you can compile with special flags to enable additional runtime checking.

Reference

W. Richard Stevens and Stephen A. Rago, Advanced Programming in the UNIX Environment, Second Edition, Addison Wesley, 2005. ISBN 0-201-43307-9. p 179-191.


Maintained by John Loomis, last updated 18 September 2006