Code Quality and Debugging
Writing Better Programs
Write Readable Programs
- Use formatting conventions / best practices
- Use
clang-formatfor automatic formatting
clang-format -i fichier.c
clang-format can be included in most code editors (vim, Sublime Text, Visual Studio…).
Before Formatting
#include <stdio.h>
#include <stdlib.h>
#include "inout.h"
// selection par Rectangle
void selectionR(unsigned char m[HAUTEUR][LARGEUR],int x,int y,int x1,int y1, unsigned char reception[HAUTEUR][LARGEUR]){
int j,i;
for(i=x;i< x1;i++)
for(j=y;j< y1;++j){
reception[i][j]=m[i][j];
m[i][j]=0;
}
}
After Formatting
#include <stdio.h>
#include <stdlib.h>
#include "inout.h"
// selection par Rectangle
void selectionR (unsigned char m[HAUTEUR][LARGEUR],
int x,
int y,
int x1,
int y1,
unsigned char reception[HAUTEUR][LARGEUR])
{
int j, i;
for (i = x; i < x1; i++)
for (j = y; j < y1; ++j)
{
reception[i][j] = m[i][j];
m[i][j] = 0;
}
}
Principles
- KISS: Keep It Stupid Simple
- DRY: Don’t Repeat Yourself
Applying DRY with PMD
Duplicates search with pmd:
$ pmd cpd --minimum-tokens 70 --language c --files projet.c
Found a 4 line (112 tokens) duplication in the following files:
Starting at line 43 of projet.c
Starting at line 79 of projet.c
void selection_ellipse(unsigned char image[HAUTEUR][LARGEUR],int C_x, int C_y, int a, int b, unsigned char selection[2*b][2*a]){
for (int y = 0; y <2*b; y++) { //balaie un rectangle de hauteur 2b et de largeur 2a (rectangle contenant l'ellipse)
for (int x = 0; x < 2*a; x++) {
if ( ( (pow((x-a),2))/(a*a) + (pow((y-b),2))/(b*b)) <= 1 ) //verifie que la portion d'image est bien dans l'ellipse
Enable Warnings
#include <stdio.h>
int main ()
{
int *p;
printf ("Val = %d\n", *p);
return 0;
}
$ gcc test_w.c
$ gcc test_w.c -W -Wall -Wextra
test_w.c:6:27: warning: variable 'p' is uninitialized when used here [-Wuninitialized]
printf("Val = %d\n", *p);
^
test_w.c:5:11: note: initialize the variable 'p' to silence this warning
int *p;
^
= NULL
1 warning generated.
Use Static Checkers
Static checkers allow you to detect common mistakes:
clang-tidy(CERT guidelines)cppcheckwith MISRA, CERT add-ons (embedded programming guidelines)ratsoclint
Example: From a Student Project
$ gcc negative.c
# no errors, no warnings
With warnings enabled:
$ gcc negative.c -c -W -Wall -Wextra
# 6 warnings
With clang-tidy:
$ clang-tidy --quiet -checks='*' negative.c --
# 88 warnings!
Static checkers can also be included in your editors (vim, Sublime Text, Visual Studio…).
Use Memory Checkers
#include <stdlib.h>
int main (void)
{
char *buffer = malloc (2);
if (buffer != NULL)
{
buffer[0] = 'a';
buffer[1] = 'b';
free (buffer);
buffer[0] = 'a'; // Use after free!
}
return 0;
}
With AddressSanitizer:
$ clang -W -Wall -g -fsanitize=address use_mem.c
$ ./a.out
=================================================================
==65839==ERROR: AddressSanitizer: heap-use-after-free on address 0x6020000000f0
WRITE of size 1 at 0x6020000000f0 thread T0
#0 0x1010b0f2a in main use_mem.c:14
...
Valgrind can be used if you don’t have the source:
$ clang -W -Wall use_mem.c
$ valgrind ./a.out
==2936== Invalid write of size 1
==2936== at 0x401186: main (in /home/imaEns/jdequidt/a.out)
==2936== Address 0x4a41040 is 0 bytes inside a block of size 2 free'd
...
Use Debugger
Use gdb (with gcc) or lldb (with clang). Compile with -g to enable debug info.
Basic Commands
| Command | Shortcut | Comment |
|---|---|---|
| run [args] | r | start the program |
| quit | q | |
| CTRL+C | break |
Breakpoints
| Command | Shortcut | Comment |
|---|---|---|
| break [where] | b | where = line number or function |
| disable # | # bp number | |
| enable # (once) | # bp number | |
| info break | informations |
Watching
| Command | Shortcut | Comment |
|---|---|---|
| watch (expr ou cond) | surveillance | |
| break # if cond | bp + watch | |
| continue | c # | until bp # |
| next # | n # | jump # lines |
| step # | s # | jump # lines (goes inside func) |
| finish | continue until func ends |
Stack Navigation
| Command | Shortcut | Comment |
|---|---|---|
| bt (full) | display stack | |
| up / down | stack navigation | |
| print expr | p expr | display expr |
Focus on Memory
Memory layout:
- text: instructions
- data: static, global initialized data
- bss: static, global uninitialized data
- heap: zone for dynamically allocated data
- stack: zone for temporary (local) data
$ size a.out
text data bss dec hex filename
1587 588 12 2187 88b a.out
Test Your Code
You need to test every function you write (= unit testing) because:
- Even the simplest may not be bug-proof
- Memory errors could lead to errors elsewhere in your code
- Sometimes some refactoring operations could lead to new bugs
You need to be sure that your code improves over time and does not regress.
Example Test with check.h
#include <check.h>
START_TEST (test_rn)
{
ck_assert (init_rn_generator () == 1);
}
END_TEST
START_TEST (test_init)
{
const unsigned int V = 10;
const unsigned int T = 5;
int array[T];
init_array (array, T, V);
for (unsigned int i = 0; i < T; i++)
{
ck_assert (array[i] >= 0);
ck_assert (array[i] < (int)V);
}
}
END_TEST
Suite *sort_suite (void)
{
Suite *s = suite_create ("SortAlgorithms");
TCase *tc_core = tcase_create ("Core");
tcase_add_test (tc_core, test_rn);
tcase_add_test (tc_core, test_init);
suite_add_tcase (s, tc_core);
return s;
}
int main (void)
{
int no_failed = 0;
Suite * s = sort_suite ();
SRunner *runner = srunner_create (s);
srunner_run_all (runner, CK_NORMAL);
no_failed = srunner_ntests_failed (runner);
srunner_free (runner);
return (no_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE;
}
Output:
./build/unit_tests
Running suite(s): SortAlgorithms
100%: Checks: 2, Failures: 0, Errors: 0
You can add options (lcov) to check the coverage of your tests.
Automation - Makefile
# Building rules
default: $(EXEC_APPLI)
# Main app
main: $(EXEC_APPLI)
# Unit Tests
tests: $(EXEC_TESTS) coverage
# Code formatting and Duplicates
lint: format pmd
# Static Checkers
checkers: tidy rats cppcheck
format:
@./scripts/run-clang-format.py -r $(SRC_DIR) $(INCL_DIR) $(TEST_DIR)
pmd:
pmd cpd --minimum-tokens 100 --language c --files $(SRC_DIR) \
$(TEST_DIR) $(INCL_DIR)
tidy:
clang-tidy --quiet -checks='*' -header-filter="^include" \
$(SRC_DIR)/*.c -- -I$(OS_DIR) -I$(INCL_DIR)
rats:
rats -l c -w 3 $(SRC_DIR)/*.c $(TEST_DIR)/*.c
cppcheck:
cppcheck --enable=warning,style,portability $(SRC_DIR)/*.c $(TEST_DIR)/*.c
coverage:
$(EXEC_TESTS)
gcov -f -b $(BUILD_DIR)/
lcov --directory $(BUILD_DIR) --base-directory $(BUILD_DIR) -c \
-o $(REPORT_DIR)/cov.info
genhtml $(REPORT_DIR)/cov.info -o $(REPORT_DIR)
Automation - GitLab CI
.gitlab-ci.yml
stages:
- codestyling
- check
- build
- test
- coverage
- clean
job:codestyling:
stage: codestyling
script: ./scripts/run-clang-format.py -r src includes tests
job:check:tidy:
stage: check
script: clang-tidy src/*.c -- -Iincludes
when: always
job:check:cppcheck:
stage: check
script: cppcheck --enable=warning,style,portability src/*.c
when: always
job:build:
stage: build
script:
- mkdir build
- make main
when: always
artifacts:
paths:
- build/
job:test:
stage: test
script:
- make tests
- build/project_tests
when: on_success
artifacts:
paths:
- build/
job:coverage:
stage: coverage
script:
- llvm-cov gcov -f -b build/*
- lcov --directory build --base-directory . -c -o cov.info
- mkdir report
- genhtml cov.info -o report
coverage: '/^\s*lines\S*\s*(\d+(?:\.\d+)?%)\s*/'
when: on_success
dependencies:
- job:test
artifacts:
paths:
- report/
Conclusion
Take-Away Messages
- Knowing compilation theory helps to find your errors / bugs
- Be humble: We make lots of mistakes, use automatic tools to detect them (clang options, static checkers…)
- Be hungry: Practice! Try new things, trials and errors are important
- Be smart: Avoid repetitive tasks, rely on automation (make, gitlab, git hooks…) or on your editor (vim, VS Code, Sublime Text can handle clang-format, clang-tidy, cppcheck…)
Vim Configuration Example
set nocompatible "be iMproved, required
filetype off "required
set rtp+=~/.vim/bundle/Vundle.vim
call vundle#begin()
Plugin 'VundleVim/Vundle.vim' "Gestionnaire de paquets
Plugin 'w0rp/ale' "Syntax analyzer
Plugin 'universal-ctags/ctags' "Ctags pour symboles
Plugin 'taglist.vim' "gestion tags du code source
Plugin 'rhysd/vim-clang-format' "clang-format
call vundle#end() "required
filetype plugin indent on "required