【问题标题】:Using realloc to expand buffer while reading from file crashes从文件崩溃中读取时使用 realloc 扩展缓冲区
【发布时间】:2012-02-16 21:22:46
【问题描述】:

我正在编写一些需要读取fasta files 的代码,所以我的部分代码(包括在下面)是一个fasta 解析器。由于单个序列可以跨越 fasta 格式的多行,我需要将从文件中读取的多个连续行连接成一个字符串。我这样做,通过在读取每一行后重新分配字符串缓冲区,使其成为序列的当前长度加上读入的行的长度。我做了一些其他的事情,比如剥离空白等。一切顺利第一个序列,但 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;
}

【问题讨论】:

  • 感谢所有关于子字符串例程的 cmets。我已经在我的代码中修复了它。然而,我也注意到我处理负索引的方式是错误的。我应该添加负索引,而不是减去它。话虽如此,我也意识到我错误地复制了 substr 函数,因为我没有在粘贴的其余代码中调用它。
  • strip() 也有问题。它会用零长度的字符串做坏事。看起来你没有用这样的字符串调用它,但我认为当它在其他地方使用时修复它会是一件好事。

标签: c realloc fasta


【解决方案1】:

这里有一个潜在问题:

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.

这里还有一个bug:

j = strlen(*s)-1;
while ((isspace(*(*s+j)))&&(j > 0)) {

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

您也没有在strip() 中检查该字符串不完全由空格组成。如果是这样,你最终会得到j &lt; i

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

【讨论】:

    【解决方案2】:

    您应该使用如下所示的字符串:

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

    这可以防止 strncpy 错误,就像您看到的那样,并允许您更快地执行 strcat 和朋友。

    您还应该为每个字符串使用一个加倍数组。这可以防止过多的分配和 memcpys。像这样的:

    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 解析器中不是问题,更多的是在处理中。
    • 由于内部 malloc 碎片,即使您不请求它也可能最终使用过多的内存。倍增数组非常受信任,但如果它们太吓人,请至少使用malloc_usable_size 之类的东西来衡量您的碎片。如果您确实选择了加倍数组,并且我的第二个建议是一起分配长度和字符串缓冲区,请注意在大小计算中包含长度,否则您可能会出现严重的碎片(如果您分配 2^n + sizeof int ,例如)。
    • char ptr[0]; 是无效的 C。你的意思是 char ptr[];,但现在这对于元素来说可能是一个坏名字,因为它是一个数组,而不是一个指针。我会称它为datacontents 或类似的东西。
    • 而且没有理由认为分配2^n+sizeof int 会导致比分配2^n 更严重的碎片化。二的幂在这里没有特殊的地位。 (如果您谈论的分配如此之大以至于无法由 vm 提供服务,例如mmap,那么它们足够大,以至于浪费一整个页面都是很小的浪费,按百分比计算,可能约为 2%)。跨度>
    • 如果您要实现自己的更高级别的字符串数据类型,您可能需要考虑评估执行此操作的大量现有库。以下是一些比较知名的列表:and.org/vstr/comparison
    【解决方案3】:

    我认为内存损坏问题可能是您如何处理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
    
    }
    

    【讨论】:

    • 您可以在这里获得一些不错的、简单的、经过测试的函数来就地修剪字符串:stackoverflow.com/a/2452438/12711 使用该链接中的trim() 将解决这个问题(以及其他潜在的错误) strip() 函数)。
    • 我用strip(和substr)解决了所有问题,但问题仍然存在。与 getline 和 linelen 的交互绝对是问题所在。感谢大家的帮助
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-04-03
    • 2015-03-03
    • 1970-01-01
    • 1970-01-01
    • 2013-09-17
    相关资源
    最近更新 更多