cs50 pset4恢复-为什么将整个文件写入内存失败check50?

时间:2020-06-27 03:59:15

标签: c cs50

我正在研究Recover,并且想知道我的方法是否存在根本缺陷。演练建议使用fread()以512字节块的形式浏览文件,查找JPEG标头,并在每次找到文件时将其写入新文件-00X.jpg。我试着用malloc()创建一个任意大的临时缓冲区,使用fread()的返回值确定文件的大小,然后将整个文件写入具有两种数据类型的结构数组中。 .B代表BYTE,以存储文件,.header代表bool,以指示每个JPEG标头的开始位置。

我遇到两个问题。一是恢复的图像没有通过check50,二是试图一次从我的数组中写入一个以上的字节会导致垃圾字节。这是我在做什么:

typedef uint8_t BYTE;
typedef struct
{
    BYTE B;
    bool header;
}
images;

这同时使用字节和布尔值定义了数据类型BYTE和我的结构。

BYTE *tmp_buffer = malloc(4000000 * sizeof(BYTE));
int counter = fread(tmp_buffer, sizeof(BYTE), 4000000, file);
images buffer[counter];

这将使用malloc()创建任意大的缓冲区,并使用它和fread的返回值确定文件的字节大小,然后在内存中创建要使用的缓冲区。

for (int copy = 0; copy < counter; copy++)
{
    buffer[copy].header = false;
    buffer[copy].B = tmp_buffer[copy];
}
free(tmp_buffer);
fclose(file);
for (int check = 0; check < counter; check++)
{
    if (buffer[check].B == 0xff && buffer[check + 1].B == 0xd8 && buffer[check + 2].B == 0xff)
    {
        buffer[check].header = true;
    }
}

这会将每个字节从“临时”缓冲区复制到永久缓冲区,将所有标头设置为false,然后关闭文件/释放内存。之后,它将找到JPEG标头并将其设置为true。我从这里开始尝试看看什么有效:

int headers_counter = 1;
for (int header_location = 0; header_location < counter; header_location+= 512)
{
    if (buffer[header_location].header == true)
    {
        printf("%i. %i\n", headers_counter, header_location);
        headers_counter++;
    }
}

这将打印原始文件中每个标头的编号和数组(而不是字节)位置,并且看起来可以正常工作。我说“出现”是因为以下代码确实可以恢复图像:

int file_number = 0;
char file_name[8];
sprintf(file_name, "%03i.jpg", file_number);
FILE *img = fopen(file_name, "w");
for (int i = 1024; i < 115200; i++)
{
    fwrite(&buffer[i].B, sizeof(BYTE), 1, img);
}

这并不是要解决整个问题,即恢复所有50张图像。它仅用于恢复000.jpg,方法是从000.jpg标头的第一个字节开始,到001.jpg标头的最后一个字节结束(编辑:这是一个硬编码的示例,使用打印在上面终端上的标头位置,也是一个例子)。看来是这样做的,但是它在check50上失败,并显示错误“恢复的图像不匹配。”

我的女友也在上课,她按照演练的建议实施了代码。我们以十六进制输出打开了000.jpg文件并进行了比较。我们并没有遍历每个字节,但是前几十行和后几十行似乎是相同的,都是空余的。

我提到的另一件事是一次写入多个字节时的垃圾字符。如果我将最终循环更改为此:

for (int i = 1024; i < 115200; i+= 512)
{
    fwrite(&buffer[i].B, sizeof(BYTE), 512, img);
}

然后它的工作效率更低,并且000.jpg说这是无效或不受支持的图像格式。我查看了十六进制输出,这是在比较原始循环的第一行和上面的那一行(将其递增512)时看到的:

ff d8 ff e0 00 10 4a 46 49 46 00 01 01 00 00 01
ff 01 d8 00 ff 00 e0 00 00 00 10 00 4a 00 46 00

每个其他位置都有一个额外的字节!我在这里茫然。在这一点上,更多的是了解这些行为。我敢肯定两者都有合理的解释,但这让我发疯了!我尝试做一个字节数组,而不是添加一个bool的结构,它做同样的事情。

1 个答案:

答案 0 :(得分:1)

如上面注释中所述,通过尝试使用结构并尝试存储要一次性写入的每个jpg,您正在做的事情比需要做的要困难得多。当说明讨论FAT文件系统(在其上获取图像的卡上)时,将每个文件的大块存储在512字节的扇区中。要扫描卡,您只需要一个512字节的缓冲区即可处理对其输出文件的读取和立即写入。不需要结构,也不需要动态分配内存。

读取的方法是从文件读取每个512数据块。然后,您需要检查该块的前4个字节是否包含jpg标头。一个简短的测试功能可以写成:

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

#define FNLEN 128       /* if you need a constant, #define one (or more) */
#define BLKSZ 512

/* check if first 4-bytes in buf match jpg header */
int chkjpgheader (const unsigned char *buf)
{
    return  buf[0] == 0xff && 
            buf[1] == 0xd8 && 
            buf[2] == 0xff && 
            buf[3] >> 4 == 0xe;
}

(您只需测试每个条件是否为true即可返回条件的结果)

考虑如何处理jpg标头的扫描和读取文件,您可以在一个循环中完成所有操作,该循环从输入中读取512个字节,并保留找到的jpg标头数量的计数器-您也可以将其用作已找到指示标头的标志。您将读取数据块,测试它是否为标题,如果不是,则为第一个标题,关闭最后写入的jpg文件的输出文件,创建新文件名,打开文件(验证每个步骤),然后在循环检查每个512字节块的开头以查找标头签名时将数据写出。重复直到文件用完。

您可以执行类似于以下操作:

/* find each jpg header and write contents to separate file_000x.jpg files.
 * returns the number of jpg files successfully recovered.
 */
int recoverjpgs (FILE *ifp)
{
    char jpgname[FNLEN] = "";       /* jpg output filename */
    unsigned char buf[BLKSZ];       /* read buffer */
    int jpgcnt = 0;                 /* found jpg header count*/
    size_t nbytes;                  /* no. of bytes read/written */
    FILE *fp = NULL;                /* FILE* pointer for jpg output */
    
    /* read until jpg header found */
    while ((nbytes = fread (buf, 1, BLKSZ, ifp)) > 0) {
        /* check if jpg header found */
        if (nbytes >= 4 && chkjpgheader(buf)) {
            /* if not 1st header, close current file */
            if (jpgcnt) {
                if (fclose (fp) == EOF) {   /* validate every close-after-write */
                    perror ("recoverjpg()-fclose");
                    return jpgcnt - 1;
                }
            }
            /* create output filename (e.g. file_0001.jpg) */
            sprintf (jpgname, "file_%04d.jpg", jpgcnt + 1);
            /* open next file/validate file open for writing */
            if ((fp = fopen (jpgname, "wb")) == NULL) {
                perror ("fopen-outfile");
                return jpgcnt;
            }
            jpgcnt += 1;    /* increment recovered jpg count */
        }
        /* if header found - write block in buf to output file */
        if (jpgcnt && fwrite (buf, 1, nbytes, fp) != nbytes) {
            perror ("recoverjpg()-fwrite");
            return jpgcnt - 1;
        }
    }
    /* if file opened, close final file */
    if (jpgcnt && fclose (fp) == EOF) {     /* validate every close-after-write */
        perror ("recoverjpg()-fclose");
        return jpgcnt - 1;
    }
    
    return jpgcnt;  /* return number of jpg files recovered */
}

注意: jpgcnt用作计数器标志,用于控制jpg文件上的第一个fclose()何时出现,以及控制何时首次写入第一个文件。)

看看退货。了解为什么在函数的不同位置返回jpgcntjpgcnt - 1。也了解为什么在写入后总是检查fclose()的返回。将最终数据刷新到文件并关闭文件时,可能会发生许多错误-上次检查最后一次写入将不会捕获这些错误。因此,规则-始终验证写后关闭。关闭输入文件时无需检查。

这就是您所需要的。在main()中,您将打开输入文件,只需将打开的文件流传递给recoverjpgs()函数,保存返回值即可知道成功恢复了多少个jpg文件。它可以很简单:

int main (int argc, char **argv) {
    
    FILE *fp = NULL;            /* input file stream pointer */
    int jpgcnt = 0;             /* count of jpg files recovered */
    
    if (argc < 2 ) {    /* validate 1 argument given for filename */
        fprintf (stderr, "error: insufficient input,\n"
                         "usage: %s filename\n", argv[0]);
        return 1;
    }
    
    /* open file/validate file open for reading */
    if ((fp = fopen (argv[1], "rb")) == NULL) {
        perror ("fopen-argv[1]");
        return 1;
    }
    
    if ((jpgcnt = recoverjpgs(fp)))
        printf ("recovered %d .jpg files.\n", jpgcnt);
    else
        puts ("no jpg files recovered.");
        
    fclose (fp);
}

那是完整的程序,只需将3件复制/粘贴在一起,然后尝试一下。

使用/输出示例

$ ./bin/recover ~/doc/c/cs50/recover/card.raw
recovered 50 .jpg files.

(将在当前目录中创建file_0001.jpgfile_0050.jpg这50个文件-您可以欣赏jgp文件中显示的气球,花朵,女孩等。)

仔细检查一下,如果还有其他问题,请告诉我。


关于分配和存储每个文件一次写入的按注释编辑

即使您想在写入一次之前完全缓冲每个文件,使用具有单个uint8_t(字节)和bool来标记该结构是否为标头字节的结构的想法也不会实现。没什么意义。为什么?它使写例程变得一团糟。写入时要检查分配的块中的每个结构大到足以容纳整个card.raw文件的位置,以捕获4个结构的序列,其中每个结构的bool标志都设置为true -本质上是全部复制在读取过程中进行的测试,以查找标头字节,并将bool结构成员true设置为开始。

如前所述,如果有成千上万的文件,您可能希望浏览card.raw的输入流,并将每个jpg的字节保存在缓冲区中,以便它们可以一次写入文件中。过程继续进行(您甚至可以将fork写入一个单独的过程,因此,如果您确实想进行调整,则可以继续读取而不必等待写入。

无论如何,方法都是一样的。如果您为buf动态分配,则可以在每个jpg文件中填充它,并在找到下一个标头时将buf的当前内容写到文件中下一个标头的开头, (将下一个标头移到buf的开头)并重复直到您用完所有要检查的输入为止。

您将在整个过程中为buf重用已分配的存储空间,只有在当前文件需要的存储空间大于当前已分配的存储空间时才进行扩展。 (因此buf的大小最终可以容纳一天结束时找到的最大jpg)。这样可以最大程度地减少分配,这意味着在遇到较大文件时,所有50个文件中唯一需要的reallocrealloc。如果接下来的20个文件全部都位于当前分配的缓冲区之内-无需进行调整,并且在从“法证图像”中恢复不同的jpg文件内容的过程中,请不断地用buf填充{ )

仅添加了一个bufsz变量来跟踪buf的当前分配大小,以及一个total变量来跟踪每个jpg文件中读取的总字节数。除此之外,您只是重新排列文件的写入位置,以便等到将一个完整的jpg读入buf之后再打开并将这些字节写入文件,然后在写入文件后立即关闭文件(编写了一个简短的函数来处理该问题-因为编写通用可重用函数将特定数量的字节从缓冲区写入给指定名称的文件是很有意义的。

完整的文件可以编写如下。

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

#define FNLEN 128       /* if you need a constant, #define one (or more) */
#define BLKSZ 512
#define JPGSZ 1<<15     /* 32K initial allocation size */

/* write 'nbytes' from 'buf' to 'fname'. returns number of bytes
 * written on success, zero otherwise.
 */ 
size_t writebuf2file (const char *fname, void *buf, size_t nbytes)
{
    FILE *fp = NULL;    /* FILE* pointer for jpg output */
    
    /* open file/validate file open for writing */
    if ((fp = fopen (fname, "wb")) == NULL) {
        perror ("writebuf2file-fopen");
        return 0;
    }
    /* write buffer to file/validate bytes written */
    if (fwrite (buf, 1, nbytes, fp) != nbytes) {
        perror ("writebuf2file()-fwrite");
        return 0;
    }
    /* close file/validate every close-after-write */
    if (fclose (fp) == EOF) {
        perror ("writebuf2file-fclose");
        return 0;
    }
    
    return nbytes;
}

/* check if first 4-bytes in buf match jpg header */
int chkjpgheader (const unsigned char *buf)
{
    return  buf[0] == 0xff && 
            buf[1] == 0xd8 && 
            buf[2] == 0xff && 
            buf[3] >> 4 == 0xe;
}

/* find each jpg header and write contents to separate file_000x.jpg files.
 * returns the number of jpg files successfully recovered.
 */
int recoverjpgs (FILE *ifp)
{
    char jpgname[FNLEN] = "";                   /* jpg output filename */
    int jpgcnt = 0;                             /* found jpg header count*/
    size_t  nbytes,                             /* no. of bytes read/written */
            bufsz = JPGSZ,                      /* tracks current allocation of buf */
            total = 0;                          /* tracks total bytes in jpg file */
    uint8_t *buf = malloc (JPGSZ);              /* read buffer */
    
    if (!buf) { /* validate every allocation/reallocation */
        perror ("malloc-buf");
        return 0;
    }
    
    /* read until jpg header found */
    while ((nbytes = fread (buf + total, 1, BLKSZ, ifp)) > 0) {
        /* check if jpg header found */
        if (nbytes >= 4 && chkjpgheader(buf + total)) {
            /* if not 1st header, write buffer to file, reset for next file */
            if (jpgcnt) {
                /* create output filename (e.g. file_0001.jpg) */
                sprintf (jpgname, "file_%04d.jpg", jpgcnt);
                /* write current buf to file */
                if (!writebuf2file (jpgname, buf, total))
                    return jpgcnt - 1;
                /* move header block to start of buf */
                memmove (buf, buf + total, BLKSZ);
                total = 0;                  /* reset total for next file */
            }
            jpgcnt += 1;    /* increment recovered jpg count */
        }
        /* if header found - began accumulating blocks in buf */
        if (jpgcnt)
            total += nbytes;
        /* check if reallocation required before next read */
        if (total + BLKSZ > bufsz) {
            /* add a fixed 32K each time reallocaiton required
             * always realloc to a temporary pointer to prevent memory leak
             * on realloc failure.
             */
            void *tmp = realloc (buf, bufsz + (1 << 15));
            if (!tmp) {                     /* validate every reallocations */
                perror ("realloc-buf");
                return jpgcnt - 1;
            }
            buf = tmp;              /* assign reallocated block to buf */
            bufsz += 1 << 15;       /* update bufsz with new allocation size */
        }
    }
    /* write final buffer to file */
    if (jpgcnt) {
        /* create output filename (e.g. file_0001.jpg) */
        sprintf (jpgname, "file_%04d.jpg", jpgcnt);
        /* write current buf to file */
        if (!writebuf2file (jpgname, buf, total))
            return jpgcnt - 1;
    }
    
    free (buf);     /* free allocated memory */
    
    return jpgcnt;  /* return number of jpg files recovered */
}

int main (int argc, char **argv) {
    
    FILE *fp = NULL;            /* input file stream pointer */
    int jpgcnt = 0;             /* count of jpg files recovered */
    
    if (argc < 2 ) {    /* validate 1 argument given for filename */
        fprintf (stderr, "error: insufficient input,\n"
                         "usage: %s filename\n", argv[0]);
        return 1;
    }
    
    /* open file/validate file open for reading */
    if ((fp = fopen (argv[1], "rb")) == NULL) {
        perror ("fopen-argv[1]");
        return 1;
    }
    
    if ((jpgcnt = recoverjpgs(fp)))
        printf ("recovered %d .jpg files.\n", jpgcnt);
    else
        puts ("no jpg files recovered.");
        
    fclose (fp);
}

在您编写的任何动态分配内存的代码中,对于任何分配的内存块,您都有2个职责:(1)始终保留指向起始地址的指针因此,(2)不再需要它时可以释放

当务之急是使用一个内存错误检查程序来确保您不会尝试访问内存或在分配的块的边界之外/之外写,尝试读取或基于未初始化的值进行条件跳转,最后,以确认您释放了已分配的所有内存。

对于Linux,valgrind是正常选择。每个平台都有类似的内存检查器。它们都很容易使用,只需通过它运行程序即可。

始终确认已释放已分配的所有内存,并且没有内存错误。

花点时间浏览代码。如果您还有其他问题,请告诉我。