15-213 (Spring 2005) Section A - Recitation #3

TA: Kun Gao       Email: kgao@cs.cmu.edu
Office Hours: Tuesday 1-2pm, Wednesday, 2-3pmLocation: Doherty Hall 4302D

Announcements:
Next quiz is going out this afternoon (after 4:30pm), will be done Tuesday by midnight. You will have 30 minutes (instead of 40 min in the last two quizzes)

Past Quiz Questions:
Consider the following two alternative ways to set register eax to 0
xorl %eax, %eax
movl $0, %eax

The difference between xorl and movl is that while movl only moves the value 0 into register %eax, xorl will set %eax to 0, and set the condition codes as well. Thus, if the subsequent operation uses the condition codes (set or jump instructions), the behavior will be different. Operations that DO NOT set the condition codes include movl, and leal.

Memory Layout
Although memory is considered a monolith chunk that is linearly addressable, we divide it into multiple pieces logically, and only do certain things in certain places in memory. For example, we only store programs in some places in memory, and we only have the stack in other places in memory. This serves many purposes, including organization and security purposes.

Memory is laid out as thus (it is architecture dependent):
---------------------- 0xFFFFFFFF
| kernel virt. mem   |
---------------------- 0xC0000000
| program stack      |
|                    |
----------------------
| shared library     |
---------------------- 0x40000000
|                    |
| heap (malloc)      |
----------------------
| read/write segment |
----------------------
| read only segment  |
---------------------- 0x08048000
| unused             |
---------------------- 0x00000000

The kernel virtual memory is invisible to user code. The program stack will grow downwards. The heap grows upwards. The read/write segment is for things such as global variables. The read only segment is for program text (code). We can see exactly where things are placed with this simple program:
int c;
int main() {
int* a = (int*)malloc(10*sizeof(int));
int b[10];
void* fn = main;

printf("local variable: 0x%x\n", b);
printf("heap variable: 0x%x\n", a);
printf("global data: 0x%x\n",&c);
printf("program text: 0x%x\n", fn);
}

Result:
local variable: 0xbffff930
heap variable: 0x8049640
global data: 0x80495ec
program text: 0x804835c

Procedure Calls and Stack:
The stack is used to store local variables, and keep track of function calls. Local variables used in functions are allocated on the stack. Whenever a function is called, its arguments are put onto the stack, and the return address in the current function is put onto the stack before jumping to the function that is called. In this way, the stack grows and shrinks based on calling and returning from functions.

For example, suppose three functions:

void x(int x1, int x2) {
int x3;
int x4;
y(x3, x4);
x3 = x1 + x2;
}
void y(int y1, int y2) {
int y3;
z(y1);
y3 = y1 + y2;
}
void z(int z1) {
z1 = z1 + 1;
}

The stack frame will look like this inside z:
-----------------------
| x arg, x2           |
-----------------------
| x arg, x1           |
-----------------------
|ret addr (x.s caller)|
-----------------------
| x.s caller.s %ebp   |
-----------------------
| x3                  |
-----------------------
| x4                  |
-----------------------
| y arg, x4           |
-----------------------
| y arg, x3           |
-----------------------
|ret addr (x3=x1+x2)  |
-----------------------
| x.s %ebp            |
-----------------------
| y3                  |
-----------------------
| z arg, z1           |
-----------------------
|ret addr (y3=y1+y2)  |
-----------------------
| y.s %ebp            | <- %ebp
-----------------------

Registers are either caller or callee save. This is just a convention to keep track of who should be responsible for saving registers. Suppose registers are caller save. On calling a function, the caller must save the contents of those registers (and restore the original value after the call). For callee save, if a callee wants to use a register, it must save the contents first (and restore the original value before returning).

The Stack in Lab 3:
Some functions have the vulnerability that they set values into local variables, but do not check how much data they are writing into the local variable. In another words, they will write into the stack, but might write too much, and overwrite other data onto the stack.

For example, consider the following stack frame:
---------------------
| ret addr to main  |
---------------------
| main.s %ebp       |
---------------------
| str[4] ... str[7] |
---------------------
| str[0] ... str[3] |
---------------------

This is obtained from code that might look like this:
int main() {
getchar();
return 1;
}
int getchar() {
char str[8];
Gets(str);
}

Where Gets will fill the str array with characters from stdin until end of line.
If we enter in more characters than str can hold, the stack above str will be overwritten as well. Lets say we enter 16 characters into the stdin. Then the stack frame will look like this:
-----------------------
| str[12] ... str[15] |
-----------------------
| str[8] ... str[11]  |
-----------------------
| str[4] ... str[7]   |
-----------------------
| str[0] ... str[3]   |
-----------------------

Both main.s %ebp, and the return address to main will have been overwritten. Thus, when we exit the function getchar, the restoration process that will restore %ebp to the saved value of main.s %ebp will get the value of str[8] . str[11], and when returning, we will instead jump to code at address str[12] . str[15] instead. This means we can get the computer to run any code we want. This is called the buffer overflow attack.

Struct/Union Layout and Alignment:

Elements of a struct are laid out in memory sequentially while respecting alignment. Union is laid out as the biggest of the elements in the union.

IA32 will work correctly regardless of how data is aligned. However, alignment improves memory performance. Linux follows the policy were 2-byte data types (short) must have address that is a multiple of 2, while larger data types (int, int *, float, double) must have address that is a multiple of 4.

Example:
struct P1 { int i; char c; int *j; char *d; double f };

vs.

union U1 { int i; char c; int *j; char *d; double f };

The total size of struct P1 is 24 bytes, while union U1 is 8 bytes.