C语言 为什么fread将输出指针设置为NULL并导致堆栈破坏错误?

9vw9lbht  于 2023-08-03  发布在  其他
关注(0)|答案(3)|浏览(97)

我之前问过这个问题,但没有提供一个可重复性最低的例子。感谢你的反馈。我试图向二进制文件中写入一个int,后面跟着一个布尔数组,其中int表示该数组的长度。
下面的代码编译后似乎可以正确生成二进制文件。当调用fread时,它设置了void* 参数,我将其传递给NULL,并触发了一个stack-smashing错误。
example.c

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

typedef struct cutlogTag{
    int len;
    bool *parts_cut;
} Cutlog;

size_t save_cutlog(const char *path, const Cutlog *p){
    
    FILE *fp;
    size_t written = 0;

    fp = fopen(path, "wb");
    if (fp == NULL){
        fprintf(stderr, "Failed to save cutlog file\n");
        return 0;
    }
    written = fwrite(&(p->len), sizeof(p->len), 1, fp);
    written += fwrite(p->parts_cut, sizeof(bool), p->len, fp);
    if(written != 1 + p->len)
        fprintf(stderr, "error writing file\n");
    else fprintf(stdout, "cutlog written to %s\n", path);
    fclose(fp);
    return written;
}

//returns cutlog with length of -1 on failure to load log
Cutlog load_cutlog(const char *path){
    
    Cutlog ret;
    FILE *fp;
    size_t read = 0;

    ret.len = -1;
    ret.parts_cut = NULL;
    
    fp = fopen(path, "rb");
    assert(fp != NULL);

    fseek(fp, 0, SEEK_SET);
    fread(&ret.len, sizeof(ret.len), 1, fp);
    ret.parts_cut = malloc(sizeof(bool) * ret.len);
    assert(ret.parts_cut);
    read = fread(&ret.parts_cut, sizeof(bool), ret.len, fp);
    if(read != ret.len){
        fprintf(stderr, "read unexpected size of data\n");
        ret.len = -1;
    }
    if (getc(fp) != EOF){
        fprintf(stderr, "expected file end. something went wrong. \n");
        ret.len = -1;
    }
    fclose(fp);
    return ret;
}

int main(int argc, char *argv[]){
    Cutlog clog;
    const char* path = "testbinary";
//initialize cutlog struct
    clog.len = 687;
    clog.parts_cut = malloc(sizeof(bool) * clog.len );
    assert(clog.parts_cut);
    for (int i = 0; i < clog.len; i++){
        clog.parts_cut[i] = false;
    }
//save to binary file and free from memory
    save_cutlog(path, &clog);
    free(clog.parts_cut);
//load from binary file
    clog = load_cutlog(path);
    fprintf(stdout, "len is %d\n", clog.len);
    return 0;
}

字符串
尝试写入一个二进制文件一个int后跟一个bools数组,其中int表示该数组的长度,然后加载回文件。
文件写得很正确,但是在阅读它的时候,我导致了堆栈崩溃。

jchrr9hc

jchrr9hc1#

read = fread(&ret.parts_cut, sizeof(bool), ret.len, fp);

字符串
&ret.parts_cutret.parts_cut的地址,它本身是一个存在于 * 堆栈 * 上的指针,所以在询问这个问题时通常是四个或八个字节(当然可以更多)。
如果你有更多的布尔值,将适合该指针的空间,那么你将损坏堆栈上的其他东西。
但是即使它 * 确实 * 适合,你几乎肯定仍然会得到不正确的行为,因为你的指针现在被设置为 * 不 * 指向你分配的内存,一组布尔值的一些奇怪的别名,如0x0101000101000001
如果您稍后尝试用类似*(ret->parts_cut)的东西去引用该指针,则不太可能有好的结果。
你应该在fread调用中使用ret.parts_cut,这是指针的 value,指向你刚刚分配了足够空间的东西:

read = fread(ret.parts_cut, sizeof(bool), ret.len, fp);


还有一点依赖assert宏来捕获可能导致问题的东西可能不是最好的主意。如果定义了NDEBUG宏(它很可能在产品代码中),则assert宏不做任何事情。
您最好使用if (! condition) handle_it()而不是assert(condition)来处理这些情况。

gpnt7bae

gpnt7bae2#

除了@paxdiablo注意到ret.part_cut已经是一个指针并且您不能在fread()中再次获取它的地址这一主要问题之外,还有其他一些地方可以改进:

  • 对于sizeof,您可以在sizof(type)中使用括号(例如,sizeof(bool)),并且您可以省略sizeof object的括号(例如sizeof p->len)的数据。编译器通常允许这两种形式,但更准确地说,
  • 如果你使用取消引用的指针来设置数组或分配块的类型大小,你永远不会得到错误的类型大小,例如。sizeof *p->parts_cut。对于短代码,这通常不是问题,但当声明可能相隔数千行,或者指针指向具有多层间接寻址的对象时,这将变得更具挑战性,
  • 虽然assert()适合于短测试代码,但最好避免使用。为什么?它只是停止程序,阻止从错误中恢复。您通常需要一种方法来恢复您的程序,而不是简单地在每个错误时停止,
  • 您应该在任何写入之后验证fclose(),以捕获通过检查写入的项数而未捕获的流错误(例如,将写入缓冲区内容刷新到磁盘时的文件流错误等),例如
/* always validate close-after-write */
    if (fclose (fp) == EOF) {
        perror ("fclose save_cutlog");
        return 0;
    }

字符串

  • 初始化您的结构总是一个好主意,以避免在成员的值不确定时引用该成员的可能性,例如Cutlog ret = { .len = 0 };(这会将len设置为所示的值,并且所有其他成员未显式地初始化为零),
  • 虽然允许使用,但read是C语言中系统调用的名称,最好选择一个不会冲突的名称,例如size_t nparts = 0;是相反的,
  • 您选择检查written的累积和来验证fwrite()有点不太正统--但很有效。最好在每次写入后进行检查,以避免在第一次写入失败时写入到处于错误状态的流中(此更正由您来完成),
  • fread()不区分EOF和stream-error,因此要找出发生了哪种错误,您需要在发生错误时调用feof()ferror(),例如
/* ret.part_cut is already a pointer to allocated memory, no & */
    nparts = fread (ret.parts_cut, sizeof *ret.parts_cut, ret.len, fp);
    if (nparts != (size_t)ret.len) {
        fprintf (stderr, "error: read unexpected size of data\n");
        /* fread doesn't distinguish between EOF and error */
        if (feof (fp)) {  /* must check EOF */
            fputs ("error: EOF on read of ret.parts_cut.\n", stderr);
        }
        else if (ferror (fp)) { /* and stream error */
            fputs ("error: stream error on read of ret.parts_cut.\n", stderr);
        }
        ret.len = -1;
        fclose (fp);
        return ret;
    }

  • 在读取之后调用getc(fp)来检查EOF没有错,但也不是真的需要,
  • 您真的不希望对文件名进行硬编码如果没有为代码提供一个参数,可以使用"testbinary"作为默认文件名,但实际上不需要为了写入不同的文件名而重新编译程序。你可以用一个简单的 * 三进制 * 来设置const char *path = argc > 1 ? argv[1] : "testbinary";
  • 您应该检查每个函数的返回值,这些值对于您的程序的继续定义的操作是必需的。save_cutlog()会传回表示成功或失败的值,但您无法在main()中使用该传回值,例如
/* validate every function call where result is necessary
     * for the continued defined operations of your code.
     */
    if (!save_cutlog (path, &clog)) {
        exit (EXIT_FAILURE);                /* exit with failure */
    }
    free (clog.parts_cut);

  • 最后,在从main()返回之前,通过释放所有分配内存来整理。是的,它会在程序退出时被释放,但当使用内存检查工具(如valgrind)时,在从main()返回之前未释放的内存将在程序退出时显示为正在使用。

总之,你知道你要去哪里,你只是在fread()中取ret.parts_cut的地址时被绊倒了。如果您遵循上述所有建议,就可以调整程式码,如下所示:

// #include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

typedef struct cutlogTag {
    int len;
    bool *parts_cut;
} Cutlog;

size_t save_cutlog (const char *path, const Cutlog *p)
{
    FILE *fp;
    size_t written = 0;

    fp = fopen (path, "wb");
    if (fp == NULL) {
        fprintf (stderr, "Failed to save cutlog file\n");
        return 0;
    }
    written = fwrite (&(p->len), sizeof p->len, 1, fp);
    written += fwrite (p->parts_cut, sizeof *p->parts_cut, p->len, fp);
    
    if (written != (size_t)(1 + p->len)) {
        fprintf(stderr, "error writing file\n");
    }
    else {
        fprintf(stdout, "cutlog written to %s\n", path);
    }
    
    /* always validate close-after-write */
    if (fclose (fp) == EOF) {
        perror ("fclose save_cutlog");
        return 0;
    }
    
    return written;
}

//returns cutlog with length of -1 on failure to load log
Cutlog load_cutlog (const char *path)
{
    Cutlog ret = { .len = 0 };    /* initialize all zero */
    FILE *fp;
    size_t nparts = 0;            /* read is a syscall, use another name */

    ret.len = -1;
    ret.parts_cut = NULL;
    
    fp = fopen (path, "rb");
    if (!fp) {                /* assert prevents any type recovery - avoid */
        perror ("fopen");
        return ret;
    }

    // fseek(fp, 0, SEEK_SET);    /* unnecessary */
    
    if (fread (&ret.len, sizeof ret.len, 1, fp) != 1) {
        if (feof (fp)) {
            fputs ("error: EOF on read of ret.len.\n", stderr);
        }
        else if (ferror (fp)) {
            fputs ("error: stream error on read of ret.len.\n", stderr);
        }
        ret.len = -1;
        fclose (fp);
        return ret;
    }
    ret.parts_cut = malloc (sizeof *ret.parts_cut * ret.len);
    /* always validate every allocation, assert just crashes out - avoid */
    if (!ret.parts_cut) {
        perror ("malloc ret.parts_cut");
        ret.len = -1;
        fclose (fp);
        return ret;
    }
    
    /* ret.part_cut is already a pointer to allocated memory, no & */
    nparts = fread (ret.parts_cut, sizeof *ret.parts_cut, ret.len, fp);
    if (nparts != (size_t)ret.len) {
        fprintf (stderr, "error: read unexpected size of data\n");
        /* fread doesn't distinguish between EOF and error */
        if (feof (fp)) {  /* must check EOF */
            fputs ("error: EOF on read of ret.parts_cut.\n", stderr);
        }
        else if (ferror (fp)) { /* and stream error */
            fputs ("error: stream error on read of ret.parts_cut.\n", stderr);
        }
        ret.len = -1;
        fclose (fp);
        return ret;
    }
    
    if (getc(fp) != EOF) { /* not really necessary - but not wrong */
        fprintf(stderr, "expected file end. something went wrong. \n");
        ret.len = -1;
    }
    
    fclose(fp);
    return ret;
}

int main (int argc, char *argv[]) {
    
    Cutlog clog = { .len = 0 };               /* initialize */
    /* use 1st argument as filename (default "testbinary") */
    const char *path = argc > 1 ? argv[1] : "testbinary";
    
    clog.len = 687;
    clog.parts_cut = malloc (sizeof *clog.parts_cut * clog.len );
    if (!clog.parts_cut) {
        perror ("malloc clog.parts_cut");
        exit (EXIT_FAILURE);                /* exit with failure */
    }
    // assert(clog.parts_cut);              /* avoid using assert */
    for (int i = 0; i < clog.len; i++){
        clog.parts_cut[i] = false;
    }
    
    //save to binary file and free from memory
    /* validate every function call where result is necessary
     * for the continued defined operations of your code.
     */
    if (!save_cutlog (path, &clog)) {
        exit (EXIT_FAILURE);                /* exit with failure */
    }
    free (clog.parts_cut);
    
    //load from binary file
    clog = load_cutlog(path);
    if (clog.len == -1) {
        exit (EXIT_FAILURE);                /* exit with failure */
    }
    
    fprintf (stdout, "len is %d\n", clog.len);
    
    free (clog.parts_cut);                  /* don't forget to free mem */
}

使用/输出示例

$ ./bin/fwrite-cutlog
cutlog written to testbinary
len is 687

内存使用/错误检查

通过valgrind运行代码以捕获任何内存错误,并验证是否释放了所有内存(这也会捕获您最初的问题--以及在编译时启用 *full warnings *)。举例来说:

$ valgrind ./bin/fwrite-cutlog
==25258== Memcheck, a memory error detector
==25258== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==25258== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info
==25258== Command: ./bin/fwrite-cutlog
==25258==
cutlog written to testbinary
len is 687
==25258==
==25258== HEAP SUMMARY:
==25258==     in use at exit: 0 bytes in 0 blocks
==25258==   total heap usage: 7 allocs, 7 frees, 11,534 bytes allocated
==25258==
==25258== All heap blocks were freed -- no leaks are possible
==25258==
==25258== For lists of detected and suppressed errors, rerun with: -s
==25258== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)


请务必确认您已释放所有已分配的内存,并且没有内存错误。
如果你有任何问题,请告诉我。

ej83mcc0

ej83mcc03#

使用clang -fsanitize=address构建源代码会产生:

=================================================================
==485824==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7fffd74354d0 at pc 0x558ecaa48b4e bp 0x7fffd7435490 sp 0x7fffd7434c60
WRITE of size 687 at 0x7fffd74354d0 thread T0
    #0 0x558ecaa48b4d in fread (/tmp/a.out+0x3cb4d) (BuildId: 532318f475d6a04e8d9ae1be96e2fd96a96490f1)
    #1 0x558ecaaeb41c in load_cutlog /tmp/t.c:48:12

Address 0x7fffd74354d0 is located in stack of thread T0 at offset 48 in frame
    #0 0x558ecaaeb11f in load_cutlog /tmp/t.c:32

  This frame has 1 object(s):
    [32, 48) 'retval' <== Memory access at offset 48 overflows this variable

字符串
解决方法是改变这一点:

read = fread(&ret.parts_cut, sizeof(bool), ret.len, fp);


对此:

read = fread(ret.parts_cut, sizeof(bool), ret.len, fp);

相关问题