Hướng dẫn dùng Valgrind để kiểm tra memory leak

Việc tìm kiếm các bug như memory leak, memory corruptions và memory access violations có thể rất khó khăn nếu bạn không có công cụ phù hợp để giúp bạn thu hẹp phạm vi điều tra và cung cấp manh mối. Với Valgrind cho các chương trình viết bằng C / C ++, nó có thể giúp bạn tiết kiệm hàng giờ vùi đầu vào tìm nguyên nhân gây ra bug.

Valgrind là công cụ được dùng để phát hiện quản lý sai bộ nhớ(memory mismanagement detector). Nó có thể giúp chúng ta biết được memory leak, lỗi về sự phân bổ(deallocation errors) data,... Trên thực tế, Valgrind là một công cụ thu thập nhiều thông tin khác nhau (ví dụ: cấu hình bộ nhớ cache); tuy nhiên, trong bài viết này mình tập trung vào công cụ mặc định của nó là memcheck.

Memcheck có thể phát hiện:

  • Memory chưa được khởi tạo
  • Đọc/ghi memory sau khi nó đã được free
  • Đọc/ghi ra phần cuối của khối malloc
  • Đọc/ghi các vùng không thích hợp trên stack
  • Memory leaks -- nơi các con trỏ đến các khối malloc đã bị mất vĩnh viễn
  • Không đồng nhất trong việc sử dụng malloc/new/new [] và free/delete/delete []
  • Dùng chồng chéo con trỏ ở src và dst trong memcpy () và các hàm liên quan
  • Một số trường hợp lạm dụng API pthreads trong POSIX

Môi trường

  • Ubuntu mọi phiên bản (hiện tại mình dùng ubuntu 20.4 server)
  • Valgrind 3.15.0

Tải valgrind với lệnh

$ sudo apt  install valgrind

Ví dụ 1

Để hiểu rõ hơn một xíu về cách sử dụng Valgrind trong một chương trình cụ thể ta có thể viết một chương trình C như sau

#include <stdio.h>

int main()
{
  char *p;

  // Allocation #1 of 19 bytes
  p = (char *) malloc(19);

  // Allocation #2 of 12 bytes
  p = (char *) malloc(12);
  free(p);

  // Allocation #3 of 16 bytes
  p = (char *) malloc(16);

  return 0;
}

Build chương trình để tạo ra file thực thi với lệnh

$ gcc -o test -g test.c

Sau khi chương trình tạo file thực thi, để có thể kiểm tra memory leak thông qua việc chạy file thực thi là execution với lệnh sau

$ valgrind --tool=memcheck --leak-check=yes --show-reachable=yes --num-callers=20 --track-fds=yes ./test

Output sẽ có dạng

==3246== Memcheck, a memory error detector
==3246== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==3246== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==3246== Command: ./test
==3246== 
==3246== 
==3246== FILE DESCRIPTORS: 3 open at exit.
==3246== Open file descriptor 2: /dev/pts/0
==3246==    <inherited from parent>
==3246== 
==3246== Open file descriptor 1: /dev/pts/0
==3246==    <inherited from parent>
==3246== 
==3246== Open file descriptor 0: /dev/pts/0
==3246==    <inherited from parent>
==3246== 
==3246== 
==3246== HEAP SUMMARY:
==3246==     in use at exit: 35 bytes in 2 blocks
==3246==   total heap usage: 3 allocs, 1 frees, 47 bytes allocated
==3246== 
==3246== 16 bytes in 1 blocks are definitely lost in loss record 1 of 2
==3246==    at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==3246==    by 0x1091A6: main (test.c:15)
==3246== 
==3246== 19 bytes in 1 blocks are definitely lost in loss record 2 of 2
==3246==    at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==3246==    by 0x10917E: main (test.c:8)
==3246== 
==3246== LEAK SUMMARY:
==3246==    definitely lost: 35 bytes in 2 blocks
==3246==    indirectly lost: 0 bytes in 0 blocks
==3246==      possibly lost: 0 bytes in 0 blocks
==3246==    still reachable: 0 bytes in 0 blocks
==3246==         suppressed: 0 bytes in 0 blocks
==3246== 
==3246== For lists of detected and suppressed errors, rerun with: -s
==3246== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)

Chúng ta cùng xem lại code C xem điều gì đã xảy ra. Allocation #1 (19 byte leak) đã bị mất bởi gì p được trở tới một nơi nào đó trước khi memory ở  Allocation #1  được free. Để giúp chúng ta có thể track được nó, Valgrind đã cho chúng ta stack trace chỉ ra nơi mà những byte được cấp phát. Trong 19 byte leak thì những byte này đã được cấp phát trong line 8 của chương trình test.c. Allocation #2 (12 byte leak) sẽ không được thấy trong output bởi vì nó đã được free.Allocation #3 sẽ giống với Allocation #1, vẫn sẽ có memory leak, và nó được Valgrind chỉ ra cho chúng ta thấy tại line số 15 của file main.c

Ví dụ 2

Ta sửa nội dung ở file test.c trên với đoạn như sau

#include<stdio.h>
#include<stdlib.h>
int main(int argc, char* argv[])
{
        char *mem1 = NULL;
        char *mem2 = NULL;

        mem1 = (char*)malloc(sizeof(char) * 16);
        mem2 = (char*)malloc(sizeof(char) * 16);

        printf("Sample for checking memory leak using valgrind tool\n");

        free(mem1);
        //free(mem2);

        return 0;
}

Thực hiện build và chạy Valgrind tương tự như trên ta có output

==3991== Memcheck, a memory error detector
==3991== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==3991== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==3991== Command: ./test
==3991== 
Sample for checking memory leak using valgrind tool
==3991== 
==3991== FILE DESCRIPTORS: 3 open at exit.
==3991== Open file descriptor 2: /dev/pts/0
==3991==    <inherited from parent>
==3991== 
==3991== Open file descriptor 1: /dev/pts/0
==3991==    <inherited from parent>
==3991== 
==3991== Open file descriptor 0: /dev/pts/0
==3991==    <inherited from parent>
==3991== 
==3991== 
==3991== HEAP SUMMARY:
==3991==     in use at exit: 16 bytes in 1 blocks
==3991==   total heap usage: 3 allocs, 2 frees, 1,056 bytes allocated
==3991== 
==3991== 16 bytes in 1 blocks are definitely lost in loss record 1 of 1
==3991==    at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==3991==    by 0x1091C3: main (test.c:9)
==3991== 
==3991== LEAK SUMMARY:
==3991==    definitely lost: 16 bytes in 1 blocks
==3991==    indirectly lost: 0 bytes in 0 blocks
==3991==      possibly lost: 0 bytes in 0 blocks
==3991==    still reachable: 0 bytes in 0 blocks
==3991==         suppressed: 0 bytes in 0 blocks
==3991== 
==3991== For lists of detected and suppressed errors, rerun with: -s
==3991== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

Valgrind sẽ cho chúng ta thấy có memory leak tại line số 9, ở trong code C chúng ta mới free vùng mem1 chứ chưa free vùng mem2

Một số error của Memcheck

Có rất nhiều loại error của memcheck mà ta có thể gặp phải, để có thể xác định được cụ thể error nào thì mình khuyên dùng manual để tra cứu

Valgrind
Official Home Page for valgrind, a suite of tools for debugging and profiling. Automatically detect memory management and threading bugs, and perform detailed profiling. The current stable version is valgrind-3.16.1.

Tạm kết

Ngay cả khi bạn không phải trace memory, với phong cách của một chuyên gia, bạn nên execute các đoạn code của mình thông qua Valgrind. Từ đó bạn có thể việc sử dụng Valgrind dễ dàng như thế nào, và biến nó thành một thói quen, một công cụ mới đắc lực cho bản thân mình.

Tham khảo

[1] [2]