By now you have typed bt then print counter more times than you would like.
GDB lets you name a sequence and call it once. It has two ways of doing that,
its own scripting language and Python, and it is worth seeing the same job done
both ways.
We are still using the counter program from the attaching lesson.
GDB’s own scripting
define bundles commands under a name:
define peek
bt 1
print counter
end
Now the whole check is one word:
(gdb) peek
#0 0x0000770df18c4b7a in __GI___clock_nanosleep (...) at clock_nanosleep.c:78
$1 = 5
bt 1 is a backtrace limited to the innermost frame, which is all you need when
you already know what the program does and only want to know where it is.
Definitions take arguments. $arg0 is the first, $arg1 the second, and
$argc is how many arrived:
define bump
set var counter = counter + $arg0
print counter
end
(gdb) bump 1000
$2 = 1005
It has flow control too, so a definition can make decisions:
define level
if counter > 1000
printf "counter is high: %d\n", counter
else
printf "counter is fine: %d\n", counter
end
end
(gdb) level
counter is fine: 0
(gdb) set var counter = 2000
(gdb) level
counter is high: 2000
Put these in the .gdbinit next to the binary and they are there every time you
open it. A definition you have to retype is not saving you anything.
The same thing in Python
GDB ships with a Python interpreter and an API onto its own internals. Anything
after python runs in it:
(gdb) python print(gdb.parse_and_eval("counter"))
1005
gdb.parse_and_eval evaluates an expression in the target and hands back a
value you can do arithmetic on. gdb.execute runs a GDB command, and with
to_string=True gives you its output as text instead of printing it.
A command is a class. Put this in peek.py:
import gdb
class Peek(gdb.Command):
"""Show where we are and what the arguments were."""
def __init__(self):
super().__init__("peek", gdb.COMMAND_USER)
def invoke(self, arg, from_tty):
frame = gdb.selected_frame()
print(f"in {frame.name()} at {frame.find_sal().line}")
for sym in frame.block():
if sym.is_argument:
print(f" {sym.name} = {sym.value(frame)}")
Peek()
invoke is what runs when the command is typed. Creating the instance at the
bottom is what registers it. Load it and use it:
(gdb) source peek.py
(gdb) break check
(gdb) run Evan
Breakpoint 1, check (name=0x7fffffffe0f7 "Evan") at hello.c:5
(gdb) peek
in check at 5
name = 0x7fffffffe0f7 "Evan"
That is doing something define cannot. It walked the frame, found which
symbols are arguments, and printed each one, without being told their names.
Functions you can use inside expressions
gdb.Function adds something callable from any GDB expression, with a $ in
front:
class IsZero(gdb.Function):
"""True when the value is zero."""
def __init__(self):
super().__init__("iszero")
def invoke(self, v):
return int(v) == 0
IsZero()
(gdb) print $iszero(0)
$1 = 1
(gdb) print $iszero(5)
$2 = 0
Which means it works in a breakpoint condition: break check if $iszero(counter).
The governor, rewritten
The attaching lesson kept the counter in range with a conditional breakpoint and a command list. Here is the same rule as a Python breakpoint:
import gdb
class Governor(gdb.Breakpoint):
def __init__(self):
super().__init__("counter.c:10")
self.silent = True
self.hits = 0
def stop(self):
if int(gdb.parse_and_eval("counter")) >= 1010:
gdb.execute("set var counter = 1000")
self.hits += 1
print(f"[governor] reset #{self.hits}")
return False
Governor()
stop runs every time the breakpoint is hit, and its return value decides
whether the process actually stops. Returning False means it never does, so
this runs as a permanent rule rather than an interruption:
counter = 1008
counter = 1009
counter = 1010
[governor] reset #1
counter = 1001
counter = 1002
The same off-by-one as before. It resumes on 1001 because line 10 is counter++
and the breakpoint fires before that line runs.
Compare it with the commands version earlier in the course. The GDB script
was shorter and needed nothing installed. The Python version can count its own hits, which is
already past what define will do for you, and it could just as easily write to
a file or make a decision no breakpoint condition could express.
Which to use
Use define for a sequence of commands you type a lot. It is quicker to write,
it lives in .gdbinit, and it has no dependencies.
Reach for Python when you need to inspect something rather than just run commands: walking frames, reading structures, keeping state between hits, or deciding something a breakpoint condition cannot express.
Both live in the same place. .gdbinit takes define blocks directly, and
Python files are loaded with source:
source ~/tools/mycommands.py
Up next, let’s configure gdb to be a reverse engineering and exploit development powerhouse.