使用realloc在读取文件时扩展缓冲区会导致崩溃

5

我正在编写一些需要读取fasta文件的代码,因此我的代码(包括如下部分)是一个fasta解析器。由于在fasta格式中单个序列可以跨越多行,因此我需要将从文件中读取的多个连续行连接成一个字符串。为此,我在每次读取一行后,通过realloc函数重新分配字符串缓冲区的大小,使其等于当前序列长度加上读入行的长度。我还进行了一些其他操作,例如去除空格等。第一个序列一切顺利,但fasta文件可能包含多个序列。因此,我有一个带有两个字符串(标题和实际序列)的结构体的动态数组,类型为“char *”。同样地,当我遇到新的标题(以'>'开头的行)时,我会增加序列数,并重新分配序列列表缓冲区。在为第二个序列分配空间时,realloc函数引发了段错误。

*** glibc detected *** ./stackoverflow: malloc(): memory corruption: 0x09fd9210 ***
Aborted

就我个人而言,我看不出来为什么会出现这种情况。 我已经通过gdb运行了它,一切似乎都工作正常(即一切都被初始化了,值看起来也很合理)...以下是代码:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#include <math.h>
#include <errno.h>

//a struture to keep a record of sequences read in from file, and their titles
typedef struct {
    char *title;
    char *sequence;
} sequence_rec;

//string convenience functions

//checks whether a string consists entirely of white space
int empty(const char *s) {
    int i;
    i = 0;
    while (s[i] != 0) {
        if (!isspace(s[i])) return 0;
        i++;
    }
    return 1;
}

//substr allocates and returns a new string which is a substring of s from i to
//j exclusive, where i < j; If i or j are negative they refer to distance from
//the end of the s
char *substr(const char *s, int i, int j) {
    char *ret;
    if (i < 0) i = strlen(s)-i;
    if (j < 0) j = strlen(s)-j;
    ret = malloc(j-i+1);
    strncpy(ret,s,j-i);
    return ret;
}

//strips white space from either end of the string
void strip(char **s) {
    int i, j, len;
    char *tmp = *s;
    len = strlen(*s);
    i = 0;
    while ((isspace(*(*s+i)))&&(i < len)) {
        i++;
    }
    j = strlen(*s)-1;
    while ((isspace(*(*s+j)))&&(j > 0)) {
        j--;
    }
    *s = strndup(*s+i, j-i);
    free(tmp);
}


int main(int argc, char**argv) {
    sequence_rec *sequences = NULL;
    FILE *f = NULL;
    char *line = NULL;
    size_t linelen;
    int rcount;
    int numsequences = 0;

    f = fopen(argv[1], "r");
    if (f == NULL) {
        fprintf(stderr, "Error opening %s: %s\n", argv[1], strerror(errno));
        return EXIT_FAILURE;
    }
    rcount = getline(&line, &linelen, f);
    while (rcount != -1) {
        while (empty(line)) rcount = getline(&line, &linelen, f);
        if (line[0] != '>') {
            fprintf(stderr,"Sequence input not in valid fasta format\n");
            return EXIT_FAILURE;
        }

        numsequences++;
        sequences = realloc(sequences,sizeof(sequence_rec)*numsequences);
        sequences[numsequences-1].title = strdup(line+1); strip(&sequences[numsequences-1].title);
        rcount = getline(&line, &linelen, f);
        sequences[numsequences-1].sequence = malloc(1); sequences[numsequences-1].sequence[0] = 0;
        while ((!empty(line))&&(line[0] != '>')) {
            strip(&line);
            sequences[numsequences-1].sequence = realloc(sequences[numsequences-1].sequence, strlen(sequences[numsequences-1].sequence)+strlen(line)+1);
            strcat(sequences[numsequences-1].sequence,line);
            rcount = getline(&line, &linelen, f);
        }
    }
    return EXIT_SUCCESS;
}

感谢大家对子字符串例程的所有评论。我已经在我的代码中修复了它。然而,我也注意到我处理负索引的方式是错误的。我应该添加负索引,而不是减去它。话虽如此,我也意识到我错误地复制了substr函数,因为我在粘贴的代码的其余部分中没有调用它。 - sirlark
strip()也有漏洞。它会对长度为零的字符串做出错误的操作。虽然你似乎没有使用这样的字符串调用它,但我认为,在它被其他地方使用时修复这个问题是一件好事。 - Michael Burr
3个回答

4

您应该使用类似于这样的字符串:

struct string {
    int len;
    char *ptr;
};

这样做可以避免类似于您看到的strncpy错误,并使您能够更快地使用strcat等函数。

对于每个字符串,您还应该使用一个双倍数组。这可以避免过多的分配和内存复制。像这样:

int sstrcat(struct string *a, struct string *b)
{
    int len = a->len + b->len;
    int alen = a->len;
    if (a->len < len) {
        while (a->len < len) {
            a->len *= 2;
        }
        a->ptr = realloc(a->ptr, a->len);
        if (a->ptr == NULL) {
            return ENOMEM;
        }
    }
    memcpy(&a->ptr[alen], b->ptr, b->len);
    return 0;
}

我现在知道你正在从事生物信息学,这意味着你可能需要比我想象的更高的性能。你应该使用像这样的字符串:

struct string {
    int len;
    char ptr[0];
};

这样,当您分配字符串对象时,调用malloc(sizeof(struct string) + len),避免了对malloc的第二次调用。 这需要更多的工作,但应该能够有帮助,提高速度和内存碎片化方面的性能。

最后,如果这并不是错误的源头,那么看起来您可能存在一些损坏。 如果gdb失败,Valgrind应该可以帮助您检测到它。


@lief:内存消耗比速度更重要。我不想以加倍的块进行分配并浪费空间。诚然,这在fasta解析器中不是问题,而是在处理中更为突出。 - sirlark
由于内部malloc碎片,即使您没有请求,您可能会使用太多的内存。双倍数组是相当可靠的,但如果它们太可怕,请至少使用类似malloc_usable_size的东西来测量您的碎片。如果您选择双倍数组,并且采用我的第二个建议,即将长度和字符串缓冲区一起分配,请注意在大小计算中包括长度,否则您可能会遇到可怕的碎片(例如,如果您分配2^n + sizeof int)。 - leif
char ptr[0]; 是无效的 C 代码。您的意思应该是 char ptr[];,但由于它是一个数组而不是指针,所以现在这可能是一个不好的元素名称。我会将其命名为 datacontents 或类似的名称。 - R.. GitHub STOP HELPING ICE
没有理由认为分配2^n+sizeof int会比分配2^n导致更严重的碎片化。在这里,二的幂次方没有特殊的地位。(如果你谈论的是由vm服务的如此大的分配,例如mmap,它们足够大,即使浪费整个页面也只是微小的浪费,百分比上可能只有~2%)。 - R.. GitHub STOP HELPING ICE
如果你要实现自己的高级字符串数据类型,你可能想考虑评估大量已经存在的库。这里有一个相当不错的列表,列出了一些比较知名的库:http://www.and.org/vstr/comparison - Michael Burr
char p[0]在我见过的每个编译器中都可以工作。大多数分配器(jemalloc、dlmalloc)都具有2的幂次方分配类,因此分配2^n+1将导致几乎所有n的近100%碎片化开销。 - leif

3

这里存在一个潜在的问题:

strncpy(ret,s,j-i);
return ret;

ret可能不会获得空终止符。请参阅man strncpy

       char *strncpy(char *dest, const char *src, size_t n);

       ...

       The strncpy() function is similar, except that at most n bytes  of  src
       are  copied.  Warning: If there is no null byte among the first n bytes
       of src, the string placed in dest will not be null terminated.

这里还有一个漏洞:
j = strlen(*s)-1;
while ((isspace(*(*s+j)))&&(j > 0)) {

如果strlen(*s)为0怎么办?你最终会读取(*s)[-1]

您还没有检查strip()中的字符串是否完全由空格组成。如果是,您将得到j < i

编辑:刚刚注意到您的substr()函数实际上没有被调用。


1

我认为内存损坏问题可能是由于您在getline()调用中处理使用的数据的方式导致的。基本上,line通过strip()中的strndup()重新分配,因此getline()跟踪的linelen中的缓冲区大小将不再准确。 getline()可能会超出缓冲区。

while ((!empty(line))&&(line[0] != '>')) {

    strip(&line);    // <-- assigns a `strndup()` allocation to `line`

    sequences[numsequences-1].sequence = realloc(sequences[numsequences-1].sequence, strlen(sequences[numsequences-1].sequence)+strlen(line)+1);
    strcat(sequences[numsequences-1].sequence,line);

    rcount = getline(&line, &linelen, f);   // <-- the buffer `line` points to might be
                                            //      smaller than `linelen` bytes

}

你可以在这里获取一些漂亮、简单、经过测试的字符串修剪函数:https://dev59.com/knVD5IYBdhLWcg3wAGiD#2452438。使用该链接中的 trim() 函数将解决此问题(以及 strip() 函数中的其他潜在错误)。 - Michael Burr
我使用strip(和substr)解决了所有问题,但仍然存在问题。与getline和linelen的交互明显是问题所在。感谢所有的帮助。 - sirlark

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接