origin

Recently, I encountered a DLL loading problem in the project. The actual project is complicated, but after the solution, it is so simple and reasonable. This article is my example engineering simulation, the real project has a different trick, but the essence of the problem is the same. This article is very similar to “Debugging practice – DLL load failure global variable initialization”, the sample code is very similar (forgive me for being lazy), interested in the small partners can read the comparison.

background

There are four projects in the sample code, one EXE and three DLLS. Base.vcxproj is a project that encapsulates the public interface and generates Base.dll. Vcxproj and extension2.vCXproj are very similar in that they generate Extension1.dll and extension2.dll respectively. Mixconfiguration.vcxproj generates MixConfiguration.exe, which loads extension1.dll and Extension2.dll and calls their export functions (symbolic calls). After the program runs, it is found that only one DLL function is normal, and the other DLL function is not normal. The diagram below:

Dumpbin has confirmed that both DLLS have functions named GetCallCount. But only one call succeeded, and the other failed.

Use the Process Explorer to observe the loading of DLLS. Only one DLL is loaded and no other DLL is found.

As with the previous question, if you look at the entire loading process with Procmon, you will see Success. I’m not going to take screenshots here. Go directly to the debugger.

The debugger

Press F5 directly in VS to start, and sure enough, it stopped in VS.

The complete call stack can be seen on the right side of the figure.

A quick look at the code. Line 15 of MixConfiguration\Entry.cpp calls Auto hDll2 = LoadLibraryA(” extension2.dll “); Load the corresponding module. The problem is in the initialization code for the global variable ctest2g, defined in line 22 of extension2\ extension2.cpp.

As you can see from the left part of the figure, the error code is 0xC0000005. Memory access is abnormal. The address accessed is 0x0000000D and the corresponding instruction address is 008B7F34.

As can be seen from the figure above, it is indeed attached to 008B7F34 MOVsX ECX, Byte PTR [EAX]. Because the value of eAX is 0xD, we need to figure out why the value of eax is 0xD. As many of you know, eAX is used to store the return value of a function call. We can focus on the Call instruction at 0x008B7F2c, which calls the _Isnil() member function.

Check the source code provided by VS as follows:

static char& _Isnil(_Nodeptr _Pnode){// return reference to nil flag in node  return((char&)_Pnode->_Isnil); }Copy the code

_Isnil simply returns the _Isnil member of _Pnode.

It is important to note that a char& is returned and a reference is returned! Returns _Pnode->_Isnil.

You can view the _Pnode argument passed to _Isnil() in the Watch window as follows:

You can see that the _Pnode value is 0 and the type is STD ::_Tree_node<… >.

STD ::_Tree_node is defined as follows:

template<class _Value_type, class _Voidptr>struct _Tree_node{ _Voidptr _Left; // offset: 0x0 _Voidptr _Parent; // offset: 0x4 _Voidptr _Right; // offset: 0x8 char _Color; // offset: 0xC char _Isnil; // offset: 0xD _Value_type _Myval; // offset: 0x10private: _Tree_node& operator=(const _Tree_node&); };Copy the code

From the definition of _Tree_node, _Isnil has an offset of 0xD (generally, 32-bit program Pointers account for 4 bytes, or 8 bytes if 64-bit).

To sum up, it is reasonable that the call instruction at address 008B7F2C reverts back to 0xD. The movsx ecx instruction at 008B7F34,byte PTR [eax] stores the return value at ecX, but since eax is 0xD, accessing 0x0000000D will normally fail.

At this point, we know the immediate cause of the crash — access to an invalid address. But what is the root cause? Why is _Pnode 0?

The _Pnode value comes from _Nodeptr _Pnode = _Root(); . _Root() = &(this->_Myhead->_Parent) When assigned to _Pnode, the value of _Pnode equals the value of this->_Myhead->_Parent. We need to look at the value of this.

We find that the _Parent value is indeed 0. Could it be that, like last time, there was no initialization? But the other members have value, which is a little different from last time. We need to further analyze the source of this value.

further

This comes from CTest2’s constructor call CObjectManager::GetMap(), a base.dll export that returns a static variable s_manager defined in GetMap(). The order of initialization should not be a problem, since the static variables defined inside GetMap() are initialized when we first call it. What else could it be?

To observe the value of s_manager in VS, I tried several ways, but failed.

Helpless, continue to ask WinDBG to appear.

Windbg appearance

Open WinDBG and attach to the process, be sure to check the Noninvasive option because the target process is being debugged by VS.

If Noninvasive option is not selected, the error shown in the following figure is reported.

After successfully attaching, we first go through X Base! Use u 004B5830 L20 to query the disassembly and find the address of s_manager. The address is 004c431c.

We cannot directly dt s_manager, but dt 004C431c.

Observe the map object with the problem. To see the difference, see the picture below:

DLL is defined with _Myproxy and the offset of _Myhead is 4. In extension2.dll, there is no _Myproxy and, naturally, the offset of _Myhead is 0. These are two different map types!

At this point, the problem is clear, s_manager looks different to the two modules, notice that the address (highlighted in yellow) in the figure above is 0x004C431c. The next step is to find out why s_manager is different in base.dll and extension2.dll.

To check

Observe the inheritance relationship in VS as shown below:

From the picture above, we can know: _Tree inherited from _Tree_comp, Tree_comp inherited from _Tree_buy, _Tree_buy inherited from _Tree_alloc, _Tree_alloc inherited from _Tree_val, _Tree_val in turn inherits from _Container_base. Map inherits from _Tree.

Here we only need to focus on _Tree_val and _Container_base.

_Tree_val is defined as follows (extraneous information is removed) :

template<class _Val_types>class _Tree_val : public _Container_base{public:  typedef typename _Val_types::_Nodeptr _Nodeptr;    // remove unrelated typedefs and member functions  _Nodeptr _Myhead;	// pointer to head node  size_type _Mysize;	// number of elements};Copy the code

_Container_base is defined as follows (extraneous information is removed) :

#if _ITERATOR_DEBUG_LEVEL == 0typedef _Container_base0 _Container_base; #elsetypedef _Container_base12 _Container_base; #endifCopy the code

You can see that if _ITERATOR_DEBUG_LEVEL is 0, _Container_base is equivalent to _Container_base0. Otherwise, _Container_base is equivalent to _Container_base12.

Continue with the definitions of _Container_base0 and _Container_base12.

_Container_base0 is defined as follows:

struct _CRTIMP2_PURE _Container_base0{  void _Orphan_all() {}  void _Swap_all(_Container_base0&) {}};Copy the code

_Container_base12 is defined as follows (with irrelevant member functions removed) :

struct _CRTIMP2_PURE _Container_base12{public:  // remove unrelated member functions_Container_proxy *_Myproxy; };Copy the code

In other words, different _iterator_debug_levels have different memory usage for maps. This is exactly the problem I encountered in my project.

The bottom of

Knowing that _ITERATOR_DEBUG_LEVEL will cause the memory structure of the map to be different, we need to further find out what causes the value of _ITERATOR_DEBUG_LEVEL to be different. Search the entire solution for _ITERATOR_DEBUG_LEVEL.

Stdafx.h in extension2.vCXPROj defines #define _ITERATOR_DEBUG_LEVEL 0. If not explicitly defined, the value of this macro is affected by _HAS_ITERATOR_DEBUGGING. In Debug mode, the value of _ITERATOR_DEBUG_LEVEL is 2. Please refer to the definition in yvals. H as shown below:

At this point we had the whole story. To sum up:

Because the _ITERATOR_DEBUG_LEVEL of the two projects is different, the map base class (_Container_base) is different. As a result, the maps in the two projects are different, especially the offset of _Myhead. Indirectly, the global variable G_t2 crashes during initialization, which leads to the failure of the corresponding DLL loading.

To combat

It is strongly recommended that you also do some actual combat, after all, the paper on the end of the light. If you want to get your hands dirty, you can download my saved dump files and debug symbols and use WinDBG to analyze them directly.

Dump file and corresponding symbol file download link:

Baidu cloud link: pan.baidu.com/s/1EkOVoevZ… Extraction code: XUi4

CSDN:download.csdn.net/download/xi…

You can also download the complete project file and use VS2013 to compile and run. If you don’t have VS2013 installed, you can manually change to another version of VS.

Complete test project download link:

Baidu cloud link: pan.baidu.com/s/1swaTU-7G… Extraction code: IWKJ

CSDN:download.csdn.net/download/xi…

conclusion

  • Do not mix DLLS generated by Debug and Release.

  • The base class of the map is different depending on the _HAS_ITERATOR_DEBUGGING.

  • If a process is already being debugged, we can use Noninvasive method to attach to the debugged process and perform some observation operations.

The resources

  • vs2013built-instlThe source code
  • Docs.microsoft.com/en-us/cpp/c…