AUG 4 Advanced Topics

From OC Systems Wiki!
Revision as of 17:11, 22 June 2018 by Swn (talk | contribs) (Linking Aprobe Into Your Application)
Jump to: navigation, search

Next Previous Top Contents Index

Aprobe User Guide

Advanced Topics


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 in $APROBE/examples/evaluate, 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 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.

Probe Nesting and Visibility Rules

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.

  1. Illegal nestings of probes are rejected by apc at compile time.
  2. Probe program directives are never nested inside of other probes.
  3. 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.

  1. The data declared in a probe thread is thread specific, meaning that each new thread gets its own new copy of that data.
  2. Probe format directives are never nested.
  3. 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.
  4. 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.

  1. 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").
  2. 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.
  3. 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.

Probe Activation and State Transitions

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.

Enabled/Disabled

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.

Enabled/Active

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.

Active_but_disabled

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.

Nested Probes and Recursive Calls

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

Example 4-1. Avoiding recursive probe activation

Parameters to Probe Actions

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_AdaExceptionPropagated
An Ada exception is being propagated through a function probe on_exit action.
ap_UnknownReason
Control reached the probe action for some other reason, possibly a longjmp().

See "Exception Support" for more information about these last two values.

Symbols, Modules and Functions

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.

Shared libraries 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 "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 [../include/aprobe.h 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%x: ", 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));
}

Example 4-2. Printing symbol and module names

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);
   }
}

Example 4-3. Printing all functions in a module

The probe all Directive

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 Dynamic instrumentation and Probe Creation.

Probe Types

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". A simple example is given below.

The probe type in "Defining and using a probe type" below performs the same function as "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");
}

Example 4-4. Defining and using a probe type

Dynamic Probe Allocation, Deletion

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);
  }
}

Example 4-5. Dynamic probe allocation

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 todelete 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".

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 [[#The probe all Directive|"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

Example 4-6. Dynamic instrumentation and probe creation

Multiple APD Files

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 "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.ual" 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 problems with using the single default APD file:

  • The APD file may fill up the disk
  • Even if there is enough disk space, the data you want is often at the end of a very large file.

The first reason motivates us to limit the total amount of data logged, and the second is a reason to keep the APD files relatively small. These two goals are accomplished using an "APD ring", which is essentially a multi-file circular buffer. For a program "main", these are a sequence of files main-1.apd, main-2.apd, ... main-n.apd, where the highest value of n is the newest data, and the lowest value is the oldest. If the amount of data exceeds the size of all the files, the oldest file is deleted. The number of files kept is the "ring size". The number of files, and the size of each file are specified as arguments to the aprobe command.

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/learn/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);
}

Example 4-7. ap_RegisterApdRingChangeCallback()

Parameters to a UAL

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.ual on the aprobe command line with an option specifying the parameters for the preceding UAL: -p "params_for_my_probe". You can have multiple occurrences of -u, each followed by a -p, so that each UAL 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. The example in $APROBE/learn/ual_functions/ shows how these variables and others relating to the UAL may be accessed.

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.

Loading Probes Without Aprobe

Normally one uses the aprobe command to execute a target program in conjunction with the probes defined in one or more UAL files. This is rather like invoking a debugger (such as gdb). However, there are times when it is inconvenient or impossible to invoke aprobe, yet one still wants to apply probes to the application. This most commonly occurs when an executable is started by another executable rather than from the command-line.

The best approach in this situation is to arrange for an automatic interception of the process as provided by OC Systems' RootCause product. However, If you don't have RootCause, there are two alternative approaches available, depending on your platform:

  • Relink your application to include aprobe logic. This approach is currently supported on AIX, Linux, and Solaris.
  • Replace your executable with a shell script that invokes aprobe.

Aprobe Options (APO) File

In both cases, since there is no aprobe command-line from which to obtain options, there is a special text file which must be available: the APO file (AProbe Options file). This file must be named executable-basename.apo, where executable-basename is the name of your executable, without a directory path, and exist in the current working directory at the time the program is started. The APO file is simply a list of valid options which can be passed to the aprobe command. The characters #, !, //, and --, when appearing as the first character(s) on a line, denote comments which continue to the end of the line.

The two mechanisms for invoking aprobe are described below.

Linking Aprobe Into Your Application

Aprobe includes a special shared library called libdal.so ("DAL" stands for "Dynamic Action Linker"). If you re-link your application with
libdal.so, it will do on startup what aprobe normally does, and your probes will be in effect. The aprobe options are read from an APO file as described above.

For example, suppose your're developing a program Abcentry which is invoked by another program called abc, and you want to probe Abcentry with a UAL file Abcentry.ual. Then you might do:

$ mv Abcentry Abcentry.noprobe
$ cc -o Abcentry Abcentry.o -L$APROBE/lib -R$APROBE/lib -ldal    # Solaris
$ xlc -o Abcentry Abcentry.o -brtl -L$APROBE/lib -ldal    # AIX
$ gcc -o Abcentry Abcentry.o -L$APROBE/lib -Wl,-rpath,$APROBE/lib -ldal    # Linux
$ echo "-u Abcentry.ual" > Abcentry.apo

Now you can just run abc as usual, it will invoke Abcentry, and as long as the Abcentry.apo and Abcentry.ual files are in the current directory when Abcentry is invoked, the probes will be loaded.

Here is an example of using libdal.so with a PowerAda executable on AIX:

$ ada -m hello.adb -o hello -ilargs -iL$APROBE/lib -iR$APROBE/lib -ldal
$ echo "-u hello.ual" > hello.apo

Replacing Your Application With A Script

If you're not on Solaris, or for other reasons it's impractical to link the application with libdal.so, you can replace your application with a script instead. The script $APROBE/bin/run_with_aprobe_apo is intended to be symbolically linked in place of any executable file "X" so that aprobe may be applied to X when X is exec'd by another program. To do this:

  1. Rename the original executable to be probed to its same name and directory, but with ".exe" appended, e.g.,
    mv X X.exe
  2. Symbolically-link this script to the original executable name, e.g.,
    ln -s $APROBE/bin/run_with_aprobe_apo X
  3. put a corresponding ".apo" file (e.g., X.apo) containing aprobe options in your current directory, as described above and in "APO file". If no .apo file is found, a warning is given and the executable is run without aprobe.

Again suppose the program abc invokes another program called Abcentry, and you want to use aprobe on Abcentry to check for heap leaks using the predefined probe coverage.ual. You could do the following:

# go to directory where program to be probed resides
cd /opt/abc/bin

# rename program to same name with .exe on the end
mv Abcentry Abcentry.exe

# replace original program with link to this script
ln -s $APROBE/bin/run_with_aprobe_apo ./Abcentry

# create an APO file with the right stuff
cd my_test_dir
echo '-u coverage.ual -p -g' > Abcentry.apo

# start program that will invoke the executable
abc test_data.dat

Example 4-8. Using run_with_aprobe_apo

A similar script, $APROBE/bin/run_with_aprobe_edit, may be used as above except instead of providing an APO file, you copy the script in place of the original executable and edit the script itself to specify the aprobe options.

Building a UAL with Unresolved References (Solaris Only)

A UAL is a shared library created by the apc compiler when you compile your probes. Generally all functions and data called from within the probe must be defined in the probe, or in a library linked with it. Accesses of data and calls to functions whose names are not known when the UAL is linked are "unresolved references", and usually render the library unusable since it's undefined what should happen when the reference is encountered at run-time. However, there are certain circumstances when unresolved references are useful, thanks to the run-time linking mechanism provided by the Solaris operating system.

NOTE: This mechanism is currently supported only on Solaris. Contact OC Systems if you believe you need it on some other platforrm.

The circumstances in which an unresolved reference in a UAL are these:

  • you have written some functions or methods in your program's native language (e.g., C ), and you wish to call these functions from your probe.
  • these functions are not already part of your target application, so you can't just call them using a target expression (see "Target Expressions").
  • these functions call other functions in your target application, but they do not reference any data from the target application.

For example, suppose you want to write a probe that creates a new object of a given class, gives it the right values, and passes it to a method in the class. This is trivial to do in a new C function, so you write one up, give in an "extern C", so it can be called from your probe (which is in C) and compile your C function into a separate object file call CallMyClass.o. CallMyClass.o contains unresolved external references to the C runtime, and to constructors of the class which are used to create the object.

Now you write your probe, MyClassProbe.apc, and compile it as follows:

apc MyClassProbe.apc CallMyClass.o -u -x MyProgram

The option -u on the apc command line specifies that unresolved symbols are allowed to remain in the UAL.

Now run aprobe:

aprobe -u MyClassProbe.ual MyProgram

This is what happens to the unresolved symbols when you run aprobe and the UAL is loaded:

Data symbols

When the UAL is loaded, all references to data symbols are resolved. If the undefined symbols are not present in the application or one of its shared libraries, the UAL will not be loaded. This is particularly important to understand at format time since the application symbols are not present at format time. Therefore, no UAL with unresolved data symbols may be used at format time, even if the references will never be executed.

Function symbols

When the UAL is loaded, function references are not resolved until the function is called. This means that symbols may remain unresolved so long as no attempt is made to call those functions. This means that at format time, UALs with references to function symbols which are only found in the application may be loaded so long as no attempt is made to call those functions This is generally the case, since only format routines and not probes are executed at format time.

This can be tricky if the C code you want to link in is complicated, or unknown to you. However, if it's something straightforward that's under your control to change, it can generally be adapted to these restrictions. As always, contact OC Systems for guidance in these advanced features.

Aprobe Performance Considerations

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.

Log Statement Overhead

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.

Logging To a Remote Disk

Avoid logging data on a remotely mounted file system 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.ual -d /tmp/trace-fred.apd fred

Avoid 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".)

Using ap_DisableProbe

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 in
[../examples/learn/disable_probe/README $APROBE/examples/learn/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".

APC Compiler Options

The apc compiler invokes the native C compiler to generate the code for the UAL. By default, the only options it passes to the compiler are those to support debugging your probes. However, if you have a significant amount of code in your probes and performance is an issue, it may be useful to tell the C compiler to optimize code using -compiler "-O", for example:

apc mainprobe.apc support.apc -o mainprobe.ual -compiler "-O"

Pragma nofloat

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);
}

Example 4-9. Using #pragma nofloat

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.


Next Previous Top Contents Index

Copyright 2006-2017 OC Systems, Inc.