接上文【逆向工程】生成能够用dnSpy调试的mono-2.0-bdwgc.dll(一)
umpacther在src\umpatcher\umpatcher\V40\SourceCodePatcher.cs中,对拷贝后的代码进行修改,总共修改了7个文件,10处代码
public void Patch() {
Patch_mono_metadata_icall_c();
Patch_mono_mini_debugger_agent_c();
Patch_mono_mini_mini_runtime_c();
Add_mono_mini_dnSpy_c();
Patch_masm_fixed_props();
Patch_bdwgc_gc_atomic_ops_h();
Patch_bdwgc_gcconfig_h();
}
在此就不一一分析了,我们着重看一下导致crash的修改。将每处修改回滚之后发现只要回滚Patch_mono_mini_mini_runtime_c 函数就可以正常打开游戏,而这个函数的作用就是在mini-runtime.c中引入了dnSpy_debugger_init ()
void Patch_mono_mini_mini_runtime_c() {
var filename = Path.Combine(solutionOptions.UnityVersionDir, "mono", "mini", "mini-runtime.c");
var textFilePatcher = new TextFilePatcher(filename);
int index = textFilePatcher.GetIndexesOfLine(line => line.Text.Contains("CHECKED_MONO_INIT ();")).Single();
textFilePatcher.Insert(++index, string.Empty);
textFilePatcher.Insert(++index, "\tdnSpy_debugger_init ();");
textFilePatcher.Write();
}
也就是说真正的crash的地方在dnSpy_debugger_init中,因此检查dnSpy.c中的dnSpy_debugger_init 函数:
// 0 = .NET 2.0-3.5 Unity
// 1 = .NET 4.0+ Unity
#ifndef DNUNITYRT
#define DNUNITYRT 0
#endif
dnSpy_debugger_init ()
{
gboolean fixDefer = FALSE;
char* envVal = getenv (ENV_VAR_NAME);
#if DNUNITYRT != 0
if (!envVal) {
envVal = getenv (ENV_VAR_NAME_V0);
fixDefer = TRUE;
}
#endif
if (!envVal) {
envVal = "--debugger-agent=transport=dt_socket,server=y,address=127.0.0.1:55555,defer=y";
fixDefer = TRUE;
}
#if DNUNITYRT != 0
#define defer_y "defer=y"
#define suspend_n "suspend=n"
const char* s = strstr (envVal, defer_y);
if (s && fixDefer) {
int envVal_len = strlen (envVal);
int defer_y_len = strlen (defer_y);
int suspend_n_len = strlen (suspend_n);
int newStr_len = envVal_len - defer_y_len + suspend_n_len;
char* newStr = (char*)malloc (newStr_len + 1);
memcpy (newStr, envVal, s - envVal);
memcpy (newStr + (s - envVal), suspend_n, suspend_n_len);
memcpy (newStr + (s - envVal) + suspend_n_len, s + defer_y_len, envVal_len - (s + defer_y_len - envVal));
newStr [newStr_len] = 0;
envVal = newStr;
}
#endif
char* argv[] = { envVal };
mono_jit_parse_options (1, (char**)argv);
mono_debug_init (MONO_DEBUG_FORMAT_MONO);
}
经过一番排查后,发现最后一行 mono_debug_init (MONO_DEBUG_FORMAT_MONO); 是导致crash的罪魁祸首。看一下mono_debug_init函数:
mono_debug_init (MonoDebugFormat format)
{
g_assert (!mono_debug_initialized);
if (format == MONO_DEBUG_FORMAT_DEBUGGER)
g_error ("The mdb debugger is no longer supported.");
mono_debug_initialized = TRUE;
mono_debug_format = format;
...
}
注意这个 g_assert (!mono_debug_initialized);
通过输出log调试可以发现在调用dnSpy_debugger_init之前,mono_profiler_enable_coverage 已经调用了mono_debug_init ,因此第二次调用会导致assert。
mono_profiler_enable_coverage (void)
{
if (mono_profiler_state.startup_done)
return FALSE;
mono_os_mutex_init (&mono_profiler_state.coverage_mutex);
if (!mono_debug_enabled ())
mono_debug_init (MONO_DEBUG_FORMAT_MONO);
return mono_profiler_state.code_coverage = TRUE;
}
而mono_profiler_enable_coverage 的调用来源又是哪,在此就不深究了
总之注释掉dnSpy_debugger_init 中的mono_debug_init 就可以避免crash了,更稳妥的改法是将mono_debug_init中的 g_assert (!mono_debug_initialized); 改成:
mono_debug_init (MonoDebugFormat format)
{
//g_assert (!mono_debug_initialized);
if (mono_debug_initialized) return;
if (format == MONO_DEBUG_FORMAT_DEBUGGER)
g_error ("The mdb debugger is no longer supported.");
mono_debug_initialized = TRUE;
mono_debug_format = format;
...
}
为了之后的通用性,还可以修改src\umpatcher\umpatcher\V40\SourceCodePatcher.cs
public void Patch() {
...
Patch_mono_metadata_mono_debug_c();
}
void Patch_mono_metadata_mono_debug_c() {
if (solutionOptions.UnityVersion.CompareTo(new UnityVersion(2019, 1, 0, "-mbe")) < 0)
return;
var filename = Path.Combine(solutionOptions.UnityVersionDir, "mono", "metadata", "mono-debug.c");
var textFilePatcher = new TextFilePatcher(filename);
var lines = textFilePatcher.Lines;
int index = textFilePatcher.GetIndexOfLine("\tg_assert (!mono_debug_initialized);");
lines[index] = lines[index].Replace("\tif (mono_debug_initialized) return;");
textFilePatcher.Write();
}
这样对于2020版本unity应该都可以顺利生成mono-2.0-bdwgc.dll了。将生成的dll替换掉原本的文件,然后就可以成功在dnSpy中进行断点调试了。
此外,dnSpy_debugger_init也还有一定的优化空间
dnSpy_debugger_init中envVal分为两类,分别对应.NET 2.0-3.5版本和.NET 4.0+版本
因此可以以一种更清晰的方式对代码进行整合:
#define ENV_VAR_NAME_V0 "DNSPY_UNITY_DBG"
#define ENV_VAR_NAME_V1 "DNSPY_UNITY_DBG2"
dnSpy_debugger_init ()
{
#if DNUNITYRT == 0
char* envVal = getenv(ENV_VAR_NAME_V0);
if (!envVal) {
envVal = "--debugger-agent=transport=dt_socket,server=y,address=127.0.0.1:55555,defer=y";
}
#else
char* envVal = getenv(ENV_VAR_NAME_V1);
// if (!envVal) {
// envVal = getenv(ENV_VAR_NAME_V0);
//
//#define defer_y "defer=y"
//#define suspend_n "suspend=n"
//
// const char* s = strstr(envVal, defer_y);
// if (s) {
// int defer_pos = s - envVal;
// int envVal_len = strlen(envVal);
// int defer_y_len = strlen(defer_y);
// int suspend_n_len = strlen(suspend_n);
// int newStr_len = envVal_len - defer_y_len + suspend_n_len;
// char* newStr = (char*)malloc(newStr_len + 1);
// memcpy(newStr, envVal, defer_pos);
// memcpy(newStr + defer_pos, suspend_n, suspend_n_len);
// memcpy(newStr + defer_pos + suspend_n_len, s + defer_y_len, envVal_len - defer_pos - defer_y_len);
// newStr[newStr_len] = 0;
// envVal = newStr;
// }
// }
if (!envVal) {
envVal = "--debugger-agent=transport=dt_socket,server=y,address=127.0.0.1:55555,suspend=n";
}
#endif
char* argv[] = { envVal };
mono_jit_parse_options (1, (char**)argv);
mono_debug_init (MONO_DEBUG_FORMAT_MONO);
}
上面注释掉的一大段是为了适配老版本的dnSpy,在原本的代码中作用是将defer=y替换成suspend=n,如果使用最新的dnSpy版本可以直接无视
dnSpy对Unity有两种调试模式,一种是dnSpy启动
另一种是外部启动,然后dnSpy附加
如果是dnSpy启动的话,envVal的值为:
–debugger-agent=transport=dt_socket,server=y,address=127.0.0.1:36663,suspend=n
如果是dnSpy附加的话,envVal的值为:
–debugger-agent=transport=dt_socket,server=y,address=127.0.0.1:55555,suspend=n
可以看出唯一的区别就是地址端口,dnSpy启动是随机分配的,而附加的话是固定的