提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
功能测试用到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 ***
分析:
- 智能指针ptrCert托管的对象是new出来的,在堆上分配内存;else分支中的对象newCert是局部变量在栈上分配。而且在构造函数中printf字段值与地址,两次对象的字段分配的地址都是不一样的。怎么会double free呢?
- 在析构函数中printf字段值与地址,发现两次析构,字段值都是一样的,这是怎么回事儿呢?
- 查看当前目录,存在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写文件是浅拷贝。