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
fprintf
not 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:

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:
#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

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:
#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:
#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 registersBELOW_finish
function (defined inbelow_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 inbelow_vendor.i
). By default, it increment the counter at indexid
in arrayBELOW_counters
.A custom finish function:
void BELOW_finish_custom()
This function is called by
BELOW_finish
function. By default, it createswedolow.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 variableBELOW_DYNAMIC_ANALYSIS
.
Here is an example of the original main.c being modified to end after 10 iterations, but only in beLow usage context:
#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:
#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:
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.

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