Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 34.428 phút đọc

Valgrind và bốn loại rò rỉ

Sau bài này bạn sẽ làm được

  • Chạy valgrind với bộ cờ đầy đủ
  • Phân biệt bốn loại rò rỉ và biết loại nào phải sửa
  • Đọc vết gọi để tìm dòng cấp phát
  • So sánh Valgrind với ASan để chọn đúng việc

Valgrind chậm hơn ASan hai mươi lần và không bắt được tràn trên ngăn xếp. Nhưng nó có hai thứ ASan không có: không cần dịch lại, và bắt được biến chưa khởi tạo. Riêng điều thứ hai đủ để giữ nó trong bộ công cụ.

#Chạy thử

# Bo co day du, nen thuoc long
valgrind --leak-check=full \
         --show-leak-kinds=all \
         --track-origins=yes \
         --error-exitcode=1 \
         ./prog

# Giai thich tung co:
#   --leak-check=full        liet ke tung cho ro ri, khong chi tong so
#   --show-leak-kinds=all    hien ca bon loai, khong chi "definitely"
#   --track-origins=yes      voi bien chua khoi tao: chi ra NOI SINH RA no.
#                            Cham hon nhung gan nhu luon dang gia
#   --error-exitcode=1       tra ve 1 khi co loi, de CI phat hien duoc
terminal
gcc -std=c11 -g -O1 -o rr ro_ri.c && valgrind --leak-check=full ./rr
==12345== Memcheck, a memory error detector
==12345== Command: ./rr
==12345==
==12345==
==12345== HEAP SUMMARY:
==12345==     in use at exit: 11 bytes in 1 blocks
==12345==   total heap usage: 2 allocs, 1 frees, 24 bytes allocated
==12345==
==12345== 11 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345==    at 0x4C2FB0F: malloc (vg_replace_malloc.c:299)
==12345==    by 0x400597: tao_ten (ro_ri.c:5)
==12345==    by 0x4005F8: main (ro_ri.c:12)
==12345==
==12345== LEAK SUMMARY:
==12345==    definitely lost: 11 bytes in 1 blocks
==12345==    indirectly lost: 0 bytes in 0 blocks
==12345==      possibly lost: 0 bytes in 0 blocks
==12345==    still reachable: 0 bytes in 0 blocks
==12345==         suppressed: 0 bytes in 0 blocks
==12345==
==12345== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

#Bốn loại rò rỉ

LoạiPhải sửaNghĩa là
definitely lostCóKhông còn con trỏ nào tới khối đó. Rò rỉ thật
indirectly lostCó, bằng cách sửa cái ở trênKhối bị mất vì cấu trúc cha đã mất
possibly lostXem xétChỉ còn con trỏ trỏ vào giữa khối, không vào đầu
still reachableThường khôngChưa giải phóng nhưng vẫn còn con trỏ tới
Bốn loại trong một chương trình
typedef struct Nut {
    int         gia_tri;
    struct Nut *sau;
} Nut;

static char *g_bo_dem;              /* bien toan cuc */

int main(void) {
    /* 1. definitely lost: mat con tro */
    char *a = malloc(10);
    a = malloc(20);                 /* khoi 10 byte khong ai giu nua */

    /* 2. indirectly lost: danh sach mat ca chuoi */
    Nut *dau = malloc(sizeof *dau);
    dau->sau = malloc(sizeof *dau); /* nut thu hai */
    dau->sau->sau = NULL;
    dau = NULL;                     /* nut 1 definitely, nut 2 indirectly */

    /* 3. possibly lost: con tro tro vao giua khoi */
    char *b = malloc(100);
    b += 50;                        /* khong con tro vao dau khoi */

    /* 4. still reachable: van co con tro toan cuc tro toi */
    g_bo_dem = malloc(30);

    free(a);
    return 0;
}
terminal
valgrind --leak-check=full --show-leak-kinds=all ./bl
==12345== LEAK SUMMARY:
==12345==    definitely lost: 26 bytes in 2 blocks
==12345==    indirectly lost: 16 bytes in 1 blocks
==12345==      possibly lost: 100 bytes in 1 blocks
==12345==    still reachable: 30 bytes in 1 blocks
==12345==         suppressed: 0 bytes in 0 blocks

#Việc chỉ Valgrind làm được

Memcheck
Công cụ mặc định của Valgrind. Nó theo dõi từng bit của bộ nhớ với hai lá cờ: bit này có hợp lệ để truy cập không, và bit này đã được ghi giá trị xác định chưa. Lá cờ thứ hai là thứ ASan không có.
chua_kt.c
#include <stdio.h>
#include <stdlib.h>

static int tinh(int *a, int n) {
    int t;                       /* quen khoi tao */

    for (int i = 0; i < n; ++i) t += a[i];

    return t;
}

int main(void) {
    int a[3] = { 1, 2, 3 };

    printf("tong = %d\n", tinh(a, 3));

    return 0;
}
terminal
# ASan hoàn toàn im lặng với lỗi này
gcc -std=c11 -g -O1 -fsanitize=address,undefined -o ck chua_kt.c && ./ck
tong = 6
gcc -std=c11 -g -O1 -o ck chua_kt.c && valgrind --track-origins=yes ./ck
==12345== Conditional jump or move depends on uninitialised value(s)
==12345==    at 0x4E88B00: vfprintf (vfprintf.c:1642)
==12345==    by 0x4E8F898: printf (printf.c:33)
==12345==    by 0x400605: main (chua_kt.c:15)
==12345==  Uninitialised value was created by a stack allocation
==12345==    at 0x400566: tinh (chua_kt.c:4)
==12345==
==12345== Use of uninitialised value of size 8
==12345==    at 0x4E86F0B: _itoa_word (_itoa.c:179)
==12345==    by 0x4E8A517: vfprintf (vfprintf.c:1642)
==12345==    by 0x400605: main (chua_kt.c:15)

#Đọc vết gọi

Thông báoNghĩa
Invalid read of size NĐọc vùng không được phép
Invalid write of size NGhi vùng không được phép
Invalid free()Giải phóng con trỏ không phải từ malloc, hoặc lần hai
Mismatched free() / deleteCấp bằng malloc mà giải bằng delete
Conditional jump depends on uninitialised valueRẽ nhánh trên giá trị chưa xác định
Use of uninitialised value of size NDùng giá trị chưa xác định làm địa chỉ
Source and destination overlapmemcpy với vùng chồng nhau
Syscall param points to uninitialised byteTruyền vùng chưa khởi tạo vào lời gọi hệ thống
==12345== Invalid write of size 4
==12345==    at 0x4005A1: main (tbd.c:8)              <- DONG GHI SAI
==12345==  Address 0x5204054 is 0 bytes after a block of size 20 alloc'd
==12345==    at 0x4C2FB0F: malloc (vg_replace_malloc.c:299)
==12345==    by 0x400591: main (tbd.c:5)              <- DONG CAP PHAT

/* Doc tu tren xuong:
     1. Loai loi va kich thuoc: ghi 4 byte
     2. Vet goi toi cho ghi, dong tren cung la dong cua ban
     3. Vi tri tuong doi: "0 bytes after a block of size 20"
     4. Vet goi toi cho cap phat

   Cau truc giong het bao cao cua ASan o Bai 34.2. Hai cong cu
   khac nhau ve co che nhung giong nhau ve nhung gi ban can biet. */

#So với ASan

ValgrindASan
Cần dịch lạiKhôngCó
Chậm hơn20 tới 50 lầnKhoảng 2 lần
Tràn trên heapBắt đượcBắt được
Tràn trên ngăn xếpKhôngBắt được
Tràn trên biến toàn cụcKhôngBắt được
Dùng sau khi giải phóngBắt đượcBắt được
Rò rỉ bộ nhớBắt được, phân bốn loạiBắt được
Biến chưa khởi tạoBắt đượcKhông
Chạy trên WindowsKhôngCó, qua MSVC
Cần mã nguồnKhông, chỉ cần -g cho tên hàmCó

#Các công cụ khác của Valgrind

# Memcheck: mac dinh, phat hien loi bo nho
valgrind ./prog
valgrind --tool=memcheck ./prog

# Callgrind: dem so lenh moi ham, de tim diem nong
valgrind --tool=callgrind ./prog
callgrind_annotate callgrind.out.12345 | head -30
kcachegrind callgrind.out.12345          # giao dien do hoa

# Massif: theo doi bo nho heap theo thoi gian
valgrind --tool=massif ./prog
ms_print massif.out.12345

# Helgrind: phat hien dua du lieu va khoa cheo
valgrind --tool=helgrind ./prog

# DRD: cung muc dich voi Helgrind, thuat toan khac
valgrind --tool=drd ./prog

# Cachegrind: mo phong bo nho dem CPU
valgrind --tool=cachegrind ./prog

Tự làm thử

  1. Chạy Valgrind trên chương trình rò rỉ và xác định lời gọi nào bị rò rỉ.
  2. Viết chương trình sinh đủ bốn loại rò rỉ và đối chiếu với bảng tổng kết.
  3. Viết chương trình đọc biến chưa khởi tạo, xác nhận ASan im lặng còn Valgrind thì không.
  4. Chạy với và không có --track-origins=yes và so hai báo cáo.
  5. Dùng Massif trên một chương trình cấp phát nhiều và đọc biểu đồ.
  6. Sinh tệp bỏ qua bằng --gen-suppressions=all cho một chương trình dùng thư viện đồ họa.

Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.

Tóm tắt

  • Bộ cờ đầy đủ là --leak-check=full --show-leak-kinds=all --track-origins=yes --error-exitcode=1.
  • Thiếu --error-exitcode=1 thì Valgrind trả về mã thoát của chương trình, nên CI không phát hiện được lỗi.
  • Bốn loại rò rỉ: definitely và indirectly phải sửa, possibly cần xem xét, still reachable thường bỏ qua được.
  • Chỉ Valgrind bắt được biến chưa khởi tạo, và --track-origins là thứ biến báo cáo đó thành có ích.
  • Valgrind không bắt được tràn trên ngăn xếp và trên biến toàn cục, đó là chỗ ASan mạnh hơn hẳn.