我需要创建一个安全的、自清理的内存缓冲区,当被清除或超出范围时,它不会在内存中留下任何内容的痕迹。
我的示例代码显示了我的第一个实验 vectorc 和 std::vector 之间的比较。您可以在输出中看到,vectorc 在调用 clear() 时覆盖了它的内存,而 std::vector 的内容在 clear() 之后仍然可以访问。
我担心编译器可能会在某些情况下优化内存覆盖,如果在 clear() 或超出范围(析构函数)后不再访问内存。使用 volatile 是否有效,就像在 clear() 中注释掉的行一样? memset() 不喜欢 volatile void*,所以我不得不再次抛弃它。必须有更好的方法来做到这一点。
我知道增长向量和调整大小可能会留下内容,因为内存块可能会移动到不同的位置,但让我们忽略这一点以保持示例简短。
我访问 size() 后面的内存直到 capacity()。这可能会导致未定义的行为,或者我这样做可以吗?
对这个话题有什么想法吗?有更好的解决方案吗?
vectorc.cpp:
#include <vector>
#include <cstring>
#include <iostream>
template<typename T> class vectorc : public std::vector<T>
{
public:
~vectorc()
{
clear();
}
void clear()
{
// volatile T* ptr = std::vector<T>::data(); ???
T* ptr = std::vector<T>::data();
memset((void*)ptr, 0, std::vector<T>::capacity()); // May have been shrinked, so size may leave memory behind?!
std::vector<T>::clear();
}
};
void printmem(const std::vector<char>& v)
{
const char* ptr = v.data();
std::cout << "capacity=" << v.capacity() << ", size=" << v.size() << ", content=\"";
for(size_t i = 0; i < v.capacity(); ++i, ++ptr)
std::cout << *ptr;
std::cout << "\"" << std::endl;
}
int main()
{
std::cout << "start" << std::endl;
std::cout << "vectorc:" << std::endl;
{
vectorc<char> vs;
vs.push_back('s');
vs.push_back('e');
vs.push_back('c');
vs.push_back('r');
vs.push_back('e');
vs.push_back('t');
printmem(vs);
vs.clear();
printmem(vs);
}
// vs went out of scope and I want all it's allocated memory set to zero.
std::cout << "std::vector:" << std::endl;
{
std::vector<char> vp;
vp.push_back('p');
vp.push_back('u');
vp.push_back('b');
vp.push_back('l');
vp.push_back('i');
vp.push_back('c');
printmem(vp);
vp.clear();
printmem(vp);
}
std::cout << "end" << std::endl;
}
编译运行:
g++ vectorc.cpp
./a.out
输出:
start
vectorc:
capacity=8, size=6, content="secret"
capacity=8, size=0, content=""
std::vector:
capacity=8, size=6, content="public"
capacity=8, size=0, content="public"
end
由于搜索引擎似乎总是将这个问题放在安全擦除 std::vector 内存的结果之上,即使评论已经表明使用自定义分配器,如果有解决方案的答案也会很有帮助。
下面是一个使用自定义分配器的实现,该分配器通过在释放时提供安全的内存擦除来扩展标准 std::allocator 类。它实际上基于一篇比 OP 问题早 3 年的博客文章:https://blog.noser.com/securely-deallocate-a-stdvector-or-a-stdstring/
// Secure erazing of memory macro for Windows and non-Windows systems
#if defined(_WIN32)
#include <windows.h>
#define secure_zeromem(mem,size) do { RtlSecureZeroMemory (mem, size); } while (0)
#else
#define secure_zeromem(mem,size) do { volatile char *burnm = (volatile char *)(mem); int burnc = size; while (burnc--) *burnm++ = 0; } while (0)
#endif
template <class T>
class SecureAllocator
: public std::allocator<T>
{
public:
template<class U> struct rebind
{
typedef SecureAllocator<U> other;
};
SecureAllocator() throw()
: std::allocator<T>()
{
}
SecureAllocator(const SecureAllocator& other) throw()
: std::allocator<T>(other)
{
}
template <class U> SecureAllocator(const SecureAllocator<U>& other) throw()
: std::allocator<T>(other)
{
}
void deallocate(T* ptr, std::size_t n) {
if (ptr != nullptr && n > 0) {
secure_zeromem(ptr, n * sizeof(T));
}
std::allocator<T>::deallocate(ptr, n);
}
};
typedef std::vector<unsigned char, SecureAllocator<unsigned char> > SecureVector;
typedef std::basic_string<char, std::char_traits<char>, SecureAllocator<char> > SecureString;
首先,你不应该实现一个继承自标准容器的类,比如
std::vector
,因为它是许多错误的来源,比如内存泄漏。
std::vector
的析构函数和clear()
成员函数的标准实现不保证容器不留下其内容的痕迹,而是要求调用元素的析构函数(cppreference)。在C++17之前,所有的标准实现都是使用allocator的destroy()
成员函数来销毁元素,但是std::destroy_at()
的引入让几乎所有的标准实现都使用了它。因此,您不能使用实现其 destroy()
成员函数的自定义分配器,因为不能保证标准容器会在后台使用它。
正如@Mounir IDRASSI 所建议的那样,您不能使用带有 deallocate()
成员函数的自定义分配器,在释放内存之前清理内存,因为 clear()
成员函数不保证底层缓冲区被释放。
所以,你应该做两件不同的事情:实现你的矢量容器,可能在引擎盖下使用
std::vector
容器;使用标准容器,但将元素 T 包装在自定义类中,当调用析构函数时,可以根据需要清理内存。
如果你想避免编译器的优化,你总是可以使用
volatile
关键字。没有问题。