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

clang-tidy và cppcheck

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

  • Chạy clang-tidy với bộ kiểm tra phù hợp
  • Sinh compile_commands.json
  • Chạy cppcheck và đọc báo cáo
  • Tắt báo giả một cách có kiểm soát

Trình biên dịch nhìn từng câu lệnh. Bộ phân tích tĩnh đi theo từng đường thực thi có thể, nên nó thấy được những thứ trình biên dịch không thấy: dùng sau khi giải phóng, rò rỉ trên nhánh lỗi, giải tham chiếu NULL ba hàm sau chỗ gán.

#Mã thử

loi.c, năm lỗi cố ý
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* 1. Ro ri bo nho tren nhanh loi */
static char *doc_ten(const char *dd) {
    FILE *t = fopen(dd, "rb");
    if (!t) return NULL;
    char *dem = malloc(256);
    if (fread(dem, 1, 255, t) == 0) {
        fclose(t);
        return NULL;              /* quen free(dem) */
    }
    dem[255] = '\0';
    fclose(t);
    return dem;
}

/* 2. Dung sau khi giai phong */
static void sau_khi_tha(void) {
    char *p = malloc(16);
    free(p);
    strcpy(p, "abc");             /* dung sau khi tha */
}

/* 3. Gan trong dieu kien, va bien chua khoi tao */
static int lam(int *p) {
    int x;
    if (p = NULL) return -1;      /* gan chu khong so sanh */
    return x + *p;                /* x chua khoi tao */
}

/* 4. Bo dem co dinh voi ham khong gioi han */
static void chep(const char *nguon) {
    char dem[16];
    strcpy(dem, nguon);           /* co the tran */
    printf("%s\n", dem);
}

/* 5. Chia cho khong */
static int chia(int a) { return a / 0; }

int main(int argc, char **argv) {
    (void)argc;
    char *t = doc_ten(argv[1]);
    if (t) { chep(t); free(t); }
    sau_khi_tha();
    printf("%d\n", chia(5));
    return 0;
}

#GCC so với clang-tidy

terminal
gcc -std=c11 -Wall -Wextra -O2 -c loi.c -o /dev/null
loi.c:29:9: warning: suggest parentheses around assignment used as truth value [-Wparentheses]
loi.c:41:35: warning: division by zero [-Wdiv-by-zero]
loi.c:27:12: warning: 'lam' defined but not used [-Wunused-function]
# so canh bao: 3
terminal
clang-tidy --version
LLVM (http://llvm.org/):
  LLVM version 19.1.1
  Optimized build.
clang-tidy loi.c --checks='-*,clang-analyzer-*,bugprone-*,cert-*' -- -std=c11
loi.c:11:9: warning: the value returned by this function should not be disregarded; neglecting it may lead to errors [cert-err33-c]
loi.c:15:5: warning: the value returned by this function should not be disregarded; neglecting it may lead to errors [cert-err33-c]
loi.c:23:5: warning: Call to function 'strcpy' is insecure as it does not provide bounding of the memory buffer. Replace unbounded copy functions with analogous functions that support length arguments such as 'strlcpy'. CWE-119 [clang-analyzer-security.insecureAPI.strcpy]
loi.c:23:5: warning: Use of memory after it is freed [clang-analyzer-unix.Malloc]
loi.c:29:11: warning: an assignment within an 'if' condition is bug-prone [bugprone-assignment-in-if-condition]
loi.c:30:16: warning: Dereference of null pointer (loaded from variable 'p') [clang-analyzer-core.NullDereference]
loi.c:36:5: warning: Call to function 'strcpy' is insecure as it does not provide bounding of the memory buffer. Replace unbounded copy functions with analogous functions that support length arguments such as 'strlcpy'. CWE-119
loi.c:41:35: warning: Division by zero [clang-analyzer-core.DivideZero]
LỗiGCCclang-tidy
Gán trong điều kiện ifCóCó
Chia cho khôngCóCó
Dùng sau khi giải phóngKhôngCó
Giải tham chiếu NULL sau phép gánKhôngCó
strcpy không giới hạnKhôngCó
Bỏ qua giá trị trả về của fcloseKhôngCó
Rò rỉ bộ nhớ trong doc_tenKhôngKhông, xem bên dưới

#Tệp .clang-tidy

.clang-tidy, đặt ở gốc dự án
---
Checks: >
  -*,
  bugprone-*,
  cert-*,
  clang-analyzer-*,
  readability-braces-around-statements,
  readability-inconsistent-declaration-parameter-name,
  -bugprone-easily-swappable-parameters,
  -cert-err33-c
WarningsAsErrors: 'clang-analyzer-*'
HeaderFilterRegex: '.*'
FormatStyle: file
Nhóm kiểm traNội dungNên bật cho C
clang-analyzer-*Phân tích theo đường: rò rỉ, NULL, dùng sau khi thảCÓ, quan trọng nhất
bugprone-*Mẫu mã dễ sinh lỗiCÓ
cert-*Quy tắc CERT C, xem Bài 59.4CÓ, phần lớn
misc-*Linh tinh, chất lượng khác nhauChọn lọc
performance-*Chủ yếu cho C++Không cần
readability-*Phong cách và độ dễ đọcChọn lọc
modernize-*Chuyển sang C++ hiện đạiKHÔNG, chỉ C++
cppcoreguidelines-*Hướng dẫn C++ CoreKHÔNG, chỉ C++

#compile_commands.json

Cơ sở dữ liệu biên dịch
Một tệp JSON liệt kê, cho mỗi tệp nguồn, đúng dòng lệnh dùng để biên dịch nó. Mọi công cụ dựa trên Clang đọc tệp này để biết cờ -I, -D, và chuẩn ngôn ngữ, thay vì bắt bạn gõ lại.
compile_commands.json
[
  {
    "directory": "/du-an",
    "command": "gcc -std=c11 -Wall -Wextra -Iinclude -c loi.c",
    "file": "loi.c"
  }
]
terminal
# CMake: mot dong
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -B build
# Make, Autotools, hay bat ky he thong nao: dung bear
bear -- make
# Meson: sinh san trong thu muc dung
meson setup build
# Khong co gi trong so tren: sinh bang mot dong shell
for f in src/*.c; do echo "{\"directory\":\"$PWD\",\"command\":\"gcc -std=c11 -Iinclude -c $f\",\"file\":\"$f\"}"; done | jq -s . > compile_commands.json
# Co tep roi thi khong can dau -- nua
clang-tidy src/*.c

#cppcheck

terminal
cppcheck --enable=all --std=c11 --inline-suppr loi.c
Checking loi.c ...
loi.c:12:9: error: Memory leak: dem [memleak]
        return NULL;
        ^
loi.c:23:5: error: Dereferencing 'p' after it is deallocated / released [deallocuse]
    strcpy(p, "abc");
    ^
loi.c:30:12: error: Uninitialized variable: x [uninitvar]
    return x + *p;
           ^
loi.c:41:34: error: Division by zero. [zerodiv]
static int chia(int a) { return a / 0; }
                                 ^
loi.c:9:17: warning: Return value of allocation function 'malloc' is not stored. [leakNoVarFunctionCall]
nofile:0:0: information: Active checkers: method... [checkersReport]
clang-tidycppcheck
Cần biên dịch được khôngCó, cần header và cờ đúngKhông, phân tích được mã không dịch được
Cần compile_commands.jsonNên cóKhông bắt buộc
Xử lý #ifdefChỉ nhánh đang bậtThử NHIỀU tổ hợp
Phân tích theo đườngRất mạnhCó, kém hơn
Báo giảÍtNhiều hơn
Tốc độChậm hơnNhanh hơn
Sửa tự độngCó, với --fixKhông
Cài đặtCần LLVMMột tệp thực thi nhỏ
Cờ cppcheck nên dùng
cppcheck \
    --enable=warning,style,performance,portability \
    --std=c11 \
    --inline-suppr \
    --error-exitcode=1 \
    --suppress=missingIncludeSystem \
    -I include \
    src/

# --enable=all bat ca "information" va "unusedFunction", cai thu hai
#   bao moi ham cong khai cua thu vien la "khong dung", nen rat on ao.
#   Liet ke ro tung nhom thay vi dung all.
# --inline-suppr cho phep tat bang binh luan trong ma.
# --error-exitcode=1 de CI do khi co loi.
# --suppress=missingIncludeSystem tat canh bao "khong tim thay
#   header he thong", vi cppcheck khong can chung.

#Tắt báo giả có kiểm soát

nolint.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void an_toan(char *dich, const char *nguon) {
    /* Nguoi goi bao dam dich >= 256 va nguon < 200, xem giao_thuc.h:41 */
    strcpy(dich, nguon);   /* NOLINT(clang-analyzer-security.insecureAPI.strcpy) */
}

void khong_an_toan(char *dich, const char *nguon) {
    strcpy(dich, nguon);
}
terminal
# Dung .clang-tidy o gôc du an, co WarningsAsErrors
clang-tidy nolint.c -- -std=c11
nolint.c:11:5: error: Call to function 'strcpy' is insecure as it does not provide bounding of the memory buffer. Replace unbounded copy functions with analogous functions that support length arguments such as 'strlcpy'. CWE-119 [clang-analyzer-security.insecureAPI.strcpy,-warnings-as-errors]
Bốn cách tắt, theo thứ tự nên ưu tiên
/* 1. NOLINT co ten kiem tra cu the, tren MOT dong. Tot nhat. */
strcpy(dich, nguon);   /* NOLINT(clang-analyzer-security.insecureAPI.strcpy) */

/* 2. NOLINTNEXTLINE, khi dong da qua dai */
/* NOLINTNEXTLINE(bugprone-branch-clone) */
if (a) lam_x(); else lam_x();

/* 3. NOLINTBEGIN va NOLINTEND cho mot khoi */
/* NOLINTBEGIN(cert-err33-c) */
fclose(t1);
fclose(t2);
fclose(t3);
/* NOLINTEND(cert-err33-c) */

/* 4. NOLINT tran, tat MOI kiem tra. Tranh dung. */
strcpy(dich, nguon);   /* NOLINT */

/* Cua cppcheck: */
/* cppcheck-suppress memleak */
return NULL;

/* Hoac trong tep suppressions.txt: */
memleak:src/cu.c:112
uninitvar:src/tu-sinh/*

Tự làm thử

  1. Chạy clang-tidy trên một tệp của bạn và đếm cảnh báo theo nhóm.
  2. Viết tệp .clang-tidy bật clang-analyzer-* và bugprone-*, tắt hai kiểm tra ồn ào nhất.
  3. Sinh compile_commands.json cho dự án của bạn.
  4. Viết một hàm có rò rỉ trên nhánh lỗi và kiểm tra clang-tidy có tìm ra không; nếu không, thử làm hàm ngắn lại.
  5. Tắt một báo giả bằng NOLINT có tên kiểm tra và bình luận giải thích.
  6. Cài cppcheck và so kết quả của nó với clang-tidy trên cùng một tệp.

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

  • GCC bắt 3 trong 5 lỗi trong ví dụ; clang-tidy bắt 6 và in ra cả đường đi dẫn tới lỗi.
  • Phân tích tĩnh là heuristic có ngân sách: không báo lỗi không có nghĩa là không có lỗi, và hàm ngắn được phân tích kỹ hơn.
  • .clang-tidy nên bắt đầu bằng -*, và WarningsAsErrors chỉ nên gồm clang-analyzer-*.
  • compile_commands.json cần cho clang-tidy, clangd, và các công cụ Clang khác.
  • cppcheck yếu hơn về phân tích theo đường nhưng thử được nhiều tổ hợp #ifdef.