In this section we’re going to have fun.
Update your section repository,
cd s02, and
make. This will built a number
of fun programs.
Let’s run one:
$ ./fun01 😿😿😿😿😿😿😿😿 no fun 😿😿😿😿😿😿😿😿
That wasn’t fun!
These programs are puzzles. Look at
fundriver.cc and you’ll see the ground
rules. The driver’s
main function first creates a single C string that
contains all program arguments, separated by spaces. It then calls the
function, passing in that string. The
fun function returns an integer; if
fun(str) returns 0, then the driver has fun, and if it returns anything
else, no fun is had (the function
no_fun() is called, which prints the
We want to have fun, how can we have fun? Well might as well look to see what
the function is doing! (Open
Looks like this
fun function will return 0 if and only if the arguments
contain an exclamation point. Let’s test that:
$ ./fun01 ! 🎉🎉🎉🎉🎊🎊🎊🎊🌽🌽🌽🎊🎊🎊🎊🎉🎉🎉🎉 FUN 🎉🎉🎉🎉🎊🎊🎊🎊🌽🌽🌽🎊🎊🎊🎊🎉🎉🎉🎉 $ ./fun01 'yay!' 🎉🎉🎉🎉🎊🎊🎊🎊🌽🌽🌽🎊🎊🎊🎊🎉🎉🎉🎉 FUN 🎉🎉🎉🎉🎊🎊🎊🎊🌽🌽🌽🎊🎊🎊🎊🎉🎉🎉🎉 $ ./fun01 'amazing!!!!!!!!!!!!!!!!!' 🎉🎉🎉🎉🎊🎊🎊🎊🌽🌽🌽🎊🎊🎊🎊🎉🎉🎉🎉 FUN 🎉🎉🎉🎉🎊🎊🎊🎊🌽🌽🌽🎊🎊🎊🎊🎉🎉🎉🎉 $ ./fun01 'amazing?' 😿😿😿😿😿😿😿😿 no fun 😿😿😿😿😿😿😿😿
The idea of not having fun is deeply painful. So is there any way that you
could, for example, prevent the
no_fun() function from running? That you
could stop the program if it reached
This calls for a debugger breakpoint. A debugger is a program that manages the execution of another program. It lets you run a program, stop it, and examine variables, registers, and the contents of memory. Among the most powerful debugger features is the ability to stop a program if it ever reaches an instruction. This is called “setting a breakpoint”: the breakpoint marks a location that, when reached, “breaks” the program and returns control to the debugger.
How would you stop the program from executing
$ gdb fun01 (gdb) b no_fun
Now if we run the program with non-fun arguments
we will stop before printing “no fun”!
If you’re not careful, though, it’s possible to accidentally step through and
print the message. You can do this one step at a time (demo
r, followed by
ses); or you can do it by continuing the program by accident (demo
r followed by
What if you wanted to make this kind of accident wicked unlikely? Well, you could set more breakpoints!
(gdb) b foo (gdb) r Breakpoint 1, main (argc=1, argv=0x7fffffffdf88) at fundriver.cc:34 34 no_fun(); (gdb) x/20i $pc => 0x400c47 <main(int, char**)+375>: lea 0x344(%rip),%rsi # 0x400f92 0x400c4e <main(int, char**)+382>: lea 0x20156b(%rip),%rdi # 0x6021c0 <_ZSt4cerr@@GLIBCXX_3.4> 0x400c55 <main(int, char**)+389>: callq 0x400a70 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt> 0x400c5a <main(int, char**)+394>: mov $0x1,%edi 0x400c5f <main(int, char**)+399>: callq 0x400a90 <exit@plt>
We’ve stopped at the first instruction in the
no_fun function (which has
actually been inlined into
main, but never mind). But can also set more
breakpoints! For example, at the second instruction and the third:
(gdb) b *0x400c4e (gdb) b *0x400c55
But what if you forgot these breakpoints??? Well, that’s a good case for
.gdbinit, a file of GDB commands that runs every time you start GDB.
Here begins a quick overview of interesting GDB commands. The commands are linked to their descriptions in the GDB manual, which also describes many more amazing commands.
||Execute file passed as command line argument to
You can supply arguments to
||Pause execution when a particular point in the code is reached
||Pause when the value of an expression changes|
||Run until the next breakpoint|
||Steps to the next line of code (enters function calls)|
||Steps to the next line of code (steps over function calls)|
||Steps to the next instruction (enters function calls)|
||Steps to the next instruction (steps over function calls)|
||Runs until the current function returns|
||Runs until a given line of code|
||Delete a breakpoint by number|
||Kill the currently-running program|
||Examine memory at a given address
||Print the value of a register or C++ expression|
||Output assembly instructions
||Show source code around the current instruction pointer|
||Print the call stack|
||Examine the context of frame number
||Move up to the caller frame|
||Move down to the callee frame|
||Change thread context in a multithreaded program|
||Enable the TUI, which shows code and control in separate “panels”|
||Change the TUI layout. Also try
||Control the TUI.
||Refresh the screen (use if things look janky)|
||Stop warning about killing programs|
||Put in your
gdb cheatsheet: http://darkdust.net/files/GDB%20Cheat%20Sheet.pdf
Many more GDB commands exist! Time spent learning a debugger is time well spent. On modern GDBs you can even run code backwards.
The LLDB debugger is better supported on Mac OS than GDB. Most GDB commands work on LLDB as well.
lldb cheatsheet: https://lldb.llvm.org/lldb-gdb.html
objdump program is useful for printing out properties of an executable.
objdump -t prints out the program’s symbol table, which includes the names
of all functions and global variables in the executable, the names of all the
functions the executable calls, and their addresses (though addresses may change when the executable is run).
objdump -d and
objdump -S disassemble all the code in an executable.
Now let’s work through a couple more funs. We’ll try to understand the operation of the funs using GDB and assembly, though for the first 6 funs, the C++ is there if you get stuck.
ASSEMBLY IS HARD. And trying to understand assembly from first principles, without running it, is really hard! As with many aspects of systems, you will have more luck with an approach motivated by experimental science. Try and guess at an input that will work, using cues from the assembly. Develop a hypothesis and test it. For the bomb, you don’t need to fully understand the assembly, you just need to find an input that passes each phase. (That said, you will often end up understanding the assembly—but only after completing the phase with the help of experiments.)
It is also often effective to alternate between working top down, starting from the entry to a function, and bottom up, starting at the return statement. Working from the bottom up, you can eliminate error paths and trace through how the desired result is calculated. Working from the top down, you can develop hypotheses about how the input should look. As long as you have breakpoints set, you can experiment with a free and easy heart. (And if the bomb goes off, who really cares?)