[Next] [Previous] [Top] [Contents] [Index]
Aprobe
This chapter is a collection of topics not covered in previous chapters but which more sophisticated, experienced Aprobe users may need to know in order to take full advantage of the product. If you're reading this, it's assumed you've worked through examples (see Chapter 1, "Aprobe By Example"), gotten some probes to work on your application and are seeing the world of possibilities the probe mechanism offers. However, even this chapter doesn't cover everything. You should look at Appendix C, "Aprobe API Reference" for complete information on the functions referenced here. For some highly sophisticated examples, see the source for the predefined probes in Aprobe\probes.
In "Nesting of Probes" we gave an example of how probes may be nested and how this can be useful. This section provides more complete details about the rules of nesting and visibility.
Illegal nestings of probes are rejected by apc at compile time.
Probe program directives are never nested inside of other probes.
Probe thread directives may be:
nested inside of a program probe, or
not nested inside of another probe at all.
Nesting a thread probe inside of a program probe is not common but does allow the thread probe (and its nested function probes) to have visibility to data declared in the program probe's declarative region. This can be useful if many independent authors are writing probes because the program probe data item names will not collide with other globally-declared data items. If the data items are instead declared globally, the names may collide as in normal C programming. Generally, probe program directives are used for their entry and exit conditions.
The data declared in a probe thread is thread specific, meaning that each new thread gets its own new copy of that data.
All probe function_name directives must be nested within another function probe or a thread probe. This implies that all function probes are nested within a thread probe. This is why the earlier examples always have probe thread enclosing the probe on individual subprograms subprogram.
C function declarations:
Declarations of both thread probes and program probes may include C function declarations, while function probe declarations may only include variable declarations. Functions declared inside of either a program probe or a thread probe will have visibility to the declarative region of those probes. These functions may be called from anywhere they are lexically visible. They could also be called from places that don't have visibility to theses function lexically, as long as they have a visibility to their prototype declaration which may be placed anywhere in the APC file. This definition allows a subprogram interface to be used for accessing variables specific to both program and thread probes.
on_line probe actions may appear only within a function probe on a specific function, not in a probe all (see "The probe all Directive") or Probe Types (see page -10).
Probes have visibility to data declared in their enclosing probes (if any) as well as all data and functions that are visible in the C programming language sense.
Each probe declares an object defined in the enclosing scope. This object comes into existence when the enclosing scope is entered and the object goes out of existence when the control leaves the enclosing scope. This is similar to local variables in C programming.
Each probe object starts its lifetime when the control enters its enclosing scope. For example, if the enclosing scope is a function probe, then the nested probe object starts its lifetime when this function is called, and it ends its lifetime when control leaves this function.
Activation of a probe consists of allocation and initialization of data declared in its declarative region and the execution of its on_entry actions. Program probes are activated just once, upon program start. Any data declared inside of program probes is considered global and is shared by all threads of execution that are nested within the program probe.
Thread probes are activated each time a new thread comes to life. Any data declared in the declarative region of thread probes is allocated on a per thread basis and thus is thread specific.
Enabled function probes are activated each time their target function is called and so the data declared in the probe's declarative region is allocated once for each such function call. This means that recursive calls to the target function of a function probe cause multiple activations, including another allocation of its local data and another execution of its on_entry or on_exit actions each time the function is invoked.
Probe objects exist in one of four states:
enabled,
disabled,
active,
active_but_disabled.
The following paragraphs describe the transitions between these states.
All probe objects are created in an enabled state. They can be transitioned between enabled and disabled states by calling the runtime routines ap_EnableProbe() and ap_DisableProbe() (see Aprobe\include\aprobe.h), or by entering and exiting the probe scope.
A probe must be enabled before it can be made active. Enabled probes will become active upon execution of the probed entity in the target application program; that is, a program probe is enabled prior to executing the main() function; a thread probe is activated shortly after thread creation; and a function probe at the first instruction of the function. An active probe moves from active to enabled state upon leaving its target probed entity.
An active_but_disabled probe moves to disabled state upon leaving its target. Calling ap_DisableProbe on an active probe makes the probe active_but_disabled, not disabled. This will affect only subsequent (for example, recursive) calls to the target entity, not the actions of the currently activated probe. This means that all actions of a currently activated probe will be executed as usual, just like an active probe, including any on_exit actions upon exit from the target entity. But for any subsequent executions of the target entity, an active_but_disabled probe is considered disabled and is not activated, whether or not the exit from the target entity has occurred yet for the currently activated probe. See "Nested Probes and Recursive Calls" below for discussion of the practical aspects of this state.
As mentioned above, each invocation of a function with a probe creates a new activation of that probe. Therefore, a recursive function R with a probe on it has one activation for each level of recursion. Thus if the level of recursion is 3 (R calls R calls R), there are three active probes on the last invocation of R.
This is of special concern when, nesting a probe under a function that is called recursively. For example if S1 calls S2 which recursively calls S1 again, then a probe on S2 nested under the probe on S1 will get activated every time S1 is called. This will create multiple copies of the probe on S2, one for every recursion level of S1.
If you don't want to worry about recursion problems you could turn the umbrella probe on S1 into a non-recursive probe by disabling it within itself. This is done by calling ap_EnableProbe(ap_ThisProbe) on entry to S1 and ap_DisableProbe(ap_ThisProbe) on exit from S1. For example:
probe thread
{
probe "S1"
{
probe "S2"
{
// Since the umbrella probe on S1 will not get
// activated by recursive calls to S1, we can be
// certain that no more than one copy of this probe
// will exist at runtime.
}
on_entry // to S1
{
// Make sure recursive calls do not trigger
// this probe
ap_DisableProbe(ap_ThisProbe);
}
on_exit // from S1
{
// Now that we are exiting the outermost call to S1
// we can enable probes on S1 again
ap_EnableProbe(ap_ThisProbe);
}
} // end probe "S1"
} // end probe thread
Each probe action directive--on_entry, on_line, and on_exit--is translated to a C function with a number of parameters. These parameters provide information about the context in which the action is executed, and some may be referenced by name in the code associated with the action. The full list of parameters is defined by the function pointer type ap_ProbeActionT defined in aprobe.h. Only two of the parameters are of interest to the user:
ap_FunctionIdT ap_FunctionId
a unique function ID for each instrumented function.
ap_ProbeActionReasonT ap_ProbeActionReason
the kind of event that triggered this action.
The parameter ap_FunctionId is particularly useful as a parameter to functions in the API which provide information about the current context, such as ap_FunctionToModule and ap_FunctionToSymbol (see example in the next section).
The parameter ap_ProbeActionReason is useful for determining whether the probe was triggered by an exception, and for passing to a regular C function which may perform different operations in different situations. The type of this parameter is an enumeration type with the following values:
ap_EntryAction
Normal on_entry to any probe action.
ap_OffsetAction
Function on_line action.
ap_ExitAction
Normal exit from a function probe.
ap_ThreadExiting
The thread is terminating. This is the value passed to the on_exit action of a format, thread, or program probe.
ap_CppExceptionPropagated
A C++ exception is being propagated through a function probe on_exit action.
ap_UnknownReason
Control reached the probe action for some other reason.
See "Exception Support" for more information about these last two values.
To be able to find a function or specific data in the probed application, Aprobe needs to be able to find the symbol associated with that function or data item. Often your probes want to do the reverse - given an address, find what is at that address.
DLLs complicate the issue somewhat, since now there are multiple symbol tables. To make matters even more complicated, the same symbol can be defined in many different shared libraries.
In Aprobe, we break down all symbols by module (the executable is a module, as is each shared library). Each module that is loaded by your application is given a unique module ID by which you can refer to this module. At format time, that same module ID will refer to the same executable or shared library.
Within each module there is a symbol table read by Aprobe. Each symbol is assigned a symbol index which, with the module ID, forms a unique symbol ID. Each symbol represents an area in target memory. That area of memory may be contain a code of a function, a value of a variable or a constant. Given the symbol ID we can determine where this symbol resides in target memory, its length, and what sort of data (or code) is there. There are a number of built-in routines, (see "The Aprobe API", and Appendix C, "Aprobe API Reference"), that provide all the kinds of operations one might want to do with modules, symbols, functions and the like.
One potentially confusing issue (particularly to non-C programmers) is related to the "filename" with which a symbol is associated. In C, it is common to declare functions and variables which are local in scope to a given file (using the static keyword). This means that you can have multiple symbols with the same name referring to different variables, etc. In order to correctly identify which symbol you are interested in, you need to specify the filename in which that symbol was declared in. External functions and data items have no filename associated in this way.
Normally you are only interested in those symbols which represent the functions and procedures within your code. You may want to build tables to show which functions were executed (to collect timing information for example). To support this, every symbol in each module which represents code is given a function ID. Like a symbol this is made up of a module ID and an index to represent the function in that module. Unlike a symbol, the function index is guaranteed not to be sparse so you can easily index through them. For example, you can query the number of functions in a given module.
Full details of the module, symbol and function support are provided in aprobe.h. Below are two examples demonstrating their use.
/* Given an address, print out the module and symbol name */
void PrintSymbol (ap_AddressT Address)
{
ap_SymbolIdT SymbolId;
ap_ModuleIdT ModuleId;
/* Print the address out */
printf ("0x%08x: ", Address);
/* Get the symbol Id */
SymbolId = ap_AddressToSymbol (Address);
/* Was anything returned? */
if (ap_IsNoSymbolId(SymbolId))
{
printf ("No symbol found\n");
return;
}
/* Print the module name */
ModuleId = ap_SymbolToModule(SymbolId);
printf ("%s: ", ap_ModuleName (ModuleId));
/* Print the symbol name */
printf ("%s\n", ap_SymbolName (SymbolId));
}
/* Loop through all modules printing out all functions. */
/* For this we use an iterator */
static ap_BooleanT MyIterator (
ap_ModuleIdT ModuleId, void *Data)
{
ap_Uint32 NumFunctions, FunctionIndex;
/* Print out the module name */
printf ("Functions for module %s\n",
ap_ModuleName (ModuleId));
/* Get the number of functions */
NumFunctions = ap_NumberOfFunctions (ModuleId);
/* Loop through all the functions */
for (FunctionIndex = 0;
FunctionIndex < NumFunctions;
FunctionIndex++)
{
printf (" %s\n",
ap_SymbolName (
ap_FunctionToSymbol (
ap_FunctionIndexToFunctionId(
FunctionIndex, ModuleId))));
}
return TRUE;
}
probe format
{
on_entry
{
/* Call the iterator */
ap_IterateThroughModules (MyIterator, (void *) 0);
}
}
The probe all directive allows one to apply the same action to all instrumented functions. However, no functions are instrumented by default. The specification of a function probe causes a function to be instrumented. However, to apply a probe to "all" functions, but you must make a call to the Aprobe API to instrument each function by name or FunctionId, on_entry to the program. This is illustrated in Example 4-6. "Dynamic instrumentation and probe creation".
Often one has to either apply a similar probe to different functions or apply a given probe to a function selected by the user input; a probe type is used to facilitate this.
A probe type is analogous to a C typedef, because it does not declare a probe object; instead it defines a type. A probe type is defined just like any other function probe, except that its definition is preceded by a keyword typedef, the target function of the probe must not be specified and the probe type must define a probe_type_name, which is the name by which the probe is referenced.
Any number of actual probe objects of this type can be created by either an object declaration or an allocator, but probes defined using probe types may be declared only in the declarative region of thread probes and function probes.
The detailed syntax is described in "Probe Syntax" in Appendix B. A simple example is given below.
The probe type in Example 4-4. "Defining and using a probe type" below performs the same function as Example 3-15. "ap_Time operations"
/* Define a probe to measure time for any function */
typedef probe
{
ap_TimeT EntryTime, ExitTime, TimeDifference;
on_entry
ap_GetTime(&EntryTime);
on_exit
{
ap_GetTime(&ExitTime);
TimeDifference = ap_SubTime(ExitTime, EntryTime);
printf("%s execution time: %g\n",
ap_SymbolName(ap_FunctionToSymbol(ap_FunctionId)),
ap_TimeToFloat(TimeDifference));
}
}TimeAndPrintProbeT;
probe thread
{
/* declare an object of the above probe type for "foo" */
TimeAndPrintProbeT fooTimeProbe("foo");
}
Probe objects can also be allocated dynamically with the new operator by providing the FunctionId of the function to be probed rather than its name, as shown below. Probes created with new must already have been dynamically instrumented as described in the next section, Programmatic Instrumentation.
probe thread
{
typedef probe { } MyProbeT;
on_entry
{
ap_FunctionIdT F = ap_SymbolToFunction(
ap_SymbolNameToId(ap_ApplicationModuleId(),
"foo",ap_NoName,ap_FunctionSymbol));
ap_ProbeInstancePtrT FooTimeProbe =
new MyProbeT(F, (ap_DataPointerT)20);
}
}
You can reference the probe instance data (the "(ap_DataPointerT)20" in the example above) in an action within the probe using the ap_ProbeData macro, which returns type ap_DataPointerT and so must be cast to the user-specified type.
A probe allocator returns a pointer of type ap_ProbeInstancePtrT, defined in aprobe.h. This pointer is needed to call ap_EnableProbe() and ap_DisableProbe() from outside the probe itself, and to delete the probe.
To delete or "free" a dynamically allocated probe, one has to use a delete operator. Note that, just as with normal probes declared without using an allocator, probes allocated with a new operator will be created within the context of the current thread. This means that an instance of a given probe must be allocated in each thread in which the probe will be used. Therefore, we recommend that dynamic probes are allocated on_entry to a probe thread. For more information, see the next section on Programmatic Instrumentation.
The patching performed by Aprobe is called instrumentation. For the most part, you need not concern yourself with this, as the Aprobe determines what minimum set of routines to instrument. It is of concern when using probe all, however.
Aprobe automatically performs the necessary instrumentation whenever a probe on a particular function is declared.
When the all keyword is used in a function probe, it will apply to all subprograms that have been instrumented, not all of the subprograms in the executable. Normally, Aprobe automatically instruments the subprogram specified in the UAL but this is not done for the all keyword. If it is desired to instrument all subprograms, then the user must explicitly instrument them in the UAL. This is accomplished by using a call to one of the following functions defined in aprobe.h:
ap_ProbeVectorIndexT ap_InstrumentFunction ( ap_FunctionIdT FunctionId);
or
ap_ProbeVectorIndexT ap_InstrumentSymbolByName ( SymbolNameT SymbolName, ModuleNameT ModuleName, FileNameT FileName);
Calls to ap_InstrumentFunction and ap_InstrumentSymbolByName should appear in the on_entry section of a probe program. There is no need to provide a call to an instrumentation subprogram for any symbols that appear in an individual probe statement, since Aprobe will automatically instrument all subprograms that are explicitly named in a probe declaration.
However, when you allocate a probe using the 'new' operator, as described above, you must also explicitly instrument the function. The following example shows how to dynamically apply the TimeAndPrintProbeT given in the example above, assuming it is defined in "timeprobe.h"
#include <stdio.h>
#include "timeprobe.h"
probe program {
// Record the function we want to time. It must be declared
// here because it must be instrumented in probe program,
// but the probe created in probe thread.
ap_FunctionIdT TimedFunctionId;
on_entry
{ // ap_UalArgc, ap_UalArgv describe the argument to
// -p for this probe -- support just 1 for now:
if (ap_UalArgc != 2)
ap_Error(
ap_ErrorSev,
"Usage: %s function_name", ap_UalArgv[0]);
else
{ // record the FunctionId we want to time.
// We need this both to instrument it
// and to create the probe:
TimedFunctionId =
ap_SymbolToFunction(
ap_SymbolNameToId(
ap_ApplicationModuleId(),
ap_UalArgv[1],
ap_NoName,
ap_FunctionSymbol));
if (ap_IsNoFunctionId(TimedFunctionId))
ap_Error(
ap_WarningSev,
"Function not found: %s.", ap_UalArgv[1]);
else
ap_InstrumentFunction(TimedFunctionId);
}
}
// Create the probe on the function identified
// and instrumented above.
probe thread {
on_entry
new TimeAndPrintProbeT(TimedFunctionId);
} // end probe thread
} // end probe program
APD files are produced as a side-effect of running an executable under control of the aprobe tool, and contain data written using the log statement. The APD files are formatted by the apformat command, which uses the associated format routines to display the logged data in human-readable form.
A complete description of how the aprobe and apformat commands control and manage the APD files is provided in Appendix B, "APD file". This section briefly addresses the practical matter of why you would want multiple APD files, and the facility to support multiple files in the Aprobe API.
If you use the "trace.dll" predefined probe with the SaveTraceDataTo APD_FILE option (which logs entry and exit to every function), or write your own probe which logs a lot of data, you will discover that the single default APD file runs out of space. When this happens, no more data is logged and an error message is given suggesting alternative actions:
Make the APD file bigger using the aprobe -s option
Use an "APD ring" of 2 or more files, using the aprobe -n option.
The second of these two options is generally preferable, for two reasons:
The transition of logging from one file to the next can be detected by your probe by registering for a callback when it occurs. In the example below, the user writes the function ApdOverflowSubprogram() which is then registered by calling ap_RegisterApdRingChangeCallback() in the on_entry part of a program probe. The full example is on-line in Aprobe\Examples\Advanced\Apd_Ring\.
static int NumberOfApdRingFiles = 0;
void ApdOverflowSubprogram()
{
NumberOfApdRingFiles++;
log("Switching to a new apd ring file #", (int)
NumberOfApdRingFiles);
}
probe program
{
on_entry
ap_RegisterApdRingChangeCallback(
ApdOverflowSubprogram);
}
It is sometimes useful or even necessary for provide command-line parameters for a probe. For example, the predefined probes included with Aprobe (see "Predefined Probes") support passing several options, including the name of a configuration file, to the probe. This is done by following each
option like -u my_probe.dll on the aprobe command line with an option specifying the parameters for the preceding dll: -p "params_for_my_probe". You can have multiple occurrences of -u, each followed by a -p, so that each UAL dll can have its own options.
All probes within a UAL share the global variables ap_UalArgc and ap_UalArgv, which are exactly analogous to the parameters argc and argv passed by the C and C++ environment to the main function:
int ap_UalArgc, char **ap_UalArgv
These may be accessed anywhere in any probe, but it is often useful to parse them into data global to the APC file.
Note that the parameters are exactly those provided on the command-line of aprobe or apformat, so you must explicitly log the parameter values in your probe, or else pass the same parameters to apformat, if you want to access the parameter values at apformat time.
One of the primary goals of Aprobe is to allow analysis of an application with as little intrusion as possible. This is supported by several features:
The log statement, which does memory-mapped data recording and defers data formatting to post-runtime.
Dynamic instrumentation, which patches only those functions explicitly probed by the user-specified UALs.
Support for dynamically enabling and disabling probes.
Control over whether floating point registers are saved over aprobe actions.
Use of the native C compiler to generate code for probes.
For the most part, Aprobe is designed to be minimally invasive and you need not worry about efficiency concerns. The speed of your probes will largely be defined by the actual C code that you place in the probes that you write, not in the Aprobe implementation.
Advanced users may, however, desire to understand how to tune their probes for maximum performance. This section provides some guidelines for using Aprobe when performance of the probed program is a consideration.
Be aware that each log statement has some overhead in both time and space. Clearly doing 1 log operation of 100 bytes is more efficient than doing 100 log operations of 1 byte each.
In terms of time, each log operation must be thread-safe, which involves some operating-system interaction, plus the time it takes to make the call to the Aprobe runtime, plus that to actually write the data. With regard to space, each log statement writes a 4-byte header identifying the log and the associated format, and each parameter to the log statement consists of the data itself, plus a 4-byte length variable-length parameters.
Considering this, it may be appropriate to collect data in a data structure within your probe and log the contents of this data structure periodically, rather than to log each word of data separately as it is collected.
Avoid logging data on a remotely mounted file system or network drive if possible. That is, run from a disk that is local to the machine on which the executable is running, or else specify a non-default APD file pathname on a local disk, e.g.,
aprobe -u trace.dll -d c:\temp\trace-fred.apd fred.exeAvoid logging constant data, especially strings. Such strings and constant data can be included at format time from within a user-defined format routine. (See "User-Supplied Formatting".)
If you wish to perform the operations in a probe only after a certain event occurs, it is tempting to simply set a global boolean and test that boolean in the body of the probe. However it is much more efficient to disable the probe (by calling ap_DisableProbe()) until that event occurs, then enable it by calling ap_EnableProbe(). This is illustrated by the on-line example inAprobe\Examples\Advanced\Disable_Probe\.
It is also important to avoid nesting probes with recursive calls, or to temporarily disable an umbrella probe that may be called recursively. This is discussed in "Nested Probes and Recursive Calls".
Use "#pragma nofloat" in the declarative part of any probe which you are sure doesn't modify floating point registers, either in its own code or in any calls it makes. This allows the apc-generated code to suppress the code necessary to save and restore these registers. For example:
probe thread
{
int ThreadWriteCount = 0;
probe "write()"
{
// This probe is activated for every call.
// It doesn't use floating point registers and we can
// speed it up by using pragma nofloat
#pragma nofloat
on_entry
ThreadWriteCount++;
}
on_exit // thread
printf("There were %d calls to 'write' in this thread\n",
ThreadWriteCount);
}
Be careful with this, though: if your probe does use floating point registers and you use #pragma nofloat, the probed program may yield corrupted floating point results.