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.