2025-07-03 12:53:49 +04:00

265 lines
11 KiB
C++

#include "il2cpp-config.h"
#include "il2cpp-class-internals.h"
#include "os/Environment.h"
#include "os/File.h"
#include "os/Image.h"
#include "os/Initialize.h"
#include "os/LibraryLoader.h"
#include "os/Locale.h"
#include "os/Path.h"
#include "utils/Collections.h"
#include "utils/PathUtils.h"
#include "utils/MemoryMappedFile.h"
#include "utils/Runtime.h"
#include "vm/Method.h"
#include "vm-utils/Debugger.h"
#include "vm-utils/DebugSymbolReader.h"
#include "vm-utils/MethodDefinitionKey.h"
#include "vm-utils/NativeSymbol.h"
#include <string>
#include <cstdlib>
namespace il2cpp
{
namespace utils
{
#if IL2CPP_ENABLE_NATIVE_STACKTRACES
static Il2CppMethodPointer MaskSpareBits(const Il2CppMethodPointer method)
{
return (Il2CppMethodPointer)((size_t)method & ~IL2CPP_POINTER_SPARE_BITS);
}
struct MethodInfoToMethodPointerConverter
{
Il2CppMethodPointer operator()(const MethodDefinitionKey& methodInfo) const
{
return MaskSpareBits(methodInfo.method);
}
};
typedef il2cpp::utils::collections::ArrayValueMap<Il2CppMethodPointer, MethodDefinitionKey, MethodInfoToMethodPointerConverter> NativeMethodMap;
static NativeMethodMap s_NativeMethods;
struct NativeSymbolMutator
{
void operator()(MethodDefinitionKey* method)
{
// So, when a function is marked as noreturn, some compilers emit a call to that function and then
// put the next function immediately after call instruction which means the return address on the stack
// will appear to point to the wrong function. This messes up our stack walking as we now are confused
// which method is actually on the stack. To work around this, we add "1" to each of the function addresses,
// so each function appears to start 1 byte later which means the address on the stack will appear as if
// it is pointing to function that called the no return function. This is okay because no function will
// ever return to the first byte of another function.
method->method = reinterpret_cast<Il2CppMethodPointer>(reinterpret_cast<intptr_t>(method->method) + 1);
}
};
void NativeSymbol::RegisterMethods(const std::vector<MethodDefinitionKey>& managedMethods)
{
s_NativeMethods.assign(managedMethods);
#if IL2CPP_MUTATE_METHOD_POINTERS
NativeSymbolMutator mutator;
s_NativeMethods.mutate(mutator);
#endif
}
static const int k_ProbableMaximumManagedMethodSizeInBytes = 5000;
static bool IsInstructionPointerProbablyInManagedMethod(Il2CppMethodPointer managedMethodStart, Il2CppMethodPointer instructionPointer)
{
if (std::abs((intptr_t)managedMethodStart - (intptr_t)instructionPointer) < k_ProbableMaximumManagedMethodSizeInBytes)
return true;
return false;
}
const VmMethod* NativeSymbol::GetMethodFromNativeSymbol(Il2CppMethodPointer nativeMethod)
{
// Address has to be above our base address
if ((void*)nativeMethod < il2cpp::os::Image::GetImageBase())
return NULL;
// And within the addresses we have our methods in.
if (il2cpp::os::Image::ManagedSectionExists())
{
if (!il2cpp::os::Image::IsInManagedSection((void*)nativeMethod))
return NULL;
}
// Get the first symbol greater than the one we want, because our instruction pointer
// probably won't be at the start of the method.
NativeMethodMap::iterator methodAfterNativeMethod = s_NativeMethods.upper_bound(nativeMethod);
// If method are all of the managed methods are in the same custom section of the binary, then assume we
// will find the proper method, so the end iterator means we found the last method in this list. If we
// don't have custom sections, then we may have actually not found the method. In that case, let's not
// return a method we are unsure of.
if (!il2cpp::os::Image::ManagedSectionExists())
{
if (methodAfterNativeMethod == s_NativeMethods.end())
return NULL;
if (!IsInstructionPointerProbablyInManagedMethod(methodAfterNativeMethod->method, nativeMethod))
return NULL;
}
// Go back one to get the method we actually want.
if (methodAfterNativeMethod != s_NativeMethods.begin())
methodAfterNativeMethod--;
return il2cpp::vm::MetadataCache::GetMethodInfoFromMethodHandle(methodAfterNativeMethod->methodHandle);
}
static void GetMethodDebugInfoWithSymbols(const MethodInfo* method, intptr_t methodPointer, int32_t size, Il2CppMethodDebugInfo& methodDebugInfo)
{
methodDebugInfo.methodPointer = (Il2CppMethodPointer)methodPointer;
methodDebugInfo.code_size = size;
// Get the source file name for the method in case usym file is available.
#if !IL2CPP_MONO_DEBUGGER
// Use usym file if debugging is not available
il2cpp::utils::SourceLocation sourceLocation;
if (il2cpp::utils::DebugSymbolReader::GetSourceLocation((void*)methodPointer, sourceLocation))
{
methodDebugInfo.file = sourceLocation.filePath;
methodDebugInfo.line = sourceLocation.lineNumber;
}
#else
const Il2CppSequencePoint* sequencePoint = il2cpp::utils::Debugger::GetSequenceFirstSequencePoint(method);
if (sequencePoint != nullptr)
{
const Il2CppSequencePointSourceFile* sequencePointSourceFile = method->klass->image->codeGenModule->debuggerMetadata->sequencePointSourceFiles + sequencePoint->sourceFileIndex;
methodDebugInfo.file = sequencePointSourceFile->file;
methodDebugInfo.line = sequencePoint->lineStart;
}
#endif
else
{
methodDebugInfo.file = NULL;
methodDebugInfo.line = 0;
}
}
#define IL2CPP_DEBUG_DUMP_NATIVE_SYMBOL_DATA 0
#if IL2CPP_DEBUG_DUMP_NATIVE_SYMBOL_DATA
static void DumpNativeSymbolData()
{
// You may want to change this to be a full path so it is easy to locate.
FILE* dumpFile = fopen("nativeMethodsData.txt", "w");
for (uint32_t i = 0; i < s_NativeMethods.size(); ++i)
{
const MethodDefinitionKey& methodKey = s_NativeMethods[i];
void* address = methodKey.method;
const MethodInfo* methodInfo = il2cpp::vm::MetadataCache::GetMethodInfoFromMethodHandle(methodKey.methodHandle);
if (methodInfo != nullptr)
fprintf(dumpFile, "%d\t[%p]:\t%s\n", i, address, vm::Method::GetFullName(methodInfo).c_str());
}
fclose(dumpFile);
}
#endif
void NativeSymbol::GetAllManagedMethodsWithDebugInfo(void(*func)(const MethodInfo* method, Il2CppMethodDebugInfo* methodDebugInfo, void* userData), void* userData)
{
#if IL2CPP_DEBUG_DUMP_NATIVE_SYMBOL_DATA
DumpNativeSymbolData();
#endif
// If we have no data, return as the logic below requires at least one method in the array.
if (s_NativeMethods.size() == 0)
return;
// Store the last image so we can detect boundaries between methods from different images.
const MethodInfo* prevMethod = nullptr;
intptr_t prevMethodAddress = 0;
size_t i = 0;
// Iterate over all native methods which are registered from imaged and generic instances
do
{
const MethodDefinitionKey& methodInfo = s_NativeMethods[i];
intptr_t methodAddress = (intptr_t)methodInfo.method;
// Skip unresolved methods
if (methodAddress != 0)
{
// Get the method info for the method handle.
// Note this will allocate memory for classes and methods.
const MethodInfo* method = il2cpp::vm::MetadataCache::GetMethodInfoFromMethodHandle(methodInfo.methodHandle);
// Report previous method with complete information
if (prevMethodAddress != 0 && prevMethod != nullptr)
{
int32_t codeSize = (int32_t)(methodAddress - prevMethodAddress);
// If the previous method is too far from the current one - we can have other native methods inbetween.
// Use heuristic to estimate the size of the method - we expect managed method to be less than
// k_ProbableMaximumManagedMethodSizeInBytes, but it still may happen that we have a native method
// in the range even between methods in the same image.
codeSize = std::min(k_ProbableMaximumManagedMethodSizeInBytes, codeSize);
Il2CppMethodDebugInfo methodDebugInfo;
GetMethodDebugInfoWithSymbols(prevMethod, prevMethodAddress, codeSize, methodDebugInfo);
func(prevMethod, &methodDebugInfo, userData);
}
prevMethod = method;
prevMethodAddress = methodAddress;
}
++i;
}
while (i < s_NativeMethods.size());
if (prevMethodAddress != 0 && prevMethod != nullptr)
{
// Report the last method using last method address.
int32_t codeSize;
// If there is a managed section we can use it to estimate the method size,
// otherwise we use a heuristic.
if (il2cpp::os::Image::ManagedSectionExists())
{
void *managedSectionStart, *managedSectionEnd;
il2cpp::os::Image::GetManagedSectionStartAndEnd(managedSectionStart, managedSectionEnd);
codeSize = std::min<int32_t>(k_ProbableMaximumManagedMethodSizeInBytes, (int32_t)((intptr_t)managedSectionEnd - prevMethodAddress));
}
else
{
codeSize = k_ProbableMaximumManagedMethodSizeInBytes;
}
Il2CppMethodDebugInfo methodDebugInfo;
GetMethodDebugInfoWithSymbols(prevMethod, prevMethodAddress, codeSize, methodDebugInfo);
func(prevMethod, &methodDebugInfo, userData);
}
}
bool NativeSymbol::GetMethodDebugInfo(const MethodInfo* method, Il2CppMethodDebugInfo* methodDebugInfo)
{
Il2CppMethodPointer nativeMethod = method->virtualMethodPointer;
if (il2cpp::os::Image::ManagedSectionExists())
{
if (!il2cpp::os::Image::IsInManagedSection((void*)nativeMethod))
return false;
}
if (methodDebugInfo != NULL)
{
methodDebugInfo->methodPointer = method->virtualMethodPointer;
methodDebugInfo->code_size = 0;
methodDebugInfo->file = NULL;
}
return true;
}
#endif
} /* namespace utils */
} /* namespace il2cpp */