memcheck: the debugger
valgrind
- это подпроект KDE
, который направлен
на отлов багов в С
и C++
программах. Краткая теория как работает valgrind
:
valgrind
- это набор утилит, построенных поверх либы-эмулятора процессора libvex
(причем libvex
может эмулировать не только тот процессор, на котором она рабоотает,
но и другие). То есть libvex
читает инструкции процессора и эмулирует их, отслеживая
что какая инструкция куда переместила (с точность до бита!).
В состав valgrind
входит утилита memcheck
(самая популярная утилита, работает по умолчанию).
В послании расскажу пока только про нее.
Она отслеживает:
- обращение за пределы выделенной памяти вроде такого:
int main() {
char * p = malloc (1);
[1] = '!'; // пишем сразу за границу буфера
p(p);
free return 0;
}
- использование неинициализированных данных
#include <stdio.h>
int main() {
int trash; // не инициализируем
("%d\n", trash);
printf return 0;
}
- утечки памяти
int main() {
char * p = malloc (1); // и не освобождаем
return 0;
}
Чтобы получить детальную иформацию о том, где произошла ошибка - нужно:
- собрать программу с отладкой. Для этого у
gcc
есть семейство ключей, начинающихся на-g
. - (не обязательно) выставить уровень оптимизаций поменьше. Снижение уровня оптимизаций улучшает точность отладочной информации: функции и шаблоны не инлайнятся, порядок инструкций не меняется и т.д. Так что изменяя оптимизации вы трейсите немного разные программы (в которых могут встречаться совсем разные баги). Но обычно это не проблема.
Собираем, запускаем (я взял пример 2.
выше):
$ gcc -ggdb -O0 main.c -o main
$ valgrind --track-origins=yes ./main
==2396== Use of uninitialised value of size 8
==2396== at 0x4E6D81B: _itoa_word (in /lib64/libc-2.11.2.so)
==2396== by 0x4E6EA7C: vfprintf (in /lib64/libc-2.11.2.so)
==2396== by 0x4E788F9: printf (in /lib64/libc-2.11.2.so)
==2396== by 0x400552: main (main.c:4)
==2396== Uninitialised value was created by a stack allocation
==2396== at 0x400534: main (main.c:2)
Из этого всего мы видим, что в строке main.c:4
произошло использование
неинициализированной переменной размера 8. Создано значение на стеке в строке main.c:2
.
Созданное значение отслеживается только со включенным ключом --track-origins=yes
.
Ключ появился в valgrind
в версии 3.5.0
.
Всё просто. Мы видим стек вызовов. В самом верху - самые близкие к ошибке строки, ниже - те, кто эти строки вызвал (размотан стек).
По умолчанию valgrind
трейсит только один процесс и пишет вывод на stdout
.
Это классно для консольных интерактивных программ, но неудобно при отладке демонов.
На этот случай у valgrind
есть вагон ключей:
--trace-children=yes
- трейсит все содраваемые процессом подпроцессы. Для этого в логе и пишетсяPID
процесса вначале:==PID==
.--log-file=<log_file_name>
- выводит лог в отдельный файл.
Есть и другие интересные полезные ключи (valgrind --help
):
--num-callers=NUM
- глубина размотки стека при отображении ошибки (valgrind
отслеживает какая функция какую вызвала). В примере выше у нас 4 уровня вложенности:_itoa_word
,vfprintf
,printf
,main
. По умолчанию отслеживается 12 вызовов. Часто это бывает мало (у нас в проекте до 30).--verbose
- показывает много всего интересного (и не очень): какие функции перехватываютсяvalgrind
и прочая ерунда.--track-fds=yes
- отслеживает незакрытые файлы при завершении программы (особо больной вопрос для демонов).--db-attach=yes
- присосаться к программе отладчиком в точке возникновения ошибки (в случае с демонами это не так просто).--leak-check=full
- ищет утечки памяти--show-reachable=yes
- показывает неосвобожденную, но неутекшую (есть ссылки из программы) память при завершении программы.--malloc-fill=val
/--free-fill=val
- забавает памят послеmalloc
/free
значениямиval
. По умолчанию 0 (не всегодя хорошо, так как гореспрограммисты любят совать проверки на 0 без повода).
У C++
программ (особенно написаных с использованием STL
) есть одна засада, усложняющая ловить ошибки:
многие функции работы с памятью выделяют больше памяти, чем это реально надо. Эта оптимизация скрывает
ошибки обращения за границы выделенной памяти в контейнерах типа std::vector
.
К счастью в шаблонах контейнеров gcc
есть волшебная переменная среды, отключающая такие оптимизации: GLIBCXX_FORCE_NEW
.
Итак, наша коммандная строка параноика довольно жирная. Слепим микроскрипт:
#!/bin/sh
# лежит где-то в ~/bin/vg.sh
GLIBCXX_FORCE_NEW=1 \
valgrind \
--track-origins=yes \
--trace-children=yes \
--num-callers=50 \
--track-fds=yes \
--leak-check=full \
--show-reachable=yes \
--malloc-fill=0xa1 \
--free-fill=0xa1 \
"$@"
Юзать так:
vg.sh --log-file=valgrind.log ./my-nice-program --my-nice-params
valgrind
позволяет не только ковыряться в уже готовых бинарниках.
В свой исходник можно вставлять специальные макросы, коотрые
работают только при сборке с поддержкой valgrind
. Сидят эти макросы в
<valgrind/valgrind.h>
и <valgrind/memcheck.h>
. С их помощью можно
решать самые разные задачи. Например, представим, что у нас в программе
есть свой распределитель памяти и мы хотим отслеживать утечки в нем:
#include <stdlib.h>
typedef unsigned char u8;
static u8 * mempool = 0;
static u8 * mempool_p = 0;
static size_t mempool_size = 0;
void mempool_init (size_t pool_size) {
= mempool = (u8*)malloc (pool_size);
mempool_p if (mempool) mempool_size = pool_size;
}
void mempool_destroy (void) { free (mempool); }
void * mempool_alloc (size_t obj_size) {
// не влезет
if (mempool_p + obj_size > mempool + mempool_size) return 0;
void * result = mempool_p; mempool_p += obj_size;
return result;
}
void mempool_free (void * obj, size_t obj_size) {
// освобождаем только последний. такая вот халтура :]
if (mempool_p == (u8*)obj + obj_size) mempool_p -= obj_size;
}
int main () {
(1000);
mempool_init
void * p1 = mempool_alloc (10);
void * p2 = mempool_alloc (20);
(p2, 20);
mempool_free // Забыли
//mempool_free (p1, 10);
();
mempool_destroy return 0;
}
$ g++ -ggdb -O0 a.c -o a
$ vg.sh ./a
...
==24548== HEAP SUMMARY:
==24548== in use at exit: 0 bytes in 0 blocks
==24548== total heap usage: 1 allocs, 1 frees, 1,000 bytes allocated
Но p1
то не освобожден!
У valgrind
есть чудомакросы, помечающие память, как выделенную из аллокатора:
(addr, sizeB, rzB, is_zeroed)
VALGRIND_MALLOCLIKE_BLOCK(addr, rzB) VALGRIND_FREELIKE_BLOCK
Их больше, но заюзаем только эти. Изменим исходнег:
#include <stdlib.h>
#include <valgrind/valgrind.h> // 1
typedef unsigned char u8;
static u8 * mempool = 0;
static u8 * mempool_p = 0;
static size_t mempool_size = 0;
void mempool_init (size_t pool_size) {
= mempool = (u8*)malloc (pool_size);
mempool_p if (mempool) mempool_size = pool_size;
}
void mempool_destroy (void) { free (mempool); }
void * mempool_alloc (size_t obj_size) {
// не влезет
if (mempool_p + obj_size > mempool + mempool_size) return 0;
void * result = mempool_p; mempool_p += obj_size;
(result, obj_size, 0, 0); // 2
VALGRIND_MALLOCLIKE_BLOCKreturn result;
}
void mempool_free (void * obj, size_t obj_size) {
// освобождаем только последний. такая вот халтура :]
if (mempool_p == (u8*)obj + obj_size) mempool_p -= obj_size;
(obj, 0); // 3
VALGRIND_FREELIKE_BLOCK}
int main () {
(1000);
mempool_init
void * p1 = mempool_alloc (10);
void * p2 = mempool_alloc (20);
(p2, 20);
mempool_free // Забыли
//mempool_free (p1, 10);
();
mempool_destroy return 0;
}
$ g++ -ggdb -O0 a.c -W -Wall -o a
a.c: In function ‘int main()’:
a.c:35: предупреждение: неиспользуемая переменная ‘p1’
# хехе, нашу переменную засекли :]
$ vg.sh ./a
...
==8958== HEAP SUMMARY:
==8958== in use at exit: 1,000 bytes in 1 blocks
==8958== total heap usage: 3 allocs, 2 frees, 1,030 bytes allocated
==8958==
==8958== 1,000 bytes in 1 blocks are still reachable in loss record 1 of 1
==8958== at 0x4C2635E: malloc (vg_replace_malloc.c:236)
==8958== by 0x4008E5: mempool_init(unsigned long) (a.c:11)
==8958== by 0x400A8C: main (a.c:33)
Оп! Мы немножко запутали valgrind
тем, что у нас p1
и mempool
- один адрес.
Из-за этого он неправильно отределяет точку выделения памяти. Это баг в valgrind
.
Главное, что псевдоутечка детектируется.
Второй пример - поиск врага, неожиданно модифицирующего память.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void init (char * p);
void process (const char * p);
int main ()
{
char * p = (char *)malloc (100);
(p);
init ("before process: p[33] = %c\n", p[33]);
printf (p);
process ("after process: p[33] = %c\n", p[33]);
printf (p);
free return 0;
}
// Делаем вид, что дальше идёт много чужого неясного кода
void init (char * p)
{
(p, '+', 100);
memset }
void process (const char * p)
{
char * q = (char *)p; // ужас! сейчас испоганит!
[33] = '-';
q}
Глядя на прототип функции process
никак не скажешь,
что она может изменить данные p[33]
, но тест printf
говорит об обратном.
Как найти врага? Сделаем память “недоступной” и проверим:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <valgrind/memcheck.h>
void init (char * p);
void process (const char * p);
int main ()
{
char * p = (char *)malloc (100);
(p);
init ("before process: p[33] = %c\n", p[33]);
printf (p, 100);
VALGRIND_MAKE_MEM_NOACCESS(p);
process ("after process: p[33] = %c\n", p[33]);
printf (p);
free return 0;
}
// Делаем вид, что дальше идёт много чужого неясного кода
void init (char * p)
{
(p, '+', 100);
memset }
void process (const char * p)
{
char * q = (char *)p; // ужас! сейчас испоганит!
[33] = '-';
q}
Запускаем:
$ g++ -ggdb -O0 a.c -W -Wall -o a
$ vg.sh ./a
...
before process: p[33] = +
==22521== Invalid write of size 1
==22521== at 0x400A79: process(char const*) (a.c:31)
==22521== by 0x400A0A: main (a.c:16)
==22521== Address 0x5931061 is 33 bytes inside a block of size 100 alloc'd
==22521== at 0x4C2635E: malloc (vg_replace_malloc.c:236)
==22521== by 0x40096F: main (a.c:12)
==22521==
==22521== Invalid read of size 1
==22521== at 0x400A13: main (a.c:17)
==22521== Address 0x5931061 is 33 bytes inside a block of size 100 alloc'd
==22521== at 0x4C2635E: malloc (vg_replace_malloc.c:236)
==22521== by 0x40096F: main (a.c:12)
==22521==
after process: p[33] = -
...
read
нас не очень интересует, а вот write
- это и есть наш запрятанный глюк:
(a.c:31)
Хватит для начала.