是否有任何可能捕获此内存错误的 gcc 编译器警告?

Is there any gcc compiler warning which could have caught this memory bug?

我已经有一段时间没有编写 C 语言了,我的指针已经退化了。我犯了一个非常基本的错误,今天早上我花了一个多小时才发现我做了什么。该错误在此处最低限度地重现:https://godbolt.org/z/3MdzarP67(我知道该程序在内存管理方面是荒谬的,只是显示发生了什么)。

realloc() 的第一次调用当然会中断,因为给它的指针指向堆栈内存,valgrind 使这一点非常明显。

我自己有一个规则,每当我追踪一个错误时,如果有一个可能发现它的警告,我就会在我的项目中启用它。通常情况并非如此,因为许多错误来自编译器无法检查的逻辑错误。

然而在这里我有点惊讶。我们 malloc() 然后立即重新分配使分配的内存无法访问的指针。很明显,返回的指针不在 if 块的范围之外,并且永远不会 free()'。也许期望编译器分析调用并意识到我们正在尝试 realloc() 堆栈内存太过分了,但令我惊讶的是我找不到任何关于 [=15= 泄漏的东西来对我大喊大叫] 返回指针。甚至 clang 的静态分析器 scan-build 也无法识别,我尝试了各种相关选项。

我能找到的最好的是 -fsanitize=address,它至少在崩溃期间打印出一些线索信息,而不是:

mremap_chunk(): invalid pointer

在 Godbolt 上,或

realloc(): invalid old size
Aborted (core dumped)

在我的机器上,两者都有些神秘(尽管是的,它们确实清楚地表明存在 一些 内存问题)。不过,这编译没有问题。

由于 Godbolt 链接不会永远存在,这里是代码的关键部分:

void add_foo_to_bar(struct Bar** b, Foo* f) {
    if ((*b)->foos == NULL) {
        (*b)->foos = (Foo*)malloc(sizeof(Foo));
        // uncomment for correction
        //(*b)->foos[(*b)->n_foos] = *f;      
        // obvious bug here, we leak memory by losing the returned pointer from malloc
        // and assign the pointer to a stack address (&f1)
        // comment below line for correction
        (*b)->foos = f; // completely wrong
        (*b)->n_foos++;
    } else {
        (*b)->foos = (Foo*)realloc((*b)->foos, ((*b)->n_foos + 1) * sizeof(Foo));
        (*b)->foos[(*b)->n_foos] = *f;
        (*b)->n_foos++;
    }
}

错误发生是因为 f 是一个指向堆栈内存的指针(有意的),但我们显然不能将本应 malloc() 分配给它的东西。

如果您的编译器足够新,请尝试 -fanalyzer。当 运行 我得到:

../main.c:30:28: warning: ‘realloc’ of ‘&f1’ which points to memory not on the heap [CWE-590] [-Wanalyzer-free-of-non-heap]
   30 |         (*b)->foos = (Foo*)realloc((*b)->foos, ((*b)->n_foos + 1) * sizeof(Foo));
      |                            ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  ‘main’: events 1-2
    |
    |   37 | int main() {
    |      |     ^~~~
    |      |     |
    |      |     (1) entry to ‘main’
    |......
    |   45 |     add_foo_to_bar(&b, &f1);
    |      |     ~~~~~~~~~~~~~~~~~~~~~~~
    |      |     |
    |      |     (2) calling ‘add_foo_to_bar’ from ‘main’
    |
    +--> ‘add_foo_to_bar’: events 3-5
           |
           |   19 | void add_foo_to_bar(struct Bar** b, Foo* f) {
           |      |      ^~~~~~~~~~~~~~
           |      |      |
           |      |      (3) entry to ‘add_foo_to_bar’
           |   20 |     if ((*b)->foos == NULL) {
           |      |        ~
           |      |        |
           |      |        (4) following ‘true’ branch...
           |   21 |         (*b)->foos = (Foo*)malloc(sizeof(Foo));
           |      |         ~~~~
           |      |          |
           |      |          (5) ...to here
           |
    <------+
    |
  ‘main’: events 6-7
    |
    |   45 |     add_foo_to_bar(&b, &f1);
    |      |     ^~~~~~~~~~~~~~~~~~~~~~~
    |      |     |
    |      |     (6) returning to ‘main’ from ‘add_foo_to_bar’
    |   46 |     add_foo_to_bar(&b, &f2);
    |      |     ~~~~~~~~~~~~~~~~~~~~~~~
    |      |     |
    |      |     (7) calling ‘add_foo_to_bar’ from ‘main’
    |
    +--> ‘add_foo_to_bar’: events 8-11
           |
           |   19 | void add_foo_to_bar(struct Bar** b, Foo* f) {
           |      |      ^~~~~~~~~~~~~~
           |      |      |
           |      |      (8) entry to ‘add_foo_to_bar’
           |   20 |     if ((*b)->foos == NULL) {
           |      |        ~
           |      |        |
           |      |        (9) following ‘false’ branch...
           |......
           |   30 |         (*b)->foos = (Foo*)realloc((*b)->foos, ((*b)->n_foos + 1) * sizeof(Foo));
           |      |                            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
           |      |                            |                     |
           |      |                            |                     (10) ...to here
           |      |                            (11) call to ‘realloc’ here
           |

不,但是,运行时测试可以节省您的时间。

如果可以节省执行开销,我已经看到许多应用程序在内存分配中添加了一个额外的层来跟踪所做的分配并找到 leaks/errors。 通常他们用包含 FILELINE

的宏替换 malloc() 和 free()

可以在此处看到一个示例(检查 Heap.c 和 Heap.h 文件) https://github.com/eclipse/paho.mqtt.c/tree/master/src

谷歌搜索“内存堆调试器”可能会找到其他示例。或者你可以自己动手。