free(): double free detected in tcache 2

0

Krótko. Nie znam się dobrze na C. Mam program napisany w C, a w nim m.in. następujący kod:

...
FILE *inFp = fopen(argv[1], "r");
...
for (int i = 0; i < linesCount; ++i)
{
  ...
  char *line = silvGetLine(inFp);
  ...
  free(line);
  ...
}
...

Program przechodzi przez 60 iteracji tej pętli i w iteracji nr 61 po wykonaniu linijki free(line); otrzymuję komunikat w GDB:

Breakpoint 2, main (argc=3, argv=0x7fffffffdea8)
    at c-extract-only-articles-content-faster-parser-multiple-files.c:84
84			free(line);
(gdb) p line
$3 = 0x4069c0 ""
(gdb) next
free(): double free detected in tcache 2

Program received signal SIGABRT, Aborted.
__GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
50	  return ret;
(gdb)

Szukałem w internecie i znalazłem kilka wzmianek o tym błędzie (błędzie?). Ale – opisywane sytuacje są bardzo specyficzne i trudno mi je zrozumieć.

Co jest nie tak? Może nie powinienem zwalniać (tutaj) pamięci?

Chciałbym, żeby działało. Ale jeśli byłoby to możliwe, chciałbym pisać maksymalnie zgodnie ze sztuką (nawet pomijając czytelność).


UPDATE: Zaktualizowałem komunikat, by zawierał więcej informacji.


UPDATE2: Funkcja silvGetLine jest moją autorską i używa malloc.

1

Pokaz zrodlo Twojej funkcji.

0

As requested:

char *silvGetLine(FILE *fp)
{
	printf("debug: 'silvGetLine' is running...\n");

	if (fp == NULL)
	{
		fprintf(stderr, "Error: silvGetLine: fp == NULL");
	}

	char c;
	int lineSize = 0;
	while ((c = getc(fp)) != EOF && c != '\n')
	{
		++lineSize;
	}
	char *result = malloc(lineSize + 1); // +1 for NULL character
	fseek(fp, -lineSize, SEEK_CUR);
	fgets(result, lineSize + 1, fp); // +1 because fgets reads "count - 1" bytes

	return result;
}
0

fgets zwraca Ci str (EDIT: str było ze strony z opisem API, w Twoim przypadku chodzi o result) czy coś innego?
EDIT: nie jestem pewien jak się funkcje z libki standardowej zachowają przy jakiś bardziej egzotycznym kodowaniu plików. Jakie są dane wejściowe? I co na to wszystko valgrind?

0
alagner napisał(a):

fgets zwraca Ci str czy coś innego?

fgets to jest z cstdio funkcja http://www.cplusplus.com/reference/cstdio/fgets/

1

Sprawa nr 2: na pewno nic Ci się nie dzieje z tym line po drodze? Tzn. problem pojawia się nawet jeżeli zrobisz tak:

FILE *inFp = fopen(argv[1], "r");
for (int i = 0; i < linesCount; ++i)
{
  char *line = silvGetLine(inFp);
  free(line);
}

Tzn. wybacz, że tak dopytuję, ale obecność "..." wskazuje, że tam się po drodze wiele może dziać ;)

0
alagner napisał(a):

fgets zwraca Ci str (EDIT: str było ze strony z opisem API, w Twoim przypadku chodzi o result) czy coś innego?

No… fgets zwraca to, co podane w API.

EDIT: nie jestem pewien jak się funkcje z libki standardowej zachowają przy jakiś bardziej egzotycznym kodowaniu plików. Jakie są dane wejściowe?

Hm... Plik inFp ma ponad 6,5 miliona linijek, więc całego Ci nie podam… Linijka nr 61 wygląda tak:

<text xml:space="preserve">#REDIRECT [[Computer accessibility]]

Ale GDB, jak napisałem, mówi, że jest pusta (tak przynajmniej sądzę z tego, co wyświetlił).

I co na to wszystko valgrind?

Nie wiem, nie znam człowieka… Mówisz, że powinienem go użyć? Może się przydać? Wolałbym pozostać przy GDB jak długo się da…

alagner napisał(a):

Sprawa nr 2: na pewno nic Ci się nie dzieje z tym line po drodze? Tzn. problem pojawia się nawet jeżeli zrobisz tak:

FILE *inFp = fopen(argv[1], "r");
for (int i = 0; i < linesCount; ++i)
{
  char *line = silvGetLine(inFp);
  free(line);
}

Tzn. wybacz, że tak dopytuję, ale obecność "..." wskazuje, że tam się po drodze wiele może dziać ;)

Dzieje się, są cztery wywołania funkcji, które pobierają line jako argument. Jeśli to pomoże, a nie zaciemni obraz, to proszę – cały program:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <math.h>

// There is the assumption that sizeof(char) == 1

int silvLinesCount(FILE *fp);
char *silvGetLine(FILE *fp);
char *silvConcat(char *s1, char *s2);
char *silvItos(int i);
char *silvSubstr(char *s, int from, int to);
bool silvStrEq(char *s1, char *s2);
int silvMin(int i1, int i2);

int main(int argc, char *argv[])
{
	printf("debug: 'main' is running...\n");

	if (argc < 3)
	{
		fprintf(stderr, "Error: main: there are less arguments than 2");
		return -1;
	}

	// The input file
	FILE *inFp = fopen(argv[1], "r");

	// size_t n = 0;

	// size_t readCharactersNumber;
	bool isContent = NULL;
	FILE *outFp = NULL;
	int fileNumber = 0;
	int linesCount = silvLinesCount(inFp);
	// printf("debug: \tlinesCount == %d\n", linesCount);

	// while ((readCharactersNumber = getline(&line, &n, inFp)) != -1)
	for (int i = 0; i < linesCount; ++i)
	{
		char *line = silvGetLine(inFp);
		char *filenameSuffix = NULL;
		char *outFilePath = NULL;

		char *expectedStart = "      <text xml";
		char *actualStart = silvSubstr(
			line,
			0,
			silvMin(strlen(line), strlen(expectedStart)));
		// printf("\tdebug: actualStart == %s\n", actualStart);

		char *expectedEnd = "</text>";
		char *actualEnd = strlen(line) > strlen(expectedEnd)
							  ? silvSubstr(
									line,
									strlen(line) - strlen(expectedEnd),
									strlen(line))
							  : line;
		// printf("\tdebug: actualEnd == %s\n", actualEnd);

		if (silvStrEq(actualStart, expectedStart))
		{
			isContent = true;
			++fileNumber;
			filenameSuffix = silvItos(fileNumber);
			outFilePath = silvConcat(argv[2], filenameSuffix);
			outFp = fopen(outFilePath, "a");
		}

		// if (isContent == true)
		// {
		// 	fputs(line, outFp);
		// }

		if (silvStrEq(actualEnd, expectedEnd))
		{
			isContent = false;
			fclose(outFp);
		}

		free(actualStart);
		free(actualEnd);
		free(line);
		free(filenameSuffix);
		free(outFilePath);

		// printf("\rLines read: %d, files created: %d...", i, fileNumber);
	}

	fclose(inFp);

	return 0;
}

int silvLinesCount(FILE *fp)
{
	printf("debug: 'silvLinesCount' is running...\n");

	if (fp == NULL)
	{
		fprintf(stderr, "Error: silvLinesCount: fp == NULL");
	}

	char c;
	int lineCount = 0;
	while ((c = fgetc(fp)) != EOF)
	{
		if (c == '\n')
		{
			++lineCount;
		}
	}
	fseek(fp, 0, SEEK_SET);

	return lineCount;
}

// Needs a corresponding call for "free" by the user
char *silvGetLine(FILE *fp)
{
	printf("debug: 'silvGetLine' is running...\n");

	if (fp == NULL)
	{
		fprintf(stderr, "Error: silvGetLine: fp == NULL");
	}

	char c;
	int lineSize = 0;
	while ((c = getc(fp)) != EOF && c != '\n')
	{
		++lineSize;
	}
	char *result = malloc(lineSize + 1); // +1 for NULL character
	fseek(fp, -lineSize, SEEK_CUR);
	fgets(result, lineSize + 1, fp); // +1 because fgets reads "count - 1" bytes

	return result;
}

// Needs a corresponding call for "free" by the user
char *silvConcat(char *s1, char *s2)
{
	printf("debug: 'silvConcat' is running...\n");

	if (s1 == NULL || s2 == NULL)
	{
		fprintf(stderr, "Error: silvConcat: either s1 == NULL or s2 == NULL");
	}

	char *result = malloc(strlen(s1) + strlen(s2) + 1); // +1 for NULL character
	memcpy(result, s1, strlen(s1));
	memcpy(result + strlen(s1), s2, strlen(s2));
	memcpy(result + strlen(s1) + strlen(s2), "\0", 1); // +1 for NULL character

	return result;
}

// Needs a corresponding call for "free" by the user
char *silvItos(int i)
{
	printf("debug: 'silvItos' is running...\n");

	if (i < 0)
	{
		fprintf(stderr, "Error: silvItos: i < 0");
	}

	int charactersNumber = (int)(floor(log10(i)) + 1); // +1 cause there's floor
	char *result = malloc(charactersNumber + 1);	   // +1 for NULL character
	sprintf(result, "%d", i);

	if (result == NULL)
	{
		fprintf(
			stderr,
			"Warning: silvItos: something went wrong, result == NULL");
	}

	return result;
}

// Needs a corresponding call for "free" by the user
char *silvSubstr(char *s, int from, int to)
{
	printf("debug: 'silvSubstr' is running...\n");

	if (s == NULL)
	{
		fprintf(stderr, "Error: silvSubstr: s == NULL");
	}

	if (from > to)
	{
		fprintf(stderr, "Error: silvSubstr: from > to");
	}

	int substrSize = to - from;
	char *result = malloc(substrSize + 1); // +1 for NULL character
	memcpy(result, s + from, substrSize);
	memcpy(result + substrSize, "\0", 1);

	return result;
}

bool silvStrEq(char *s1, char *s2)
{
	printf("debug: 'silvStrEq' is running...\n");

	if (s1 == NULL || s2 == NULL)
	{
		fprintf(stderr, "Error: silvStrEq: either s1 == NULL or s2 == NULL");
	}

	if (strcmp(s1, s2) == 0)
	{
		return true;
	}
	else
	{
		return false;
	}
}

int silvMin(int i1, int i2)
{
	printf("debug: 'silvMin' is running...\n");

	if (i1 <= i2)
	{
		return i1;
	}
	else
	{
		return i2;
	}
}
0

No… fgets zwraca to, co podane w API.

Chodziło mi o to, czy nie sygnalizuje błędu w postaci NULLa.

To mi śmierdzi

        char *actualEnd = strlen(line) > strlen(expectedEnd)
                              ? silvSubstr(
                                    line,
                                    strlen(line) - strlen(expectedEnd),
                                    strlen(line))
                              : line; //szybki test: wsadź tu strdup(line)

/* ciach */
        free(actualEnd);
        free(line);

Generalnie polecałbym objechać debugerem te ostatni blok free i zobaczyć czy coś Ci się nie zazębia/powtarza.

EDIT:
a to je valgrind
https://valgrind.org/
Niestety Linux only (nie wiem jak z BSD). Na Macu Instruments z XCode'a działają też całkiem przyzwoicie.
Na Windzie nie wiem ;P

0

@alagner: teraz tak patrzę… Czy dobrze widzę, że powinienem kopiować, a ja sobie radośnie przypisuję wskaźnik? A może to jest zgodnie ze sztuką (to char *actualEnd = … ? … : line)?

0

Jedno i drugie jest zgodne ze sztuką, pytanie jak to potem chcesz zwolnić. Ale radziłbym być konsekwentnym

0

Hm, hm, hm… sam nie wiem. :/

Ale już chyba rozumiem… Pomijając to, że linijka 61 nie jest pusta, to skoro program myśli, że jest pusta, to przypisuje wskaźnik na nią zmienną przechowującą wskaźnik na nią do zmiennej actualEnd, a następnie chce zwolnić zarówno pamięć za pośrednictwem zmiennej actualEnd – która przechowuje ten sam wskaźnik, co zmienna line – jak i pamięć za pośrednictwem zmiennej line

Pytanie teraz, dlaczego linijka 61 jest pusta? Ale to chyba nie należy do tego wątku.

Jeszcze to spróbuję dziś w tym kierunku przetestować i napiszę, co mi wyszło.

0

Pytanie czy faktycznie trafiasz w ten warunek i robisz to przypisanie wskaźnika.
Ja za Ciebie tego nie sprawdzę. ;) Mogę co najwyżej powiedzieć, co wygląda podejrzanie.
Tak jak mówię, pierwsze co to sprawdziłbym wartości wszystkich zmiennych z bloku free.

BTW, jaka to platforma i czemu koniecznie C? Użycie std::stringa z C++ bardzo wiele by ułatwiło.

0

Odpowiedź na BTW: system operacyjny to Fedora, jeśli to masz na myśli pytając o platformę. C++ nie, ponieważ C++ – tak, jak ja to widzę – wprowadza zarówno własne konstrukcje, jak i konstrukcje C. Żebym się w tym nie pogubił, wybrałem C, które ma tylko konstrukcje C.


PS. Pisząc "pogubił" mam na myśli zarówno sam język, jak i narzędzia do niego – kompilatory, linkery itp.

0

IMHO to błąd bo skazujesz się na ręczne zarządzanie pamięcią, ale Twój wybór.

0

Odchodząc troszeczkę od głównego problemu.
Pomyśl nad funkcją wczytującą wiersz z pliku do prealokowanego bufora. W obecnej wersji, skoro plik ma ~6,5mln wierszy = 6,5mln alokacji. To nie będzie wydajne nawet w C ;)
Albo jeśli możesz to skorzystaj z gotowca: getline

0

Sprostowanie: mój błąd, przepraszam Was, w pliku wejściowym jest około 4,6 mln linii, a nie 6,5 mln, jak pisałem w komentarzach.


UPDATE: Ale w sumie to przypadek, mogłem wybrać i większy plik.

0

Po zmianie z przypisywania wskaźnika na łańcuch znaków na kopiowanie łańcucha znaków (o której pisałem tutaj: https://4programmers.net/Forum/C_i_C++/334473-free_double_free_detected_in_tcache_2?p=1642968#id1642968), program leci już do końca pliku. Nie jest przerywany ani wspomnianym w tytule, ani żadnym innym błędem.

Co prawda są wciąż inne / doszły nowe problemy, ale one już nie dotyczą tego wątku.


PS. To znaczy, zmieniłem to:

        char *actualEnd = strlen(line) > strlen(expectedEnd)
                              ? silvSubstr(
                                    line,
                                    strlen(line) - strlen(expectedEnd),
                                    strlen(line))
                              : line;

na to:

		char *actualEnd = strlen(line) > strlen(expectedEnd)
							  ? silvSubstr(
									line,
									strlen(line) - strlen(expectedEnd),
									strlen(line))
							  : silvStrCpy(line);

gdzie silvStrCpy to nowo utworzona funkcja, która wygląda tak:

// Needs a corresponding call for "free" by the user
char *silvStrCpy(const char *src)
{
	char *result = malloc(strlen(src) + 1); // +1 for NULL character
	strcpy(result, src);

	return result;
}

UPDATE: Głupi ja, taka funkcja jest już w standardzie, jak @alagner był uprzejmy mi powiedzieć w komentarzach: strdup. Co prawda należy zauważyć, że, jak piszą na stronie en.cppreference.com:

Merged into ISO C The functionality described on this page was merged into the mainline ISO C standard as of 6/2019, see strdup (since C2x)

0

Ta funkcja wczytująca wiersze tekstu to Ci produkuje dobre łańcuchy znaków? Bo nie chce mi się dziś testować kompilatorem, ale wygląda, że zliczasz znaki do osiągnięcia \n, ale zatrzymujesz się po przeczytaniu \n przez co seek w tył robisz o jeden znak za mało. Zanim jednak zaczniesz poprawiać rozważ problem z Windows, gdzie koniec wiersza ma fizycznie znaki \r\n. Lepiej byłoby przed pętlą zapamiętać pozycję long bookmark = ftell( fp );, a po pętli odtworzyć fseek( fp, bookmark, SEEK_SET );.
Inny sposób na czytanie wiersza o nieznanej długości, to wczytywanie kolejnych kawałków do tymczasowej tablicy, po zapełnieniu której zgromadzone znaki są dołączane do budowanego łańcucha wyjściowego.

1 użytkowników online, w tym zalogowanych: 0, gości: 1