Alois Kraus

blog

  Home  |   Contact  |   Syndication    |   Login
  133 Posts | 8 Stories | 368 Comments | 162 Trackbacks

News



Archives

Post Categories

Image Galleries

Programming

The nice thing about unintended changes is that you never think it could happen until you get bitten by a nasty change. Microsoft did publish a complete list of breaking changes here which is complete to my knowledge. But although the intentional changes are listed there are side effects which can cause you to search for hours your (non) fault.

One change was to create a new GAC for .NET 4 assemblies to prevent breaking applications which rely on the structure of the GAC introduced with .NET 2.0.

Feature: Global assembly cache location change

Differences from 3.5 SP1: For .NET Framework 4 assemblies, the global assembly cache has been moved from the Windows directory (%WINDIR%) to the Microsoft.Net subdirectory (%WINDIR%\Microsoft.Net). Assemblies from earlier versions remain in the older directory.

The unmanaged ASM_CACHE_FLAGS enumeration contains the new ASM_CACHE_ROOT_EX flag. This flag gets the cache location for .NET Framework 4 assemblies, which can be obtained by the GetCachePath function.

Recommended changes: None, assuming that applications do not use explicit paths to assemblies, which is not a recommended practice.

Ok MS thinks that you do not need to make any changes to your application until you rely on the structure of the GAC. Well all our applications rely on the structure of the GAC. Not directly on the file structure as the writer of this change did think but we are all bound to the limitations of the file system.

If you create an installer for your managed assemblies and you do install into the GAC then it can happen that you see this:

 

image

When you turn on MSI logging (e.g. msiexec /i <your msi> /lvx* c:\temp\log.txt) for the faulting msi you will get this

Error 1935. An error occurred during the installation of assembly

'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab,version="1.0.0.0",culture="neutral",publicKeyToken="F9DE1EF17FB257B6",processorArchitecture="MSIL"'. Please refer to Help and Support for more information. HRESULT: 0x8007006F. assembly interface: IAssemblyCacheItem, function: Commit, component: {BE634139-4AD2-5A1B-9C96-B0D01F8F7AEB}
MSI (s) (1C:14) [22:32:24:210]: Note: 1: 2205 2:  3: Error
MSI (s) (1C:14) [22:32:24:211]: Note: 1: 2228 2:  3: Error 4: SELECT `Message` FROM `Error` WHERE `Error` = 1709
MSI (s) (1C:14) [22:32:24:211]: Product: AA_GacInstaller -- Error 1935. An error occurred during the installation of assembly 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab,version="1.0.0.0",culture="neutral",publicKeyToken="F9DE1EF17FB257B6",processorArchitecture="MSIL"'. Please refer to Help and Support for more information. HRESULT: 0x8007006F. assembly interface: IAssemblyCacheItem, function: Commit, component: {BE634139-4AD2-5A1B-9C96-B0D01F8F7AEB}

 

As you see the HRESULT from IAssemblyCacheItem you get the Win32 error code 0x6F. What could that mean?

0x0000006F
ERROR_BUFFER_OVERFLOW

The file name is too long.

Do you remember the old MAX_PATH variable which has value of 256 characters. No full path on NTFS can be any longer than that. If you have a perfectly working application with .NET 3.5 and switch over to 4.0 everything will continue to work fine until you try to deploy the new version to the GAC. Installation does fail and you search for hours the fault on your side because a switch to a new .NET version requires many non trivial changes.

The innocent looking change of the GAC location did cause our application to break because we had a quite long assembly name (if this is a good idea is another topic) which did just work with .NET 3.5 but stopped working when the same assembly is compiled for .NET 4 and then deployed via MSI into the new GAC.

 

.NET Version GAC Location
3.5 C:\Windows\assembly\GAC_MSIL\<Name>\<Version>__<hash>\<Name>.dll
4.0 C:\Windows\Microsoft.NET\assembly\GAC_MSIL\<Name>\v4.0_<Version>__<hash>\<Name>.dll

 

As you can see the .NET 4.0 GAC directory has an added “Microsoft.NET\” and “v4.0_“ for every .NET 4 assembly. This results in the loss of 19 characters for the complete path. Since the assembly name is part of the directory name as well we get a 9 character shorter maximal assembly name length with .NET 4.

.NET Version Maximum Assembly Name Length in characters
2.0,3.5,3.5 SP1 99
4.0 90

 

Oh and while I am at it. What about this change?

Feature: Assembly loading

Differences from 3.5 SP1: To prevent redundant loading of assemblies and to save virtual address space, the CLR now loads assemblies by using only the Win32 MapViewOfFile function. It no longer also calls the LoadLibrary function.

This change affects diagnostic applications in the following ways:

  • A ProcessModuleCollection will no longer contain any modules from a class library (.dll file), as obtained from a call to Process.GetCurrentProcess().Modules.

  • Win32 applications that use the EnumProcessModules function will not see all managed modules listed.

Recommended changes: None

No changes necessary? Really? What about a diagnostic application that can switch on tracing while the app is running? The list of processes is filtered by only these processes which have loaded the diagnostic assembly in their process space. After a switch to .NET 4 I am quite certain that the process list will be empty. Since there is no documented way from MS how to list for all processes their file mappings there is no easy workaround possible. Of course I can simply create a named mutex or some other named kernel object but this suffers from security issues due to session 0 isolation. Only processes with the privilege SE_CREATE_GLOBAL_NAME are allowed to create cross session objects. This makes it e.g. very hard to switch on tracing for a non privileged system service from an interactive user session.

As a fun fact I do know the true story behind this change. A long time ago MS did develop Visual Studio 2010 and .NET 4.0. Everybody was adding features and all was running smoothly. Smoothly? One minor issue was that Visual Studio was crashing and running out of memory very quickly.

Hypothetical PM discussion with Dev:

Dev: Visual Studio runs out of memory.

PM: No problem. We will address this in a future version.

Dev: But we have got plenty of customer complaints about performance and footprint.

PM: Hm. Do we know what is causing this?

Dev: Nope.

PM: Hm.

…..

Nobody did know what was going on until someone realized that the CLR did load every assembly twice. One time via LoadLibrary and a second time as memory mapped file. That is no problem perf wise since the data is always read from the OS cache. But for a 32 bit application such a Visual Studio it meant that every dll consumed twice the virtual memory it needed to. The fix was to load every assembly only once as it is the case with .NET 4.

You can see this still today in every .NET 3.5 SP1 application with VMMap. There you can see in seconds that the assembly UnitTest.dll is loaded twice into different address spaces. With other tools such as a debugger it is nearly impossible to find such issues except if you do kernel debugging as your day job.

image

If you look deeper with VMMap you can find the following differences between .NET 3.5 SP 1 and .NET 4

.NET Framework Version Behaviour
.NET 3.5 SP1 Every assembly is loaded twice.
If a native image is present it is also loaded.
Code is loaded up to 3 times in every process!
.NET 4.0 Every assembly is loaded only once.
If a native image is present nothing from the GAC is loaded.
Code is loaded only once in every process

What does this mean for my UI? If the application is NGened .NET 4 will load the native image via LoadLibrary as usual and it will show up as native image in the ProcessModuleCollection. The breaking change is luckily not so breaking since native images are still loaded the “old” way. One more reason to NGen every assembly to speed up warm startup.

With .NET 4.5 there is Managed Profile Guided Optimization possible that allows us to reduce cold startup times as well by a factor two and reduce the footprint also significantly. The trick is to profile the application during startup in a real product scenario and allow the optimizer to reorder the native image pages with the instrumented assembly which does contain profiling information.

The code is reordered so that the “hot path” used during startup is stored in subsequent pages which results in a linear read for the hard disc which is good. Exceptional code such as exception handlers are put far away into the cold pages which are hopefully not needed during startup. By reordering the native code (I am sure the are other tricks also applied) this way the application will use less physical memory since all loaded pages contain only code that will be executed during the startup. This was done already for the CLR assemblies of .NET 3.5 SP1 but with .NET 4.5 we get this cool tool also.

posted on Friday, September 23, 2011 10:13 AM