Machine Model, Part 2

This is part two of our exploration of the underlying machine model. A primary focus will be on the way local objects and their underlying memory is managed by the function call stack. We'll also cover different mechanisms for parameter passing and returned results from functions.

Updated Fall 2024

1: Intro to Lobster

Before we start, let me cover a few basics for the Lobster program visualization tool, which we'll use throughout several lecture examples and exercises in the future.


You can find the main Lobster page at https://lobster.eecs.umich.edu. However, there are also Lobster exercises embedded directly in these asynchronous lectures, and you can just work on them here (your work will be saved along with any other interactive exercises). You don't need to go to the separate Lobster page unless you want to work on other problems outside of these lectures.



2: Functions and The Call Stack
2.1 Not Started

The memory allocated for each function is generally called an activation record or (more commonly) a stack frame. Each function takes up a certain amount of memory that depends on how many local variables it may need to store, and this memory is allocated and freed as needed during the program.

Because of the way that functions call work (i.e. the called function has to finish and return before you can start back up in the original function), it's natural to use a stack to represent the memory frames for each function. Whichever function is called most recently is added to the top of the stack, and will always be removed before any other functions that were already on the stack (this is called the "Last In First Out" or "LIFO" property).


2.1 Exercise: Stack Frames

Consider the following code. Trace through the code either manually or using the Lobster simulation and answer the questions below.

Which function has the largest stack frame (in terms of memory use)? How can you tell? Is this a compile-time property or a runtime property?

What is the maximum amount of memory on the (call) stack needed by the program at any one given time? Assume an int takes up 4 bytes, and that the memory to store local int objects is the only memory used by the program.

How many different stack frames are created for the min() function throughout the execution of the program?

You're also welcome to check out this walkthrough video where I talk through the questions.




3: Mechanisms for Parameters/Returns
3.1 Not Started

The two primary mechanisms for parameter passing are pass-by-value and pass-by-reference. A similar choice applies for returning results from a function. Let's take a look at the differences between the two, as well as how they relate to function stack frames.


3.1 Exercise: Parameter Passing

Consider this code that defines a function with both pass-by-value and pass-by-reference parameters.

What are the values of each variable at the end of the main function? (You can also use the Lobster simulation to check.)

a:      b:     c:     d:

Sample solution…

a: 1      b: 3     c: 3     d: 4



4: Passing/Returning Pointers
4.1 Not Started

We can achieve an effect similar to pass-by-reference by using a pointer instead. Here's the basic idea - just like with pass-by-reference, we want to work with the original object (e.g. in a main() function) without making a copy when we pass it in as a parameter. So, instead of passing the original object, we pass its address as a pointer parameter. That parameter is technically copied (i.e. this is technically a pass-by-value), but who cares! A copy of an address will still get you back to the original object's location.


It also turns out that the compiler offers pass-by-reference as feature of the C++ language, but it's ultimately turned into pass-by-pointer in the compiled machine code. Here's a brief explanation:


4.1 Exercise: Pass-by-Pointer

The code below contains a broken swap function that doesn't actually do anything. Fix it by modifying the function to use pass-by-pointer, so that you can swap the original objects through pointer parameters. Once you're done, the values of the original variables in main should be swapped correctly! (Note that Lobster will show a completed checkpoint once you've got the right output, and may also try to give you some hints along the way if you run into any bugs.)



5: Null and Uninitialized Pointers
5.1 Not Started

A regular pointer contains the address of some other object in your program, and will lead you to that object when you dereference it. But there are a few exceptional cases we should consider:


(Please note a typo near the end of the video - the return type of find_by_length() should be string *, not int *.)


To recap:

  • Uninitialized pointers: Just like with any other (primitive) variable, if you don't initialize a pointer, it's value is determined by memory junk. That means it's pointing randomly off into space.

  • Null pointers: Sometimes we want to definitively say "this pointer isn't pointing to anything right now", and the way to do that is point it at address 0.

Some more examples:

int x = 3;

int *ptr1 = &x; // Initialized with the address of x, this pointer points to x
*ptr1 = 10;     // Follows ptr1 to x and sets x to 10

int *ptr2;      // Uninitialized pointer, points at some random address (eeeewww)
*ptr2 = 10;     // Follows ptr2 off to some random part of memory and slaps down a 10
                // causing undefined behavior depending on how important that memory was

int *ptr2 = nullptr; // Null pointer, "not pointing at anything right now"
*ptr2 = 10;          // Tries to write a 10 to address 0 in memory, which will almost
                     // certainly crash (easier to debug than undefined behavior though!)

While uninitialized pointers are pretty much always bad, it's useful to have a nullable pointer to represent something "optional". But, to safely use pointer that might be null, you need to check the pointer before dereferencing it. For example:

// Assume we have a pointer called ptr that might be null

if (ptr != nullptr) {
  // If we get in here, it's safe to dereference and do something with *ptr
}

You can also shorten that up by replying on an implicit conversion from pointer types to bool- non-null pointers will convert to true and null pointers will convert to false. (Kind of like the way nonzero numbers convert to true and 0 converts to false, considering a null pointer contains address 0x0.)

// Assume we have a pointer called ptr that might be null

if (ptr) {
  // If we get in here, it's safe to dereference and do something with *ptr
  // That's because ptr would only turn into true if it wasn't null
}
5.1 Exercise: Null and Uninitialized Pointers

For each of the following code snippets, briefly describe what the last line of code does. (For example, "sets the value of a to 3" or "dereferences a null pointer - program crashes".)

int main() {
  int a = 2;
  int *ptr1 = nullptr;
  int *ptr2;

  *ptr1 = 4; // What does this line do?
}
int main() {
  int a = 2;
  int *ptr1 = nullptr;
  int *ptr2;

  ++*ptr2; // What does this line do?
}
int main() {
  int a = 2;
  int *ptr1 = nullptr;
  int *ptr2;

  *ptr2 = a; // What does this line do?
}
int main() {
  int a = 2;
  int *ptr1 = nullptr;
  int *ptr2;

  ptr2 = &a; // What does this line do?
}


You're welcome to check your solution with this walkthrough video:




6: Dangling Pointers

Finally, let's take a look at the case of dangling pointers, which are pointers that used to point to a valid object, but the object's lifetime has since ended. The pointer still holds the same address and is still pointing at the memory location where it used to be, but the data there is no longer valid to use.


You've reached the end of this lecture! Your work on any exercises will be saved if you re-open this page in the same web browser.

Participation Credit
Make sure to sign in to the page, complete each of the exercises, and double check the participation indicator at the top left of this page to ensure you've earned credit.