在 C 中写入文件会覆盖变量
Posted
技术标签:
【中文标题】在 C 中写入文件会覆盖变量【英文标题】:Writing to file in C overwrites variables 【发布时间】:2022-01-17 16:32:07 【问题描述】:说明:
我创建了一个小程序,用于将文件的名称和校验和存储在结构中,用于目录中的每个文件。当使用 printf 将输出写入标准输出时,一切似乎都很好,但是如果我们使用 fputs
或 fprintf
写入文件,值会被覆盖,可能是因为某些缓冲区溢出?
带有打印的主输出。
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;
main.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);
- 但它已被释放。您能否发布所有代码的完整单块,包括#include
s?你在使用 Linux 吗?如果您是 -> 使用 -faddress=sanitize -Wall
编译。请发布所有代码 - 请发布minimal reproducible example,错误可以在listdir
。 filename
指向哪里?
【参考方案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->d_name;
,但是direntry 指向的值在每次迭代中都会发生变化,因此file.relative_path
指向的值也会发生变化。此外,存储在 file.relative_path 中的字符串的大小从未指定,如果我们使用 strcpy,这将是一个问题。
解决方案是为file.relative_path
指定一个大小,并使用strcpy
复制direntry->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
。
检查所有返回值。 malloc
、fopen
和 fputs
返回一个可用于错误检查的值。
将formatted_bytes
替换为具有您认为的值的硬编码字符串。
了解如何创建Minimal, Reproducible Example 以及如何创建debug small c programs。这是我前段时间写的指南。
【讨论】:
以上是关于在 C 中写入文件会覆盖变量的主要内容,如果未能解决你的问题,请参考以下文章