CS61C Summer 2018 Lab 2 - Bitwise (nitty-gritty) Number Manipulation and Practices of Good C Memory Management

WELCOME TO LAB 2. This text was bolded for no reason other than to get your attention. As the person in charge of the labs, I want to make you feel like lab is as much of a personalized learning experience as discussion and that we don't just mindlessly toss these exercises together for you. That's why I'm trying to make the tone of voice of the instructions sound more silly and human.

This is the image of the day. Now stop looking at pictures of cats and get to work.



I heard some of you didn't know how to get the lab files onto your local machine. That was my bad for not explicitly giving you the commands to do so. Here's my rundown on your options to get the lab files. I will repeat these instructions on every single lab until you are literally sick of looking at them.

Option 1: the classic approach. Copy the contents of ~cs61c/labs/02 to a suitable location in your home directory ON YOUR CLASS ACCOUNT like so:

$ cp -r ~cs61c/labs/02/ ~/labs/02

This assumes you have logged on to a hive machine OR are ssh'd into a hive machine. From here, if you are also physically on the lab machines, you can edit the files with your favorite text editor on the lab machine. If you are working via ssh, however, you will have to use a command-line text editor such as vim. I won't provide a vim guide here, but you can just Google search it.

Option 2: the "I want to do it on my own computer" approach. Secure-copy (scp) the contents of ~cs61c/labs/02 on the lab machines onto a suitable location on your local machine.

$ scp -r cs61c-xxx@hive10.cs.berkeley.edu:~cs61c/labs/02 ~/YOUR_FILEPATH

This will copy the folder containing the lab2 files onto ~/YOUR_FILEPATH on your local machine. From here, you can obviously edit them with your favorite text editor locally, BUT you may or may not be able to run the programs which this lab requires because they may or may not be installed on your computer. These programs however are installed on the hive machines, which is why we generally want you to work on them.

Once you're done editing your lab files, you can secure-copy them back on to your lab account with:

$ scp -r ~/YOUR_FILEPATH/02 cs61c-xxx@hive10.cs.berkeley.edu:~/YOUR_LABACCT_FILEPATH

This will copy the folder ~/YOUR_FILEPATH/02 (which should contain the lab2 files) into ~/YOUR_LABACCT_FILEPATH on your class account.

Option 3: the "best of both worlds" approach.There are some GUI-based solutions to work on the Hive machines remotely like X2Go and Cyberduck, and you can look up these programs and how to set them up online. Unfortunately I've never used them, but I've heard that people generally like them.

Exercise 0: Bit Operation Basics (Optional)

You may skip this exercise if you're fluent in bit operations. In particular, if you know how the &, |, ^, ~, <<, and ^ operations work, you should skip this exercise.

Task: Look up how the above six operations work in C. Use your favorite search engine. Personally, I like Google, but here is a list of some esoteric ones.

Understanding check:

Exercise 1: Bit Operations

For this exercise, you will complete bit_ops.c by implementing the following three bit manipulation functions. You will want to use bitwise operations such as and (&), or (|), xor (^), not (~), left shifts (<<), and right shifts (>>). Avoid using any loops or conditional statements.

WHOA does that say no loops or conditional statements? Yes, indeed it does. That means WHILE you try to work through this exercise with your partner, you do not have the right to type the words if, else, while, for, switch, or anything else of that nature. Sorry! Tell a staff member if you feel that your rights have been violated, and we'll respond not by giving you your rights back, but by giving you a hint to the exercise.

Read this: n should be treated as a 0-indexed index into the bits of x.

// Return the nth bit of x.
// Assume 0 <= n <= 31
unsigned get_bit(unsigned x,
                 unsigned n);

// Set the nth bit of the value of x to v.
// Assume 0 <= n <= 31, and v is 0 or 1
void set_bit(unsigned * x,
             unsigned n,
             unsigned v);

// Flip the nth bit of the value of x.
// Assume 0 <= n <= 31
void flip_bit(unsigned * x,
              unsigned n);

Hint for set_bit: zero it out then shift v in. What is "it?" That's for you to figure out, my friend.

Once you complete these functions, you can compile and run your code using the following commands.

$ make bit_ops
$ ./bit_ops

This will print out the result of a few limited tests.

Checkoff [1/4] (whoa, a progress bar)

Exercise 2: Linear feedback shift register

In this exercise, you will implement a lfsr_calculate() function to compute the next iteration of a linear feedback shift register (LFSR). Applications that use LFSRs are: Digital TV, CDMA cellphones, Ethernet, USB 3.0, and more! This function will generate pseudo-random numbers using bitwise operators. For some more background, read the Wikipedia article on Linear feedback shift registers. In lfsr.c, fill in the function lfsr_calculate() so that it does the following:

Hardware diagram (see explanation below)

Explanation of the above diagram

After you have correctly implemented lfsr_calculate(), compile lfsr.c and run it. Your output should be similar to the following:

$ make lfsr
$ ./lfsr
My number is: 1
My number is: 5185
My number is: 38801
My number is: 52819
My number is: 21116
My number is: 54726
My number is: 26552
My number is: 46916
My number is: 41728
My number is: 26004
My number is: 62850
My number is: 40625
My number is: 647
My number is: 12837
My number is: 7043
My number is: 26003
My number is: 35845
My number is: 61398
My number is: 42863
My number is: 57133
My number is: 59156
My number is: 13312
My number is: 16285
 ... etc etc ...
Got 65535 numbers before cycling!
Congratulations! It works!

Checkoff [2/4]

Exercise 3: Memory Management

This exercise uses vector.h, vector-test.c, and vector.c, where we provide you with a framework for implementing a variable-length array. This exercise is designed to help familiarize you with C structs and memory management in C. In other words, don't worry about the real-life practicalities of this wonky data structure. Just don't.

Your task is to fill in the functions vector_new(), vector_get(), vector_delete() and vector_set() in vector.c so that our test code vector-test.c runs without any memory management errors.

Here's how a vector_t works

It's time to look at the code in the vector.c if you haven't already. Here, additional comments in the code also describe how the functions should work. Again, the user of your vector_t data structure should be able to assume that all entries in the vector are 0 unless set by the user. Keep this in mind as malloc() does not zero out the memory it allocates.

Task: Look at the functions bad_vector_new() and also_bad_vector_new(). These creatively named functions are examples of bad ways to initialize structs in C. Be ready to discuss why this is the case.

Task: Complete vector_new(), the good version. There are exactly six (6) places for you to fill in with an expression in C. They are marked with a comment which says, exactly: /* YOUR CODE HERE */. Write an expression in these blanks. That means no more than one line of code. Additional comments describe what should happen in the line of code directly underneath.

Task: Complete vector_get() in the same manner as you completed vector_new(): respectfully, eager to learn, with an open mind, and consciously aware of what you are coding, as this is best practice.

Task: Complete vector_delete(). A sufficient solution to shouldn't take more than 2 lines of code.

Task: Complete vector_set(). This is the more involved one. Welcome to the big leagues. The problem with setting an arbitrary index/location in a vector v is that we might not have malloc'd enough space in v->data for it (yes, that means you should have mallocd space for v->data). Think about how you can allocate enough memory for that index/location, what you need to do with the old data, and what other things you need to do in your new block of data. Hint: recall that all unset indices/locations should be zero. There are a few different ways you can complete this function. Consider the 3 __alloc functions. They could all be useful here...

Using Valgrind

To help you to find memory bugs, we have installed a copy of Valgrind Memcheck. Valgrind is ONLY on the lab machines in the Hive and the Orchard. That means it PROBABLY isn't on your own computer. This program will run an executable while keeping track of what registers and regions of memory contain allocated and/or initialized values. The program will run much slower (by a factor of about 10 to 50) so that this information can be collected, but Valgrind Memcheck can identify many memory errors automatically at the point at which they are produced. You will need to learn the basics of how to parse the Valgrind output.

You can test your code in the following two ways:

// 1) to check functionality:
$ make vector-test
$ ./vector-test

// 2) to check memory management using Valgrind:
$ make vector-memcheck

The Makefile calls Valgrind as follows:

$ valgrind --tool=memcheck --leak-check=full --track-origins=yes [OS SPECIFIC ARGS] ./<executable>

If you look through this icky formatting, you'll realize that the basic format of using Valgrind is the same as how you used CGDB in the previous lab: you run it on the executable created by gcc EXCEPT that you need to also add the dot-slash in front of the executable.

The --track-origins flag attempts to identify the sources of unitialized values. The --leak-check=full option tries to identify memory leaks. [OS SPECIFIC ARGS] are simply a set of arguments to Valgrind that differ across operating systems (in our case, Ubuntu (Linux)). If you are interested in learning more about these, see the Makefile.

The last line in the Valgrind output is the line that will indicate at a glance if things have gone wrong. Here's a sample output from a buggy program:

==47132== ERROR SUMMARY: 1200039 errors from 24 contexts (suppressed: 18 from 18)

If your program has errors, you can scroll up in the command line output to view details for each one. For our purposes, you can safely ignore all output that refers to suppressed errors. In a leak-free program, your output will look like this:

==44144== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 18 from 18)

Again, any number of suppressed errors is fine; they do not affect us.

Feel free to also use a debugger or add printf statements to vector.c and vector-test.c to debug your code.

Checkoff [3/4]

malloc is a powerful tool which you have at your disposal. You can think of it kinda like this: when you malloc some chunk of memory for yourself, you place a steel cage around it which signals to everyone else that you own it and no one else can touch it. While the steel cage is very helpful for maintaining your structs and arrays, you need to keep in mind that once you're done using it, it needs to be lifted (this is what free does). Otherwise, you'll have steel cages lying around all over your memory, preventing it from being used by anyone. This is wasteful and greedy and why we stress the importance of having no memory leaks in 61C. Likewise in real life, it's important to know how to properly use (and not abuse) our most powerful tools and to clean up after ourselves, whether it be in the environment, in your own home, in your own bed, or whatever. And that's where I'll halt this train of thought.

Exercise 4: Some More Practical Valgrind

When you used Valgrind above, it may not have looked very useful or it may have been somewhat hard to understand what was going on. For this exercise, we're going to look at some more practical and interpretable examples of code where we can use Valgrind. We have provided 4 programs for you to run Valgrind on: program_a.c, program_b.c, program_c.c, program_d.c.

You can run a memory check on a program by running the following command, shown for program_a.c:

$ make a-memcheck
gcc -ggdb -Wall -std=c99 -o program_a program_a.c; \
valgrind --tool=memcheck --leak-check=full --track-origins=yes ./program_a; \
rm program_a
==29708== Memcheck, a memory error detector
==29708== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==29708== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
==29708== Command: ./program_a
*pi is 3
==29708== HEAP SUMMARY:
==29708==     in use at exit: 4 bytes in 1 blocks
==29708==   total heap usage: 3 allocs, 2 frees, 1,032 bytes allocated
==29708== 4 bytes in 1 blocks are definitely lost in loss record 1 of 1
==29708==    at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==29708==    by 0x400611: main (program_a.c:14)
==29708== LEAK SUMMARY:
==29708==    definitely lost: 4 bytes in 1 blocks
==29708==    indirectly lost: 0 bytes in 0 blocks
==29708==      possibly lost: 0 bytes in 0 blocks
==29708==    still reachable: 0 bytes in 0 blocks
==29708==         suppressed: 0 bytes in 0 blocks
==29708== For counts of detected and suppressed errors, rerun with: -v
==29708== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

Alternatively, you can compile each .c file yourself, and then run the Valgrind command with whatever specific flags you would like (for example the flag -v):

// compile program_a
$ gcc -o program_<x> program_<x>.c

// now run a memory check using Valgrind
$ valgrind --tool=memcheck --leak-check=full --track-origins=yes [OS SPECIFIC ARGS] ./program_<x>

Read each program_<x>.c file as well as run <x>-memcheck on every program and try to determine what is happening that is causing memory problems in each program (e.g. memory out of bounds, segmentation fault, etc.).

Checkoff [4/4]

Exercise 4.5 (lol): How to get a 100% response rate on a survey

Task: Complete the Week 1 Feedback Form.

Checkoff [4.5/4]