In this section weβre going to have fun.
College students are required to attend section live. TFs will record your attendance. For more on the attendance policy and what to do if you miss section refer to the syllabus here and here.
Extension students are welcome to attend section live (in-person or zoom) or watch the recording (Canvas > DCE Class Recordings). Both methods count towards your participation grade. If you attend a live section, TFs will record your attendance. If you watch the recording, fill out this reflection form.
Setup
Update your cs61-sections
repository, and then cd asms1
. If you don't have the section repository yet, run git clone git@github.com:cs61/cs61-sections
.
Run make
. This will build a number of fun programs.
ARM64 instructions
ARM64-based computers, such as Macs with Apple Silicon and some Windows laptops with Snapdragon, require a special procedure to run. Only the first 6
fun
programs will build on the native terminal; you will need tomake clean; make
inside Docker to get the rest. Inside Docker, you need special instructions for using GDB; see below.If you have problems with the ARM64 instructions in this section, check that within Docker,
cs61-docker-version
reports22.arm64
or later. If it does not, pull from acs61-lectures
orcs61-psets
directory and build your Docker environment again (cd cs61-XXX/docker; ./cs61-build-docker
).
Letβs run one:
$ ./fun01
πΏπΏπΏπΏπΏπΏπΏπΏ no fun πΏπΏπΏπΏπΏπΏπΏπΏ
That wasnβt fun!
These programs are puzzles. Look at fundriver.cc
for 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 fun
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 no fun
message).
We want to have fun, how can we have fun? Might as well see what the function
does! (Open fun01.cc
)
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 πΏπΏπΏπΏπΏπΏπΏπΏ
GDB
It is deeply painful to have no fun. So is there any way that you could
prevent the no_fun()
function from running? That you could stop the program
if it reached no_fun()
?
You can do this with 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 no_fun()
?
First, launch GDB on the fun01
program.
$ gdb fun01
Next, set a breakpoint on no_fun
.
$ (gdb) b no_fun
Now if we r
/run
the program with non-fun arguments...
(gdb) r awesome?
...we will stop before printing βno funβ! (Try it!)
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 (type r
, followed by
several s
es); or you can do it by continuing the program by accident (type
r
followed by c
).
What if you wanted to make this kind of accident wicked unlikely? Well, you could set more breakpoints!
If you have an ARM64-based computer, remember to use rf
instead of r
.
(gdb) b no_fun
(gdb) r
Breakpoint 1, no_fun () at fundriver.cc:15
15 std::cerr << "πΏπΏπΏπΏπΏπΏπΏπΏ no fun πΏπΏπΏπΏπΏπΏπΏπΏ\n";
(gdb) x/20i $pc
=> 0x4000001305 <main(int, char**)+389>: lea 0xd5e(%rip),%rsi # 0x400000206a
0x400000130c <main(int, char**)+396>: lea 0x2e4d(%rip),%rdi # 0x4000004160 <_ZSt4cerr@GLIBCXX_3.4>
0x4000001313 <main(int, char**)+403>: call 0x4000001130
0x4000001318 <main(int, char**)+408>: mov $0x1,%edi
0x400000131d <main(int, char**)+413>: call 0x4000001150
0x4000001322 <main(int, char**)+418>: endbr64
0x4000001326 <main(int, char**)+422>: mov %rax,%rbx
These addresses and offsets may be different in your system. Weβve stopped at the first instruction in the no_fun
function. But thereβs
nothing stopping us from setting more breakpoints! For example, at the second
instruction and the third:
(gdb) b *0x400000130c
Breakpoint 2 at 0x400000130c: file fundriver.cc, line 15.
(gdb) b *0x4000001313
Breakpoint 3 at 0x4000001313: file fundriver.cc, line 15.
Alternatively, you could set your breakpoints using function names and offsets instead of direct memory addresses.
(gdb) b *main+396 Breakpoint 2 at 0x400000130c: file fundriver.cc, line 15. (gdb) b *main+403 Breakpoint 3 at 0x4000001313: file fundriver.cc, line 15.
Type info breakpoints
to see the breakpoints you've set.
Now type k
(to kill
the program) and q
or Ctrl-D
(to quit
GDB).
Relaunch GDB and type info_breakpoints
again. If you're going to be using GDB many times on one program, it's annoying to have to reset your breakpoints every time your launch it. You also don't want to accidentally forget to set them (and run the risk of not having fun π).
This is a good case for .gdbinit
, a file of GDB commands that runs every time you start
GDB.
(Optional) TUI introduction
When stepping through a program using GDB, it's really helpful to be able to see the contents of registers and the upcoming instructions. Many students like using TUI, a user interface mode in GDB. TUI has many possible configurations, but the following is a great jumping off point. Launch GDB and then type:
tui enable
layout asm
layout regs
Some good things to know:
- Ctrl-L to refresh the window if it ever looks really messed up
- Ctrl-X O to change the "active window" (repeated applications of this command will cycle between the regs, asm, and terminal windows. This is useful if you want to be able to use the up/down arrow keys in one of those windows)
GDB cheatsheet
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.
Execution commands
Command | Description |
---|---|
run (r ) |
Execute file passed as command line argument to gdb You can supply arguments to r ; if none, uses the last set passed ARM processors require a different command |
break (b ) |
Pause execution when a particular point in the code is reached Examples: break FILENAME:LINE , break FUNCTION , break FILENAME:FUNCTION |
watch |
Pause when the value of an expression changes |
continue (c ) |
Run until the next breakpoint |
step (s ) |
Steps to the next line of code (enters function calls) |
next (n ) |
Steps to the next line of code (steps over function calls) |
stepi (si ) |
Steps to the next instruction (enters function calls) |
nexti (ni ) |
Steps to the next instruction (steps over function calls) |
finish |
Runs until the current function returns |
advance LINE |
Runs until a given line of code |
info breakpoints |
List breakpoints |
delete N (d ) |
Delete a breakpoint by number |
kill (k ) |
Kill the currently-running program |
quit (q ) |
Quit GDB |
Examination commands
Command | Description |
---|---|
x ADDR (examine ) |
Examine memory at a given address Examples: x/dw $rax (print in decimal format [d ] the 4-byte int [w ] starting at %rax ; x/10xg $rsi (print in hex the 10 unsigned long s [g ] starting at %rsi ; x/10i $rip |
print EXPR (p ) |
Print the value of a register or C++ expression |
display EXPR (disp ) |
Like print , but prints each time a step is taken |
disassemble (disas ) |
Output assembly instructions Examples: disas FUNCTION , disas ADDR1,ADDR2 , disas ADDR,+LENGTH |
list (l ) |
Show source code around the current instruction pointer |
backtrace (bt ) |
Print the call stack |
frame NUM (f ) |
Examine the context of frame number NUM (so you can see caller variables, for example) |
up (u ) |
Move up to the caller frame |
down (d ) |
Move down to the callee frame |
thread N |
Change thread context in a multithreaded program |
info registers |
Show registers |
Control commands
Command | Description |
---|---|
tui enable |
Enable the TUI, which shows code and control in separate βpanelsβ |
layout next |
Change the TUI layout. Also try layout help |
Ctrl-X |
Control the TUI. Ctrl-X 1 shows two panels, Ctrl-X 2 shows three, Ctrl-X o moves focus |
Ctrl-L |
Refresh the screen (use if things look janky) |
set confirm off |
Stop warning about killing programs |
add-auto-load-safe-path DIRECTORY |
Put in your ~/.gdbinit file; tells GDB to read the DIRECTORY/.gdbinit file if it starts in DIRECTORY |
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.
Other programs
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
The 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.
More fun
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 pset, 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?)
Though you already know what input satisfies fun01
, try stepping through it with these new tools to draw connections between the C++ code and assembly.