Post-build static analysis - pfunct and pahole

Only after writing about Post-build static analysis, I found a nice suite of tools that can help to inspect binaries and gather information based on the debug Information. I found pahole and pfunct all from the dwarves project.

The Debian package actually contains many more utilities

  • codiff: diff-tool for comparing how changes in source affects resulting binaries

  • ctracer: trace execution of functions

  • dtagnames: lists tag names

  • pahole: finds alignment holes in structs and classes

  • pdwtags: pretty-prints DWARF information

  • pfunct: displays information about functions, like inlines

  • pglobal: lists global symbols

  • prefcnt: counts DWARF tags usage

  • scncopy: to copy ELF sections

  • syscse: system call sign extender

Unfortunately, the documentation is not that great.

pahole

As already mentioned, it helps to find suboptimal structure layouts. As struct that have more than one member variable are normally padded, changing the order of the member variable can change its layout size.

For example, given following structure

struct a {
	short b1;
	int b2;
	short b3;
};

The output of pahole is (if struct a has been used somewhere)

struct a {
        short int                  b1;                   /*     0     2 */

        /* XXX 2 bytes hole, try to pack */

        int                        b2;                   /*     4     4 */
        short int                  b3;                   /*     8     2 */

        /* size: 12, cachelines: 1, members: 3 */
        /* sum members: 8, holes: 1, sum holes: 2 */
        /* padding: 2 */
        /* last cacheline: 12 bytes */
};

While the output for

struct a {
        int b2;
        short b1;
        short b3;
};

shows that there is no wasted space

struct a {
        int                        b2;                   /*     0     4 */
        short int                  b1;                   /*     4     2 */
        short int                  b3;                   /*     6     2 */

        /* size: 8, cachelines: 1, members: 3 */
        /* last cacheline: 8 bytes */
};

As all of the three major compilers (GCC, Clang, and MSVC) can emit warnings at compile time about padding, for example with GCC -Wpadded the output will be

main.cpp:17:6: warning: padding struct to align ‘a::b2’ [-Wpadded]
   17 |  int b2;
      |      ^~
main.cpp:15:8: warning: padding struct size to alignment boundary [-Wpadded]
   15 | struct a{
      |        ^

One would think that there is no good reason to use pahole.

It should be normally preferred to do the check at compile time and not after, also because pahole needs debugging information.

pahole also gives the possibility to diagnose how to reduce the structure packing. With -R, --reorganize and for clarity -S, --show_reorg_steps it will enlist the steps:

/* Moving 'b3' from after 'b2' to after 'b1' */
struct a {
        short int                  b1;                   /*     0     2 */
        short int                  b3;                   /*     2     2 */
        int                        b2;                   /*     4     4 */

        /* size: 8, cachelines: 1, members: 3 */
        /* last cacheline: 8 bytes */
}

/* Final reorganized struct: */
struct a {
        short int                  b1;                   /*     0     2 */
        short int                  b3;                   /*     2     2 */
        int                        b2;                   /*     4     4 */

        /* size: 8, cachelines: 1, members: 3 */
        /* last cacheline: 8 bytes */
};   /* saved 4 bytes! */

This will only work if using -C, --class_name=CLASS_NAME, thus it necessary to invoke pahole for every structure we are interested in.

As it can always be useful to double-check if there is unnecessarily wasted space, it is still useful to know that tools like pahole are available.

pfunct

pfunct is a program that displays information about functions.

If invoked without arguments, it will simply list all functions, but notice that it omits information like parameters (type and numbers), overloads and namespaces (the dwarves suite was originally intended for C source code and not C++).

nm [--extern-only] --demangle <binary file> | grep ' T ' will show all those information for all functions (only exported if invoked with --extern-only).

An interesting option of pfunct is --goto_labels, which shows the number of goto labels per function. As far as I know, GCC, Clang and MSVC do not have any flag for diagnosing the use of goto labels (and/or statements). While the usage of goto has its valid use-cases, it is often possible to use a more structured language feature to accomplish the same task, which often results easier to understand code.

Given the following function

int foo(int j){
	int a = 1;
	here:
	++a;
	if(a<j){
		goto here;
	}
	there:
	return a;
}

pfunct emits foo: 2, indicating that function foo has two labels: here and there. Labels inside switch statements are (correctly) not counted. If a second function named foo, like an overload or in a different namespace, is defined somewhere pfunct will analyze only one, which limit its capabilities when analyzing C++ code.

Other probably useful options are

  • -s, --sizes, as it shows the size of functions, useful for finding big functions and splitting them

  • -S, --nr_variables, as it counts the number of local variables

  • -G, --cc_uninlined, as it lists those function declared inline, but that the compiler did not inline

  • -H, --cc_inlined, as it lists those function not declared inline, but that the compiler inlined anyway (probably less useful than -cc_uninlined if compiling with optimizations)

pglobal

This tool shows global symbols (functions and variables). As other tools like readelf, nm and objdump are already capable of displaying this type of data even without debugging information, and since pglobal, unlike pahole and pfunct, does not do any analysis, I decided to not invest much time in it.

cscope

After reading the description, I realized that diffoscope from the reproducible-builds practices is another tool for static post-build-analysis.

diffoscope is a much more versatile tool, with a much broader audience and scope, and does not require debug information. I tried installing it, it notified me that it would have occupied 1.750 MB of additional space (YMMV). It has java and some android-packages as dependencies on Debian, fortunately, they are all marked as optional. With apt-get install --no-install-recommends diffoscope I was able to get the tool only for 655 kB of additional disk space.

ctracer, dtagnames, pdwtags, prefcnt, scncopy and syscse

While surely not less important, I failed to see how to integrate those tools in my daily workflow.

They are surely useful and have their use-cases, but at the moment I’ll leave them in my toolbox.