Saturday, July 21, 2007

Functions

* Memory Footprint of a Process
- "To understand what stack buffers are we must first understand how a process is organized in memory. Processes are divided into three regions:
Text, Data, and Stack.

The text region is fixed by the program and includes code (instructions) and read-only data. This region corresponds to the text section of the executable file. This region is normally marked read-only and any attempt to write to it will result in a segmentation violation.

The data region contains initialized and uninitialized data. Static variables are stored in this region. The data region corresponds to the data-bss sections of the executable file. Its size can be changed with the brk system call. If the expansion of the bss data or the user stack exhausts available memory, the process is blocked and is rescheduled to run again with a larger memory space. New memory is added between the data and stack segments."

Therefore, the memory footprint of one process is like this: (from high address to low address): Stack (grow from high to low), uninitialized data region, initialized data region and text region.

* Stack and Frame
For single threading programs, all of the subroutines would share the same stack which is allocated when it is executed. In general three addresses are needed for proper stack operations, and they are stored in processor registers. They are stack pointer(SP), stack base and stack limit.

One more register, namely frame pointer(FP), is needed for the normal function calling. When procedure is called, after input parameters (the pushing order of input parameters is from right to left) and return address (it is the current PC and then *PC = addr of procedure) are pushed, the frame pointer is pushed into the stack, and FP = SP (here SP points to the last occupied address instead of the first free location of the stack). Then the local variables would be allocated. All of these elements consist of a stack frame.

Frame pointer is used to support variable length of the stack. Once returned, the function would assign SP = FP, which points to the previous FP. Pop it up to assign FP. Thus the context goes back to the last one. Pop up one more to get return PC and continue the process execution. Meanwhile, during the execution of the callee function, FP would be the base address to access the local variables and input parameters, i.e., the offsets of local variables are negative and offsets of input parameters are positive. For example,
void function(int a, int b, int c)
{
     char buffer1[5];
     char buffer2[10];
}
void main()
{
     function(1,2,3);
}
The assembly of codes by using "gcc -S -o example1.s example1.c" is
     pushl $3
     pushl $2
     pushl $1
     call function
This pushes the 3 arguments to function backwards into the stack, and calls function(). The instruction 'call' will push PC onto the stack.The first thing done in function is the procedure prolog:
     pushl %ebp /* EBP is FP */
     movl %esp,%ebp /* ESP is SP */
     subl $20,%esp /* allocate buffer1 */
The stack now looks like:
     buffer1 FP RET a b c (Bottom of stack)
    [      ] [] [] [][][]
Note: the head of buffer1 locates in the low address (left side). So once the overflow happens on buffer1, FP, RET would be overwritten and segmentation fault occurs.

* Function Name
The function name is the address of the function codes in text region. It could directly be assigned to one pointer to function. Meanwhile, it is allowable to specify any address in text region to a pointer to function (no function body at all) and execute it. This is typically used to reset system.

* Function Parameters Casting
"It is the programmer's responsibility to ensure that the argument to a function are of the right type."

* Function Parameters
The function parameter is always passed by value into function stack, never by reference in C. That means the copies of function parameter, instead of themselves, are used within the function. Therefore any changes on copies can not be seen outside the function. The only way to achieve such changes on original variables is to use pointers as parameters and dereference them inside. Keep in mind the key here is to change the variables pointed by pointers. The pointers themselves within the function are still the copies of original ones. Changes on pointers within functions are not meaningful. For example,
void GetMem(char *p, int num)
{
        p = (char *)malloc(num * sizeof(char));
}
void FreeMem(char *p)
{
        free(p);
        p = NULL;
}
Two solutions for this: to use **p or to return the local pointer.

* Local Variables
Local variables are allocated in function stack and they would freed automatically when function returns. Generally local variables, like pointers (to non-local memory, like static variable), struct, union, etc., could be returned by value, not reference. The exception is arrays. For one thing, arrays could not be treated as a whole unit in C and therefore could not be returned. On the other hand, the memory of arrays is freed once the function returns. No way to dereference this data outside the function.

One typical example for this case is when using the library function setbuf(stdout, buf). Before handling control back to the OS, the library would flush(not free) the remaining in the buf. This happens just after the main function returns generally. If this buf is allocated in stack by using arrays, this last flush would be wrong since the memory has been freed. The solution is to use a global array or a static array. What if the buf in the heap?

No comments: