Bug记录
问题是这样发生的:
为了将两个对象合并成一条记录,我将两个对象的int
型id合并成了一个long
型id,结果在解析这个long
型id时,得到的并不是我期望的结果。
public static void Splite(long key, out int a, out int b)
{
unsafe
{
int* p = (int*)&key;
a = *p++;
b = *p;
}
}
一切都看上去很正常,平时运行,打包安卓都很正常,直到我们打了iOS包,发现基于它的功能都失效了:返回去的值a与b,总是与预期的不一致(最终排查发现是b总返回一些神奇的值)。
为了验证问题,我们专门使用XCode打了Debug包,并且因为我们使用的是IL2CPP方案,所以专门深入IL2CPP对应代码中进行断点。神奇的事又发生了:一切都变好了。
问题排查
其实进行到这一步,已经可以大致推测出是因为XCode使用Clang进行Release打包,导致这段代码出现了问题。这里展示IL2CPP代码如下:
IL2CPP_EXTERN_C IL2CPP_METHOD_ATTR void Splite (int64_t ___key0, int32_t* ___a1, int32_t* ___b2, const RuntimeMethod* method)
{
int32_t* V_0 = NULL;
{
// int* p = (int*)&key;
V_0 = (int32_t*)((uintptr_t)(&___key0));
// a = *p++;
int32_t* L_0 = ___a1;
int32_t* L_1 = V_0;
int32_t* L_2 = L_1;
V_0 = ((int32_t*)il2cpp_codegen_add((intptr_t)L_2, 4));
int32_t L_3 = *((int32_t*)L_2);
*((int32_t*)L_0) = (int32_t)L_3;
// b = *p;
int32_t* L_4 = ___b2;
int32_t* L_5 = V_0;
int32_t L_6 = *((int32_t*)L_5);
*((int32_t*)L_4) = (int32_t)L_6;
// }
return;
}
}
观察这段代码,似乎找不到有什么会被Release优化导致___a1
与___b2
返回的值甚至指针与预期不符的情况。所以我又单独建立了一个纯C++的命令行工程,在CLion中使用CLang进行Release编译,以验证问题究竟出在哪里。
经过我本地的排查,一切的问题指向了这一句:
V_0 = ((int32_t*)il2cpp_codegen_add((intptr_t)L_2, 4));
如果我们把这一句直接替换为
V_0++;
那么无论Debug或Release编译,都可以得到我们预期的结果值。
其实根据经验,我们也可以猜得出,就是在V_0
修改之后,如果立即有一次调用,也可以修正这个问题。我试着在这里单纯加了一个输出语句将V_0
输出,计算结果也正确了。
本来还想通过反编译,看看这里的被Release之后究竟变成了什么,可碍于经验和经历,最终没有这么做,也希望有经验的小伙伴可以试着按这个思路继续下去,看看CLang在这里究竟对Release做了怎样的优化,才会导致出现了这个问题。
关于这个bug的建议
简单粗暴版:在C#中,不要用unsafe,不要用指针
谨小慎微版:unsafe实现的代码,最好单独拿出在CLang编译环境下测试验证,没问题再添加到工程中。
后续补充
其实我后来有把这个方法修改使其能够正常运行。
public static (int, int) Splite(long key)
{
unsafe
{
int* p = (int*)&key;
int a = *p++;
int b = *p;
return (a, b);
}
}
其实乍看之下,这与原来的方法变化并不大,但是IL2CPP却还是有一些差别的。
IL2CPP_EXTERN_C IL2CPP_METHOD_ATTR ValueTuple Splite (int64_t ___key0, const RuntimeMethod* method)
{
static bool s_Il2CppMethodInitialized;
if (!s_Il2CppMethodInitialized)
{
il2cpp_codegen_initialize_runtime_metadata((uintptr_t*)&ValueTuple_2__ctor_mF5D8FB18DBF2C4B2F879F8E8E12D8FB8FCDB5477_RuntimeMethod_var);
s_Il2CppMethodInitialized = true;
}
int32_t* V_0 = NULL;
int32_t V_1 = 0;
{
// int* p = (int*)&key;
V_0 = (int32_t*)((uintptr_t)(&___key0));
// int a = *p++;
int32_t* L_0 = V_0;
int32_t* L_1 = L_0;
V_0 = ((int32_t*)il2cpp_codegen_add((intptr_t)L_1, 4));
int32_t L_2 = *((int32_t*)L_1);
// int b = *p;
int32_t* L_3 = V_0;
int32_t L_4 = *((int32_t*)L_3);
V_1 = L_4;
// return (a, b);
int32_t L_5 = V_1;
ValueTuple_2_t973F7AB0EF5DD3619E518A966941F10D8098F52D L_6;
memset((&L_6), 0, sizeof(L_6));
ValueTuple_2__ctor_mF5D8FB18DBF2C4B2F879F8E8E12D8FB8FCDB5477((&L_6), L_2, L_5, /*hidden argument*/ValueTuple_2__ctor_mF5D8FB18DBF2C4B2F879F8E8E12D8FB8FCDB5477_RuntimeMethod_var);
return L_6;
}
}
寻找差别,似乎可以从IL入手,所以我又扒了它们的IL代码,发现虽然实现很相似,但它们的IL还是有细微差别。
它们的IL差别主要集中在这几句上:
a = *p++; // line1
b = *p; // line2
针对line1,原始版本的IL如下:
IL_0006: ldarg.1 // a
IL_0007: ldloc.0 // p
IL_0008: dup
IL_0009: ldc.i4.4
IL_000a: add
IL_000b: stloc.0 // p
IL_000c: ldind.i4
IL_000d: stind.i4
而新版本IL如下:
IL_0006: ldloc.0 // p
IL_0007: dup
IL_0008: ldc.i4.4
IL_0009: add
IL_000a: stloc.0 // p
IL_000b: ldind.i4
IL_000c: stloc.1 // a
针对line2,原始版本的IL如下:
IL_000e: ldarg.2 // b
IL_000f: ldloc.0 // p
IL_0010: ldind.i4
IL_0011: stind.i4
而新版本IL如下:
IL_000d: ldloc.0 // p
IL_000e: ldind.i4
IL_000f: stloc.2 // b