Commit 01bb7136 authored by Etienne Pierre-doray's avatar Etienne Pierre-doray Committed by Chromium LUCI CQ

[Clank SSM]: Inject module factory to ModuleCache.

This CL avoids expensive and repeating work of filling ModuleCache
with modules in NativeUnwinderAndroid::AddInitialModules.
Instead, ModuleCache::RegisterAuxiliaryModuleProvider is used
to register a provider that lazily creates non-elf modules when
needed.

To make that possible, this CL makes explicit new constraints on
Unwinder: a single ModuleCache is "associated" with a Unwinder for its
lifetime and ModuleCache must be outlive by any Unwinders it's
associated with.
A follow up CL will refactor the Unwinder interface to better fit these
constraints.

Change-Id: I05374ed8989061c81cde0ba09605b54e6b2309b9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2514743
Commit-Queue: Etienne Pierre-Doray <etiennep@chromium.org>
Reviewed-by: default avatarMike Wittman <wittman@chromium.org>
Cr-Commit-Position: refs/heads/master@{#842185}
parent 4b59436d
......@@ -31,15 +31,21 @@ struct ModuleAddressCompare {
} // namespace
ModuleCache::ModuleCache() = default;
ModuleCache::~ModuleCache() = default;
ModuleCache::~ModuleCache() {
DCHECK_EQ(auxiliary_module_provider_, nullptr);
}
const ModuleCache::Module* ModuleCache::GetModuleForAddress(uintptr_t address) {
if (const ModuleCache::Module* module = GetExistingModuleForAddress(address))
return module;
std::unique_ptr<const Module> new_module = CreateModuleForAddress(address);
if (!new_module && auxiliary_module_provider_)
new_module = auxiliary_module_provider_->TryCreateModuleForAddress(address);
if (!new_module)
return nullptr;
const auto result = native_modules_.insert(std::move(new_module));
// TODO(https://crbug.com/1131769): Reintroduce DCHECK(result.second) after
// fixing the issue that is causing it to fail.
......@@ -126,6 +132,18 @@ const ModuleCache::Module* ModuleCache::GetExistingModuleForAddress(
return nullptr;
}
void ModuleCache::RegisterAuxiliaryModuleProvider(
AuxiliaryModuleProvider* auxiliary_module_provider) {
DCHECK(!auxiliary_module_provider_);
auxiliary_module_provider_ = auxiliary_module_provider;
}
void ModuleCache::UnregisterAuxiliaryModuleProvider(
AuxiliaryModuleProvider* auxiliary_module_provider) {
DCHECK_EQ(auxiliary_module_provider_, auxiliary_module_provider);
auxiliary_module_provider_ = nullptr;
}
bool ModuleCache::ModuleAndAddressCompare::operator()(
const std::unique_ptr<const Module>& m1,
const std::unique_ptr<const Module>& m2) const {
......
......@@ -66,6 +66,21 @@ class BASE_EXPORT ModuleCache {
virtual bool IsNative() const = 0;
};
// Interface for lazily creating a native module for a given |address|. The
// provider is registered with RegisterAuxiliaryModuleProvider().
class AuxiliaryModuleProvider {
public:
AuxiliaryModuleProvider() = default;
AuxiliaryModuleProvider(const AuxiliaryModuleProvider&) = delete;
AuxiliaryModuleProvider& operator=(const AuxiliaryModuleProvider&) = delete;
virtual std::unique_ptr<const Module> TryCreateModuleForAddress(
uintptr_t address) = 0;
protected:
~AuxiliaryModuleProvider() = default;
};
ModuleCache();
~ModuleCache();
......@@ -103,6 +118,19 @@ class BASE_EXPORT ModuleCache {
// ModuleCache.
void AddCustomNativeModule(std::unique_ptr<const Module> module);
// Registers a custom module provider for lazily creating native modules. At
// most one provider can be registered at any time, and the provider must be
// unregistered before being destroyed. This is intended to support native
// modules that require custom handling. In general, native modules will be
// found and added automatically when invoking GetModuleForAddress(). If no
// module is found, this provider will be used as fallback.
void RegisterAuxiliaryModuleProvider(
AuxiliaryModuleProvider* auxiliary_module_provider);
// Unregisters the custom module provider.
void UnregisterAuxiliaryModuleProvider(
AuxiliaryModuleProvider* auxiliary_module_provider);
// Gets the module containing |address| if one already exists, or nullptr
// otherwise. The returned module remains owned by and has the same lifetime
// as the ModuleCache object.
......@@ -156,6 +184,9 @@ class BASE_EXPORT ModuleCache {
// because it can contain multiple modules that were loaded (then subsequently
// unloaded) at the same base address.
std::vector<std::unique_ptr<const Module>> inactive_non_native_modules_;
// Auxiliary module provider, for lazily creating native modules.
AuxiliaryModuleProvider* auxiliary_module_provider_ = nullptr;
};
} // namespace base
......
......@@ -411,5 +411,68 @@ TEST(ModuleCacheTest, CheckAgainstProcMaps) {
}
#endif
// Module provider that always return a fake module of size 1 for a given
// |address|.
class MockModuleProvider : public ModuleCache::AuxiliaryModuleProvider {
public:
explicit MockModuleProvider(size_t module_size = 1)
: module_size_(module_size) {}
std::unique_ptr<const ModuleCache::Module> TryCreateModuleForAddress(
uintptr_t address) override {
return std::make_unique<FakeModule>(address, module_size_);
}
private:
size_t module_size_;
};
// Check that auxiliary provider can inject new modules when registered.
TEST(ModuleCacheTest, RegisterAuxiliaryModuleProvider) {
ModuleCache cache;
EXPECT_EQ(nullptr, cache.GetModuleForAddress(1));
MockModuleProvider auxiliary_provider;
cache.RegisterAuxiliaryModuleProvider(&auxiliary_provider);
auto* module = cache.GetModuleForAddress(1);
EXPECT_NE(nullptr, module);
EXPECT_EQ(1U, module->GetBaseAddress());
cache.UnregisterAuxiliaryModuleProvider(&auxiliary_provider);
// Even when unregistered, the module remains in the cache.
EXPECT_EQ(module, cache.GetModuleForAddress(1));
}
// Check that ModuleCache's own module creator is used preferentially over
// auxiliary provider if possible.
MAYBE_TEST(ModuleCacheTest, NativeModuleOverAuxiliaryModuleProvider) {
ModuleCache cache;
MockModuleProvider auxiliary_provider(/*module_size=*/100);
cache.RegisterAuxiliaryModuleProvider(&auxiliary_provider);
const ModuleCache::Module* module =
cache.GetModuleForAddress(reinterpret_cast<uintptr_t>(&AFunctionForTest));
ASSERT_NE(nullptr, module);
// The module should be a native module, which will have size greater than 100
// bytes.
EXPECT_NE(100u, module->GetSize());
cache.UnregisterAuxiliaryModuleProvider(&auxiliary_provider);
}
// Check that auxiliary provider is no longer used after being unregistered.
TEST(ModuleCacheTest, UnregisterAuxiliaryModuleProvider) {
ModuleCache cache;
EXPECT_EQ(nullptr, cache.GetModuleForAddress(1));
MockModuleProvider auxiliary_provider;
cache.RegisterAuxiliaryModuleProvider(&auxiliary_provider);
cache.UnregisterAuxiliaryModuleProvider(&auxiliary_provider);
EXPECT_EQ(nullptr, cache.GetModuleForAddress(1));
}
} // namespace
} // namespace base
......@@ -127,10 +127,14 @@ NativeUnwinderAndroid::NativeUnwinderAndroid(
process_memory_(process_memory),
exclude_module_with_base_address_(exclude_module_with_base_address) {}
NativeUnwinderAndroid::~NativeUnwinderAndroid() = default;
NativeUnwinderAndroid::~NativeUnwinderAndroid() {
if (module_cache_)
module_cache_->UnregisterAuxiliaryModuleProvider(this);
}
void NativeUnwinderAndroid::AddInitialModules(ModuleCache* module_cache) {
AddInitialModulesFromMaps(*memory_regions_map_, module_cache);
void NativeUnwinderAndroid::InitializeModules(ModuleCache* module_cache) {
module_cache_ = module_cache;
module_cache_->RegisterAuxiliaryModuleProvider(this);
}
bool NativeUnwinderAndroid::CanUnwindFrom(const Frame& current_frame) const {
......@@ -215,34 +219,14 @@ UnwindResult NativeUnwinderAndroid::TryUnwind(RegisterContext* thread_context,
return UnwindResult::UNRECOGNIZED_FRAME;
}
// static
void NativeUnwinderAndroid::AddInitialModulesFromMaps(
const unwindstack::Maps& memory_regions_map,
ModuleCache* module_cache) {
// The effect of this loop is to create modules for the executable regions in
// the memory map. Regions composing a mapped ELF file are handled specially
// however: for just one module extending from the ELF base address to the
// *last* executable region backed by the file is implicitly created by
// ModuleCache. This avoids duplicate module instances covering the same
// in-memory module in the case that a module has multiple mmapped executable
// regions.
for (const std::unique_ptr<unwindstack::MapInfo>& region :
memory_regions_map) {
if (!(region->flags & PROT_EXEC))
continue;
// Use the standard ModuleCache POSIX module representation for ELF files.
// This call returns the containing ELF module for the region, creating it
// if it doesn't exist.
if (module_cache->GetModuleForAddress(
static_cast<uintptr_t>(region->start))) {
continue;
}
// Non-ELF modules are represented with NonElfModule.
module_cache->AddCustomNativeModule(
std::make_unique<NonElfModule>(region.get()));
std::unique_ptr<const ModuleCache::Module>
NativeUnwinderAndroid::TryCreateModuleForAddress(uintptr_t address) {
unwindstack::MapInfo* map_info = memory_regions_map_->Find(address);
if (map_info == nullptr || !(map_info->flags & PROT_EXEC) ||
map_info->flags & unwindstack::MAPS_FLAGS_DEVICE_MAP) {
return nullptr;
}
return std::make_unique<NonElfModule>(map_info);
}
void NativeUnwinderAndroid::EmitDexFrame(uintptr_t dex_pc,
......
......@@ -27,7 +27,8 @@ class UnwindStackMemoryAndroid : public unwindstack::Memory {
};
// Native unwinder implementation for Android, using libunwindstack.
class NativeUnwinderAndroid : public Unwinder {
class NativeUnwinderAndroid : public Unwinder,
public ModuleCache::AuxiliaryModuleProvider {
public:
// Creates maps object from /proc/self/maps for use by NativeUnwinderAndroid.
// Since this is an expensive call, the maps object should be re-used across
......@@ -35,8 +36,9 @@ class NativeUnwinderAndroid : public Unwinder {
static std::unique_ptr<unwindstack::Maps> CreateMaps();
static std::unique_ptr<unwindstack::Memory> CreateProcessMemory();
// |exclude_module_with_base_address| is used to exclude a specific module
// and let another unwinder take control. TryUnwind() will exit with
// |memory_regions_map| and |process_memory| must outlive this unwinder.
// |exclude_module_with_base_address| is used to exclude a specific module and
// let another unwinder take control. TryUnwind() will exit with
// UNRECOGNIZED_FRAME and CanUnwindFrom() will return false when a frame is
// encountered in that module.
NativeUnwinderAndroid(unwindstack::Maps* memory_regions_map,
......@@ -48,24 +50,25 @@ class NativeUnwinderAndroid : public Unwinder {
NativeUnwinderAndroid& operator=(const NativeUnwinderAndroid&) = delete;
// Unwinder
void AddInitialModules(ModuleCache* module_cache) override;
void InitializeModules(ModuleCache* module_cache) override;
bool CanUnwindFrom(const Frame& current_frame) const override;
UnwindResult TryUnwind(RegisterContext* thread_context,
uintptr_t stack_top,
ModuleCache* module_cache,
std::vector<Frame>* stack) const override;
// Adds modules found from executable loaded memory regions to |module_cache|.
// Public for test access.
static void AddInitialModulesFromMaps(
const unwindstack::Maps& memory_regions_map,
ModuleCache* module_cache);
// ModuleCache::AuxiliaryModuleProvider
std::unique_ptr<const ModuleCache::Module> TryCreateModuleForAddress(
uintptr_t address) override;
private:
void EmitDexFrame(uintptr_t dex_pc,
ModuleCache* module_cache,
std::vector<Frame>* stack) const;
// InitializeModules() registers self as an AuxiliaryModuleProvider. A pointer
// to the ModuleCache is saved to unregister self in destructor.
ModuleCache* module_cache_ = nullptr;
unwindstack::Maps* const memory_regions_map_;
unwindstack::Memory* const process_memory_;
const uintptr_t exclude_module_with_base_address_;
......
......@@ -92,17 +92,17 @@ void StackSamplerImpl::Initialize() {
std::make_move_iterator(unwinders.rend()));
for (const auto& unwinder : unwinders_)
unwinder->AddInitialModules(module_cache_);
unwinder->InitializeModules(module_cache_);
was_initialized_ = true;
}
void StackSamplerImpl::AddAuxUnwinder(std::unique_ptr<Unwinder> unwinder) {
// Initialize() invokes AddInitialModules() on the unwinders that are present
// Initialize() invokes InitializeModules() on the unwinders that are present
// at the time. If it hasn't occurred yet, we allow it to add the initial
// modules, otherwise we do it here.
if (was_initialized_)
unwinder->AddInitialModules(module_cache_);
unwinder->InitializeModules(module_cache_);
unwinders_.push_front(std::move(unwinder));
}
......
......@@ -126,8 +126,8 @@ class StackSamplingProfiler::SamplingThread : public Thread {
: collection_id(next_collection_id.GetNext()),
params(params),
finished(finished),
sampler(std::move(sampler)),
profile_builder(std::move(profile_builder)) {}
profile_builder(std::move(profile_builder)),
sampler(std::move(sampler)) {}
~CollectionContext() = default;
// An identifier for this collection, used to uniquely identify the
......@@ -137,12 +137,12 @@ class StackSamplingProfiler::SamplingThread : public Thread {
const SamplingParams params; // Information about how to sample.
WaitableEvent* const finished; // Signaled when all sampling complete.
// Platform-specific module that does the actual sampling.
std::unique_ptr<StackSampler> sampler;
// Receives the sampling data and builds a CallStackProfile.
std::unique_ptr<ProfileBuilder> profile_builder;
// Platform-specific module that does the actual sampling.
std::unique_ptr<StackSampler> sampler;
// The absolute time for the next sample.
TimeTicks next_sample_time;
......@@ -217,7 +217,7 @@ class StackSamplingProfiler::SamplingThread : public Thread {
// signalled. The |collection| should already have been removed from
// |active_collections_| by the caller, as this is needed to avoid flakiness
// in unit tests.
void FinishCollection(CollectionContext* collection);
void FinishCollection(std::unique_ptr<CollectionContext> collection);
// Check if the sampling thread is idle and begin a shutdown if it is.
void ScheduleShutdownIfIdle();
......@@ -506,7 +506,7 @@ StackSamplingProfiler::SamplingThread::GetTaskRunnerOnSamplingThread() {
}
void StackSamplingProfiler::SamplingThread::FinishCollection(
CollectionContext* collection) {
std::unique_ptr<CollectionContext> collection) {
DCHECK_EQ(GetThreadId(), PlatformThread::CurrentId());
DCHECK_EQ(0u, active_collections_.count(collection->collection_id));
......@@ -518,7 +518,11 @@ void StackSamplingProfiler::SamplingThread::FinishCollection(
profile_duration, collection->params.sampling_interval);
// Signal that this collection is finished.
collection->finished->Signal();
WaitableEvent* collection_finished = collection->finished;
// Ensure the collection is destroyed before signaling, so that it may
// not outlive StackSamplingProfiler.
collection.reset();
collection_finished->Signal();
ScheduleShutdownIfIdle();
}
......@@ -612,7 +616,7 @@ void StackSamplingProfiler::SamplingThread::RemoveCollectionTask(
size_t count = active_collections_.erase(collection_id);
DCHECK_EQ(1U, count);
FinishCollection(collection.get());
FinishCollection(std::move(collection));
}
void StackSamplingProfiler::SamplingThread::RecordSampleTask(
......@@ -658,7 +662,7 @@ void StackSamplingProfiler::SamplingThread::RecordSampleTask(
DCHECK_EQ(1U, count);
// All capturing has completed so finish the collection.
FinishCollection(collection);
FinishCollection(std::move(owned_collection));
}
void StackSamplingProfiler::SamplingThread::ShutdownTask(int add_events) {
......
......@@ -36,9 +36,12 @@ class Unwinder {
public:
virtual ~Unwinder() = default;
// Invoked to allow the unwinder to add any modules it recognizes to the
// ModuleCache.
virtual void AddInitialModules(ModuleCache* module_cache) {}
// Invoked to allow the unwinder to add any modules it recognizes or register
// a module factory to the ModuleCache. This associates this Unwinder with
// |module_cache| for the remaining of its lifetime, which is reused in
// subsequent methods UpdateModules() and TryUnwinder(). Thus, |module_cache|
// must outlive this Unwinder.
virtual void InitializeModules(ModuleCache* module_cache) {}
// Invoked at the time the stack is captured. IMPORTANT NOTE: this function is
// invoked while the target thread is suspended. To avoid deadlock it must not
......
......@@ -85,7 +85,7 @@ V8Unwinder::V8Unwinder(v8::Isolate* isolate)
V8Unwinder::~V8Unwinder() = default;
void V8Unwinder::AddInitialModules(base::ModuleCache* module_cache) {
void V8Unwinder::InitializeModules(base::ModuleCache* module_cache) {
// This function must be called only once.
DCHECK(modules_.empty());
......@@ -109,7 +109,7 @@ void V8Unwinder::OnStackCapture() {
}
// Update the modules based on what was recorded in |code_ranges_|. The singular
// embedded code range was already added in in AddInitialModules(). It is
// embedded code range was already added in in InitializeModules(). It is
// preserved by the algorithm below, which is why kNonEmbedded is
// unconditionally passed when creating new modules.
void V8Unwinder::UpdateModules(base::ModuleCache* module_cache) {
......
......@@ -22,7 +22,7 @@ class V8Unwinder : public base::Unwinder {
V8Unwinder& operator=(const V8Unwinder&) = delete;
// Unwinder:
void AddInitialModules(base::ModuleCache* module_cache) override;
void InitializeModules(base::ModuleCache* module_cache) override;
void OnStackCapture() override;
void UpdateModules(base::ModuleCache* module_cache) override;
bool CanUnwindFrom(const base::Frame& current_frame) const override;
......
......@@ -222,7 +222,7 @@ TEST(V8UnwinderTest, EmbeddedCodeRangeModule) {
V8Unwinder unwinder(v8_environment.isolate());
base::ModuleCache module_cache;
unwinder.AddInitialModules(&module_cache);
unwinder.InitializeModules(&module_cache);
v8::MemoryRange embedded_code_range;
v8_environment.isolate()->GetEmbeddedCodeRange(
......@@ -239,7 +239,7 @@ TEST(V8UnwinderTest, EmbeddedCodeRangeModulePreservedOnUpdate) {
UpdateModulesTestUnwinder unwinder(v8_environment.isolate());
base::ModuleCache module_cache;
unwinder.AddInitialModules(&module_cache);
unwinder.InitializeModules(&module_cache);
unwinder.SetCodePages({{reinterpret_cast<void*>(1), 10},
GetEmbeddedCodeRange(v8_environment.isolate())});
......@@ -264,7 +264,7 @@ TEST(V8UnwinderTest, EmbeddedCodeRangeModulePreservedOnOverCapacityUpdate) {
UpdateModulesTestUnwinder unwinder(v8_environment.isolate());
base::ModuleCache module_cache;
unwinder.AddInitialModules(&module_cache);
unwinder.InitializeModules(&module_cache);
const int kDefaultCapacity = v8::Isolate::kMinCodePagesBufferSize;
std::vector<v8::MemoryRange> code_pages;
......@@ -291,7 +291,7 @@ TEST(V8UnwinderTest, UpdateModules_ModuleAdded) {
UpdateModulesTestUnwinder unwinder(v8_environment.isolate());
base::ModuleCache module_cache;
unwinder.AddInitialModules(&module_cache);
unwinder.InitializeModules(&module_cache);
unwinder.SetCodePages({{reinterpret_cast<void*>(1), 10},
GetEmbeddedCodeRange(v8_environment.isolate())});
unwinder.OnStackCapture();
......@@ -312,7 +312,7 @@ TEST(V8UnwinderTest, UpdateModules_ModuleAddedBeforeLast) {
UpdateModulesTestUnwinder unwinder(v8_environment.isolate());
base::ModuleCache module_cache;
unwinder.AddInitialModules(&module_cache);
unwinder.InitializeModules(&module_cache);
unwinder.SetCodePages({{reinterpret_cast<void*>(100), 10},
GetEmbeddedCodeRange(v8_environment.isolate())});
......@@ -338,7 +338,7 @@ TEST(V8UnwinderTest, UpdateModules_ModuleRetained) {
UpdateModulesTestUnwinder unwinder(v8_environment.isolate());
base::ModuleCache module_cache;
unwinder.AddInitialModules(&module_cache);
unwinder.InitializeModules(&module_cache);
unwinder.SetCodePages({{reinterpret_cast<void*>(1), 10},
GetEmbeddedCodeRange(v8_environment.isolate())});
......@@ -362,7 +362,7 @@ TEST(V8UnwinderTest, UpdateModules_ModuleRetainedWithDifferentSize) {
UpdateModulesTestUnwinder unwinder(v8_environment.isolate());
base::ModuleCache module_cache;
unwinder.AddInitialModules(&module_cache);
unwinder.InitializeModules(&module_cache);
unwinder.SetCodePages({{reinterpret_cast<void*>(1), 10},
GetEmbeddedCodeRange(v8_environment.isolate())});
......@@ -387,7 +387,7 @@ TEST(V8UnwinderTest, UpdateModules_ModuleRemoved) {
UpdateModulesTestUnwinder unwinder(v8_environment.isolate());
base::ModuleCache module_cache;
unwinder.AddInitialModules(&module_cache);
unwinder.InitializeModules(&module_cache);
unwinder.SetCodePages({{{reinterpret_cast<void*>(1), 10},
GetEmbeddedCodeRange(v8_environment.isolate())}});
......@@ -408,7 +408,7 @@ TEST(V8UnwinderTest, UpdateModules_ModuleRemovedBeforeLast) {
UpdateModulesTestUnwinder unwinder(v8_environment.isolate());
base::ModuleCache module_cache;
unwinder.AddInitialModules(&module_cache);
unwinder.InitializeModules(&module_cache);
unwinder.SetCodePages({{{reinterpret_cast<void*>(1), 10},
{reinterpret_cast<void*>(100), 10},
......@@ -429,7 +429,7 @@ TEST(V8UnwinderTest, UpdateModules_CapacityExceeded) {
UpdateModulesTestUnwinder unwinder(v8_environment.isolate());
base::ModuleCache module_cache;
unwinder.AddInitialModules(&module_cache);
unwinder.InitializeModules(&module_cache);
const int kDefaultCapacity = v8::Isolate::kMinCodePagesBufferSize;
......@@ -466,7 +466,7 @@ TEST(V8UnwinderTest, UpdateModules_CapacitySubstantiallyExceeded) {
UpdateModulesTestUnwinder unwinder(v8_environment.isolate());
base::ModuleCache module_cache;
unwinder.AddInitialModules(&module_cache);
unwinder.InitializeModules(&module_cache);
const int kDefaultCapacity = v8::Isolate::kMinCodePagesBufferSize;
const int kCodePages = kDefaultCapacity * 3;
......@@ -500,7 +500,7 @@ TEST(V8UnwinderTest, CanUnwindFrom_V8Module) {
UpdateModulesTestUnwinder unwinder(v8_environment.isolate());
base::ModuleCache module_cache;
unwinder.AddInitialModules(&module_cache);
unwinder.InitializeModules(&module_cache);
unwinder.SetCodePages({{reinterpret_cast<void*>(1), 10},
GetEmbeddedCodeRange(v8_environment.isolate())});
......@@ -518,7 +518,7 @@ TEST(V8UnwinderTest, CanUnwindFrom_OtherModule) {
UpdateModulesTestUnwinder unwinder(v8_environment.isolate());
base::ModuleCache module_cache;
unwinder.AddInitialModules(&module_cache);
unwinder.InitializeModules(&module_cache);
unwinder.SetCodePages({GetEmbeddedCodeRange(v8_environment.isolate())});
unwinder.OnStackCapture();
......@@ -536,7 +536,7 @@ TEST(V8UnwinderTest, CanUnwindFrom_NullModule) {
UpdateModulesTestUnwinder unwinder(v8_environment.isolate());
base::ModuleCache module_cache;
unwinder.AddInitialModules(&module_cache);
unwinder.InitializeModules(&module_cache);
// Insert a non-native module to potentially exercise the Module comparator.
unwinder.SetCodePages({{reinterpret_cast<void*>(1), 10},
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment