.net的OpenGL程序混淆后某些情况会崩溃

一个非常奇怪的bug,搞了好几天,终于被搞定了。先大致描述一下问题,我厂的产品的release版经过混淆,在build过程中,混淆版本也需要跑一次test case,test case中包括了渲染的测试,这个一直都没有问题,混淆后的版本能跑过,也能正常输出渲染的图片。

然后上周按公司内另外一个team的要求,给他们做个sample来描述下如何使用这个,这本来应该是一件不花时间的事情,把test case整理个GUI出来加点注释给他们就行,结果代码吭哧吭哧写完后,一调试,不好了,程序直接崩溃了,崩溃的错误出现过NullReferenceException也出过AccessViolationException,而且异常都没有调用堆栈。

最后按照“夹逼法”大致定位到问题出在OpenGL的函数调用上,首个OpenGL函数是glGetString,调用的时候出错,glGetError返回的是GL_INVALID_ENUM,而非混淆版本则调用成功,当时猜测是OpenGL 的 context有误,则将注意力转移到初始化Context的地方,对于非GUI渲染,我是创建了一个不可见的窗口,然后在这个窗口的DC上创建的OpenGL上下文,难道是初始化失败?但是所有的OpenGL扩展加载成功,说明上下文不正常,当时想是不是混淆版下创建的OpenGL不具有渲染能力只具有加载扩展的能力呢?为了验证想法,将原本的初始化流程改了,改成:
1)创建不可见窗口,在这窗口上创建OpenGL上下文以加载扩展
2)创建内存兼容DC,然后再这个DC上创建OpenGL context,所有的offline rendering都是基于这个DC

可是发现还是不对头,调试发现在wglChoosePixelFormatARB里失败了,难道混淆版下获取不到任何像素格式?这个不科学啊,操作系统怎知道我混淆了没有,而且混淆了我的程序也不管显卡驱动的事啊。

然后又写了一堆代码,dump出可用的像素格式,非混淆版能输出上百个,而混淆版一个都没有,这个时候我快崩溃了,这个太玄学了,不符合我唯物主义世界观。

后来把代码退回到之前的版本,继续跟踪glGetString,发现这个函数其实是调用正常,栈返回的时候出错了,我是怎么知道的呢?对于所有OpenGL的调用,我没有使用.net标准的P/Invoke,因为有一堆函数不是通过PE的导出表导出的,而是靠wglGetProcAddress导出的,所以为了使用那些函数,在.net里唯一直接的办法是通过Marshal类将wglGetProcAddress返回的函数指针包装成delegate,而对于OpenGL不太现实,因为要用到的函数太多了,所以我当时为了解决这个问题,我是自己写了个空的Attribute,然后给所有需要用到的OpenGL函数声明成extern,然后标记上那个Attribute,这样生成的目标文件里是不可以调用这些函数的,调用会出错,所以我在Post-build stage里执行了我自己写的一个工具,用来将标记了特殊Attribute的extern函数的函数体进行修改,利用MSIL的calli指令跳转到wglGetProcAddress返回的函数指针上,这样这么个调用过程几乎没有性能上的损耗,也不会给GC带来压力,为了调试OpenGL,我在calli前后弄了Precall/Postcall两组方法用来拦截调用,相当于实现了一个AOP,我发现Precall/postcall里glGetString是正常的,而在Postcall到调用glGetString的地方这里返回栈的时候就会出错。

所以我猜测是混淆器把栈损坏了导致的问题,然后立即写信给混淆器的作者,作者反应也很快马上给我回复,告诉我怎么禁用优化。今早继续试了下,禁用优化没有起到任何作用,依然还是昨天的问题,我想会不会是混淆器混淆的代码没有处理好MSIL的堆栈?然后我用ildasm对比了混淆前后的MSIL,也自己在纸上跟踪了下混淆后的MSIL代码对CLR虚拟堆栈操作,最后执行下来,堆栈是平衡的,也就是说生成的MSIL在理论上是不会对堆栈产生破坏的。最后无意中在一堆MSIL指令中发现了一个不一样的地方:

未混淆版本:

IL_001f: calli unmanaged stdcall native int(uint32)

混淆版本:

IL_001f: calli native int(uint32)

差别很明显,少了个unmanaged stdcall,为了验证是否是这个引起的问题,我写了个小C#程序,使用Mono.Cecil库,修改了DLL中所有的calli指令,将call site的调用约定从default改成了stdcall,保存成新的DLL后使用ildasm反编译对比了下,就正常了,这还不行,替换掉sample project里的DLL,再跑,就正常了。

现在在作者解决这个混淆器的bug之前,我只能把我写的工具加到build脚本里去修复混淆器的bug。这个bug基本上不会影响作者其他用户,因为C#编译器不会生成包含calli指令的代码,所以对于绝大部分人来说,是遇不到这个bug的。

那么之前为什么混淆版的DLL能跑过test case?我猜测这么两种情况

  1. 我笔记本是Intel/NVIDIA双显卡,可能两个进程使用的显卡不一样,而OpenGL实现使用的编译器对调用约定处理的不一样?感觉这个可能性不大
    2)release test和我们产品的DLL build的平台选的是Any CPU,然后跑test case的进程可能是build到特定架构(比如x86),在特定架构下.net的默认调用约定在ABI上和stdcall是等价的?我感觉这个可能性比较大
    既然问题解决了我就不研究是哪种原因导致能跑过test case了。

Last modified on 2016-07-12