Systems Se
Systems Se
David G. Andersen
Contents
1 Introduction 5
2 Revision Control 7
2.1 Revision Control Concepts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.2 A practical introduction to Subversion . . . . . . . . . . . . . . . . . . . . . . . . 9
2.2.1 Resolving conflicts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.3 Thoughts on using revision control . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.4 Other source control systems . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.5 Trying it out on your own . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.6 Recommended Reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
4 Tools 19
4.1 Tools for Code Checking . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
4.1.1 Compiler Checks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
4.1.2 Catching Errors Early with -D_FORTIFY_SOURCE . . . . . . . . . . . . 19
4.1.3 Catching Errors Early with glibc variables . . . . . . . . . . . . . . . . . . 19
5 Debugging 21
5.1 A Debugging Mindset . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
5.2 Good old printf done better: Debug macros . . . . . . . . . . . . . . . . . . . . . 23
5.3 Debugging tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
5.3.1 gdb . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
5.3.2 Using GDB to track down common problems . . . . . . . . . . . . . . . . 26
5.3.3 System call tracing: ktrace, strace, and friends . . . . . . . . . . . . . . . 26
5.3.4 Memory Debugging with Valgrind . . . . . . . . . . . . . . . . . . . . . . 27
5.3.5 Memory Debugging with Electric Fence . . . . . . . . . . . . . . . . . . . 28
5.3.6 Tracing packets with tcpdump and Wireshark . . . . . . . . . . . . . . . . 29
5.3.7 Examining output . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
5.4 Debugging other peoples code . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
3
6 Documentation and Style 31
6.0.1 Style . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
6.1 Read other code! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
6.2 Communication . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
6.3 Further Reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
8 Scripting 37
8.0.1 Which Language? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
8.0.2 One-liners . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
8.0.3 The Shell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
8.0.4 Other useful tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
8.0.5 Scripting languages go well beyond . . . . . . . . . . . . . . . . . . . . 39
8.0.6 A Ruby Primer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
9 Program Design 41
9.1 Design for Incremental Happiness . . . . . . . . . . . . . . . . . . . . . . . . . . 43
9.2 Design for Testability . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
9.2.1 An Adversarial Mindset . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
9.3 Test Automation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
9.4 Recommended Reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
10 Coding Tricks 47
11 Human Factors 49
11.1 Time Management . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
11.1.1 Planning and Estimating . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
11.2 An Attitude for Software Development . . . . . . . . . . . . . . . . . . . . . . . . 49
11.2.1 Program Deliberately! . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
11.2.2 Fix bugs early . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
11.3 Team Dynamics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
11.3.1 Structuring code to work as a team . . . . . . . . . . . . . . . . . . . . . . 51
11.3.2 Structuring your development environment for team work . . . . . . . . . 51
11.3.3 Structuring your personal interaction . . . . . . . . . . . . . . . . . . . . . 51
12 Editors 53
12.0.4 Emacs Tips . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
12.0.5 Tags . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
12.0.6 Integrated Development Environments . . . . . . . . . . . . . . . . . . . . 55
13 Recommended Resources 57
4
Chapter 1
Introduction
These notes attempt to capture some of the techniques that experienced systems hackers use to
make their lives easier, their development faster, and their programs better. In our journey, we
will scrupuously avoid metrics, excessive formalism, and the traditional trappings of very large-
scale software engineering that are so often taught in undergraduate software engineering courses.
While these techniques have their place and value, there is a middle ground of practical software
engineering techniques that work well for programming in small groupssay, one to five people. We
focus our attention on systems programming, the often low-level and intricate software development
in operating systems, networks, compilers, and the other components that build the foundation of
higher layer software systems.
We will explore both practices and tools that facilitate software development. Our view of these
tools is that they should be low-overhead. They should be easy to start using, provide significant
bang for the buck, and be capable of supporting a project if it grows. The benefit from the practices
should be clear and realizable without spending a lot of time on them.
An important question to ask over time is how much time to spend learning new techniques. Our
view is to take the middle road. Be willing to keep your knowledge of tools fresh. This process
is one of balancing between expertise with your current tool set, avoiding fads, and learning useful
new tools that can significantly improve your productivity. Pick your tools carefully; most take
some time to learn, and longer to become fluent. The process is much like that of optimizing the
performance of a program: identify the small set of things that consume the most time, and optimize
those first.
This same guideline applies to the suggestions we make in this text. While we believe that nearly
all of them apply to anyone doing systems programming, some of the details may not match your
style. Approach the problem from a puzzle-solving standpoint: How can you optimize the amount
of time you get to spend doing the things that you enjoy?
5
6
Chapter 2
Revision Control
We begin our journey into software engineering before we write a single line of code. Revision
control systems (RCSes) such as Subversion or CVS are astoundingly useful for single-developer
projects, and essential for even the smallest group projects. These systems allow deveopers to obtain
a copy of the current source code, work on it, and then check in their changes. The systems permit
you to go back to any earlier version, and see the changes that were made between revisions. A good
revision control system provides several benefits:
Super-undo: You can go back to arbitrary saved versions of your source code.
Backups: Using a revision control system means that you can keep the source code master
copy on a safe, well-maintained machine, and edit/compile/develop on an arbitrary machine
(such as your laptop).
Tracking changes: The RCS can give you a list of all of the changes that affected a particular
file (or project). This can be very useful in figuring out when something brokeand who
broke it.
Concurrent access: Many RCSes are designed to allow concurrent, safe access to the source
code. More about this later.
Snapshots: Is your code at a particular good working point? (e.g., a release, or in the case
of 15-441, ready for a checkpoint.) An RCS should let you snapshot the code at that point
with a memorable name so that you can easily access it later.
Branches: A branch allows you to commit changes to an alternate copy of the code without
affecting the main branch. A good example is the optimization contest in the second 15-441
assignment. With a branch, you can create a separate, derived tree for your code where you
play around with ideas, knowing that they wont break the main, safe branch. You dont have
to use branches, and theyre a mildly advanced feature, but if youre putting serious effort
into the optimization contest, you might want to consider using them.
As a random aside, note that revision control is useful beyond just code. I (Dave) use subversion
to store papers that my research group is working on, my c.v., my web pages, the configuration files
for some of my machines, and so on. I use it exactly as I mentioned above in backups to keep all
of my documents and projects on a server, while being able to edit them on my laptop.
7
Original file Modified file
trunk
start of development
1.0 security branch
version 1.0
Concept 2: Every revision is available. When you commit to the repository, the changes youve
made since your previous commit (or since check-out) are all saved to the repository. You can
later see each of these revisions; perhaps more importantly, you can view a diff between arbitrary
versions, showing only what changed. Diffs are most commonly expressed in unix diff format
(Figure 2.1).
Concept 3: Multiple branches of development. Most RC systems support the idea of multiple
lines, or branches of development. For example, consider a project that has a main line of de-
velopment (which we will call the trunk). At some time, this project makes a public release of
version 1.0. Afterwords, it releases only critical security updates to version 1.0, while continuing to
add features to the trunk in preparation for version 2.0. (Figure 2.2).
8
Branches are a great idea, but they also add complexity for the developer, because someone must
often ensure that desirable changes to one branch get propagated to the other branch, which may have
somewhat different code. This process is called merging, and is similar to resolving conflicts (see
below).
Concept 4: Concurrent development. The final benefit from an RC system is that it lets multiple
developers work on the code concurrently. Each checks out his or her own copy of the code, and
begins editing. Developers commit their changes independently, and can update to receive changes
that have been committed to the repository since they checked out the code or last updated.
This works perfectly if the developers are working on different parts of the code, but what if
they are editing the same file at the same time? The answer depends. If the diffs are reasonably
separate (different parts of the file, for example), then the system will usually be able to merge them
automatically. If, however, the updates touch similar lines of code, then the updates will conflict,
and the revision control system will report this to the user. The user must then merge the changes
manually.
This will create a local directory called 441proj1-group1 (please replace with your own group
name as appropriate), and fill it with the latest version of your source code.
Note that this step will schedule the file to be added, but until you commit, it will just be in
your local copy.
3. Add a directory
You can add a directory to the repository by either making it using mkdir and adding it,
or by using svn mkdir. We suggest using the latter, because it means that subversion is
immediately aware that the directory exists, and can use it as a target for move operations.
9
4. Commit your changes
svn commit
You can commit from either the top of your directory tree (in which case all of your outstand-
ing changes will be committed), or from a sub-directory, or just for a particular file by naming
it after the commit (e.g., svn commit foo). This will send your local changes to the repos-
itory. When you commit, svn will prompt you for a log message to describe, for the benefit of
you and your fellow developers, the changes youve made. Dont leave this blankgood log
messages are very useful for debugging and to help coordinate with your partner.
If the version of the file you were editing is not the latest one in the repository, commit will
fail and let you know. At this point, youll need to update (5) and perhaps resolve any conflicts
between your edits and the previous ones (Section 2.2.1 below).
5. Update to get the latest changes
svn update
This will check out the latest changes from the repository. Subversion will print messages
indicating what files were changed. For example, if the update added a new README file
and changed the Makefile, subversion would indicate this as:
A src/trunk/README
U src/trunk/Makefile
You can also see the difference between arbitrary revisions of the file. For example, to see the
difference between revision 1 and revision 2 of a file:
You can also use svn log to see what changes have been recorded to a file.
Table 2.1 lists other commands you may find useful:
10
svn remove Remove a file or directory
svn move Move or rename a file or directory
svn status Show what files have been added, removed, or changed
svn log Show the log of changes to a file (the human-entered comments)
svn blame Show each line of a file together with who last edited it (also svn annotate).
Table 2.1: Other useful Subversion comments
~/conflict-example> svn up
C testfile
Updated to revision 17.
If you look in the directory, youll see that subversion has put a few copies of the file there for
you to look at:
~/conflict > ls
testfile testfile.mine testfile.r16 testfile.r17
The file testfile has the conflict listed. The text from the two revisions is separated by
seven < = and > markers that show which text came from which revision, such as:
You can see that one version has a line that says This line was added on machine 1 and the
other version has a line that says This line was added on machine 2 - hah, I got it committed first!
You have a few options for resolving the conflict:
11
1. Throw out your changes. If you just want your partners changes, you can copy her file on
top of yours. Subversion conveniently supplies you with a copy of the latest version of the
file; in this case, its testfile.r17.
cp testfile.r17 testfile.
Then tell subversion youre done merging:
svn resolved testfile
and commit.
2. Overwrite your partners changes. Just like the previous example, but copy testfile.mine
onto testfile instead.
3. Merging by hand. Open testfile in an editor and search for the conflict markers. Then
select which of the versions (if either) you want to preserve, and update the text to reflect that.
When youre done, svn resolved and commit.
Update, make, test, and then commit. Its good to test your changes with the full repository
before you commit them, in case they broke something.
Merge relatively often. We come from the merge often school. See the discussion in the
next section about breaking down your changes into manageable chunks.
Commit formatting changes separately. Why? So that you can more accurately identify the
source of code and particular changes. A commit that bundles new features with formatting
changes makes it difficult to examine exactly what was changed to add the features, etc.
Check svn diff before committing. Its a nice way to check that youre changing what
you meant to. Weve often discovered that we left in weird debugging changes or outright
typos and have avoided committing them by a quick check.
Try not to break the checked in copy. Its convenient for you and your partner if the current
version of the source code always at least compiles. We suggest breaking your changes down
into manageable chunks and committing them as you complete them.
For example, in Project 1, you may start out by first creating a server that listens for connec-
tions and closes them immediately. This would be a nice sized chunk for a commit. Next,
you might modify it to echo back all text it receives. Another commit. Then create your data
structure to hold connection information, and a unit test case to make sure the data structure
works. Commit. And so on.
If youre going to make more invasive changes, you may want to think about using a branch.
A good example of branches in 15-441 is the optimization contest for the second project:
You may want to experiment with techniques that could break your basic operation, but you
dont want to risk failing test cases just to make your program faster. Ergo, the optimization
12
branch. You can make changes in this branch (and save your work and get all of the benefits
of source control) without affecting your main branch. Of course, if you create a branch and
like the ideas from it, youll have to merge those changes back later. You can either use svn
merge, or you can svn diff and then patch the files manually. The nice thing about using
svn merge is that it records which change youre propagating.
Use meaningful log messages. Even for yourself, its great to be able to go back and say,
Ah-ha! Thats why I made that change. Reading a diff is harder than reading a good log
message that briefly describes the changes and the reason for them.
Make your program modular. If one person can work independently on, say, the user
IRC message parsing code while the other works on the routing code, it will reduce
the chances of conflictsand its good programming practice that will reduce your
headaches in lots of other ways. Have this modularity reflected in the way you put
what code in what file.
Coordinate out of band with your partner. Dont just sit down and start working on
"whatever"let your partner know what youre working on.
svn checkout \
svn+ssh://[email protected]/afs/andrew....../svn/test
13
RCS An early source control system. Allows files to be locked and un-
locked; does not address concurrent use and conflict resolution.
Sometimes used for web pages and configuration files where
changes occur slowly but revision control is useful.
CVS The Concurrent Version System. CVS is built atop RCS and uses
its underlying mechanisms to version single files. Very popular.
Major users include the FreeBSD project and many other open
source systems.
Subversion Subversion is designed to fix many of the flaws in CVS while
retaining a familiar interface. Adds a number of capabilities
to CVS (e.g., the ability to rename files and directories) with-
out breaking the basics. Quickly gaining popularity among open
source projects.
Bitkeeper A commercial distributed source control system. BitKeeper used
to be used for the Linux kernel.
Git A distributed source control system used for the Linux kernel.
Does not have one central repository; each participant merges
changes into their own tree.
Visual SourceSafe Microsofts source control system.
Perforce A popular, heavy-weight commercial revision control system.
Table 2.2: Popular revision control systems.
Bibliography
[1] Ben Collins-Sussman, Brian W. Fitzpatrick, and C. Michael Pilato. Version Control with
Subversion. OReilly and Associates. ISBN 0-596-00448-6. Available online at http:
//svnbook.red-bean.com/.
14
Chapter 3
repeated over the lifetime of even a small project, the 12 character difference will add up to a
qualitative improvement in your enjoyment of writing your software. Easier to build means easier to
test and revise; easier to test and revise makes for happier programs and happier programmers. And
few projectsparticularly not those youre going to write in this classstay that simple forever.
Start out with a makefile from minute one.
Dont develop bad habits with a project, because theyre hard to break. Dont develop ad-hoc
fixes (shell aliases, etc.) that will just break later on. Make is a tool that any serious programmer
should learn to use well. It will repay your investment in much saved time, even just over the course
of this class. Just as an example, I (Dave) use makefiles for building code, running latex and bibtex
on papers, and creating Web pages and pushing them to my server. The notes youre reading are
created by a 44 line makefile and a handful of smaller makefiles for the examples.
15
# This is a comment
CFLAGS=-Wall -g
prog: prog.o
${CC} prog.o -o $@
In fairness, we note that test automation in systems programming can be hard. We dont suggest
going overboard in 15-441 because the limited lifetime of the project wont reward a completely
rich, amazing test environment. However, having a basic level of automated testing that can start
two instances of your programs, see if they can talk nicely with each other, etc., will be well worth
your effort. At an even more basic level, automatically running the unit tests for bits and pieces of
your system is easy to implement and pays big dividends.
First target default: The first target defined in the makefile is the default. In this case, prog.
Implicit compilation rules: Make defines implicit rules for compiling, e.g., .c files into .o
files. By convention, that implicit rule uses the variable CFLAGS to define the C compilation
flags.
Variables: The makefile assigns a new value to the variable CFLAGS so that we get compiler
warnings. It uses the already-defined variable CC to represent the C compiler.
Special Variables: The makefile uses the special variable $@. This special variable means
the target (in this case, prog). Special variables can let you avoid needless typing, and
make your rules a little more easy to change and reuse.
16
# This is a comment
CFLAGS=-Wall -g
LIBS=lib.o lib2.o
HEADERS=lib.h lib2.h
BINS=prog lib_test lib2_test
all: ${BINS}
test:
./lib_test
./lib2_test
clean:
/bin/rm -rf ${BINS} *.o core *.core
Examine the Makefile in figure 3.2. Note that it adds three new targets that are common to most
Makefiles:
17
18
Chapter 4
Tools
19
20
Chapter 5
Debugging
Some errors are easily deduced from a debugger backtrace: Using the wrong variable in an index,
etc. Others, particularly when the programs behavior is unexpected but does not result in a crash, is
easier to debug with application-level debugging output.
make cleansometimes you may have failed to recompile a particular bit of code. It
happens more than youd think.
check compiler warningstheyre there to let you be lazy. Let them help you. Always use
-Wall. Consider also using -Wextra, -Wshadow, -Wunreachable-code.
Next, avoid the its not my bug syndrome. Youll see bugs that cant happen! (But it did
you have a core dump or error to prove it.) You may be tempted to say the compilers buggy! (the
OS, the course staff, etc.). Its possible, but its very unlikely. The bug is most likely in your code,
and its even more likely to be in the new code youve just added.
As a result, some debugging is easyyou can identify it quickly by looking at the output of the
program, examining the debugger output (a stack trace is very useful), and take a minute to think
about what could have caused the problem. Oftentimes, this is sufficient to find the bug, or narrow
it down to a few possibilities. This is particularly true in the fairly common case when the bug is
in a small amount of recently added code, so look there first. The svn diff command can come
in very handy for reminding yourself whats changed! Sometimes you may overlook a simple
change, or have forgotten something done at 3am the night before.
Some very useful steps in debugging, many stolen shamelessly from Kernighan & Pike:
Use a stack trace. The stack trace might tell you exactly where the bug is, if youre lucky.
Read the code carefully. Before you start tweaking, read the code and think about it for a bit.
21
Explain your code to someone else. Oftentimes when you read your own code, youll read
what you expect to see, not whats really there. Explain the code to anyone, even a pet rock.
It helps.
Think about the symptoms (crashing, incorrect result, etc.) of the bug. Look at (or remember)
the code involved. Identify the possible and likely causes of these symptoms.
Think about the correct behavior that you expected the program to exhibit, and identify your
reasoning / assumptions about what state or logic flow in the program would make it actually
do so.
Identify an experiment that you can perform that will most effectively narrow down the uni-
verse of possible causes of the bug and reasons that the program didnt behave as expected.
Repeat.
Add consistency checks to find out where your assumptions about the program state went
wrong. Is the list really sorted? Did the buffer really contain a full packet?
Add debugging output to show the state of particular bits of the program, or to identify which
parts of the program were being reached (correctly or incorrectly) before the crash.
Remove parts of the code that may be contributing to the bug/crash/etc., to see if theyre really
responsible. Note that steps like this are a lot safer if youve recently committed your code,
perhaps to a development branch.
Some of this depends on experience: over time, youll have seen more common bugs and can
identify the patterns by which they manifest themselves. But think about it like a binary search
process: is the bug caused by one of these causes, or one of those? Can you perform an experiment
to cut the field in half?
Making the bug easily reproducible is a very important first step. Imagine that your IRC server
crashed when there were 20 clients connected and theyd each sent 1000s of lines of text. The
situation that caused the bug tells you little about the reason the program failed. Does the bug still
happen if you have only 10 clients? 5? If they send only 10 lines of code? Simplify the cause as
much as possible; in its simplest verison, it may directly point out the bug!
Writing a log file is a great way to help debug. Well talk about this a little more in Section 5.2.
Being able to grep through the logfile or analyze it can make the debugging process much easier.
Using tools such as electric fence or system call tracers (below) can help identify particular bugs
rapidly.
Finally, if a bug is really persistent, start writing things down. Youll save yourself repeating
tests needlessly and will be more likely to cover the space of possibilities.
Once youve found a bug, think about two things:
22
1. Have I made this bug elsewhere in the code? Bugs that result from misunderstanding inter-
faces, etc., are likely to show up in multiple places. Do a quick check to proactively eliminate
other bugs.
2. How can I avoid making this mistake in the future? You might be able to add test cases to
automatically find them, add assertions to the code to detect if the bug happens again, use
compiler warnings to automatically detect them, or change the way you write the code to
make it impossible to make the mistake.
For the last form, you can get the PID by using the ps command. Often useful is
ps auxww | grep your_program_name
23
#ifndef _DEBUG_H_
#define _DEBUG_H_
#include "err.h"
/*
* Add some explanatory text if you add a debugging value.
* This text will show up in -d list
*/
#ifdef __cplusplus
extern "C" {
#endif
int set_debug(char *arg); /* Returns 0 on success, -1 on failure */
#ifdef __cplusplus
}
#endif
#endif /* _DEBUG_H_ */
Figure 5.1: An example set of debug macros included via the debug.h file.
24
Command Function
Executing Code
run Begin executing the program
c Continue running after breaking
s Step into next source line, entering functions
n Run until next source line, stepping over functions
Getting Information
bt Display a backtrace of all stack frames
p expr Print the value of expr
e.g., p (5*5) or p *f
bn Set a breakpoint at line n
b func Set a breakpoint at function func
list List the source code at the current point
list func List the source code for function func
up Go up to the previous stack frame
down Go down to the next stack frame
info locals Show the local variables for the stack frame
Table 5.1: Common gdb functions.
Your value of coredumpsize may be set to zero, in which case core dumps will be disabled.
To fix this, type unlimit coredumpsize (using csh), or ulimit -c unlimited (using sh
or bash). Sometimes, you dont want core files (because they take time and space to save). You can
prevent them by typing limit coredumpsize 0 (csh) or ulimit -c 0 (sh/bash).
Different operating systems have different conventions for naming core dump files. Linux may
name them simply core or core.PID (e.g., core.7337). BSD names them program.core. On
Mac OS X, the system stores all core dump files in the directory /cores.
25
Many systems prevent core dumps from processes that are setuid or setgid. If you want to get a
core from such executables, youll need to execute them as their owner.
2. GDB doesnt work. When I run it, I get a shell prompt! The most likely cause of this is that
youre execing a different shell as part of your .cshrc or .bashrc file. The best way to change
your shell is to use the chsh command to change it globally. Barring that, you should restrict the
exec to only change the shell for interactive logins. The easiest way to do so is to do the exec only
in your .login file.
Strace produces output listing each system call, its parameters, and results:
Strace runs quickly and provides a dynamic trace of the programs execution at the system call
level. From the example above, the socket() and bind() calls are listed, along with their return
codes. Even from the trace, its clear that the bind call was being made without properly initialized
variables.
Strace output can be very useful to observe the last few system calls made before a program
crashed.
26
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
int
main()
{
int s;
struct sockaddr_in sin;
memset(&sin, 0, sizeof(sin));
Running it under valgrind produces copious output. Examining some of that shows:
27
#include <stdlib.h>
#include <stdio.h>
int
main()
{
int *buf;
buf = (int *)malloc(10 * sizeof(int));
printf("Buf[0] contains %d (deliberate uninitialized access)\n", buf[0])
exit(0);
}
Like Electric Fence, below, valgrind also detects other memory errors. Its a great tool to use
when confronted with tricky memory leaks or hard-to-reproduce heisenbugs.
For the most part, Valgrind is only available on Linux.
ef <executable> [args]
You can also compile your code directly against Electric Fence by linking it with -lefence.
Using electricfence, an attempt to access memory outside of the allocated chunk will cause the
program to seg fault instead of causing strange, unpredictable behavior.
For example, if you were to type in and run the program in Figure 5.4 without electric fence, it
would (typically) run to completion:
However, if you run it under electric fence, it will crash at exactly the instruction that writes
outside the malloc buffer:
28
#include <stdlib.h>
#include <stdio.h>
int
main()
{
char *buf;
buf = malloc(64);
fprintf(stderr, "accessing start of buf\n");
buf[0] = \0;
fprintf(stderr, "accessing end of buf\n");
buf[63] = \0;
fprintf(stderr, "accessing past end of buf\n");
buf[64] = \0;
fprintf(stderr, "done with overwrite test\n");
exit(0);
}
The error message from ElectricFence isnt particularly useful (though it does tell you the pro-
cess ID that died, which is handy if youre on a system like the Andrew Linux machines that name
core dump based on PID), but the resulting core dump file is very valuable:
29
to raw packets; its easy to use from the command line and produces useful output. Wireshark
understands many more protocols, has a more powerful set of functions (such as reassembling TCP
streams), and has a handy GUI.
XXX-TODO: Add a tcpdump or wireshark example!
Send text-only data. It may seem obvious, but if you make your data contain only ASCII
text, youll be able to examine it more easily with conventional UNIX command-line tools
such as wc, diff and grep.
Use the cmp command to compare them. cmp will tell you the byte and line at which the
two files first differ.
Use the od command to view binary data. od will convert your data into a hex or octal
dump format. You can then diff the output of this.
Open it in an editor. Emacs and vi will display binary data in a way that will allow you to
visually scan for differences. Its not perfect, but as a quick hack, it may help you get over a
hump and show you some obvious difference in the files.
Examine more powerful diff tools. Students in the past have had luck using a visual binary
diff tool:
http://home.comcast.net/~chris-madsen/vbindiff/
These techniques are not specific to project 2: theyre useful in dealing with any system that
outputs data.
30
Chapter 6
Finding the sweet spot in documenting your code can be difficult. On one hand, a camp of developers
argues that the code is the documentation. Their rationale is that if code can change independently
of documentation, then documentation can and will get out of date. On the other hand, a new user
of the code or libraries developed by people in the first camp may disagreestrongly.
We dont know the perfect answer to this debate, but we do have a few suggestions:
Dont document the obvious, and make your code obvious.
Comments in code like the above add unnecessary clutter to the obvious. Why is it obvious? We
have several hints:
Note the example in Figure 6.1. This code snippet from a previous 15-441 project shows two
bugs. First, the project uses a magic consant for LSA announcements (how should we remember
that type 1 is an LSA announcement?) Ironically, the constant INCOMING_ADVERTISEMENT is
defined earlier in the file; the programmers just forgot to use it in this case. Imagine the fun tracking
31
down a bug if they later changed the value used to indicate LSA announcements in the #define and
only half the code changed? Second,the code is almost completely self-documenting, in a good way.
The comments are extraneous.
6.0.1 Style
Two spaces? Four? A tab? Hungarian notation? Gnu style?
Within bounds, it doesnt matter.
What matters is that you pick a style, and use it consistently. The style should do a few things
for you:
Distinguish important things, like class names in C++, variables, globals, etc.
Not be excessively long and painful. Naming all of your variables bgfooBarThisIsADog will
only cause you grief and slow you down. Use naming that is appropriate to the circumstance.
The variable i in the above code fragment is clearits used immediately after its declared
(presumably), its a standard variable name, etc. On the other hand, a global variable named
config.maxLineLen (or config.max_line_len) is probably more appropriate than calling it
c.mll, which may leave subsequent readers bitter.
Using the style consistently is much more important than the details. That said, over time a few
conventions have popped up that can greatly help make code more readable:
Name functions with active verbs. isValid() is a better name than checkValid(),
because the semantics are more clear.
Introduce functions with a brief comment explaning their semantics and anything the caller
really needs to know about using it.
The source code to the primes game/utility. Remember that Sieve of Eratosthenes that you
probably wrote a few times in earlier CS classes? This program has a really nice implementa-
tion of it that doesnt bother with a lot of micro-optimizations (like storing things in bitmaps)
but instead takes a bunch of macro-optimizations and ends up faster than a kitten in the
dryer. Its about 300 lines of code. http://www.cs.cmu.edu/~dga/systems-se/
samples/primes.tar.gz
32
6.2 Communication
At this point, you may well ask: What in the world is a chapter about writing and graphics doing in
a book about hacking? The answer is that programs do not stand in a vacuum, and the similarities
between good code and good writing are startling.
In the words of E.B. White, Eliminate Unnecessary Words! Good technical communication
is concise and precise. Use the right words to describe exactly what you mean, no more and no
less. Strive for clarity in all that you write. Youll note that this is much like good programming.
A well-written program contains a minumum of unnecessary, repetitive cruft, is easy to understand,
and does exactly what its supposed to do and nothing more. So too does a well-written technical
document.
Bibliography
[1] Brian W. Kernighan and Rob Pike. The Practice of Programming. Addison-Wesley. ISBN
0-201-61586-X.
33
34
Chapter 7
35
36
Chapter 8
Scripting
8.0.2 One-liners
Most scripting languages have some kind of support for one-liners quick bits of code that you
may even type in each time at the shell to accomplish a particular task. A good example of a one-
liner is changing every occurrance of foo to bar in a group of files:
37
8.0.3 The Shell
Just because youve learned a high level scripting language, dont neglect the basics of the shell. Its
a tool youll interact with every time you run a command, move files, list directories, etc. As a tool
you use so often, learn some of its time-saving features!
Things anyone should be able to do with the shell:
For loops
Redirection
sed s/search/replace/
which will take all text input to it (though a pipe, etc.) and replace the first occurrence of search
with replace. Append /g if you want it to replace all occurrences.
Learn the basics of grep. I use some handy aliases to rapidly grep through collections of files
recursively from the current directory: These aliases use the find command to traverse all files of
particular types and pass them to grep. In csh aliases format, the aliases are:
38
8.0.5 Scripting languages go well beyond
A nice thing about many of the so-called scripting languages is that they go well beyond simple
automation. For many small to medium sized (or sometimes larger) programs, Ruby or Python is
an ideal implementation environment. In our research, we use Ruby for complex data analysis (with
appropriate C-based libraries to do fast math, etc.). Perl, Python, and Ruby all form the basis of
innumerable complex, fully featured and powerful Web sites, and the pain to develop these sites is a
small fraction of what it would be in a lower-level language such as C.
Time invested in learning a high level, interpreted language will pay itself back fast.
Bibliography
[1] Dave Thomas, Chad Fowler, and Andy Hunt. Programming Ruby: The Pragmatic Program-
mers Guide. Pragmatic Bookshelf, second edition, October 2004. ISBN 978-0-9745140-5-5.
39
40
Chapter 9
Program Design
How do you design a program? What is involved in its design? There are two critical aspects of the
design that youll need to address:
Data structures: What data does the program operate on? How does it store it?
Modules: How is the program divided into components? How do those components interact?
Some programs are defined primarily by the way they operate on data. For example, suppose you
were writing a program to predict the weather. A great deal of your programs design would center
around the way you represent huge volumes of weather sensor data, models of the atmosphere, and
so on.
Other programs are defined more by what they do or how they interact with the rest of the world.
The IRC server project falls into this category: it must send and receive data from users, process
their commands, and figure out which other users should receive messages. But beneath this surface
description still lingers the issue of data structures: How should your IRC server remember things
like:
The second question addresses how you decompose the functionality in your program into in-
dividual pieces of source code. There are a nearly arbitrary number of ways you could break this
down, but in practice, there are ways that make a lot more sense than others.
DRY is a great rule of thumb for deciding what code should be pulled into a module. If you
find yourself repeating the same or nearly the same code over and overor, worse, copy and
pasting it!its time to consider separating it into a nice, clean module of its own that you can
use elsewhere.
41
2. Design principle 2: Hide Unnecessary Implementation Details
Doing so will make it easier to change those details later without changing all the code that
makes use of a module. For example, consider the difference between two functions:
int send_message_to_user(struct user *u, char *message)
int send_message_to_user(int user_num, int user_sock, char *message)
The first encapsulates the details of the user into a struct. If those details change, the caller
functions dont have to do anything different. In contrast, the second exposes more internal
details about how the function is going to get data to the user.
Putting these two principles together, suppose that you had a snippet of code in your IRC server
like this:
int
send_to_user(char *username, char *message)
{
....
struct user *u;
for (u = userlist; u != NULL; u = u->next) {
if (!strcmp(u->username, user)) {
...
}
}
}
and you found yourself using that same linked list search again to perform some other operation
(say, kick this user out of a channel). This would be a good time to sit back and think, ah-ha! I
could really go for a find_user(username) function:
struct user *
find_user(username)
{
struct user *u;
for (u = userlist; u != NULL; u = u->next) {
if (!strcmp(u->username, user)) {
return u;
}
}
return NULL;
}
If the linked list user search became a bottleneck when your system became more popular than
Google and had 100,000 users, you could change the search to a hash table lookup without
changing any of the callers.
42
Youve replaced several lines of code with one simple function call thats clearly named and
describes exactly what its doing. The code is more readable and your partner is happier.
Corolary: Write shy code: Dont reveal your details to others, and dont interact with too
many other modules. If you do need to modify the way a particular module works, youll need to
change all of the places that modify it. Keeping your code relatively shy (not interacting with
too many other places, when possible) is a good way to help keep the size of the modules interface
small and focused. In general, youll probably find that there are some modules that can be extremely
independent: utility functions, lists, hashes, etc.; other modules are more like integrators that make
use of these other modules. The important point is that while its very reasonable for, say, your
connection management routines to depend on linked lists, your linked lists shouldnt depend on
your connection management routines!
Design principle 3: Keep it simple!
Design some things for reuse: lists, hashes, etc. Put a bit of effort into making them reusable,
add good tests, make them a bit general. Youll use them in both project 1 and 2, and beyond.
The first step yields fairly classical modules that implement clearly defined functions. Once you
have a general idea of the way your program will work, you should be able to implement a few of
these modules.
The second step says to pare down your program to its basic essence before you start to add on
the details. In a word processing program, its probably best to allow the user to enter text before
you start working on the spell checking; in your IRC server, youd probably be better served by
accepting connections and buffering lines before you start implementing message routing.
Note that this tip is not just design incrementally, but design for incremental happiness. Try
to pick milestones that:
43
Have clearly defined, useful behavior
The first suggestion means that you want to be able to test whether the functions youve imple-
mented work, and to reduce the amount of work you do, youd like to ensure that any tests you write
for the incremental version also work for the final version.
The second suggestion is more for your own motivation: getting a particular bit of your code
working for the first time is a great way to stay excited and constantly see real progress in your code,
as opposed to feeling like youre hitting a big brick wall that you have to leap over in a single bound.
Connection test: Can a client connect to the port that your server is listening on?
Alternate listen port test: Can you specify an alternate port on which to have your server
listen?
and so on. In general, for these tests, you may be better off writing the tests in a scripting
language. For the class, we write many of our tests in Ruby, and many of them using the expect
package XXX-have we eliminated expect yet?
44
Insert an item, delete it, and then try to retrieve it
Delete every item in the hash table and then insert more
Note the order in which those tests are expressed: simple ones first and complex ones later. Its
likely that the first time you try a simple test, youll find some bugs. Once youve fixed those, move
on to more complex tests to turn up harder bugs, etc., etc. Much like you would in debugging, try to
keep the tests as simple as possible while tickling the corner cases you want to explore, so that when
you have to debug the problems they find, you can do so easily.
Bibliography
[1] Brian W. Kernighan and Rob Pike. The Practice of Programming. Addison-Wesley. ISBN
0-201-61586-X.
45
46
Chapter 10
Coding Tricks
Do tests against constants with the constant first: if (NULL == x). If you leave out an =, it
cant be mistaken for assignment, because youre assigning to a constant.
47
48
Chapter 11
Human Factors
If you dont, its much more likely that your code doesnt work robustly. It may rely on strange
side-effects; it may fail under unexpected boundary conditions; it may break when you add more
code. Instead, spend the time to understand how to make the code work properly:
49
Youll learn something. A new API, a new language feature, a deeper understanding of the
spec, etc.
And think about it: You could spend twenty frustrating, pointless minutes trying random things,
and get code that maybe works sometimes and fails some of the tricky test cases that the evil TAs,
professors, and real-world users feed it. Or you could spend the same amount of time (or a bit longer)
really learning how to make the code work, and come away a better programmer. Frustration, or
making yourself more skilled and valuable? Tough choice. :)
This kind of programming often creeps up when using complex APIs in which operations must
be performed in particular orders; examples include things like GUIs or operating system device
drivers. (Maybe if just set that register to zero first...) However, weve seen the same thing creep
up in 15-441 when debugging. Its most easily identified by watching the process you take while
programming. As soon as you find yourself thinking Ill just try X, watch out. Youre walking
very close to the cliff of coincidence.1
1. How much time would it take you to write in C a program that took a (smallish) list of integers
on the command line and output them in sorted order? Assume you dont have to be too
rigorous about input checking, etc.
2. How much time will it take you to track down the bug thats causing your IRC server to crash
after its been running for a few minutes?
Starting from a blank editor and not using any existing code, it took me about six minutes to
write the first program to the point where it worked, and just under five more minutes to add basic
error handling. The program is 41 lines long including whitespace. So a reasonable rough answer
for question one would be 10 to 30 minutes or less if a programmer happened to remember the
error-handling syntax of the strtoul function, which I didnt.2
Question two is hard just to answer! With a tricky bug, it could easily take a half an hour or
longer just to find a way to reproduce the problem. Or an hour. Or a day, depending on the subtelty
of the bug.
As both a programmer and a student in this class, you would be much better off having to
implement a bit of new functionality 60 minutes before the deadline than having to debug something
that causes your entire assignment to fail. Fix bugs early! It will help smooth out the unpredictable
time requirements you run into when working on the rest of the assignment.
1 Note that we dont mind trying in a design sense. Ill try using the system strstr() function and see if its
fast enough is a perfectly valid approach. Ill try calling frobzwiggle() twice to see if it makes the code work is
not.
2 As an interesting comparison, the same program, sans error handling, took two lines and less than one minute to
write in Ruby. One line was the path to the Ruby interpreter...
50
11.3 Team Dynamics
For many of you, this class is the first time youve had to program with a partner. Getting the most
out of a team can take a little effort to make sure things work well, but here again, a bit of effort
invested is worth ityour team can complete the project with each person doing about 51% as much
work as it would take to complete it solo. You can approach the problem from three angles (and we
recommend tackling all three!):
Communication
51
52
Chapter 12
Editors
Knowing your editor and knowing it well can save you countless keystrokes and small bits of time.
In aggregate, these savings add up rapidly.
The vi vs emacs debates have raged and will continue to rage about peoples favorite editor.
In many cases, it doesnt matter. There are outstanding, efficient systems hackers who use either.
This doesnt mean that all choices are equivalentwriting code with notepad or cat isnt going
to cut it. An advantage to either of these tools is that they are cross-platform. We suggest picking
one, and becoming fluent in using it. The skills will serve you well for years.
There are a number of features an editor can offer you (not all do, and that doesnt make them
bad), but some things to look for include:
Syntax highlighting: Shading operators, function calls, comments, etc., differently can make
it much easier to scan through code for the thing youre looking for.
Tags: Several editors and integrated development environments (IDEs) offer single click or
keystroke commands to let you quickly bounce to and from the file in which a function is
declared or implemented.
Documentation cross-references: Some editors and IDEs can pop up context-sensitive help
on function names or partial function names. Can be quite handy when using a complex API.
53
Conventions
Commands and typed text is in Mono-spaced font. The keystroke C-e means control-e. The
keystroke M-C-e means Meta-Control-e. In Emacs, Meta is accomplished either by holding down
the Alt key or by first typing Escape.
Basic Movement
In keeping with our principles, we seek movement keys that require minimum hand motion and
provide us with the most bang for the buck. The following commands seem to accomplish that as a
good starting point:
Command Effect
C-f Next character
C-b Previous character
C-p Previous line
C-n Next line
M-f Next word
M-b Previous word
C-e End of line
C-a Beginning of line
C-v Next page (down)
M-v Previous page (up)
Command Effect
C-s and C-r Incremental search forward or backward.
Command Effect
M-C-n Jump forward to the matching parenthesis
M-C-p Jump backward to the previous parenthesis
Multiple Windows
Command Effect
C-x o Switch to other window
C-x b Exchange the current buffer with a different one
C-x 4 b Exchange the other half screen buffer with a different one, creat-
ing that buffer if it didnt exist.
Auto-completion
Command Effect
M-/ Find closest word, dynamic abbreviation. Looks backwards and
expands to the closest word that starts with the same letters al-
ready typed. Searches forward if it couldnt find anything back.
M-TAB Expand tab: Tries to complete the word to a function or constant
name. May open a separate buffer for disambiguation.
54
Cutting and Pasting
Emacs stores all deleted text (other than backspaced characters) in a kill ring. If you kill multiple
things in a row without intervening commands, it appends these to the same buffer. Otherwise, it
starts a new buffer in the kill ring.
C-y will yank text from the kill ring. If you hit M-y after yanking some text, emacs will
change the yanked text to the next older entry in the kill ring, and so on. Give it a try.
Macros
An amazingly useful feature in emacs is the quick construction of small macros.
Command Effect
C-x ( Start recording a keyboard macro
C-x ) End recording
C-x e Invoke the last created macro
M-x name-last-kbd-macro Assigns a name to the last created macro.
M-x insert-kbd-macro Inserts a definition of the keyboard macro into the current buffer.
You can use this to capture your recorded macro and save it in
your .emacs file for later use. Consider binding
12.0.5 Tags
Tags are a handy, powerful feature in good editors that give the editor a rudimentry knowledge of the
symbols, function names, etc., in your programs. Using tags, you can quickly jump to the definition
or prototype for a function or use auto-completion for longer symbol and function names.
Emacs uses a TAGS file created by the program etags. Creaing the file is simple: run etags
*.[ch]. Actually, I suggest either creating a makefile target or an alias for it thats a little more
complete:
etags *.{c,C,cc,cpp,h,hh,hpp,cpp,java} Makefile
to cover most of the bases. Once the TAGS file exists, using it is a piece of cake:
Command Effect
M-. Visit tags table.
C-u M-. Finds the next tag
M-0 M-. Synonym for find-next tag. Easier to type.
C-u -M-. Find previous tag (negative modifier)
M-tab Tag complete symbol. Type the first few characters of a function
or variable and hit M-tab to autocomplete it.)
M-, Search for next tab. Find tag is a strict match; this finds sub
expressions that contain what youre looking for. Very handy.
Tab In some prompts from emacs, the tab key will also complete on
tag values.
55
for you features in some IDEs can work against you in a systems programming context, when
debugging involves multiple processes or machines or operating system kernel code.
56
Chapter 13
Recommended Resources
57