понедельник, 30 января 2012 г.

WinSxS


The Problem

At work, we recently upgraded to Visual C++ 2005 Service Pack 1 (SP1). At the same time, our application stopped working for some users.
Everybody that had SP1 was able to run the application. Some people *without* SP1 were also able to run the application.
My system (not SP1) could not run the application built by the newly upgraded SP1 build machines, so I tried to figure out why.  I used Dependency Walker to find out why my application would not load.
Dependency Walker showed that the C runtime library (msvcr80.dll) could not be found. I noticed the full path of where the application was trying to get msvcr80.dll was:
C:\Windows\winsxs\x86_microsoft.vc80.crt_1fc8b3b9a1e18e3b_
  8.0.50727.
762_none_24c8a196583ff03b\msvcr80.dll
I searched my system and I *did* have msvcr80.dll in my \windows folder, but not in the path the application was using.
I had not seen a path like that before for a DLL. I had no idea why the application was looking there instead of in \Windows\system32 like it normally would.
I tried copying msvcr80.dll locally to the application folder and refreshed Dependency Walker. That had no effect...the application could not find msvcr80.dll.
That used be the failsafe way to make an application see a DLL...put it in the same directory as the application. Something was very different now.


The Cause

Something had changed. It was Microsoft's answer to DLL Hell that was introduced with Windows XP: side-by-side assemblies (winsxs).
Visual C++ 2005 is the first version of Visual C++ that uses side-by-side assemblies for the C/C++ runtime libraries.
The definition of an assembly is not straight forward. For the purposes of this discussion, I'm going to consider an assembly the collection of DLL's that make up the C runtime library.
Side-by-side assemblies changes where a DLL is stored for global access. Instead of using C:\Windows\System32, they are stored in the side-by-side directory, C:\Windows\winsxs.
The advantage of winsxs over system32 is that you can have more than one version of a particular DLL with the same name. With system32, you had to overwrite a DLL if you wanted to add a new version with the same name. Winsxs uses directories with version information to isolate DLL's from each other.
image
In the above picture I highlighted 4 directories. These 4 directories contain 4 versions of the C runtime DLL's. Each directory contains the same three C runtime DLL's:
  • msvcr80.dll: C runtime library
  • msvcm80.dll: C runtime library for managed code (i.e. .NET support)
  • msvcp80.dll: C++ runtime library
All of these DLL's end in 80 because they are from Visual C++ 8.0 (a.k.a. Visual C++ 2005). The prefix is from MicroSoft Visual C (msvc).
The directory name is used when an application wants to load a particular version of the C runtime library. The directory name contains various pieces of information about the DLL's inside. Each piece of information is separated by an underscore. For example, consider this directory:
x86_microsoft.vc80.crt_1fc8b3b9a1e18e3b_
  8.0.50727.762_none_10b2f55f9bffb8f8
  • x86 (processor architecture): Built for x86 (32-bit Intel Processor and compatibles)
  • microsoft.vc80.crt (name): Microsoft Visual C 8.0 C RunTime
  • 1fc8b3b9a1e18e3b (public key token): A unique ID for C Runtime
  • 8.0.50727.762 (version): Version of the DLL's (right click on the DLL, select "Properties", and then the "Details" tab, and you should see this number)
  • none (language): The language these DLL's are designed for
  • 10b2f55f9bffb8f8: Not sure, but I assume this is a unique ID for this particular version of the DLL's
So how does an application know what version of a DLL it needs to load? That is where a "manifest" comes into play. A manifest is an XML file that contains information used to create the directory in C:\Windows\winsxs so that a particular DLL can be loaded. If you write an application that uses the C runtime library (99% of the time you will), then you are going to need a manifest.
Here is the manifest for an application designed to work with the above C runtime (auto-generated by Visual C++):

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <dependency>
    <dependentAssembly>
      <assemblyIdentity
        type="win32"
        name="Microsoft.VC80.CRT"
        version="8.0.50727.762"
        processorArchitecture="x86"
        publicKeyToken="1fc8b3b9a1e18e3b">
      </assemblyIdentity>
    </dependentAssembly>
  </dependency>
</assembly>

The main line that is of interest is the "assemblyIdentity", which contains a "name", "version", "processorArchitecture," and a "publicKeyToken" like the above example winsxs directory.
There are two ways to store a manifest:
  1. External .manifest file. The manifest file has the same name as the .DLL or .EXE, with ".manifest" added on to the end. For example, helloWorld.exe would have an external manifest called "helloWorld.exe.manifest." The manifest is located in the same directory as the .EXE or .DLL.
  2. Embedded manifest. This is the default option for Visual C++ 2005. The XML file is store as a text resource in the .EXE or .DLL.
To chose an embedded assembly or external file, go to Visual C++ 2005 -> Project -> App Properties -> Configuration Properties -> Manifest Tool -> Input and Output -> Embed Manifest -> Yes or No.
image
Once an application is built, you may want to change which DLL's it is using. How do you change a manifest without rebuilding the application/DLL? For an external .manifest file, just edit the .manifest file and you are done.
Changing an embedded manifest is a bit more tricky. Here's what you do:
  1. Create a text file that has the manifest in it that you want to use.
  2. Select all the text and copy it to the clipboard (we'll use it later)
  3. Visual C++ 2005 -> File -> Open -> File...
  4. Select the .EXE or .DLL you want to modify and click "Open"
  5. The manifest is stored in a resource called "RT_MANIFEST" with an ID of 1. Double-click the "1" to open it in an editor.
  6. Ctrl-A to select all the text in the editor
  7. Ctrl-V to overwrite the text with the text in the clipboard.
  8. Ctrl-S to save the updated text in the .EXE/.DLL
image
The search order for DLL's has changed with winsxs. Now side-by-side directories (\windows\winsxs) are searched even before the local application directory. Here is an article about search order.
Since the winsxs directory is searched before the local application directory, it is impossible to override a .DLL in a winsxs directory, right? WRONG! This article shows how to do it. Basically, you just remove "public key token" from the application manifest and then create a manifest file for the DLL's you want to use (an assembly), like Microsoft.VC80.CRT.Manifest.
You may know the name of the DLL's you are interested in, but what is the assembly name for these DLL's? This page has a nice list of assemblies and the DLL's they point to.
I showed you earlier how to change a manifest without rebuilding an application. The next time you build the application, your manifest changes will be overwritten by Visual C++. To make your manifest changes permanent, you need to update the project settings so they are output every time you build your application.
To add information about an assembly to the application manifest...
  1. Visual C++ 2005 -> Project -> App Properties -> Configuration Properties -> Linker -> Manifest File -> Additional Manifest Dependencies
  2. In this text box, put the contents the <assemblyIdentity>, but leave out the <assemblyIdentity> open and close tags.
  3. Replace all double quotes with single quotes
  4. Enclose the entire line in double quotes
If I wanted to put the following into the auto-generated manifest file for an application...
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <dependency>
    <dependentAssembly>
      <assemblyIdentity
        
type="win32"         name="Microsoft.VC80.CRT"
        version="8.0.50727.762"
        processorArchitecture="x86" >
      </assemblyIdentity>
    </dependentAssembly>
  </dependency>
</assembly>
...then just put this line in "Additional Manifest Dependencies"...
"type='win32' name='Microsoft.VC80.CRT'
  version='8.0.50727.762' processorArchitecture='x86'"
 

The Solution

So why were some people able to run the application even though they did not install SP1?
It appears that people that installed iTunes were able to run our application without SP1 installed. My theory is that version of iTunes was built against the SP1 C runtime and it installed the proper DLL in winsxs.
How did you get the application to work for people without SP1?
We decided to check to see what versions of the C runtime were available on a users system. If the version was older than the version our application was built against, we installed the SP1 C runtime libraries via the Microsoft Visual C++ 2005 Redistributable Package (x86).
  • For Visual C++ 2005, use this
  • For Visual C++ 2005 SP1, use this
Do you have to install the SP1 runtime in order to get an app built with SP1 to run?
That is certainly the preferred method, but you can also put the DLL's in the same directory as the app and bypass winsxs. One reason to not bypass winsxs is that if Microsoft pushes a fix for libraries that may have a security issue, you won't ever get the updates an your application will be left with the security holes.
We found another way that lets you work in the safety of winsxs on systems that may not have the same runtime libraries. Instead of using an explicit version of the C runtime (which requires that exact version present on your computer or else the application will crash on startup), there is a version that means "use the most recent compatible version of the C runtime". If we would have used this version in our application, it is likely that users would be able to run the application even if they don't have the SP1 C runtime library present. To get this "special" version, find the appropriate .manifest file in the C:\Windows\Winsxs\Manifests directory. Open this file and you will see a line with the XML tags "<bindingRedirect>". Basically, if you use a version in the range of "oldVersion", then the actual version that is loaded is given by "newVersion". I learned about this technique from this forum post.

image
In my case, versions 8.0.41204.256 through 8.0.50608.0 and 8.0.50727.42 through 8.0.50727.762 mapped to 8.0.50727.762. If you can find a version that always maps to a valid version via <bindingRedirect>, then use this version in your application's manifest and your app should work regardless of which versions of the DLL's are installed. You will need to check your user's machines to make sure they all redirect correctly in order for this method to work.

Conclusion

I knew *nothing* about winsxs (windows side-by-side) on Friday. It is a mess of complex topics that most people don't understand. There is a lot of information on the internet about winsxs, but it is spread all over the place and varies in degree of helpfulness. I'm certainly no expert, but I've learned a lot in the last couple of days.
Now that I understand the problem that winsxs is fixing, I can appreciate it, even if it is very messy and hard to understand.
The net result is applications will be more stable and reliable as more and more applications switch to winsxs. This is a good thing.
I wrote all this down mainly because I knew I'd forget about by the time I needed to address winsxs issues again. Hopefully this is helpful to others as well.

Комментариев нет:

Отправить комментарий