absl::Hash 的一个误用
近期在写 LingDong 的 PlantUML 插件时,一开始使用了 absl::Hash
来计算 PlantUML 输出的 svg 图内容的哈希值,作为输出文件名。
auto hash_value = absl::Hash<std::string>{}(absl::StrJoin(codeblock->lines, "\n"));
发现:即使文章中 plantuml 原始绘图代码没有变更,每次构建,都会产出新的 svg 文件(文件名是文件内容的哈希值,从文件名看都是新文件)。
一开始猜测 PlantUML 生成的 svg 图内容中暗含了什么生成时间点之类的字符串。
经单测分析:对于相同的原始字符串,一次单测运行中两次生成的哈希值是相同的,不同次单测运行中生成的哈希值不同。那么基本可以确定问题原因是 absl::Hash
哈希依赖于某个不确定的种子。分析源码会发现:
absl::Hash<std::string>{}(...)
会间接调用 MixingHashState::hash
,其实现为:
template <typename T, absl::enable_if_t<!IntegralFastPath<T>::value, int> = 0>
static size_t hash(const T& value) {
return static_cast<size_t>(combine(MixingHashState{}, value).state_);
}
其中 MixingHashState{}
调用私有的无参构造函数进行实例化:
MixingHashState() : state_(Seed()) {}
state_
成员变量 和 Seed()
成员函数的定义如下所示:
// Seed()
//
// A non-deterministic seed.
//
// The current purpose of this seed is to generate non-deterministic results
// and prevent having users depend on the particular hash values.
// It is not meant as a security feature right now, but it leaves the door
// open to upgrade it to a true per-process random seed. A true random seed
// costs more and we don't need to pay for that right now.
//
// On platforms with ASLR, we take advantage of it to make a per-process
// random value.
// See https://en.wikipedia.org/wiki/Address_space_layout_randomization
//
// On other platforms this is still going to be non-deterministic but most
// probably per-build and not per-process.
ABSL_ATTRIBUTE_ALWAYS_INLINE static uint64_t Seed() {
#if (!defined(__clang__) || __clang_major__ > 11) && \
(!defined(__apple_build_version__) || \
__apple_build_version__ >= 19558921) // Xcode 12
return static_cast<uint64_t>(reinterpret_cast<uintptr_t>(&kSeed));
#else
// Workaround the absence of
// https://github.com/llvm/llvm-project/commit/bc15bf66dcca76cc06fe71fca35b74dc4d521021.
return static_cast<uint64_t>(reinterpret_cast<uintptr_t>(kSeed));
#endif
}
static const void* const kSeed;
uint64_t state_;
而 kSeed
静态成员变量的初始化逻辑为 - 存储自身的地址:
ABSL_CONST_INIT const void* const MixingHashState::kSeed = &kSeed;
C++ 程序中,静态变量的地址在编译期可知(链接时确定),静态变量在程序启动时初始化(早于 main 执行)。
基于安全考虑,现代操作系统都会使用“地址空间配置随机加载(ASLR)” 这一机制,导致:同一程序中的同一静态变量,不同次运行或者在不同进程中,内存地址不同。
absl::Hash
基于这一机制来生成哈希的不确定种子值。Abseil 官方指南中其实也有一处不太显眼的说明:
NOTE: the hash codes computed by absl::Hash are not guaranteed to be stable across different runs of your program, or across different dynamically loaded libraries in your program.
这也就意味着:基于 absl::Hash
生成的哈希值以及 Abseil 中依赖哈希逻辑的容器不能持久化以后续使用。
Abseil 库中似乎也未提供方法以绕过这一机制。只好改成使用 C++ 标准库中的 std::hash
:
auto hash_value = std::hash<std::string>{}(absl::StrJoin(codeblock->lines, "\n"));
std::hash
生成的哈希值是确定性的,其实现基于 MurmurHash2 和 CityHash 哈希算法。