VC++ .net 2005运行库解析

Origin缘由:
  最近在研究Perl的图形处理功能,跑到CPAN上面乱找了一通,发现了这个叫做Win32-GUI的库,拥有包括AxWindows、Constants、DIBitmap等图形库,相当的强大。使用的方法类似于Tk,很直观简洁,与Python上面的Tk类似。不过Perl的这些和Win32打交道的库都有一个问题,本质上它们都是被OLE生成,到运行期再加载DLL执行指令。

  为了使用Win32::GUI::DIBitmap库需要从源代码编译。打开readme.html,发现了这些。

  抱着试试看的心态打开VC7的命令行模式,由于我只需要DIBitmap库,所以我就干脆先编译这个模块。可是到了一半出现函数指针错误,出现在MID的定义部分,于是干脆把MIDIBitmap的声明都给注释了,再来一次。编译没有错了,可是总是无法链接。不甘心。

  我打开Win32-DIBitmap的c源代码文件一看,真是想马上晕倒 —— 我还从来没有看过这样写C代码的,真的,没有。贴一个函数。

 1XS(XS_Win32__GUI__DIBitmap_GetBPP); /* prototype to pass -Wmissing-prototypes */
 2XS(XS_Win32__GUI__DIBitmap_GetBPP)
 3{
 4    dXSARGS;
 5    if (items != 1)
 6    Perl_croak(aTHX_ "Usage: Win32::GUI::DIBitmap::GetBPP(dib)");
 7    {
 8    Win32__GUI__DIBitmap    dib;
 9    UINT    RETVAL;
10    dXSTARG;
11
12    if (sv_derived_from(ST(0), "Win32::GUI::DIBitmap")) {
13        IV tmp = SvIV((SV*)SvRV(ST(0)));
14        dib = INT2PTR(Win32__GUI__DIBitmap,tmp);
15    }

16    else
17        Perl_croak(aTHX_ "dib is not of type Win32::GUI::DIBitmap");
18#line 1320 "DIBitmap.xs"
19    RETVAL = FreeImage_GetBPP(dib);
20#line 1750 "DIBitmap.c"
21    XSprePUSH; PUSHi((IV)RETVAL);
22    }

23    XSRETURN(1);
24}

  注意到了末尾的那个#line 1320了么?这函数引用DIBitmap.xs中的声明,类似于内联IDL定义一样。

Bebind the truth真相背后:
  对比Visual Studio C++每一代,变化最大的莫过于编译器和库,尤其是从(事实上也是好几年前的东西)Visual Studio 2003开始,编译器对标准C++的支持提升了不少,编译速度也有了很大的提高。可是无论当应用层面的开发进化到了何种层面,标准C运行库(以下简称CRL)都是程序最最底层的实现。

  按照MSDN的说法在VC++ 2005中,C\C++运行库(比如MSVCM80.DLL)都储存在在Global Assembly Cache(Nearly WinSxS)。不过Martyn Lovell(Visual C++ Team Software Development Lead)在他的Blog上写了一片名为《Why does VC8 install libraries to WinSxS》文章详细的以一个设计者的角度解释为什么这样做。一些比较关键的章节如下:

  其实微软的目的很简单,安全。静态链接的原理就是编译器首先把所有的源文件编译成二进制Obj文件,然后查询源文件所使用的接口,然后把缺少的二进制代码补全,合并成一个可执行的EXE文件。静态库中的源代码和我们自己的程序是不可分割的整体。由此带来的情况是,万一静态库代码有了Bug,那么数以万计的程序就都有了一摸一样的Bug,这是非常不安全的。为了共享代码,我们早就使用了动态链接库。但是这样的话就存在DLL Hell问题,这个问题在Windows2000以前的Windows操作系统上相当的常见。事实上在《Improved Portability of Shared Libraris》一文中揭示,其实在开源Linux平台上出现这种问题的情况比微软Windows更多。因为Linux社区中有许多共享的库程序,许多爱好者开发的程序也都会引用这些库文件,于是乎出现了Dependency Hell,和DLL Hell造成的麻烦比起来有之过而无不及。由此从VC7以来,微软建议将CRT DLL放到程序的文件夹下面,而不是一味的放到臃肿庞大不堪的System(32)目录中去。

  但是微软主要从安全上考虑 —— 毕竟程序因为相应的DLL冲突而无法运行导致的麻烦要比骇客利用漏洞搞破坏的损失和麻烦要小的多,将“古老的”CRL(第一个SCRL出现于70年代,那个时候可没有多线程没有虚拟机)放到了适合它待的地方WinSxS中。WinSxS是微软从Windows XP开始引入的,原始的目的是为了让COM和共享DLL隔离开来,用%SystemRoot%\WinSxS来存放各个版本的运行库(你可以打开瞧瞧)。插一句,有个技术落后的蠕虫病毒的文件名就是WinSxS。

  可以说这样仅仅解决了一个极小的运行问题。我们有数以万计由VC6编译出来的程序(比如以前的超级解霸),当更新更精良的装备VC++ .net出世的时候,人们当然会开始迁移以前的工程到新的开发环境下。说实话对于VS 2005不兼容2003的sln文件我是相当的生气,虽然说通过修改XML也可以实现,不过这是个很麻烦的问题——不少开源工程同时提供了2个SLN文件。迁移的相关注意事项大家可以去Google,很多就不再复述。

  这里肯定会有朋友需要在VC7中使用VC6编译出来的静态库或者DLL,也许运气好编译可以通过,链接甚至也可以,可是有些程序在运行的时候却马上崩溃。这是因为,从VC7开始,微软下大功夫重构了以前的CRL\CppRL源代码。一直到现在的2005为止,STL的兼容性面貌已经有了很大的改观,而且越来越贴近ISO C++标准。带来的结果就是,STL的实现和VC6完全不同,堆栈的实现也完全不同,造成了许多地方都不再兼容。后者是关键。而我们有时候会使用“系出名门”的SGI STL时,需要链接多线程库,因为SGI的实现是线程安全的,微软的不是。而且冲突的真正窍门在于,VC6编译的静态库代码潜伏在VC7的程序中,运行时程序载入的是VC7的CRL,而不是那部分祸害代码熟悉的VC6的CRT,由此与VC7程序的其他部分发生了冲突。

Compiler Switch关于编译器开关:

  先复习一下编译器指令/MT和/MD。曾经看过一个人的问题,多线程情况下如何共享STL容器,我的解答是,根据你的情况来,最好使用相应的第三方多线程库PTHREAD实现跨平台或者是自己实现关键块代码,不能够简单的用全局对象来实现。大家可以参考《程序员2006合定版》的一篇文章使用C++ template实现的简单库。微软VC7 IDE默认开关是的是/MT,MSDN的说法是:

Causes your application to use the multithread, static version of the run-time library. Defines _MT and causes the compiler to place the library name LIBCMT.lib into the .obj file so that the linker will use LIBCMT.lib to resolve external symbols.

  如果你的程序希望使用多线程静态版本的运行库 —— 注意注意,你的程序使用多线程版本的库不代表你的程序就是多线程的。可是从定义_MT开关,编译器将在Obj文件中加入LIBCMT.LIB的引用记录用来解析外部函数变量符号。这个没有多少文章,简单的Hello World!而已。

  /MD,同样引用MSDN的说法,

Causes your application to use the multithread- and DLL-specific version of the run-time library. Defines _MT and _DLL and causes the compiler to place the library name MSVCRT.lib into the .obj file. 
Applications compiled with this option are statically linked to MSVCRT.lib. This library provides a layer of code that allows the linker to resolve external references. The actual working code is contained in MSVCR80.DLL, which must be available at run time to applications linked with MSVCRT.lib.
When /MD is used with _STATIC_CPPLIB defined (/D_STATIC_CPPLIB) it will cause the application to link with the static multithread Standard C++ Library (libcpmt.lib) instead of the dynamic version (msvcprt.lib) while still dynamically linking to the main CRT via msvcrt.lib.

  如果你的程序需要使用多线程而且是DLL版本指定的运行库(比如VC6生成的库需要MSVCR60.DLL),定义了_MT与_DLL(看出来了吧,MD = MT + DLL)后,程序将使用MSVCRT.DLL链接各个Obj文件。程序使用了这个开关都是静态链接到MSVCRT.LIB,真正的执行代码在MSVCR80.DLL中。而且如果你的代码中有使用C++标准库,那么也有两个版本可以链接,使用/D_STATIC_CPPLIB或者_STATIC_CPPLIB。意思就是,你可以分别选择你的C语言部分与C++代码部分是静态还是动态链接,至于如何识别那是编译器链接器的事情。

DLL and Thread\Process动态链接库与线程进程
  为了解释的更加清楚,让我们先弄清楚DLL和进程线程的关系。打开《Windows核心编程》,看下列节选,

  一旦D L L的文件映像被映射到调用进程的地址空间中, D L L的函数就可以供进程中运行的所有线程使用。实际上, D L L几乎将失去它作为D L L的全部特征。对于进程中的线程来说,D L L的代码和数据看上去就像恰巧是在进程的地址空间中的额外代码和数据一样。当一个线程调用D L L函数时,该D L L函数要查看线程的堆栈,以便检索它传递的参数,并将线程的堆栈用于它需要的任何局部变量。此外, D L L中函数的代码创建的任何对象均由调用线程所拥有,而D L L本身从来不拥有任何东西。

    单个地址空间是由一个可执行模块和若干个D L L模块组成的。这些模块中,有些可以链接到静态版本的C / C + +运行期库,有些可以链接到一个D L L版本的C / C + +运行期库,而有些模块(如果不是用C / C + +编写的话)则根本不需要C / C + +运行期库。许多开发人员经常会犯一个常见的错误,因为他们忘记了若干个C / C + +运行期库可以存在于单个地址空间中。

      D L L函数分配的内存块是由E X E的函数释放的吗?答案是可能的。如果E X E和D L L都链接到D L L的C / C + +运行期库,那么上面的代码将能够很好地运行。但是,如果两个模块中的一个或者两个都链接到静态C / C + +运行期库,那么对free函数的调用就会失败。我经常看到编程人员编写这样的代码,结果都失败了。
  
  一旦D L L的文件映像被映射到调用进程的地址空间中, D L L的函数就可以供进程中运行的所有线程使用。实际上, D L L几乎将失去它作为D L L的全部特征。对于进程中的线程来说,D L L的代码和数据看上去就像恰巧是在进程的地址空间中的额外代码和数据一样。当一个线程调用D L L函数时,该D L L函数要查看线程的堆栈,以便检索它传递的参数,并将线程的堆栈用于它需要的任何局部变量。此外, D L L中函数的代码创建的任何对象均由调用线程所拥有,而D L L本身从来不拥有任何东西。

  如你所知,可执行文件的全局变量和静态变量不能被同一个可执行文件的多个运行实例共享。Windows 98能够确保这一点,方法是在可执行文件被映射到进程的地址空间时为可执行文件的全局变量和静态变量分配相应的存储器。Windows 2000确保这一点的方法是使用第1 3章介绍的写入时拷贝(c o p y - o n - w r i t e)机制。D L L中的全局变量和静态变量的处理方法是完全相同的。当一个进程将D L L的映像文件映射到它的地址空间中去时,系统将同时创建全局数据变量和静态数据变量的实例。

  Jeffrey Richter所说的失败的原因就是:线程是进程任务(包括CPU)的执行单位,而进程是提供线程运行时所需要资源(内存以及内核对象)的容器。当一个个进程(单个地址空间)开始执行时,所需要的DLL都被映射到地址空间中。如果几个DLL链接到了相同的静态CRL,那么就有了多个实例。如果你的进程是单线程的——不过是使用了多个DLL中的函数,而且所有的DLL都是动态链接组合,我们自己就可以保证不出错,实际上也不会出错。可是如果引入的DLL中,创建这些DLL的时候统统使用了静态链接,那么结果就是,Crash。因为这时有若干个C\C++运行库在一个地址空间生存,很有可能对同样的内存区肆虐。最后,如果你的程序是多线程的,而且如果没有引入其他的DLL,那么你可以结束阅读,因为协调工作完全可以自己完成。

  简而言之:无论哪个模块和静态库链接,如果运行的时候都有自己的CRL STATE变量。这对于主进程来说是不需要的,因为主进程应该只要检测到了错误,就应该停止运行,而不是让错误发生在各个部件的CRL STATE中,无法捕获。所以说,编译时应该尽量把DLL与MSVCRT.LIB链接(也就是多线程与基于DLL库),而不是按照VC7默认的MT(多线程但是静态)来生成。

Final Solution最终解决方案
  当我们只生成一个PE(EXE或者DLL)或者LIB,编译后一般需要与MSVCRT.LIB(也就是MSVCRT.DLL的静态函数符号库)链接。如果不是一个编译器产生的文件,链接时就可能会出现兼容性问题,发生在诸如函数名、偏移等链接器打交道的方面。比如当你用VC6编译出来的Obj文件想用VC7的链接器和VC7的库组成程序文件,就会出现这种错误。微软的编译器和Borland编译器所产生的函数符号完全不同一样,微软的链接器也不能链接Borland编译器产生的库。假如你不想链接MSVCRT.LIB,那么你的程序执行时将依赖于MSVCR80.DLL而不是MSVCRT.DLL。

  MSVCRT.DLL被越来越当作系统本生的一个组建使用,它是一个Windows自己拥有的运行库,在以后将只作为系统库使用。MSVCR80.DLL看名字也晓得,是随着编译器来的C运行库。我觉得之所以会送来两个版本可能是Windows系统开发组和Visual Studio开发组对C运行库的不同态度决定的,不知道对不对。未来的方向也是相当的明显,随着Vista的逐渐普及,.net CLR将承担越来越多的工作。

  假如我们的工程中需要引入多个DLL或者EXE(多进程合作),那么根据你使用的VC版本不同,可能会有不止一个CRT被使用。很容易理解,假如有多个DLL中都需要打开文件进行操作,而打开文件的函数代码可能就会存在多份实例(静态链接)。上面已经说过。可是这取决于编译条件。比如,如果我们有多个CRL库使用/MD开关链接到单个CRT DLL,那么会出现冲突。这和我们把一个CRT链接到多个DLL中产生的问题一样 —— 都是多份代码操作一个实例,势必必然出现问题。但是现在,MSVCRT.DLL被重新命名为MSVCR80.DLL,程序,无论链接到MSVCRT.DLL还是MSVCR80.DLL,二进制都是完全一样的。现在是否可以这样说,VC2005的出现其实从开发者的角度就统一了Win32 API,无论你的程序是依赖于操作系统支持还是依赖于IDE提供的“高级”CRL,那么用VC2005编译后,本质上都一样。如果是从旧平台移植过来的多线程多进程工程,最好用VC2005,选择合适的开关(DLL链接)重新生成一次。
posted on 2007-03-02 22:48  Bo Schwarzstein  阅读(12255)  评论(8编辑  收藏  举报