写读二进制文件,引发double free的core dump

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

功能测试用到curl工具传输二进制文件。于是想写个demo将应用中的结构体以二进制写至文件中,而这中间掉进“double free”的坑。深感“C/C++路漫漫修远兮,吾将上下而求索”。


一、测试代码

如下是:double_free.h

#include <memory>
#include <tr1/memory>

#define VERSION_LEN  8
#define ROLE_LEN     8
#define URI_LEN  32

class Cert
{
public:
	Cert(unsigned long long num)
	{
		printf("\nCert begin\n");	// 调试信息
		m_number = num;  

		m_version = new char[VERSION_LEN+1];
		m_role = new char[ROLE_LEN+1];
		m_uri = new char[URI_LEN+1];
				
		printf("m_number value: %llu\n", m_number);
		printf("m_version addr: %p\n", m_version);
		printf("   m_role addr: %p\n", m_role);
		printf("    m_uri addr: %p\n", m_uri);	

		// 置0
		memset(m_version, 0, VERSION_LEN + 1);
		memset(m_role, 0, ROLE_LEN + 1);
		memset(m_uri, 0, URI_LEN + 1);	
		printf("Cert end\n");		
	}
	~Cert()
	{
		printf("\n~Cert begin\n");
		printf("m_number value: %llu\n", m_number);
		printf("m_version addr: %p\n", m_version);
		printf("   m_role addr: %p\n", m_role);
		printf("    m_uri addr: %p\n", m_uri);		
		
		if (NULL!=m_version)
		{
			delete []m_version;
		}
		if (NULL!=m_role)
		{
			delete []m_role;
		}
		if (NULL!=m_uri)
		{
			delete []m_uri;
		}
		printf("~Cert end\n");
	}

public:
	unsigned long long  m_number;

	char  *m_version;
	char  *m_role;
	char  *m_uri;
};

typedef std::tr1::shared_ptr<Cert>	Cert_Ptr;

如下是:double_free.cpp

#include <iostream>
#include <string.h>
#include <stdio.h>
#include "double_free.h"

using namespace std;

int main()
{
	Cert_Ptr ptrCert = Cert_Ptr(new Cert(11111111));	// 智能指针 托管Cert对象指针
	memcpy(ptrCert->m_version, "2.0.1", VERSION_LEN);
	memcpy(ptrCert->m_role, "root", ROLE_LEN);
	memcpy(ptrCert->m_uri, "www.google.com.hk", URI_LEN);

	char filename[] = "cert.bin";
	FILE* fp = fopen(filename, "w+");
	if (fp == NULL)
	{
		printf("fopen %s failed.", filename);
		printf("\n");
	}
	else
	{
		fwrite(ptrCert.get(), sizeof(Cert), 1, fp);		// 写文件
		rewind(fp);
		
		Cert newCert(22222222);		// 局部变量	
		fread(&newCert, sizeof(Cert), 1, fp);		// 读文件
		
		printf("\nfread result: \n");	// 打印读取文件的结果
		printf("m_number[%llu] version[%d][%s] role[%d][%s] uri[%d][%s].", 
			newCert.m_number, 
			strlen(newCert.m_version), newCert.m_version,
			strlen(newCert.m_role), newCert.m_role,
			strlen(newCert.m_uri), newCert.m_uri);
		printf("\n");
		
		fclose(fp);
	}
		
	return 0;
}

如下是:编译脚本build.sh

#!/bin/bash
g++ -g double_free.cpp -o double_free -I. -L. -lpthread

二、代码剖析

1. 错误分析

执行demo程序,结果如下:我的天呐,double free!!!

[root@localhost test]$ ./double_free 
Cert begin
m_number value: 11111111
m_version addr: 0x1fec040
   m_role addr: 0x1fec060
    m_uri addr: 0x1fec080
Cert end

Cert begin
m_number value: 22222222
m_version addr: 0x1fec320
   m_role addr: 0x1fec340
    m_uri addr: 0x1fec360
Cert end

fread result: 
m_number[11111111] version[5][2.0.1] role[4][root] uri[17][www.google.com.hk].

~Cert begin
m_number value: 11111111
m_version addr: 0x1fec040
   m_role addr: 0x1fec060
    m_uri addr: 0x1fec080
~Cert end

~Cert begin
m_number value: 11111111
m_version addr: 0x1fec040
   m_role addr: 0x1fec060
    m_uri addr: 0x1fec080
*** Error in `./double_free': double free or corruption (fasttop): 0x0000000001fec080 ***

分析:

  1. 智能指针ptrCert托管的对象是new出来的,在堆上分配内存;else分支中的对象newCert是局部变量在栈上分配。而且在构造函数中printf字段值与地址,两次对象的字段分配的地址都是不一样的。怎么会double free呢?
  2. 在析构函数中printf字段值与地址,发现两次析构,字段值都是一样的,这是怎么回事儿呢?
  3. 查看当前目录,存在cert.bin,意味着fwrite写文件是成功的。使用Beyond Compare工具,以16进制打文件cert.bin,发现只有32字节。这与对象各个字段内容的总字节数不一致。那文件内容是什么呢?对比printf的指针字段,发现下图红框中的数据是内存地址。而且与ptrCert托管对象的指针字段值一致。
    在这里插入图片描述

结论:
由于文件记录的是ptrCert托管对象的指针字段的值,fread调用将局部变量newCert的指针字段赋值为ptrCert指针字段的值。在执行完else分支,newCert在栈上被释放,且调用析构函数对指针字段free释放内存。而在程序return前,智能指针ptrCert再次调用析构函数释放内存,由于newCert析构时已经对指针指向内存进行free,此时再次free,显然会出现double free。

2. 代码纠错

出现double free是由于fread读文件,而后释放局部变量触发。虽然去掉fread,程序不再core dump,但显然文件内容不符合期望。这是为什么呢?
分析如下:
fwrite以二进制形式将类实例或结构体写文件属于浅拷贝。为满足需求。只能采用深拷贝的方式写文件,思路:申请足够的内存buffer,将指针字段所指向的内容拷贝至buffer中,然后调用fwrite写文件。

#include <iostream>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include "double_free.h"

using namespace std;

int main()
{
	FILE* fp = NULL;
	char *buffer = NULL;
	
	do
	{
		// 1. 构造类对象,并给对象成员赋值
		Cert_Ptr ptrCert = Cert_Ptr(new Cert(11111111));
		if (ptrCert == NULL)
		{
			printf("new %d Bytes failed.\n", sizeof(Cert));
			break;
		}
		memcpy(ptrCert->m_version, "2.0.1", VERSION_LEN);
		memcpy(ptrCert->m_role, "root", ROLE_LEN);
		memcpy(ptrCert->m_uri, "www.google.com.hk", URI_LEN);
		
		// 2. 按照类成员待分配字节总数 申请内存
		int bufferLen = sizeof(unsigned long long) + VERSION_LEN + ROLE_LEN + URI_LEN;
		buffer = (char *)malloc(bufferLen);
		if (buffer == NULL)
		{
			printf("malloc %d Bytes failed.\n", bufferLen);
			break;
		}
		memset(buffer, 0, bufferLen);  // 内存块数据清0
		
		// 3. 将对象的各个成员值 写入buffer,深拷贝
		int offset = 0;
		memcpy(buffer, &ptrCert->m_number, sizeof(unsigned long long));		// 参数2是 拷贝源的地址,要对unsigned long long类型字段m_number取地址
		offset += sizeof(unsigned long long);
		
		memcpy(buffer+offset, ptrCert->m_version, strlen(ptrCert->m_version));
		offset += VERSION_LEN;
		
		memcpy(buffer+offset, ptrCert->m_role, strlen(ptrCert->m_role));
		offset += ROLE_LEN;

		// 字段m_uri实际字节数并不等于URI_LEN,按照URI_LEN长度拷贝最后字段m_uri,导致脏数据被拷贝至buffer
		// 因此,做如下修改: 
		// 均按字段实际所占字节数进行memcpy,但是偏移量offset按照各字段的最大字节数进行偏移,如此接收端按照各字段的固定最大字节数解析即可
		//memcpy(buffer+offset, ptrCert->m_uri, URI_LEN);
		memcpy(buffer+offset, ptrCert->m_uri, strlen(ptrCert->m_uri));	
		offset += URI_LEN;		
		printf("offset = %d. \n", offset);
		
		char filename[] = "cert.bin";
		fp = fopen(filename, "w+");
		if (fp == NULL)
		{
			printf("fopen %s failed.\n", filename);
			break;
		}
		
		fwrite(buffer, bufferLen, 1, fp);
	
	} while(false);
	
	free(buffer);		// free NULL指针是安全的
	buffer = NULL;

	if (fp != NULL)
	{
		fclose(fp);		// fclose NULL文件指针是不安全的,因此先判空
		fp = NULL;
	}
		
	return 0;
}

运行结果如下:

在这里插入图片描述

3. 错误疑惑

有三个指针字段,明明都存在double free,而每次gdb却总是显示在对第3个字段m_uri进行delete时发生double free;如下:

if (NULL!=m_uri)
{
	delete []m_uri;
}
(gdb) bt
#0  0x00007ffff70141d7 in raise () from /lib64/libc.so.6
#1  0x00007ffff70158c8 in abort () from /lib64/libc.so.6
#2  0x00007ffff7053f07 in __libc_message () from /lib64/libc.so.6
#3  0x00007ffff705b503 in _int_free () from /lib64/libc.so.6
#4  0x00000000004014bd in Cert::~Cert (this=0x604010, __in_chrg=<optimized out>) at double_free.h:50
#5  0x000000000040191a in std::tr1::_Sp_deleter<Cert>::operator() (this=0x6040c8, __p=0x604010) at /usr/include/c++/4.8.2/tr1/shared_ptr.h:285
#6  0x00000000004018bf in std::tr1::_Sp_counted_base_impl<Cert*, std::tr1::_Sp_deleter<Cert>, (__gnu_cxx::_Lock_policy)2>::_M_dispose (this=0x6040b0)
    at /usr/include/c++/4.8.2/tr1/shared_ptr.h:257
#7  0x000000000040160c in std::tr1::_Sp_counted_base<(__gnu_cxx::_Lock_policy)2>::_M_release (this=0x6040b0) at /usr/include/c++/4.8.2/tr1/shared_ptr.h:141
#8  0x000000000040154f in std::tr1::__shared_count<(__gnu_cxx::_Lock_policy)2>::~__shared_count (this=0x7fffffffe3e8, __in_chrg=<optimized out>)
    at /usr/include/c++/4.8.2/tr1/shared_ptr.h:341
#9  0x00000000004014e6 in std::tr1::__shared_ptr<Cert, (__gnu_cxx::_Lock_policy)2>::~__shared_ptr (this=0x7fffffffe3e0, __in_chrg=<optimized out>)
    at /usr/include/c++/4.8.2/tr1/shared_ptr.h:541
#10 0x0000000000401500 in std::tr1::shared_ptr<Cert>::~shared_ptr (this=0x7fffffffe3e0, __in_chrg=<optimized out>) at /usr/include/c++/4.8.2/tr1/shared_ptr.h:985
#11 0x000000000040119d in main () at double_free.cpp:44

难道是gdb的缘故,不得其解,暂时遗留


总结

结论:fwrite以整个类实例或结构体写文件,写入文件的是字段的值,而非字段指向的内容。尤其要注意类型为指针的字段。例子程序中,想当然地认为fwrite会将指针字段指向的内容写至文件,实际上fwrite只是将“指针字段的值,即内存地址”记录至文件。对于携带指针类型的类成员或结构体成员,fwrite写文件是浅拷贝。

想要以二进制形式查看二进制文件,可以使用操作系统自带的二进制编辑器或者命令行工具来实现。下面以 Windows 和 Linux 操作系统为例,介绍如何以二进制形式查看二进制文件。 在 Windows 操作系统中,可以使用自带的 hex 编辑器打开二进制文件。具体步骤如下: 1. 打开文件资源管理器,找到要查看的二进制文件。 2. 右键点击二进制文件,选择 "打开方式" -> "记事本"。 3. 在记事本中,点击 "文件" -> "另存为"。 4. 在 "另存为类型" 中选择 "所有文件",并将文件名后缀改为 ".hex"。 5. 保存文件后,再次右键点击文件,选择 "打开方式" -> "记事本"。 6. 在记事本中,点击 "格式" -> "十六进制",即可以二进制形式查看二进制文件。 在 Linux 操作系统中,可以使用 hexdump 命令或 xxd 命令来查看二进制文件。具体命令如下: ```bash # 使用 hexdump 命令查看二进制文件 hexdump -C filename.bin # 使用 xxd 命令查看二进制文件 xxd filename.bin ``` 执行以上命令后,会在终端中以十六进制格式显示二进制文件的内容。例如: ``` 00000000: 50 4b 03 04 0a 00 00 00 00 00 8c 21 2c 2f 00 00 PK........!,/.. 00000010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000020: 00 00 00 00 00 00 00 00 00 00 00 00 14 00 00 00 ................ ... ``` 需要注意的是,以上方式仅供参考,具体实现方式取决于具体的需求和使用的操作系统或命令行工具。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值