6.3.6.1 Call Graph Tables

A typical call graph table can look like the extract shown in the diagram below. Look for Call Graph Tables: in the list file.

Figure 6-4. Call Graph Form

The graph table starts with the function main(). Note that the function name will always be shown in the assembly form, thus the function main() appears as the symbol _main. The main() function is always the root of one call tree. Interrupt functions will form separate trees.

All the functions that main() calls, or can call, are shown in the lines below, in the Calls column. So in this example, main() calls aOut() and initSPI(). These have been grouped in the orange box in the figure. If a star (*) appears next to the function’s name, this implies the function has been called indirectly via a pointer. A function’s inclusion into the call graph does not imply the function was actually called, but there is a possibility that the function was called. For example, code such as:

int test(int a) {
    if(a)
        foo();
    else
        bar();
}

will list foo() and bar() under test(), as either can be called. If a is always true, then the function bar() will never be called, even though it appears in the call graph.

In addition to the called functions, information relating to the memory allocated in the compiled stack for main() is shown. This memory will be used for the stack-based variables that are defined in main(), as well as a temporary location for the function’s return value, if appropriate.

In the orange box for main() you can see that it defines 12 auto and temporary variable (under the Autos column). It defines no parameters under the Params column. There is a total of 34134 references in the assembly code to local objects in main(), shown under the Refs column. The Used column indicates the total number of bytes of local storage, i.e., the sum of the Autos and Params columns.

Rather than the compiled stack being one block of memory in one memory space, it can be broken up into multiple blocks placed in different memory spaces to utilize all of the available memory on the target device. This breakdown is shown under the memory summary line for each function. In this example, it shows that 5 bytes of auto objects for main() are placed in the bank 0 component of the compiled stack (Space column), at an offset of 43 (Base column) into this stack. It also shows that 7 bytes of auto objects were placed in the bank 1 data component of the compiled stack at an offset of 0. The name listed under the Space column, is the same name as the linker class which will hold this section of the stack.

Below the information for main() (outside the orange box) you will see the same information repeated for the functions that main() called, i.e., aOut() and initSPI(). For clarity, only the first few functions of this program are shown in the figure.

Before the name of each function (in brackets) is the call stack depth for that particular function. A function can be called from many locations in a program, and the stack depth could be different at each of those locations. The maximum call depth is always shown for a function, regardless of its position in the call table. The main() function will always have a depth of 0. The starting call depth for interrupt functions assumes a worst case and will start at the start depth of the previous call graph tree plus one. If a function makes recursive calls, the stack depth is marked as a question mark, (?), and (recursive) is printed following its name.

After each tree in the call graph, there is an indication of the maximum stack depth that might be realized by that tree. For more detailed information on stack depths, consider using the stack guidance feature, described in 5.2.13 Stack Guidance. Stack depths are not printed if any functions in the graph use the software stack. In this case, a single stack depth estimate is printed for the entire program at the end of the graphs. If there are recursive function calls in a program, the maximum stack depth is indicated as being unknown due to recursion. In the example shown, the estimated maximum stack depth is 6. Check the associated data sheet for the depth of your device’s hardware stack (see 5.7.1 Function Return Address Stack). The stack depth indicated can be used as a guide to the stack usage of the program. No definitive value can be given for the program’s total stack usage for several reasons:

  • Certain parts of the call tree may never be reached, reducing that tree’s stack usage.
  • The exact contribution of interrupt (or other) trees to the main() tree cannot be determined as the point in main’s call tree at which the interrupt (or other function invocation) will occur cannot be known. The compiler assumes the worst case situation of interrupts occurring at the maximum main() depth.
  • The assembler optimizer may have replaced function calls with jumps to functions, reducing that tree’s stack usage.
  • The assembler’s procedural abstraction optimizations can have added in calls to abstracted routines, increasing the stack depth. Checks are made to ensure this does not exceed the maximum stack depth.
  • Functions which are inlined are not called, reducing the stack usage.

The compiler can be configured to manage the hardware stack for PIC10/12/16 devices only (see 4.6.1.24 Stackcall Option). When this mode is selected, the compiler will convert calls to jumps if it thinks the maximum stack depth of the device is being exceeded. The stack depth estimate listed in the call table will reflect the stack savings made by this feature, and thus, the stack depth and call depth will not be the same.

Note that main() is jumped to by the runtime startup, not called; so, main() itself does not consume a level of stack.

The code generator produces a warning if the maximum stack depth appears to have been exceeded and the stack is not being managed by the compiler. For the above reasons, this warning, too, is intended to be a only a guide to potential stack problems.