2014年7月20日 星期日

VS2013写的dll被内存加载失败原因一例

  在公司写的某dll终于完成了,和公司的壳一同使用时,公司的一种壳可以稳定的从内存加载使用我写的dll,而另一种时好时坏(大多数情况都无 法正常加载),在我来这里工作之前,这两个壳在内存加载他们使用低版本VS编译出的dll时都可以正常工作,我因为使用了一些C++11语法特性,实在不 想做降版这蛋疼事,调试分析了这其中的原因。
  经过一番调试,终于把前因后果捋顺了:
· ->DLLEntryPoint:走到_DllMainCRTStartup
· _DllMainCRTStartup->_CRT_INIT:初始化CRT环境
· _CRT_INIT->_cinit:这还真不知道是初始化啥
· 其中_cinit中有段代码是这样写的:
if (_FPinit != NULL &&
     _IsNonwritableInCurrentImage((PBYTE)&_FPinit))
{
      //这里代码具体记不清楚了,是调用了一个函数初始化浮点数运算相关环境
}

代码意思比较好懂,如果_FPinit不为零同时这个变量在当前映像中是可写入的就初始化浮点数运算环境。而问题就出在_IsNonwritableInCurrentImage的判断上了:
BOOL __cdecl _IsNonwritableInCurrentImage(char *pTarget)
{
  _IMAGE_SECTION_HEADER *pTargetSectoinHeader; 
  BOOL bResult = FALSE;
  if ( _ValidateImageBase((char *)0x10000000)
&& (pTargetSectoinHeader = 
  _FindPESection((char *)0x10000000, (unsigned int)(pTarget - 0x10000000))) != 0 )
  {
    bResult = ~(pTargetSectoinHeader->Characteristics >> 31) & 1;
  }
  return bResult;
}


这个函数先调用_ValidateImageBase判断了当前映像基址是否为合法的PE文件头,然后调用_FindPESection找出变量所在的区段的区段头,检查区段头中记录的该区段是否具有写入权限,返回结果。代码中写死的0x10000000地址都会在dll加载时系统通过重定位表将这些地址改写为映像基址。
但是在_ValidateImageBase判断头两个字节是否为MZ时程序就非法内存访问然后崩溃了。
经过了与维护壳的同事沟通确认,产生问题的壳在最初分析完我的dll之后就把PE头抹掉了,使用了一个自定义的结构体代替PE头记录了部分PE头内的信息供内存加载dll初始化时使用,在为我的dll代码申请内存空间时根本就没有留出PE头的空,当然更没有把PE头还原回去
内存空间就成了这样:
未分配的内存,本应为PE
DLL申请的内存
ImageBase
于是经过重定位表重定位后传递给_ValidateImageHeader的映像基址就指向了一处非法地址,最终产生了非法内存访问。在极其偶然的情况下该地址可能指向了另一块内存区域的一部分,指向的内容正好不是MZ开头,而这个dll又凑巧不需要初始化浮点数运算环境,才会出现偶尔可以正常启动的情况。
至此这个问题的前因后果已经搞明白,具体该如何解决还等和领导沟通后再定吧。
--8:28 PM 7/21/2014
解决方案:
方法一(最省事相对稳定):由于_IsNonwritableInCurrentImage总共就判断两个变量,所以可以根据自身程序情况写个代替函数写死了,修改工程设置人为规定EntryPoint,调用_CRT_INIT初始化之前修改内存权限把自己写的函数memcpy过去。
方法二(相对省事可能不稳定):修改工程设置人为规定EntryPoint,然后参照IDA F5VS安装目录下的crt源代码,写自己的_DllMainCRTStartup_CRT_INIT_cinit。不稳定因素有二:1.由于msvcrt.lib有两个obj都有某全局变量,所以实现_CRT_INIT时对着两个全局变量是没法初始化了2.可能正是由于不稳定因素1,调用初始化环境的函数会失败。
方法三(最稳定最费时):搞个自己的msvcrt.lib

沒有留言:

張貼留言