在 C 中写入文件会覆盖变量

Posted

技术标签:

【中文标题】在 C 中写入文件会覆盖变量【英文标题】:Writing to file in C overwrites variables 【发布时间】:2022-01-17 16:32:07 【问题描述】:

说明:

我创建了一个小程序,用于将文件的名称和校验和存储在结构中,用于目录中的每个文件。当使用 printf 将输出写入标准输出时,一切似乎都很好,但是如果我们使用 fputsfprintf 写入文件,值会被覆盖,可能是因为某些缓冲区溢出?

带有打印的主输出。

Name: 2.txt. Checksum: fc769d448ed4e08bd855927bad2c8e43efdf5315a6daa9f28577758786d52eaf 
Name: 1.txt. Checksum: 2d46cffd0302c5537ddb4952a9cca7d66060dafecd56fe3a7fe8e5e5cabbbbf9 
Name: 3.txt. Checksum: 37bb2e5563e94eee68fac6b07501c44f018599482e897a626a94dd88053b4b7e

但是,如果我们将checksumMaps[0] 的值打印到文件中, 值 checksumMaps[0].filename 被覆盖(使用校验和字符串的最后 2 个字节),如下所示:

FILE *fp = fopen("mychecksums.txt", "w");
  char formatted_bytes[32*2+1];
  char *filename = checksumMaps[0].filename;
  format_bytes(formatted_bytes, checksumMaps[0].checksum);

  fputs(filename, fp);
  fputs(formatted_bytes, fp);

  // We print the value of `filename` again in order to see that it has been overwritten.
  printf("%s \n", filename);
  fclose(fp);

程序将aftxt 写入标准输出而不是2.txt。 使用gdb,我可以看到filename 的值在fputs(formatted_bytes, fp); 行之后从2.txt 变为aftxt。这可能是什么原因?

最小可重现示例

ArchiveFile.h

typedef struct ArchiveFile
  char *uuid;
  char *checksum;
  char *relative_path;
  int is_binary;
 ArchiveFile;

typedef struct file_content
  unsigned char* bytes;
  unsigned long file_size;
 file_content;

void set_uuid(ArchiveFile *file, char* uuid);

char* get_absolute_path(ArchiveFile *file, char* root);

char* get_file_text(ArchiveFile *file,  char* root);

void get_bytes(ArchiveFile *file, char* root, unsigned char *buffer, size_t fsize);

long get_file_size(ArchiveFile *file, char *root);

ArchiveFile.c

#include <sys/stat.h>
#include <stdlib.h>
#include <stdio.h>
#include "ArchiveFile.h"
#include <string.h>

void set_uuid(ArchiveFile* file, char* uuid)
  file->uuid = uuid;


char* get_absolute_path(ArchiveFile *file, char* root)
  /* Allocate space according to the relative path +
   the root path + null terminating byte.*/
  char* absolute_path = malloc(strlen(file->relative_path) + strlen(root) + 1);
  // Add the root path.
  strcpy(absolute_path, root);
  // Concatonate the root with the rest of the path.
  strcat(absolute_path, file->relative_path);
  return absolute_path;


char* get_file_text(ArchiveFile *file, char* root)

  char* absolute_path = get_absolute_path(file, root);
  FILE *fp = fopen(absolute_path, "r");
  if(fp == NULL)
    printf("Could not open file %s \n", absolute_path);
  
  // Platform independent way of getting the file size in bytes.
  fseek(fp, 0, SEEK_END);
  long fsize = ftell(fp);
  fseek(fp, 0, SEEK_SET);  /* same as rewind(f); */

  char *buffer = malloc(fsize);
  if(fp)
    fread(buffer, sizeof(char), fsize, fp);
  
  fclose(fp);
  free(absolute_path);
  return buffer;



void print_bytes2(unsigned char* md, size_t size)
  for (size_t i = 0; i < size; i++) 
        printf("%02x ", md[i]);
    
    printf("\n");



void get_bytes(ArchiveFile *file, char *root, unsigned char *buffer, size_t fsize)

  char* absolute_path = get_absolute_path(file, root);
  FILE *fp = fopen(absolute_path, "rb");
  
  
  if(fp)
    fread(buffer, 1, fsize, fp);
  

  free(absolute_path);
  fclose(fp);


long get_file_size(ArchiveFile *file, char *root)
  
  char* filepath = get_absolute_path(file, root);
  FILE *fp = fopen(filepath, "rb");

  fseek(fp, 0, SEEK_END);
  long fsize = ftell(fp);
  fseek(fp, 0, SEEK_SET);  /* same as rewind(f); */
  free(filepath);
  fclose(fp);
  
  return fsize;




校验和/校验和.h

// Used to store information about filename and checksum.
typedef struct ChecksumMap
  char* filename;
  unsigned char checksum [32];
 ChecksumMap;


int calculate_checksum(void* input, unsigned long length, unsigned char* md);

校验和/校验和.h

#include <stdio.h>
#include <openssl/sha.h>
#include "checksum.h"


int calculate_checksum(void* input, unsigned long length, unsigned char* md)
  SHA256_CTX context;
  if(!SHA256_Init(&context))
        return 0;
  if(!SHA256_Update(&context, (unsigned char*)input, length))
  return 0;

  if(!SHA256_Final(md, &context))
        return 0;

  return 1;




ma​​in.c

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <dirent.h>
#include <sys/types.h>
#include "ArchiveFile.h"
#include "checksum/checksum.h"



void format_bytes(char* buffer, unsigned char* md)
  for (int i = 0; i < 32; i++) 
        sprintf(&buffer[i*2], "%02x", md[i]);
    
  buffer[32*2] = '\0';
    



void *listdir(char *name, int count, ChecksumMap *checksumMaps)

  
    DIR *dir;
    struct dirent *direntry;

    if (!(dir = opendir(name)))
        return NULL;

    while ((direntry = readdir(dir)) != NULL) 
      // If we reach a directory (that is not . or ..) then recursive step.
        if (direntry->d_type == DT_DIR) 
            char path[1024];
            if (strcmp(direntry->d_name, ".") == 0 || strcmp(direntry->d_name, "..") == 0)
                continue;
            snprintf(path, sizeof(path), "%s/%s", name, direntry->d_name);
            
            listdir(path, count, checksumMaps);
         else 
            

            unsigned char md[32];
            ArchiveFile file;
            
            file.relative_path = direntry->d_name;
            
            // Get the full path of the file:
            char parent_name[strlen(name)+1];
            memset(&parent_name[0], 0, sizeof(parent_name));
            strcat(parent_name, name);
            strcat(parent_name, "/");

            size_t fsize = get_file_size(&file, parent_name);
            unsigned char *bytes = malloc(sizeof(char) * fsize);
            get_bytes(&file, parent_name, bytes, fsize);
            
            calculate_checksum((void*) bytes, fsize, md);
            ChecksumMap checksumMap = .filename=file.relative_path;
            memcpy(checksumMap.checksum, md, 
            sizeof(checksumMap.checksum));

            free(bytes);
            
            
              
        
    
    closedir(dir);
    return NULL;



int main(int argc, char const *argv[]) 

  FILE *fp = fopen("mychecksums.txt", "w");
  char formatted_bytes[32*2+1];
  char *filename = checksumMaps[0].filename;
  format_bytes(formatted_bytes, checksumMaps[0].checksum);

  fputs(filename, fp);
  fputs(formatted_bytes, fp);

  // We print the value of `filename` again in order to see that it has been overwritten.
  printf("%s \n", filename);
  fclose(fp);



用 gcc 编译:

gcc -Wall -Wextra main.c ArchiveFile.c checksum/checksum.c -lcrypto

【问题讨论】:

free(checksumMaps); 然后format_bytes(formatted_bytes, checksumMaps[0].checksum); - 但它已被释放。您能否发布所有代码的完整单块,包括#includes?你在使用 Linux 吗?如果您是 -> 使用 -faddress=sanitize -Wall 编译。请发布所有代码 - 请发布minimal reproducible example,错误可以在listdirfilename 指向哪里? 【参考方案1】:

这里有一个错误:

char parent_name[strlen(name)+1]; 
memset(&parent_name[0], 0, sizeof(parent_name)); // could have been parent_name[0]='\0'; instead
strcat(parent_name, name); // Now the parent_name buffer is full and null terminated.
strcat(parent_name, "/"); // this overwrites the null terminator and writes a new one out-of-bounds

你应该这样做:

size_t length = strlen(name);
char parent_name[length+1+1];
memcpy(parent_name, name, length); // copies characters only (fast) but not the null term
parent_name[length]   = '/';  // append this single character 1 symbol past "name" string
parent_name[length+1] = '\0'; // add manual null termination

【讨论】:

【参考方案2】:

更新

代码似乎存在一些不同的问题。 首先要注意的是file.relative_path = direntry-&gt;d_name;,但是direntry 指向的值在每次迭代中都会发生变化,因此file.relative_path 指向的值也会发生变化。此外,存储在 file.relative_path 中的字符串的大小从未指定,如果我们使用 strcpy,这将是一个问题。

解决方案是为file.relative_path指定一个大小,并使用strcpy复制direntry-&gt;d_name的值。此外,不需要 checksumMap 结构,因为 ArchiveFile 已经可以存储相同的信息(再次指定校验和的大小)。

在 C 语言中使用字符串、缓冲区、数组时要记住的事项:

请记住,C 中的字符串基于 char 数组,它们本身基于指向第一个元素的指针。当您实际上想要复制字符串的值而不是第一个元素的地址时,将一个字符串的值分配给另一个字符串可能会返回意外行为。

【讨论】:

【参考方案3】:

程序将 aftxt 而不是 2.txt 写入标准输出。使用gdb,我可以看到在fputs(formatted_bytes, fp); 行之后filename 的值从2.txt 变为aftxt。这可能是什么原因?

很难说,因为我们处于 UB 领域(未定义的行为)。但这里有两个明显的候选人。

    formatted_bytes 未正确终止,导致 fputs 读取数组,调用 UB。

    fp 不是有效的流。原因可能是它没有被初始化,或者改变,或者流被关闭等等。

启用-Wall -Wextra -fsanitize=address。你也可以试试-fsanitize=undefined

检查所有返回值。 mallocfopenfputs 返回一个可用于错误检查的值。

formatted_bytes 替换为具有您认为的值的硬编码字符串。

了解如何创建Minimal, Reproducible Example 以及如何创建debug small c programs。这是我前段时间写的指南。

【讨论】:

以上是关于在 C 中写入文件会覆盖变量的主要内容,如果未能解决你的问题,请参考以下文章

在C语言中,fopen一个文件 如何能够在写入新的数据覆盖原文件中指定长度的内容

写入文本文件而不覆盖它

如何让python循环写入文档的内容不被后面写入的内容覆盖

覆盖文件时写入的扇区?

PHP 中如何在同一个文件中写入而不覆盖以前写的内容

c语言中怎样在文件写入时换行?