【问题标题】:Subtlety in strstr?strstr 中的微妙之处?
【发布时间】:2017-01-18 23:29:50
【问题描述】:

我有一个二进制数据文件,其中散布着各种字符串。我正在尝试编写一个 C 代码来查找文件中用户指定字符串的第一次出现。 (我知道这可以用 bash 完成,但出于其他原因我需要 C 代码。)目前的代码是:

#include <stdio.h>
#include <string.h>

#define CHUNK_SIZE 512

int main(int argc, char **argv) {
    char *fname = argv[1];
    char *tag = argv[2];
    FILE *infile;
    char *chunk;
    char *taglcn = NULL;
    long lcn_in_file = 0;
    int back_step;
    fpos_t pos;

    // allocate chunk
    chunk = (char*)malloc((CHUNK_SIZE + 1) * sizeof(char));

    // find back_step
    back_step = strlen(tag) - 1;

    // open file
    infile = fopen(fname, "r");

    // loop
    while (taglcn == NULL) { 
        // read chunk
        memset(chunk, 0, (CHUNK_SIZE + 1) * sizeof(char));
        fread(chunk, sizeof(char), CHUNK_SIZE, infile);
        printf("Read %c\n", chunk[0]);
        // look for tag
        taglcn = strstr(chunk, tag);
        if (taglcn != NULL) {
            // if you find tag, add to location the offset in bytes from beginning of chunk
            lcn_in_file += (long)(taglcn - chunk);
            printf("HEY I FOUND IT!\n");
        } else {
            // if you don't find tag, add chunk size minus back_step to location and ...
            lcn_in_file += ((CHUNK_SIZE - back_step) * sizeof(char)); 
            // back file pointer up by back_step for next read 
            fseek(infile, -back_step, SEEK_CUR);
            fgetpos(infile, &pos);
            printf("%ld\n", pos);
            printf("%s\n\n\n", chunk);
        }
    }
    printf("%ld\n", lcn_in_file);

    fclose(infile);
    free(chunk);
}

如果您想知道,back_step 用于处理不太可能发生的问题,即相关字符串被chunk 边界分割。

我要检查的文件大小约为 1Gb。问题是,由于某种原因,我可以在前 9000 个字节左右找到任何字符串,但除此之外,strstr 不知何故没有检测到任何字符串。也就是说,如果我在文件中查找超过 9000 字节左右的字符串,strstr 不会检测到它。代码会读取整个文件,但始终找不到搜索字符串。

我尝试将 CHUNK_SIZE 从 128 更改为 50000,但结果没有变化。我也尝试过改变back_step。当strstr 找不到字符串时,我什至输入了诊断代码以逐个字符打印出chunk,而且果然,字符串正是它应该在的位置。 pos 的诊断输出始终正确。

谁能告诉我哪里出错了? strstr 是不是在这里用错了工具?

【问题讨论】:

  • 虽然这不一定是问题,但为了使用任意搜索(如来自SEEK_CUR 的负偏移),您必须以二进制模式打开流。您的信息流以文本模式打开。
  • 另外,您是否有机会搜索二进制文件,即其中包含零字节的文件?
  • @AnT 是的,大概就是这样。谢谢。

标签: c string strstr


【解决方案1】:

由于您说您的文件是二进制文件,strstr() 将在文件中的第一个空字节处停止扫描。

如果您希望在二进制数据中查找模式,那么memmem() 函数是合适的,如果它可用的话。它在 Linux 和其他一些平台(BSD、macOS 等)上可用,但它没有被定义为标准 C 或 POSIX 的一部分。它与strstr() 的关系与memcpy()strcpy() 的关系大致相同。


请注意,您的代码应检测fread() 读取的字节数,并仅搜索该字节数。

char   *tag = …;     // Identify the data to be searched for
size_t  taglen = …;  // Identify the data's length (maybe strlen(tag))
int     nbytes;
while ((nbytes = fread(chunk, 1, (CHUNK_SIZE + 1), infile)) > 0)
{
    …
    tagcln = memmem(chunk, nbytes, tag, taglen);
    if (tagcln != 0)
        …found it…
    …
}

目前还不清楚为什么你在块大小上有+1fread() 函数不会在数据末尾添加空字节或类似内容。我没有改变这方面,但可能不会在我自己的代码中使用它。

您最好注意识别跨越两个块之间边界的标签。

【讨论】:

  • 他有bash,所以他可能有memmem()
  • @Jasen:有一个合理的机会,是的,但我曾在没有memmem() 的 Bash 机器上工作过——当然,它们没有运行 Linux。
  • @JonathanLeffler 感谢您的出色回答。不得不与 chrqlie's 一起去,因为他麻烦地给出了 memmem() 的实现,但这有很大帮助。非常感谢。
【解决方案2】:

strstr 在您的代码中失败的最可能原因是文件中存在空字节。此外,您应该以二进制模式打开文件,以使文件偏移量有意义。

要扫描块中的字节序列,请使用memmem() 函数。如果它在您的系统上不可用,这里有一个简单的实现:

#include <string.h>

void *memmem(const void *haystack, size_t n1, const void *needle, size_t n2) {
    const unsigned char *p1 = haystack;
    const unsigned char *p2 = needle;

    if (n2 == 0)
        return (void*)p1;
    if (n2 > n1)
        return NULL;

    const unsigned char *p3 = p1 + n1 - n2 + 1;
    for (const unsigned char *p = p1; (p = memchr(p, *p2, p3 - p)) != NULL; p++) {
        if (!memcmp(p, p2, n2))
            return (void*)p;
    }
    return NULL;
}

你可以这样修改你的程序:

#include <errno.h>
#include <stdio.h>
#include <string.h>

void *memmem(const void *haystack, size_t n1, const void *needle, size_t n2);

#define CHUNK_SIZE 65536

int main(int argc, char **argv) {

    if (argc < 3) {
        fprintf(sderr, "missing parameters\n");
        exit(1);
    }

    // open file
    char *fname = argv[1];
    FILE *infile = fopen(fname, "rb");
    if (infile == NULL) {
        fprintf(sderr, "cannot open file %s: %s\n", fname, strerror(errno));
        exit(1);
    }

    char *tag = argv[2];
    size_t tag_len = strlen(tag);
    size_t overlap_len = 0;
    long long pos = 0;

    char *chunk = malloc(CHUNK_SIZE + tag_len - 1);
    if (chunk == NULL) {
        fprintf(sderr, "cannot allocate memory\n");
        exit(1);
    }

    // loop
    for (;;) { 
        // read chunk
        size_t chunk_len = overlap_len + fread(chunk + overlap_len, 1, 
                                               CHUNK_SIZE, infile);
        if (chunk_len < tag_len) {
            // end of file or very short file
            break;
        }
        // look for tag
        char *tag_location = memmem(chunk, chunk_len, tag, tag_len);
        if (tag_location != NULL) {
            // if you find tag, add to location the offset in bytes from beginning of chunk
            printf("string found at %lld\n", pos + (tag_location - chunk));
            break;
        } else {
            // if you don't find tag, add chunk size minus back_step to location and ...
            overlap_len = tag_len - 1;
            memmove(chunk, chunk + chunk_len - overlap_len, overlap_len);
            pos += chunk_len - overlap_len;
        }
    }

    fclose(infile);
    free(chunk);
    return 0;
}

请注意,文件是以CHUNK_SIZE 字节为单位读取的,如果CHUNK_SIZE 是文件系统块大小的倍数,这是最佳选择。

【讨论】:

    【解决方案3】:

    对于一些非常简单的代码,您可以使用mmap()memcmp()

    错误检查和正确的头文件留给读者作为练习(至少有一个错误 - 另一个练习供读者查找):

    int main( int argc, char **argv )
    {
        // put usable names on command-line args
        char *fname = argv[ 1 ];
        char *tag = argv[ 2 ];
    
        // mmap the entire file
        int fd = open( fname, O_RDONLY );
        struct stat sb;
        fstat( fd, &sb );
        char *contents = mmap( NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0 );
        close( fd );
    
        size_t tag_len = strlen( tag );
    
        size_t bytes_to_check = 1UL + sb.st_size - tag_len;
    
        for ( size_t ii = 0; ii < bytes_to_check; ii++ )
        {
            if ( !memcmp( contents + ii, tag, tag_len ) )
            {
                 // match found
                 // (probably want to check if contents[ ii + tag_len ]
                 //  is a `\0' char to get actual string matches)
            }
        }
    
        munmap( contents, sb.st_len );
    
        return( 0 );
    }
    

    这可能不会是最快的方式(一般来说,mmap() 不会接近性能赢家,尤其是在这种简单地从头到尾流式传输文件的用例中),但是这简单

    (注意mmap()在读取时如果文件大小发生变化也会出现问题。如果文件变大,您将看不到额外的数据。如果文件被缩短,您将得到SIGBUS您尝试读取已删除的数据。)

    【讨论】:

      【解决方案4】:

      二进制数据文件将包含充当字符串结尾的“\0”字节。那里的内容越多,strstr 要搜索的区域就越短。注意strstr 一旦达到 0 字节,就会认为它的工作已经完成。

      你可以像这样的间隔扫描内存

      while (strlen (chunk) < CHUNKSIZE) 
         chunk += strlen (chunk) + 1;
      

      即只要您仍在该块中,在该块中的一个空字节后重新启动。

      【讨论】:

      • 听起来不错。除了逐字节遍历之外,是否有其他修复方法,即一些等效于strstr 的方法适用于一般字节数组?
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-08-23
      • 1970-01-01
      • 1970-01-01
      • 2012-09-29
      • 1970-01-01
      • 2022-01-09
      • 2013-08-12
      相关资源
      最近更新 更多