前置知识: C

静态分析与调试

00:00
3 min Intermediate 2026/6/14

代码静态分析与调试技巧

概述

静态分析和动态调试是保障C代码质量的两大手段。静态分析在不运行程序的情况下检查代码中的潜在问题(如未初始化变量、内存泄漏、空指针解引用);动态调试则在程序运行时检查其行为(如断点、变量监视、内存错误检测)。掌握这些工具可以显著提高开发效率和代码可靠性。

基础概念

静态分析 vs 动态分析

方式说明工具
静态分析不运行代码,分析源码clang-tidy, cppcheck, splint
动态分析运行代码,检测运行时问题GDB, Valgrind, AddressSanitizer
编译器警告编译时检查gcc -Wall -Wextra

调试的基本流程

  1. 复现问题:确定问题可以稳定复现
  2. 问题:使用断点日志缩小范围
  3. 分析原因:检查变量调用栈
  4. 修复验证:修改代码并确认问题已解决

快速上手

使用 GDB 调试

# 编译时加入调试信息
gcc -g -O0 program.c -o program

# 启动 GDB
gdb ./program

GDB 常用命令

# 断点
break main          # 在 main 函数设置断点
break file.c:42     # 在第42行设置断点
break func if x > 5 # 条件断点

# 执行
run                 # 运行程序
next                # 单步执行(不进入函数)
step                # 单步执行(进入函数)
continue            # 继续运行到下一个断点
finish              # 运行到当前函数返回

# 查看变量
print variable      # 打印变量值
print *ptr          # 打印指针指向的值
print arr[0]@5      # 打印数组前5个元素
display variable    # 每次暂停时自动显示变量

# 调用栈
backtrace           # 显示调用栈
frame 2             # 切换到第2层栈帧
info locals         # 显示当前函数的局部变量

# 其他
set variable x = 10 # 修改变量值
watch variable      # 监视变量变化
info breakpoints     # 查看断点列表
delete 1            # 删除1号断点

使用编译器警告

# GCC/Clang 推荐的警告选项
gcc -Wall -Wextra -Wpedantic -Werror \
    -Wformat=2 -Wconversion -Wshadow \
    -std=c17 -g -O0 program.c -o program

详细用法

AddressSanitizer(内存错误检测)

AddressSanitizer(ASan)可以检测以下内存错误

  • 缓冲区溢出
  • 缓冲区溢出
  • 全局缓冲区溢出
  • 使用已释放的内存
  • 重复释放
  • 内存泄漏
#include <stdio.h>
#include <stdlib.h>

// 编译: gcc -fsanitize=address -g program.c -o program
int main(void) {
    int *arr = malloc(5 * sizeof(int));

    // 堆缓冲区溢出:访问越界
    arr[5] = 42;  // ASan 会检测到并报告

    free(arr);

    // 使用已释放的内存
    printf("%d\n", arr[0]);  // ASan 会检测到

    return 0;
}

UndefinedBehaviorSanitizer(未定义行为检测)

#include <stdio.h>

// 编译: gcc -fsanitize=undefined -g program.c -o program
int main(void) {
    int x = 2147483647;  // INT_MAX

    // 有符号整数溢出(未定义行为)
    int y = x + 1;  // UBSan 会检测到

    // 空指针解引用
    int *p = NULL;
    // *p = 42;  // UBSan 会检测到

    // 数组越界
    int arr[5];
    // arr[10] = 0;  // UBSan 会检测到

    printf("x = %d, y = %d\n", x, y);
    return 0;
}

Valgrind 内存检查

# 编译时不加优化,加入调试信息
gcc -g -O0 program.c -o program

# 运行 Valgrind
valgrind --leak-check=full --show-leak-kinds=all ./program
#include <stdlib.h>

// Valgrind 可以检测的典型问题
int main(void) {
    // 内存泄漏:分配后未释放
    int *leak = malloc(100 * sizeof(int));

    // 未初始化的读取
    int *uninit = malloc(sizeof(int));
    if (*uninit > 0) {  // Valgrind 会报告:条件跳转依赖未初始化的值
        // ...
    }

    free(uninit);
    // 忘记 free(leak)
    return 0;
}

cppcheck 静态分析

# 基本用法
cppcheck program.c

# 启用所有检查
cppcheck --enable=all --suppress=missingIncludeSystem program.c

# 检查整个项目
cppcheck --enable=all src/

cppcheck 可以检测问题包括:

  • 未使用变量函数
  • 可能的指针引用
  • 数组越界
  • 资源泄漏
  • 无效的迭代器使用
  • 逻辑错误

clang-tidy 静态分析

# 基本用法
clang-tidy program.c -- -std=c17

# 使用特定检查集
clang-tidy -checks='bugprone-*,modernize-*,performance-*' program.c -- -std=c17

# 自动修复
clang-tidy -fix program.c -- -std=c17

常见场景

场景一:调试段错误

#include <stdio.h>
#include <string.h>

// 故意制造段错误的程序
void process_string(const char *str) {
    // 忘记检查空指针
    size_t len = strlen(str);  // 如果 str 是 NULL,段错误
    printf("长度: %zu\n", len);
}

int main(void) {
    const char *names[] = {"Alice", "Bob", NULL, "Charlie"};

    for (int i = 0; i < 4; i++) {
        process_string(names[i]);  // i=2 时段错误
    }

    return 0;
}

使用 GDB 调试

gcc -g -O0 segfault.c -o segfault
gdb ./segfault
(gdb) run
# 程序崩溃后
(gdb) backtrace        # 查看调用栈
(gdb) frame 0          # 切换到崩溃的栈帧
(gdb) print str        # 查看变量值
(gdb) print i          # 查看循环变量

场景二:检测内存泄漏

#include <stdio.h>
#include <stdlib.h>

typedef struct Node {
    int value;
    struct Node *next;
} Node;

Node *create_list(int n) {
    Node *head = NULL;
    for (int i = 0; i < n; i++) {
        Node *node = malloc(sizeof(Node));
        node->value = i;
        node->next = head;
        head = node;
    }
    return head;
}

// 忘记释放链表
void process_list(Node *head) {
    Node *curr = head;
    while (curr) {
        printf("%d ", curr->value);
        curr = curr->next;
    }
    printf("\n");
    // 忘记调用 free_list(head)
}

void free_list(Node *head) {
    while (head) {
        Node *next = head->next;
        free(head);
        head = next;
    }
}

int main(void) {
    Node *list = create_list(5);
    process_list(list);
    free_list(list);  // 修复:释放链表
    return 0;
}

使用 Valgrind 检测

valgrind --leak-check=full ./program

场景三:使用 assert 进行运行时检查

#include <stdio.h>
#include <assert.h>
#include <stdlib.h>

// 安全的数组访问
int safe_array_get(int *arr, int size, int index) {
    assert(arr != NULL && "数组指针不能为空");
    assert(index >= 0 && index < size && "数组索引越界");
    return arr[index];
}

// 安全的除法
double safe_divide(double a, double b) {
    assert(b != 0.0 && "除数不能为零");
    return a / b;
}

int main(void) {
    int arr[] = {10, 20, 30};

    printf("arr[1] = %d\n", safe_array_get(arr, 3, 1));  // 正常
    // printf("arr[5] = %d\n", safe_array_get(arr, 3, 5));  // 断言失败

    printf("10/3 = %.2f\n", safe_divide(10, 3));  // 正常
    // printf("10/0 = %.2f\n", safe_divide(10, 0));  // 断言失败

    return 0;
}

注意事项

调试时关闭优化

编译器优化可能重排代码、内联函数或消除变量,导致调试信息不准确:

# 调试时使用 -O0
gcc -g -O0 program.c -o program

# 发布时使用优化
gcc -O2 program.c -o program

assert 在发布版本中可能被禁用

assert 定义NDEBUG 时会被编译为空操作

// 发布版本中 assert 不起作用
#define NDEBUG
#include <assert.h>

// 对于安全关键的检查,使用自定义断言
#define always_assert(cond, msg) do { \
    if (!(cond)) {                    \
        fprintf(stderr, "断言失败: %s, %s:%d: %s\n", \
                msg, __FILE__, __LINE__, #cond); \
        abort();                      \
    }                                 \
} while (0)

Sanitizer 的性能开销

AddressSanitizer 会使程序降低约2倍,内存使用增加约3倍。应在测试环境中使用,不要在生产环境启用

进阶用法

GDB 自定义命令和脚本

# gdb_script.py - GDB Python 脚本
import gdb

class PrintLinkedList(gdb.Command):
    """打印链表: print_linked_list <head_pointer>"""

    def __init__(self):
        super().__init__("print_linked_list", gdb.COMMAND_USER)

    def invoke(self, arg, from_tty):
        node = gdb.parse_and_eval(arg)
        while node != 0:
            value = node['value']
            gdb.write(f"节点: {value}\n")
            node = node['next']

PrintLinkedList()

使用方式:

gdb -x gdb_script.py ./program
(gdb) print_linked_list head

使用 core dump 分析崩溃

# 启用 core dump
ulimit -c unlimited

# 运行程序(崩溃后生成 core 文件)
./program

# 分析 core dump
gdb ./program core
(gdb) backtrace
(gdb) frame 0
(gdb) print variable

ThreadSanitizer(线程数据竞争检测)

#include <stdio.h>
#include <pthread.h>

// 编译: gcc -fsanitize=thread -g program.c -o program

int shared_counter = 0;

void *increment(void *arg) {
    for (int i = 0; i < 1000000; i++) {
        shared_counter++;  // 数据竞争!
    }
    return NULL;
}

int main(void) {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, increment, NULL);
    pthread_create(&t2, NULL, increment, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    printf("counter = %d\n", shared_counter);  // 可能不等于2000000
    return 0;
}

自定义日志宏辅助调试

#include <stdio.h>
#include <stdarg.h>
#include <time.h>

// 调试级别
typedef enum { LOG_TRACE, LOG_DEBUG, LOG_INFO, LOG_WARN, LOG_ERROR } LogLevel;

static int g_log_level = LOG_TRACE;

static const char *level_names[] = {
    "TRACE", "DEBUG", "INFO", "WARN", "ERROR"
};

void log_write(LogLevel level, const char *file, int line,
               const char *fmt, ...) {
    if (level < g_log_level) return;

    time_t now = time(NULL);
    struct tm *t = localtime(&now);
    char time_buf[20];
    strftime(time_buf, sizeof(time_buf), "%H:%M:%S", t);

    fprintf(stderr, "[%s] [%s] %s:%d: ",
            time_buf, level_names[level], file, line);

    va_list ap;
    va_start(ap, fmt);
    vfprintf(stderr, fmt, ap);
    va_end(ap);

    fprintf(stderr, "\n");
}

#define LOG(level, fmt, ...) log_write(level, __FILE__, __LINE__, fmt, ##__VA_ARGS__)

#define TRACE(fmt, ...) LOG(LOG_TRACE, fmt, ##__VA_ARGS__)
#define DEBUG(fmt, ...) LOG(LOG_DEBUG, fmt, ##__VA_ARGS__)
#define INFO(fmt, ...)  LOG(LOG_INFO, fmt, ##__VA_ARGS__)
#define WARN(fmt, ...)  LOG(LOG_WARN, fmt, ##__VA_ARGS__)
#define ERROR(fmt, ...) LOG(LOG_ERROR, fmt, ##__VA_ARGS__)

int main(void) {
    TRACE("程序启动");
    DEBUG("变量 x = %d", 42);
    INFO("处理文件: %s", "data.txt");
    WARN("磁盘空间不足: %d%%", 90);
    ERROR("无法打开文件: %s", "config.txt");
    return 0;
}

知识检测

学习进度

-- 已学文档
--% 知识覆盖率

学习推荐

专注模式