【问题标题】:Why valgrind complains about Invalid reads of size 8 on single linked list node removal?为什么 valgrind 在删除单个链表节点时抱怨大小为 8 的无效读取?
【发布时间】:2016-09-01 09:09:34
【问题描述】:

根据以下文章/SO 问题,我编写了一个单链表的小型实现,具有删除具有给定值的节点的功能:

http://grisha.org/blog/2013/04/02/linus-on-understanding-pointers/

How do pointer to pointers work in C?

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

typedef struct node
{
  int val;
  struct node* next;
} node;

node* newNode(int val)
{
  node* n = (node*)malloc(sizeof(node));
  n->val = val;
  n->next = NULL;
  return n;
}

void clean(node* head)
{
  while (head)
  {
    node* curr = head;
    head = head->next;
    free(curr);
  }
}

void append(node* head, int val)
{
  if(head == NULL)
  {
    head = newNode(val);
    return;
  }

  while (head->next)
    head = head->next;
  head->next = newNode(val);
}

void removeIf(node** head, int val)
{
  //node **curr = head;
  //while (*curr != NULL) {
  //  if ((*curr)->val == val) {
  //    node *next = (*curr)->next;
  //    free(*curr);
  //    *curr = next;
  //  } else {
  //    curr = &((*curr)->next);
  //  }
  //}

  node **pp = head; /* pointer to a pointer */
  node *entry = *head;

  while (entry) {
      if (entry->val == val)
      {
        node* tmp = *pp;
        *pp = entry->next;
        free(tmp);
      }
      else
        pp = &entry->next;

      entry = entry->next;
  }
}

int size(node* head)
{
  int sz = 0;
  while (head)
  {
    head = head->next;
    ++sz;
  }

  return sz;
}

void tests();

int main()
{
  tests();
  return 0;
}

/*
 * Here are the tests for lists functionalities
 */
void tests()
{
  {
    node* root = newNode(1);
    append(root, 1);
    append(root, 1);
    append(root, 1);
    append(root, 2);
    append(root, 3);
    append(root, 5);
    assert(size(root) == 7);
    removeIf(&root, 1);
    assert(size(root) == 3);
    clean(root);
  }
}

问题在于最后一行的removeIf() 函数(main.c:68):

entry = entry->next;

注释的代码有效,valgrind 没有抱怨,而未注释的代码有效,但 valgrind 抱怨如下:

==31665== Memcheck, a memory error detector
==31665== Copyright (C) 2002-2012, and GNU GPL'd, by Julian Seward et al.
==31665== Using Valgrind-3.8.1 and LibVEX; rerun with -h for copyright info
==31665== Command: ./a.out
==31665== 
==31665== Invalid read of size 8
==31665==    at 0x40067C: removeIf (main.c:68)
==31665==    by 0x400785: tests (main.c:106)
==31665==    by 0x4006C7: main (main.c:88)
==31665==  Address 0x4c33048 is 8 bytes inside a block of size 16 free'd
==31665==    at 0x4A06430: free (vg_replace_malloc.c:446)
==31665==    by 0x400669: removeIf (main.c:63)
==31665==    by 0x400785: tests (main.c:106)
==31665==    by 0x4006C7: main (main.c:88)
==31665== 
==31665== 
==31665== HEAP SUMMARY:
==31665==     in use at exit: 0 bytes in 0 blocks
==31665==   total heap usage: 7 allocs, 7 frees, 112 bytes allocated
==31665== 
==31665== All heap blocks were freed -- no leaks are possible
==31665== 
==31665== For counts of detected and suppressed errors, rerun with: -v
==31665== ERROR SUMMARY: 4 errors from 1 contexts (suppressed: 6 from 6)

有人可以评论可能是什么问题吗?

【问题讨论】:

  • -的人可以评论吗?
  • 如果我不得不猜测,那是因为你的代码示例太多了。您还应该在代码中标记第 80、100、152 和 176 行。
  • 对不起,我不是那个人,我正在审查你的代码。 @mch 此行用于回溯调用,只有第一行有真正的问题。
  • @mch 我已经把代码示例变小了。
  • 只是路过的评论——removeIf() 中的 cmets 会很有礼貌。

标签: c pointers memory-management linked-list valgrind


【解决方案1】:

我想我知道发生了什么。让我们逐步回顾流程并假设我们遇到了第一个要删除的节点(我们将其称为 ENTRY)。从您的代码中,这意味着以下状态:

-- pp 显然包含 ENTRY 指针的地址,因为它是第一个找到的,因为 pp 一直更新为条目迭代器的地址(我们总是有“其他”情况)。

接下来会发生什么?我们将 ENTRY 分配给 tmp,将 pp 分配给 ENTRY 的下一个,然后释放 tmp,本质上是释放 ENTRY 的内存,因为 tmp 和 ENTRY 包含相同的地址。这引入了两个问题:

1) 前一个节点的下一个指针(即 NODE->NEXT == ENTRY)变成了一个悬空指针,因为它从未被重新分配,并且您的函数的当前实现无法做到这一点。

2) 释放 ENTRY 的内存后,您访问成员 'next' 以向前移动您的条目迭代器。我认为这正是 valgrind 告诉您您访问了您没有所有权的内存的确切位置和情况。

【讨论】:

  • 很确定*pp = entry-&gt;next; 确实更新了前一个节点的next-pointer,与您的观点1相反)。但是,您的观点 2) 是正确的,这是一个 use-after-free 错误。
  • @EOF,你可能是对的,但现在我只看到直到第一个有效元素,两个指针都指向同一个节点,因为:两者都有相同的“起点”(头)和在每次循环迭代时向上迭代。
【解决方案2】:

根据 GDB 调试

  1. removeif(head=0x7fffffffdee0,...)
  2. (gdb) 输入 $11 = (节点 )**0x7fffffffdec0
  3. 能否请您检查条目和头部地址? 我怀疑 8 个字节是因为这个。

【讨论】:

    【解决方案3】:

    这是您修复的代码:

    void removeIf(node** head, int val)
    {
      node** last_next_pointer = head;
      node* entry = *head;
    
      while (entry != NULL) {
        if (entry->val == val) {
          node* tmp = entry;
          *last_next_pointer = entry->next;
          entry = entry->next;
          free(tmp);
        } else {
          last_next_pointer = &(entry->next);
          entry = entry->next;
        }
      }
    }
    

    不需要变量dd,因为它没有跟踪前一个节点,因此您不能将前一个节点重新链接到下一个节点,然后释放当前节点。

    您的问题是 else 括号。

    【讨论】:

    • 为什么是两个底片?它不能解决问题吗?有更好的方法来实现吗?那么,请告诉我如何,评论我,但不要在不告诉我原因的情况下给我投反对票。谢谢。
    • 尝试使用它来删除 last 节点。
    • @EOF 谢谢!您是对的,但是使用此代码无法删除最后一个节点,因为它不会将最后一个节点作为注释代码进行跟踪。该吃午饭了,我会检查代码并更好地解释为什么 valgrind 抱怨原始代码。
    • 此代码不正确,因为如果负载为val 的节点之一恰好是最后一个节点,则该函数会取消引用NULL 指针。这是未定义的行为(并且在许多实现中崩溃)。
    • @EOF,请立即检查。现在我正在跟踪最后一个节点或头部的最后一个“下一个”字段。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-12-15
    • 1970-01-01
    • 1970-01-01
    • 2014-05-04
    • 1970-01-01
    • 1970-01-01
    • 2015-07-06
    相关资源
    最近更新 更多