Manual dynamic analysis

This sections shows to you how to run a manual dynamic analysis with beLow

When is manual dynamic analysis relevant?

Though beLow allows you to run dynamic analysis automatically, this is not always possible, depending on your use case.

Automated dynamic analysis will not be possible if :

  • You are running your application in an environment unable to print data to a file (usage of fprintfnot possible)

  • Running your application is not scriptable (e.g, requires clicking in some Windows software, requires human interaction, etc)

In this context, there are two possibilities:

  • Running static analysis only: beLow will still be able to find some optimizations, but their impact will certainly be under or over-evaluated. An optimization in an initialization function and an optimization in a deep for-loop would have the same weight, while in the runtime reality, the first one would be executed once and the second one million times.

  • Running manual dynamic analysis: in this case, we provide you an instrumented code that you manually have to build, run, and retrieve data from.

Of course, manual dynamic analysis is not suitable in a fully automated environment like CI/CD, but you might spontaneously want to have some accurate insights about what beLow is able to improve in your code, and we recommend it.

How to run a manual dynamic analysis?

When setting up a project without automated dynamic analysis, before running analysis, you get the following card:

Before analysis

At this point, you have 2 choices:

  • Running a static analysis by clicking Run analysis, or

  • Having a manual dynamic analysis.

The steps to run a manual dynamic analysis are:

  • Downloading instrumented code

  • Merging instrumented code to the original project (or a copy)

  • Optionally, modifying instrumented code to match your project specificities

  • Building the instrumented code

  • Running your application in a prod-like environment

  • Retrieving profiling data from the run

  • Uploading the profiling data to beLow

  • Running analysis

In this section, we develop a full example of how to run a full manual dynamic analysis on a simple project.

Example project

Our example projet is composed of a single main.c file:

main.c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>

#define N 5

void vect_add(int *a, int *b, int *c, int n) {
    for (int i = 0; i < n; i++) {
        c[i] = a[i] + b[i];
    }
}


int main() {
    int *a = malloc(N * sizeof(int));
    int *b = malloc(N * sizeof(int));
    int *c = malloc(N * sizeof(int));
    if (!a || !b || !c) {
        fprintf(stderr, "Memory allocation failed\n");
        return 1;
    }

    srand(time(NULL));

    while (1) {
        for (int i = 0; i < N; i++) {
            a[i] = rand() % 100;
            b[i] = rand() % 100;
        }

        vect_add(a, b, c, N);

        printf("a: ");
        for (int i = 0; i < N; i++) printf("%d ", a[i]);
        printf("\nb: ");
        for (int i = 0; i < N; i++) printf("%d ", b[i]);
        printf("\nc: ");
        for (int i = 0; i < N; i++) printf("%d ", c[i]);
        printf("\n\n");

        sleep(1);
    }

    free(a);
    free(b);
    free(c);
    return 0;
}

This project sums two random integer vectors every 1 second, endlessly.

This project is built with the following command:

gcc -O2 -o exec main.cpp

After setting up the project (see Getting started guide), let's download the instrumented code for manual dynamic analysis.

Download instrumented code

Download instrumented code for manual dynamic analysis

To download the instrumented code for manual dynamic analysis, click the corresponding button. This downloads a file called instrumented-code.zip. Unzip it.

When unzipping it, you have 3 code files:

  • main.c: the instrumented code. This should not be modified if possible.

  • below_vendor.i: beLow vendor generated code which is not supposed to be modified.

  • below_instr.i : beLow customizable code which should be modified by you.

The unzipped code needs to be merged into the original code. You may do that directly in your original code (you may revert it later, e.g using git, or in a copy of your code.

The goal of the instrumented code is to count the number of times each block of the code (function, loop, if/else statements, etc) and format this data.

To do that, the default behavior is:

  • Allocating a global array of counters,

  • Incrementing the counters, with one index per counter in the code,

  • Printing the counters as text into a file called wedolow.prof when the program exits.

This behavior is also used in automated dynamic analysis, but manual dynamic analysis allows you to adapt it to your needs.

Let's dive into the instrumented code. The modified main.c has the following content:

main.c (instrumented)
#include "below_instr.i"
void BELOW_COUNT(int id);
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>

#define N 5

void vect_add(int *a, int *b, int *c, int n) {
{BELOW_COUNT(1);}

    for (int i = 0; i < n; i++) {
    {BELOW_COUNT(2);}
    
        c[i] = a[i] + b[i];
    }
    {BELOW_COUNT(3);}
    
}


int main() {
{BELOW_COUNT(0);}

    int *a = malloc(N * sizeof(int));
    int *b = malloc(N * sizeof(int));
    int *c = malloc(N * sizeof(int));
    if (!a || !b || !c) {
    {BELOW_COUNT(4);}
    
        fprintf(stderr, "Memory allocation failed\n");
        return 1;
    }
    {BELOW_COUNT(5);}
    

    srand(time(NULL));

    while (1) {
    {BELOW_COUNT(6);}
    
        for (int i = 0; i < N; i++) {
        {BELOW_COUNT(8);}
        
            a[i] = rand() % 100;
            b[i] = rand() % 100;
        }
        {BELOW_COUNT(9);}
        

        vect_add(a, b, c, N);

        printf("a: ");
        for (int i = 0; i < N; i++) {
        
        {BELOW_COUNT(10);}
        printf("%d ", a[i]);
        }
        {BELOW_COUNT(11);}
        
        printf("\nb: ");
        for (int i = 0; i < N; i++) {
        
        {BELOW_COUNT(12);}
        printf("%d ", b[i]);
        }
        {BELOW_COUNT(13);}
        
        printf("\nc: ");
        for (int i = 0; i < N; i++) {
        
        {BELOW_COUNT(14);}
        printf("%d ", c[i]);
        }
        {BELOW_COUNT(15);}
        
        printf("\n\n");

        sleep(1);
    }
    {BELOW_COUNT(7);}
    

    free(a);
    free(b);
    free(c);
    return 0;
}

As you can see, calls to function BELOW_COUNT were added in the code, as well as an include to below_instr.i .

below_instr.i has the following content:

below_instr.i
#ifndef BELOW_INSTR_I
#define BELOW_INSTR_I

#include "below_vendor.i"

#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#include <string.h>

// You may want to change the base type of the counters to smaller types
typedef long long below_counter_t;
below_counter_t* BELOW_counters = NULL;

// This will be called when the first BELOW_COUNT is called or when BELOW_finish is called if
// BELOW_COUNT is never called
void BELOW_init_custom(){
    // Initialize the counters with BELOW_N_COUNTERS (defined in below_vendor.i)
    BELOW_counters = (below_counter_t*)malloc(sizeof(below_counter_t) * BELOW_N_COUNTERS);
    memset(BELOW_counters, 0, sizeof(below_counter_t)*BELOW_N_COUNTERS);

    // BELOW_finish must be called before the program exits. It calls BELOW_finish_custom
    // If atexit is not available, you may use BELOW_DYNAMIC_ANALYSIS definition in your original
    // code to call BELOW_finish directly only in the dynamic analysis context
    atexit(BELOW_finish);
}

void BELOW_COUNT_custom(int id) {
    // Counters must be incremented here
    BELOW_counters[id]++;
}

void BELOW_finish_custom() {
    // When your run script is over, counters must be written to wedolow.prof file
    // in the run script directory
    // If you can't write files, you must find another way to print the counter and use your
    // run script to read them and write them to wedolow.prof file
    FILE* f = fopen("wedolow.prof", "w+");
    // First write the number of counters
    fprintf(f, "%d,", BELOW_N_COUNTERS);
    // Then write the counters
    for(int i=0; i<BELOW_N_COUNTERS; i++){
        fprintf(f, "%lld,", BELOW_counters[i]);
    }
    fclose(f);
    // Clean up before exit.
    // Be sure that no counter increment is done after this point
    free(BELOW_counters);
}

#endif

It is composed of the following elements:

  • The counters array and type definition:

    typedef long long below_counter_t;
    below_counter_t* BELOW_counters = NULL;

    Here, the array type (long long) may be modified depending on your target platform and the maximum number that you expect for the counters.

  • A custom initialization function:

    void BELOW_init_custom()

    This function is called only once, when BELOW_COUNT function is called for the first time. By default, it allocates memory the counters array, and registers BELOW_finish function (defined in below_vendor.i) to be executed when the program exits.

  • A custom count function:

    BELOW_COUNT_custom(int id)

    This function is called by BELOW_COUNT function (defined in below_vendor.i). By default, it increment the counter at index id in array BELOW_counters.

  • A custom finish function:

    void BELOW_finish_custom()

    This function is called by BELOW_finish function. By default, it creates wedolow.prof profiling file, formats and writes the counters into it.

below_vendor.i contains vendor code which should be modified at your own risk, but should not need to be.

However, it is interesting to notice that below_vendor.i defines the following pre-processor variable:

#define BELOW_DYNAMIC_ANALYSIS

This variable may be used in your original code to run specific code when the context is beLow dynamic analysis.

For instance, in main.c , you may have noticed a problem: the program never exits because of the while(1) statement, which often happens when running bare metal applications on a microcontroller.

Therefore, there are two possibilities here:

  • Modifying main.c in the instrumented code. This is not the best solution, as your modifications will need to be applied again when you want to perform a new manual dynamic analysis.

  • Modifying original main.c before creating beLow project. As you don't want to change your code's initial behavior in general, you should use pre-processing variable BELOW_DYNAMIC_ANALYSIS.

Here is an example of the original main.c being modified to end after 10 iterations, but only in beLow usage context:

main.c (modified)
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>

#define N 5

void vect_add(int *a, int *b, int *c, int n) {
    for (int i = 0; i < n; i++) {
        c[i] = a[i] + b[i];
    }
}


int main() {
    int *a = malloc(N * sizeof(int));
    int *b = malloc(N * sizeof(int));
    int *c = malloc(N * sizeof(int));
    if (!a || !b || !c) {
        fprintf(stderr, "Memory allocation failed\n");
        return 1;
    }

    srand(time(NULL));

#ifdef BELOW_DYNAMIC_ANALYSIS
    int loop_counter = 0;
#endif
    while (1) {
        for (int i = 0; i < N; i++) {
            a[i] = rand() % 100;
            b[i] = rand() % 100;
        }

        vect_add(a, b, c, N);

        printf("a: ");
        for (int i = 0; i < N; i++) printf("%d ", a[i]);
        printf("\nb: ");
        for (int i = 0; i < N; i++) printf("%d ", b[i]);
        printf("\nc: ");
        for (int i = 0; i < N; i++) printf("%d ", c[i]);
        printf("\n\n");

        sleep(1);
#ifdef BELOW_DYNAMIC_ANALYSIS
        loop_counter++;
        if (loop_counter >= 10) {
            break; // Stop after 10 iterations
        }
#endif
    }

    free(a);
    free(b);
    free(c);
    return 0;
}

Then, the corresponding instrumented main.c is:

main.c (modified, instrumented)
#include "below_instr.i"
void BELOW_COUNT(int id);
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>

#define N 5

void vect_add(int *a, int *b, int *c, int n) {
{BELOW_COUNT(1);}

    for (int i = 0; i < n; i++) {
    {BELOW_COUNT(2);}
    
        c[i] = a[i] + b[i];
    }
    {BELOW_COUNT(3);}
    
}


int main() {
{BELOW_COUNT(0);}

    int *a = malloc(N * sizeof(int));
    int *b = malloc(N * sizeof(int));
    int *c = malloc(N * sizeof(int));
    if (!a || !b || !c) {
    {BELOW_COUNT(4);}
    
        fprintf(stderr, "Memory allocation failed\n");
        return 1;
    }
    {BELOW_COUNT(5);}
    

    srand(time(NULL));

#ifdef BELOW_DYNAMIC_ANALYSIS
    int loop_counter = 0;
#endif
    while (1) {
    {BELOW_COUNT(6);}
    
        for (int i = 0; i < N; i++) {
        {BELOW_COUNT(8);}
        
            a[i] = rand() % 100;
            b[i] = rand() % 100;
        }
        {BELOW_COUNT(9);}
        

        vect_add(a, b, c, N);

        printf("a: ");
        for (int i = 0; i < N; i++) {
        
        {BELOW_COUNT(10);}
        printf("%d ", a[i]);
        }
        {BELOW_COUNT(11);}
        
        printf("\nb: ");
        for (int i = 0; i < N; i++) {
        
        {BELOW_COUNT(12);}
        printf("%d ", b[i]);
        }
        {BELOW_COUNT(13);}
        
        printf("\nc: ");
        for (int i = 0; i < N; i++) {
        
        {BELOW_COUNT(14);}
        printf("%d ", c[i]);
        }
        {BELOW_COUNT(15);}
        
        printf("\n\n");

        sleep(1);
#ifdef BELOW_DYNAMIC_ANALYSIS
        loop_counter++;
        if (loop_counter >= 10) {
            break; // Stop after 10 iterations
        }
#endif
    }
    {BELOW_COUNT(7);}
    

    free(a);
    free(b);
    free(c);
    return 0;
}

When built, this instrumented code will stop after 10 iterations, while the original code will run forever as expected.

After merging the instrumented code into the original project, let's build and execute it.

> gcc -O2 -o exec main.cpp
> ./exec
a: 4 40 69 24 32 
b: 90 40 3 42 20 
c: 94 80 72 66 52 

[...]

a: 2 79 63 28 87 
b: 27 77 91 93 30 
c: 29 156 154 121 117

After the execution stops, a file called wedolow.prof is created. Let's have a look at its content:

wedolow.prof
16,1,10,50,10,0,1,10,1,50,10,50,10,50,10,50,10,

The first number in the list is the number of counters (16). It is used for consistency verification by beLow. The following numbers are the values of the counters after the execution.

This file may now be uploaded to beLow using the corresponding call-to-action.

Upload profiling data

Once uploaded, you may click Run analysis. The analysis will take the execution information into account.

Adapting manual dynamic analysis to your usage

As seen above, successfully running a manual dynamic analysis is about extracting the counters injected in the instrumented code. For any project, you will always have:

  • A modified version of your project files (content depends on your project's state)

  • below_vendor.i (always the same content)

  • below_instr.i (always the same content)

All the custom logic (counters allocation, initialization, termination) is in below_instr.i . It should be enough to modify only this file. This way, you can keep a modified version somewhere in your project, to avoid rewriting code at each manual dynamic analysis.

Also, your execution should end at some point. Don't hesitate using pre-processing variable BELOW_DYNAMIC_ANALYSIS in your original code if required.

As an example of custom modification, if you are running your code on a microcontroller, you may need to initialize an UART and write profiling data to it, instead of creating a file, and then manually copy-paste the output to a wedolow.prof file on your computer from your debug tools.

Another example: if you have debug tools accessing your program's memory, you may directly access the counters array values from your debug tools and format it offline

In both examples, modifying below_instr.i should be sufficient.

Last updated