C++常见的三种内存破坏的场景和分析
比如某个变量整形,在程序中只可能初始化或者赋值为 1
或者2
, 但是在使用的时候却发现其为0
或者其他的情况。对于其他类型,比如字符串等,可能出现了一种出乎意料
的值!程序在堆上申请内存或者释放内存的时候,在内存充足的情况下,居然出现了堆错误。
1. 内存破坏之强制类型转换
#include
#include
class DemoClass
{
public:
DemoClass() : m_bInit(true), m_tRecordTime(0)
{
time((time_t *)(&m_tRecordTime));
};
void DoSomething()
{
if (m_bInit)
std::cout << "Do Task!" << std::endl;
}
private:
int m_tRecordTime;
bool m_bInit;
};
int main()
{
DemoClass testObj;
testObj.DoSomething();
return 0;
}
time_t time( time_t *destTime );
,在VC6
中time_t
默认是32位,而在VS2017
中默认是64位。早期程序以为32位中表达最大的时间是2038
年,那时候完全够用,但随着计算机本身的发展64位
逐渐成为主流time_t
在最新的编译器中也默认采用64位
,这样时间完全够用以亿年
为单位了,那时候计算机发展超出我们想象了。程序的问题所在m_tRecordTime
采用的是int
类型,默认为32位,那么其地址作为time_t time( time_t *destTime );
函数实参后,在VC6
中time_t
本身为32位自然也不会出错,但是在VS2017
中因为time_t
为64位,则time((time_t *)(&m_tRecordTime));
后写入了一个64位
的值。结合下图,看下这个对象的内存布局,m_bInit
的值将会被覆盖,而这里原先的m_bInit
的值为1
,被覆盖为0
,从而导致内存破坏,导致程序执行意想不到的结果。这里只是不输出,那在真实程序中,可能会导致某个逻辑错乱,发生严重的问题。
m_tRecordTime
定义为time_t
类型就可以了。在定义类型的时候,尽量和原始类型一致,比如这里的 time_t
有些程序员可能惯性的认为就是32位
,那就定义一个时间戳的时候就定义为int
了,而我们要做的应该是和原始类型匹配(也就是函数的输入类型),将其定义为time_t
,于此类似的还有size_t
等,这样可以避免未来在数据集变化或者做平台迁移的时候造成不必要的麻烦。在有一些复杂的场景的下,也许你不得不做类型转换,而这个时候就格外的需要注意或者了解清楚,转换带来的情况和后果,保持警惕,否则就可能是一个潜在的 bug
。这和开车一样,当你开车的时候如果看到前方车辆忽然产生一个不合常理的变道行为,首先要做的不是喷那辆车,而是集中注意力,看看是否更前方有障碍物或者事故放生,做出相应的反应。
2. 字符串拷贝溢出
#include
#define BUFER_SIZE_STR_1 5
#define BUFER_SIZE_STR_2 8
class DemoClass
{
public:
void DoSomething()
{
strcpy(m_str1, "Hi Coder!");
std::cout << m_str1 << std::endl;
std::cout << m_str2 << std::endl;
}
private:
char m_str1[BUFER_SIZE_STR_1] = { 0 };
char m_str2[BUFER_SIZE_STR_2] = { 0 };
};
int main()
{
DemoClass testObj;
testObj.DoSomething();
return 0;
}
errno_t strcpy_s(
char *dest,
rsize_t dest_size,
const char *src
);
3. 随机性的内存被修改
#include
#define BUFER_SIZE_STR_1 5
#define BUFER_SIZE_STR_2 8
class DemoClass
{
public:
void DoSomething()
{
strcpy_s(m_str2, BUFER_SIZE_STR_2, "Coder");
strcpy_s(m_str1, BUFER_SIZE_STR_1, "Test");
//Notice this line:
m_str1[BUFER_SIZE_STR_2 - 1] = '\0';
std::cout << m_str1 << std::endl;
std::cout << m_str2 << std::endl;
}
private:
char m_str1[BUFER_SIZE_STR_1] = { 0 };
char m_str2[BUFER_SIZE_STR_2] = { 0 };
};
int main()
{
DemoClass testObj;
testObj.DoSomething();
return 0;
}
#include
#define BUFER_SIZE_STR_1 5
#define BUFER_SIZE_STR_2 8
#define BUFFER_SIZE_UNUSED 100
class DemoClass
{
public:
void DoSomething()
{
strcpy_s(m_str2, BUFER_SIZE_STR_2, "Coder");
strcpy_s(m_str1, BUFER_SIZE_STR_1, "Test");
//Notice this line:
m_str1[BUFER_SIZE_STR_2 - 1] = '\0';
std::cout << m_str1 << std::endl;
std::cout << m_str2 << std::endl;
}
private:
char m_str1[BUFER_SIZE_STR_1] = { 0 };
char m_strUnused[BUFFER_SIZE_UNUSED] = { 0 };
char m_str2[BUFER_SIZE_STR_2] = { 0 };
};
int main()
{
DemoClass testObj;
testObj.DoSomething();
return 0;
}
0:000> bp ObjectMemberBufferOverFllow!main
*** WARNING: Unable to verify checksum for ObjectMemberBufferOverFllow.exe
0:000> g
Breakpoint 0 hit
eax=010964c0 ebx=00e66000 ecx=00000000 edx=00000000 esi=75aae0b0 edi=0109b390
eip=003a1700 esp=00defa00 ebp=00defa44 iopl=0 nv up ei pl nz na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206
ObjectMemberBufferOverFllow!main:
003a1700 55 push ebp
0:000> dv /t /v
00def984 class DemoClass testObj = class DemoClass
0:000> ba w1 00def984+7
0:000> g
0:000> k
# ChildEBP RetAddr
00 00def97c 003a1720 ObjectMemberBufferOverFllow!DemoClass::DoSomething+0x41 [......\strcpybufferoverflow.cpp @ 16]
01 00def9fc 003a1906 ObjectMemberBufferOverFllow!main+0x20 [......\strcpybufferoverflow.cpp @ 30]
02 (Inline) -------- ObjectMemberBufferOverFllow!invoke_main+0x1c [d:\agent\_work\3\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 78]
03 00defa44 75818494 ObjectMemberBufferOverFllow!__scrt_common_main_seh+0xfa [d:\agent\_work\3\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 288]
04 00defa58 770a40e8 KERNEL32!BaseThreadInitThunk+0x24
05 00defaa0 770a40b8 ntdll!__RtlUserThreadStart+0x2f
06 00defab0 00000000 ntdll!_RtlUserThreadStart+0x1b
总结
评论