Revision: $Id: agentexpr.html,v 1.1 1999/10/05 22:19:35 msnyder Exp $
In some applications, it is not feasable for the debugger to interrupt the program's execution long enough for the developer to learn anything helpful about its behavior. If the program's correctness depends on its real-time behavior, delays introduced by a debugger might cause the program to fail, even when the code itself is correct. It is useful to be able to observe the program's behavior without interrupting it.
Using GDB's trace and collect commands, the user can
specify locations in the program, and arbitrary expressions to evaluate
when those locations are reached. Later, using the tfind
command, she can examine the values those expressions had when the
program hit the trace points. The expressions may also denote objects
in memory -- structures or arrays, for example -- whose values GDB
should record; while visiting a particular tracepoint, the user may
inspect those objects as if they were in memory at that moment.
However, because GDB records these values without interacting with the
user, it can do so quickly and unobtrusively, hopefully not disturbing
the program's behavior.
When GDB is debugging a remote target, the GDB agent code running on the target computes the values of the expressions itself. To avoid having a full symbolic expression evaluator on the agent, GDB translates expressions in the source language into a simpler bytecode language, and then sends the bytecode to the agent; the agent then executes the bytecode, and records the values for GDB to retrieve later.
The bytecode language is simple; there are forty-odd opcodes, the bulk of which are the usual vocabulary of C operands (addition, subtraction, shifts, and so on) and various sizes of literals and memory reference operations. The bytecode interpreter operates strictly on machine-level values -- various sizes of integers and floating point numbers -- and requires no information about types or symbols; thus, the interpreter's internal data structures are simple, and each bytecode requires only a few native machine instructions to implement it. The interpreter is small, and strict limits on the memory and time required to evaluate an expression are easy to determine, making it suitable for use by the debugging agent in real-time applications.
The agent represents bytecode expressions as an array of bytes. Each
instruction is one byte long (thus the term bytecode). Some
instructions are followed by operand bytes; for example, the goto
instruction is followed by a destination for the jump.
The bytecode interpreter is a stack-based machine; most instructions pop their operands off the stack, perform some operation, and push the result back on the stack for the next instruction to consume. Each element of the stack may contain either a integer or a floating point value; these values are as many bits wide as the largest integer that can be directly manipulated in the source language. Stack elements carry no record of their type; bytecode could push a value as an integer, then pop it as a floating point value. However, GDB will not generate code which does this. In C, one might define the type of a stack element as follows:
union agent_val {
LONGEST l;
DOUBLEST d;
};
where LONGEST and DOUBLEST are typedef names for
the largest integer and floating point types on the machine.
By the time the bytecode interpreter reaches the end of the expression,
the value of the expression should be the only value left on the stack.
For tracing applications, trace bytecodes in the expression will
have recorded the necessary data, and the value on the stack may be
discarded. For other applications, like conditional breakpoints, the
value may be useful.
Separate from the stack, the interpreter has two registers:
pc
start
goto and if_goto instructions.
Neither of these registers is directly visible to the bytecode language itself, but they are useful for defining the meanings of the bytecode operations.
There are no instructions to perform side effects on the running program, or call the program's functions; we assume that these expressions are only used for unobtrusive debugging, not for patching the running code.
Most bytecode instructions do not distinguish between the various sizes of values, and operate on full-width values; the upper bits of the values are simply ignored, since they do not usually make a difference to the value computed. The exceptions to this rule are:
refn)
ext instruction
exists for this purpose.
ext n)
If the interpreter is unable to evaluate an expression completely for some reason (a memory location is inaccessible, or a divisor is zero, for example), we say that interpretation "terminates with an error". This means that the problem is reported back to the interpreter's caller in some helpful way. In general, code using agent expressions should assume that they may attempt to divide by zero, fetch arbitrary memory locations, and misbehave in other ways.
Even complicated C expressions compile to a few bytecode instructions;
for example, the expression x + y * z would typically produce
code like the following, assuming that x and y live in
registers, and z is a global variable holding a 32-bit
int:
reg 1 reg 2 const32 address of z ref32 ext 32 mul add end
In detail, these mean:
reg 1
x) onto the
stack.
reg 2
y).
const32 address of z
z onto the stack.
ref32
z with z's value.
ext 32
z is a signed integer.
mul
y * z.
add
x + y * z.
end
Each bytecode description has the following form:
add (0x02): a b => a+b
In this example, add is the name of the bytecode, and
(0x02) is the one-byte value used to encode the bytecode, in
hexidecimal. The phrase "a b => a+b" shows
the stack before and after the bytecode executes. Beforehand, the stack
must contain at least two values, a and b; since the top of
the stack is to the right, b is on the top of the stack, and
a is underneath it. After execution, the bytecode will have
popped a and b from the stack, and replaced them with a
single value, a+b. There may be other values on the stack below
those shown, but the bytecode affects only those shown.
Here is another example:
const8 (0x22) n: => n
In this example, the bytecode const8 takes an operand n
directly from the bytecode stream; the operand follows the const8
bytecode itself. We write any such operands immediately after the name
of the bytecode, before the colon, and describe the exact encoding of
the operand in the bytecode stream in the body of the bytecode
description.
For the const8 bytecode, there are no stack items given before
the =>; this simply means that the bytecode consumes no values
from the stack. If a bytecode consumes no values, or produces no
values, the list on either side of the => may be empty.
If a value is written as a, b, or n, then the bytecode treats it as an integer. If a value is written is addr, then the bytecode treats it as an address.
We do not fully describe the floating point operations here; although this design can be extended in a clean way to handle floating point values, they are not of immediate interest to the customer, so we avoid describing them, to save time.
float (0x01): =>
add (0x02): a b => a+b
sub (0x03): a b => a-b
mul (0x04): a b => a*b
div_signed (0x05): a b => a/b
div_unsigned (0x06): a b => a/b
rem_signed (0x07): a b => a modulo b
rem_unsigned (0x08): a b => a modulo b
lsh (0x09): a b => a<<b
rsh_signed (0x0a): a b => (signed)a>>b
rsh_unsigned (0x0b): a b => a>>b
log_not (0x0e): a => !a
bit_and (0x0f): a b => a&b
and.
bit_or (0x10): a b => a|b
or.
bit_xor (0x11): a b => a^b
or.
bit_not (0x12): a => ~a
equal (0x13): a b => a=b
less_signed (0x14): a b => a<b
less_unsigned (0x15): a b => a<b
ext (0x16) n: a => a, sign-extended from n bits
ext bytecode.
zero_ext (0x2a) n: a => a, zero-extended from n bits
zero_ext bytecode.
ref8 (0x17): addr => a
ref16 (0x18): addr => a
ref32 (0x19): addr => a
ref64 (0x1a): addr => a
refn, fetch an n-bit value from addr, using the
natural target endianness. Push the fetched value as an unsigned
integer.
Note that addr may not be aligned in any particular way; the
refn bytecodes should operate correctly for any address.
If attempting to access memory at addr would cause a processor
exception of some sort, terminate with an error.
ref_float (0x1b): addr => d
ref_double (0x1c): addr => d
ref_long_double (0x1d): addr => d
l_to_d (0x1e): a => d
d_to_l (0x1f): d => a
dup (0x28): a => a a
swap (0x2b): a b => b a
pop (0x29): a =>
if_goto (0x20) offset: a =>
pc register to start + offset.
Thus, an offset of zero denotes the beginning of the expression.
The offset is stored as a sixteen-bit unsigned value, stored
immediately following the if_goto bytecode. It is always stored
most signficant byte first, regardless of the target's normal
endianness. The offset is not guaranteed to fall at any particular
alignment within the bytecode stream; thus, on machines where fetching a
16-bit on an unaligned address raises an exception, you should fetch the
offset one byte at a time.
goto (0x21) offset: =>
pc register to start + offset.
The offset is stored in the same way as for the if_goto bytecode.
const8 (0x22) n: => n
const16 (0x23) n: => n
const32 (0x24) n: => n
const64 (0x25) n: => n
ext bytecode.
The constant n is stored in the appropriate number of bytes
following the constb bytecode. The constant n is
always stored most significant byte first, regardless of the target's
normal endianness. The constant is not guaranteed to fall at any
particular alignment within the bytecode stream; thus, on machines where
fetching a 16-bit on an unaligned address raises an exception, you
should fetch n one byte at a time.
reg (0x26) n: => a
reg bytecode. It is always stored most
signficant byte first, regardless of the target's normal endianness.
The register number is not guaranteed to fall at any particular
alignment within the bytecode stream; thus, on machines where fetching a
16-bit on an unaligned address raises an exception, you should fetch the
register number one byte at a time.
trace (0x0c): addr size =>
trace_quick (0x0d) size: addr => addr
trace opcode.
This bytecode is equivalent to the sequence dup const8 size
trace, but we provide it anyway to save space in bytecode strings.
trace16 (0x30) size: addr => addr
trace_quick16, for consistency.
end (0x27): =>
Here is a sketch of a full non-stop debugging cycle, showing how agent expressions fit into the process.
Some targets don't support floating-point, and some would rather not
have to deal with long long operations. Also, different targets
will have different stack sizes, and different bytecode buffer lengths.
Thus, GDB needs a way to ask the target about itself. We haven't worked out the details yet, but in general, GDB should be able to send the target a packet asking it to describe itself. The reply should be a packet whose length is explicit, so we can add new information to the packet in future revisions of the agent, without confusing old versions of GDB, and it should contain a version number. It should contain at least the following information:
long long is supported
Some of the design decisions apparent above are arguable.
> or <= operators?
less_ opcodes with log_not, and swap the order
of the operands, yielding all four asymmetrical comparison operators.
For example, (x <= y) is ! (x > y), which is ! (y <
x).
log_not?
ext?
zero_ext?
log_not is equivalent to const8 0 equal; it's used in half
the relational operators.
ext n is equivalent to const8 s-n lsh const8
s-n rsh_signed, where s is the size of the stack elements;
it follows refm and reg bytecodes when the value
should be signed. See the next bulleted item.
zero_ext n is equivalent to constm mask
log_and; it's used whenever we push the value of a register, because we
can't assume the upper bits of the register aren't garbage.
ref operators?
ref operators, and we
need the ext bytecode anyway for accessing bitfields.
ref operators?
ref operators again, and
const32 address ref32 is only one byte longer.
refn operators have to support unaligned fetches?
goto ops PC-relative?
goto ops?
reg bytecode take a 16-bit register number?
trace and trace_quick?
x->y->z, the agent must record the values of x and
x->y as well as the value of x->y->z.
trace bytecodes make the interpreter less general?
trace bytecodes, they don't get in
its way.
trace_quick consume its arguments the way everything else does?
trace_quick is a kludge to save space; it
only exists so we needn't write dup const8 SIZE trace
before every memory reference. Therefore, it's okay for it not to
consume its arguments; it's meant for a specific context in which we
know exactly what it should do with the stack. If we're going to have a
kludge, it should be an effective kludge.
trace16 exist?
dup const16
size trace in those cases.
Whatever we decide to do with trace16, we should at least leave
opcode 0x30 reserved, to remain compatible with the customer who added
it.
This document was generated on 10 Febuary 1999 using the texi2html translator version 1.51a.