美文网首页
记录一个Unity中unsafe代码在iOS下的bug

记录一个Unity中unsafe代码在iOS下的bug

作者: 卅云川 | 来源:发表于2023-02-16 19:15 被阅读0次

    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
    

    相关文章

      网友评论

          本文标题:记录一个Unity中unsafe代码在iOS下的bug

          本文链接:https://www.haomeiwen.com/subject/fywpkdtx.html