Common things covered in MSVC4(win9x ages)

Discuss Windows 95, 98 and ME.
Post Reply
ProMiNick
Posts: 39
Joined: Thu Nov 14, 2019 9:36 am

Common things covered in MSVC4(win9x ages)

Post by ProMiNick »

If someone wish to analize DLL files from win9x:
1. Compiler was MSVC4.
2. If DLL is COM server (it has exports DllCanUnloadNow, DllGetClassObject, and optionaly DllRegisterServer, DllUnregisterServer) than definitely it is build with help of ATL (usualy via ATL2.0).
How looks like things covered from programmer?:

in assembler:
(NOTE:this is not a binary identic realization to output of MSVC4+ATL)
(NOTE:I moved tryRawDllMain in separate function, this done only for readability, no one MSVC don`t do it this way, they check rawDLLmain inline in DllEntryPoint)
(NOTE:In original preserved much more registers and logical blocks are a bit messed with each other - I show only fully workable optimized for size and readability functional identic code - for understanding what is happened)
(NOTE:Used real assembler dialect FASM with altered macro procedure with esp stack realization, for readable trickering added macros prepare & invoks)

Code: Select all

tryRawDllMain: procedure (hinstDLL, fdwReason, lpvReserved)
        mov     eax, 1 ; TRUE
        cmp     [rawDLLmain],eax
        jb      .locret
        stdcall [rawDLLmain],hinstDLL,fdwReason,lpvReserved
  .locret:
        ret
endp

CRT_INIT: procedure (hinstDLL, fdwReason, lpvReserved)
        mov     eax, DLL_PROCESS_ATTACH
       ;mov     eax, TRUE - same as above
        cmp     [fdwReason], eax
        ja      .locret
        je      .DLL_PROCESS_ATTACH_case
  .DLL_PROCESS_DETACH_case:
        xor     eax, eax
        cmp     [need_DLL_PROCESS_DETACH_counter], eax
        jle     .locret
        dec     [need_DLL_PROCESS_DETACH_counter]
        inc     eax
        cmp     [Memory], eax
        jb      .locret
        mov     eax, [Memory.End]
  .loop_slots:
        sub     eax, 4
        cmp     eax, [Memory]
        jb      .done_slots
        push    eax
        mov     eax, [eax]
        test    eax, eax
        jz      .skip_call
        call    eax
  .skip_call:
        pop     eax
        jmp     .loop_slots
  .done_slots:
        cinvoke free,[Memory]
        xor     eax, eax
        mov     [Memory],eax
        inc     eax
        jmp     .locret

  .DLL_PROCESS_ATTACH_case:
        cinvoke malloc, $80
        mov     [Memory], eax
        test    eax, eax
        jz      .locret
        mov     [Memory.End], eax
        mov     dword [eax], 0
        cinvoke initterm,InitTerm.Start,InitTerm.End
        inc     [need_DLL_PROCESS_DETACH_counter]
        mov     eax, 1
  .locret:
        ret
endp


DllEntryPoint : procedure (hinstDLL, fdwReason, lpvReserved)
; if we`ll look on it from side of DisableThreadLibraryCalls launched in DllMain
; there are left only DLL_PROCESS_ATTACH, DLL_PROCESS_DETACH & wine specific dwReasons
; b.t.w. wine specific dwReasons historicaly successfully handled until the opposite is determined
        mov     eax, DLL_PROCESS_ATTACH
       ;mov     eax, TRUE - same as above
        cmp     [fdwReason], eax
        ja      .locret ; before locret should be calls of DllMain & CRT_INIT that
        jb      .DLL_PROCESS_DETACH_case
  .DLL_PROCESS_ATTACH_case:
        stdcall tryRawDllMain(hinstDLL, fdwReason, lpvReserved)
        test    eax, eax
        jz      .locret
        stdcall CRT_INIT(hinstDLL, fdwReason, lpvReserved)
        test    eax, eax
        jz      .locret
        stdcall DllMain(hinstDLL, fdwReason, lpvReserved)
        test    eax, eax
        jnz     .locret
        stdcall CRT_INIT(hinstDLL, DLL_PROCESS_DETACH, lpvReserved)
        xor     eax, eax
        jmp     .locret
  .DLL_PROCESS_DETACH_case:
        cmp     [need_DLL_PROCESS_DETACH_counter], eax
        jz      .locret
        stdcall DllMain(hinstDLL, fdwReason, lpvReserved)

        stdcall CRT_INIT(hinstDLL, fdwReason, lpvReserved)
        test    eax, eax
        jz      .locret
        stdcall tryRawDllMain(hinstDLL, fdwReason, lpvReserved)
  ;     jmp     .skip_not_PROCESS_reasons
  ;.not_PROCESS_reasons:
  ;     stdcall DllMain(hinstDLL, fdwReason, lpvReserved) ; return value ignored - it allways true for not_PROCESS_reasons
  ;.skip_not_PROCESS_reasons:

  .locret:
        ret

endp

DllMain: procedure (hinstDLL, fdwReason, lpvReserved)
        cmp     [fdwReason], DLL_PROCESS_ATTACH
        ja      .retTrue
        jb      .DLL_PROCESS_DETACH_case
  .DLL_PROCESS_ATTACH_case:
        mov     eax, [hinstDLL]
        mov     [hInstance], eax
        prepare DisableThreadLibraryCalls,hinst
        stdcall ComModule.Init,ComModule,ObjectMap,eax
        invoks  DisableThreadLibraryCalls,stack
        jmp     .retTrue
  .DLL_PROCESS_DETACH_case:
        stdcall ComModule.Term,ComModule
  .retTrue:
        mov     eax,1
        ret
endp
and same in HLL (C or pseudo C):

Code: Select all

extern “C” BOOL WINAPI tryRawDllMain(HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpvReserved)
{       if (rawDLLmain == NULL) return TRUE
        else return rawDLLmain(hinstDLL, fdwReason, lpvReserved);
}

extern “C” BOOL WINAPI CRT_INIT(HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpvReserved)
{       if (dwReason == DLL_PROCESS_ATTACH)
        {       Memory = malloc($80);
                if (Memory == NULL) return FALSE;
                Memory.End = Memory;
                Memory = NULL;
                initterm(InitTerm.Start,InitTerm.End);
                need_DLL_PROCESS_DETACH_counter++;
        }
        else if (dwReason == DLL_PROCESS_DETACH)
                if (need_DLL_PROCESS_DETACH_counter > 0)
                {       need_DLL_PROCESS_DETACH_counter--;
                        if (Memory == NULL) return TRUE;
                        temp = Memory.End
                        while (&temp-&Memory >0)
                        {
                                pointer(&temp)--;
                                if (temp != NULL) temp();
                        };
                        free(Memory);
                        Memory = NULL;
                }
                else return FALSE;
        }
        return TRUE;
}

extern “C” BOOL WINAPI DllEntryPoint(HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpvReserved)
{       if (dwReason == DLL_PROCESS_ATTACH)
        {       if (tryRawDllMain(hinstDLL, fdwReason, lpvReserved) && CRT_INIT(hinstDLL, fdwReason, lpvReserved))
                {       if (DllMain(hinstDLL, fdwReason, lpvReserved)) return TRUE
                        else return tryRawDllMain(hinstDLL, fdwReason, lpvReserved);
                }
                return FALSE;
        }
        else if (dwReason == DLL_PROCESS_DETACH)
                if (!(need_DLL_PROCESS_DETACH_counter == 0))
                {       DllMain(hinstDLL, fdwReason, lpvReserved);
                        if (CRT_INIT(hinstDLL, fdwReason, lpvReserved)) return tryRawDllMain(hinstDLL, fdwReason, lpvReserved)
                        else return FALSE;
                }
        else return TRUE;
}

extern “C” BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpvReserved)
{       if (dwReason == DLL_PROCESS_ATTACH)
        {       hInstance = hinstDLL
                ComModule.Init(ObjectMap,hinstDLL);
                DisableThreadLibraryCalls(hinstDLL);
        }
        else if (dwReason == DLL_PROCESS_DETACH) ComModule.Term();
        return TRUE;
}
In any MSVC builded PE file there are contained the same code of DllEntryPoint (but I simplified it due to knowledge of used DisableThreadLibraryCalls).DllMain left untouched too.
I never met any OS file were rawDLLmain was defined. Profit of usage rawDLLmain is zero - because it don`t replace CRT_INIT but only precede it.
Internal content of CRT_INIT evaluated from version to version (of MSVC).

If that topic will not be banned and someone will interest in it I can continue describe internals hidden even in HLL opensource.
I can repeat (here I going to describe only things relative to obsolete technology ATL2.0 & MSVC4 - from times when components of win9x were written on it).
Sorry, but I can`t refer to single person by You. My soul & spirit require acronim thou instead.

ProMiNick
Posts: 39
Joined: Thu Nov 14, 2019 9:36 am

Re: Common things covered in MSVC4(win9x ages)

Post by ProMiNick »

ComModule.Init subcall of DllMain lead us on 2 interesting ATL objects:ComModule and ObjectMap.
Because we talking about ATL 2.0 content of these structures is next:

Code: Select all

struct ATL_MODULE20
        cbSize                  dd ?
        m_hInst                 dd ?
        m_hInstResource         dd ?
        m_hInstTypeLib          dd ?
        m_pObjMap               dd ?
        m_nLockCnt              dd ?
        m_hHeap                 dd ?
        m_csTypeInfoHolder      RTL_CRITICAL_SECTION
        m_csWindowCreate        RTL_CRITICAL_SECTION
        m_csObjMap              RTL_CRITICAL_SECTION
ends 
ComModule is structured as ATL_MODULE20

Code: Select all

struct ATL_OBJMAP_ENTRY20
        pclsid                  dd ?
        pfnUpdateRegistry       dd ?
        pfnGetClassObject       dd ?
        pfnCreateInstance       dd ?
        pCF                     dd ?
        dwRegister              dd ?
        pfnGetObjectDescription dd ?
ends
ObjectMap is array of ATL_OBJMAP_ENTRY20 structures. Every COM object realized in DLL represented as entry in ObjectMap array. This array is NULL-element terminated (all fields of last element are zeroes, but you will see below only first dword of last element is checked, so last element could be represented as just single zeroed dword)

As you understand logic of ComModule.Init & ComModule.Term are completely dependent from structures of ATL_MODULE20 & ATL_OBJMAP_ENTRY20.

So here they are:

Code: Select all

ComModule_Init: procedure (pComModule, pEntries, hInst)
        mov     eax, [pComModule]
        test    eax, eax
        jz      .retE_INVALIDARG
        ;cmp     [eax+ATL_MODULE20.cbSize], sizeof.ATL_MODULE20 ; The C compiler doesn't trust itself
        ;jz      .retE_INVALIDARG
        add     eax, ATL_MODULE20.m_csObjMap
        prepare InitializeCriticalSection,eax
        sub     eax, sizeof.RTL_CRITICAL_SECTION
        prepare InitializeCriticalSection,eax
        sub     eax, sizeof.RTL_CRITICAL_SECTION
        prepare InitializeCriticalSection,eax
        sub     eax, ATL_MODULE20.m_csTypeInfoHolder-ATL_MODULE20.m_hHeap
        xchg    eax,esp ; economy registers: fill structure at trickery stack
        push    0
        push    0
        push    [pEntries+eax-esp-espFixer] ; rebase params from esp to eax
        push    [hInst+eax-esp-espFixer]    ; same rebase
        push    [hInst+eax-esp-espFixer]    ; same rebase
        push    [hInst+eax-esp-espFixer]    ; same rebase
        xchg    eax,esp
        mov     [eax+ATL_MODULE20.cbSize], sizeof.ATL_MODULE20 ; But I trust C compiler
        invoks  InitializeCriticalSection,stack
        invoks  InitializeCriticalSection,stack
        invoks  InitializeCriticalSection,stack
        xor     eax, eax
        jmp     .locret
  .retE_INVALIDARG:
        mov     eax, E_INVALIDARG
  .locret:
        ret
endp
and

Code: Select all

ComModule_Term: procedure (pComModule)
        mov     eax, [pComModule]
        test    eax, eax
        jz      .retE_INVALIDARG
        push    esi
        push    edi
        mov     esi, eax
        lea     eax, [eax+ATL_MODULE20.m_csObjMap]
        prepare DeleteCriticalSection,eax
        sub     eax, sizeof.RTL_CRITICAL_SECTION
        prepare DeleteCriticalSection,eax
        sub     eax, sizeof.RTL_CRITICAL_SECTION
        prepare DeleteCriticalSection,eax
        mov     edi, [eax+ATL_MODULE20.m_pObjMap]
        test    edi, edi
        jz      .skipObjMapWalkaround ; if there is no ObjMap at all

  .ObjMapWalkaround:
        cmp     [edi+ATL_OBJMAP_ENTRY20.pclsid], 0
        jz      .lastElementReached
        mov     eax, [edi+ATL_OBJMAP_ENTRY20.pCF]
        test    eax, eax
        jz      .skipReleaseof_pCF
        comcall eax,IUnknown,Release
        mov     [edi+ATL_OBJMAP_ENTRY20.pCF],0
  .skipReleaseof_pCF:
        add     edi, sizeof.ATL_OBJMAP_ENTRY20
        jmp     .ObjMapWalkaround

  .lastElementReached:
  .skipObjMapWalkaround:
        invoks DeleteCriticalSection,stack
        invoks DeleteCriticalSection,stack
        invoks DeleteCriticalSection,stack
        mov     eax, [esi+ATL_MODULE.m_hHeap]
        pop     edi
        pop     esi
        jz      .noheap
        invoke  HeapDestroy,eax
  .noheap:
        xor     eax, eax
        jmp     .locret
  .retE_INVALIDARG:
        mov     eax, E_INVALIDARG
  .locret:
        ret
endp 
Last edited by ProMiNick on Mon Dec 23, 2019 10:15 am, edited 4 times in total.
Sorry, but I can`t refer to single person by You. My soul & spirit require acronim thou instead.

ProMiNick
Posts: 39
Joined: Thu Nov 14, 2019 9:36 am

Re: Common things covered in MSVC4(win9x ages)

Post by ProMiNick »

function DllCanUnloadNow looks same everywhere where it exist (no matter in x86 or x64)
in case of ATL - ServerLockCount is a m_nLockCnt member of ComModule structure in other cases it is standalone variable.
This is most elegant variant of its realization - all modern compilers harcoode DllCanUnloadNow this way. In times of MSVC4 it was look similar but a bit unoptimized.

Code: Select all

DllCanUnloadNow: procedure()
        xor     eax,eax
        cmp     eax,[ComModule.m_nLockCnt]
        jz      .locret
        inc     eax
      .locret:
        ret
endp
But as you see nothing interest in this function - only it primary task - check that ServerLockCount is zeroed and notice that DLL can be unloaded.

Code: Select all

DllRegisterServer: procedure()
        stdcall ComModule.RegisterServer,ComModule,TRUE,NULL ; registered relative to each COM object stuff
        test    eax, eax
        jnz     .locret
        jmp    DllRegisterServer_commonPart ; In case of successed COM objects registration registered things relative to whole library
      .locret:
        ret
endp

Code: Select all

DllUnregisterServer: procedure()
        stdcall DllUnregisterServer_commonPart 
        stdcall ComModule.UnregisterServer,ComModule,NULL
        xor     eax, eax
        ret
endp
content of DllRegisterServer_commonPart & DllUnregisterServer_commonPart could be very huge and complex but there are realized the most simplest things ever: in registry added(or cleared) values, this registry changes could be realized via different ANSY|UNICODE functions with checking accessibility of such functions in OS, and according to OS versions could be affected registry nodes specific to OS version.

Code: Select all

DllGetClassObject: procedure(rclsid,riid,ppv) ; just delegate its work to ComModule.GetClassObject
        stdcall ComModule.GetClassObject,ComModule,rclsid,riid,ppv
        ret
endp
Sorry, but I can`t refer to single person by You. My soul & spirit require acronim thou instead.

ProMiNick
Posts: 39
Joined: Thu Nov 14, 2019 9:36 am

Re: Common things covered in MSVC4(win9x ages)

Post by ProMiNick »

seamless to all ComModule methods ComModule.RegisterServer & ComModule.UnregisterServer are walking around COM OBJECT MAP:

Code: Select all

ComModule_RegisterServer: procedure (pComModule, bRegTypeLib, pCLSID)
        mov     eax, [pComModule]
        test    eax, eax
        jz      .retE_INVALIDARG

        push    ebx
        push    esi
        push    edi
        mov     ebx, [eax+ATL_MODULE20.m_pObjMap]
        test    ebx, ebx
        jz      .skipObjMapWalkaround ; if there is no ObjMap at all
  .ObjMapWalkaround:
        mov     edi, [ebx+ATL_OBJMAP_ENTRY20.pclsid]
        test    edi, edi
        jz      .lastElementReached
        mov     esi, [pCLSID]
        test    esi, esi
        jz     .GetObjectDescription
        xor     eax, eax
        mov     ecx, 4
        repe    cmpsd
        jmp     .UpdateRegistryIfNeeded
  .GetObjectDescription:
        call    [ebx+ATL_OBJMAP_ENTRY20.pfnGetObjectDescription]
        test    eax, eax
  .UpdateRegistryIfNeeded:
        jnz     .gotoNextEntry
        stdcall [ebx+ATL_OBJMAP_ENTRY20.pfnUpdateRegistry],TRUE
        test    eax, eax
        jl      .WalkaroundbreackedWithError
  .gotoNextEntry:
        add     ebx, sizeof.ATL_OBJMAP_ENTRY20
        jmp     .ObjMapWalkaround
  .WalkaroundbreackedWithError:
  .lastElementReached:
  .skipObjMapWalkaround:
        pop     edi
        pop     esi
        pop     ebx
        jl      .locret ; in case of error skip rest
        xor     eax, eax
        cmp     [bRegTypeLib], eax
        jz      .locret
        stdcall ComModule.RegisterTypeLib,ComModule,eax
        jmp     .locret
  .retE_INVALIDARG:
        mov     eax, E_INVALIDARG
  .locret:
        ret
endp

Code: Select all

ComModule_UnregisterServer: procedure (pComModule, pCLSID)
        mov     eax, [pComModule]
        test    eax, eax
        jz      .retE_INVALIDARG

        push    ebx
        push    esi
        push    edi
        mov     ebx, [eax+ATL_MODULE20.m_pObjMap]
  .ObjMapWalkaround:
        mov     edi, [ebx+ATL_OBJMAP_ENTRY20.pclsid]
        test    edi, edi
        jz      .lastElementReached
        mov     esi, [pCLSID]
        test    esi, esi
        jz     .GetObjectDescription
        xor     eax, eax
        mov     ecx, 4
        repe    cmpsd
        jmp     .UpdateRegistryIfNeeded
  .GetObjectDescription:
        call    [ebx+ATL_OBJMAP_ENTRY20.pfnGetObjectDescription]
        test    eax, eax
  .UpdateRegistryIfNeeded:
        jnz     .gotoNextEntry
        stdcall [ebx+ATL_OBJMAP_ENTRY20.pfnUpdateRegistry],FALSE
  .gotoNextEntry:
        add     ebx, sizeof.ATL_OBJMAP_ENTRY20
        jmp     .ObjMapWalkaround
  .lastElementReached:
  .skipObjMapWalkaround:
        pop     edi
        pop     esi
        pop     ebx
        jmp     .locret
  .retE_INVALIDARG:
        mov     eax, E_INVALIDARG
  .locret:
        ret
endp
surprizingly ComModule_UnregisterServer don`t check presense of COM OBJECT MAP it just walking throw it.
Sorry, but I can`t refer to single person by You. My soul & spirit require acronim thou instead.

ProMiNick
Posts: 39
Joined: Thu Nov 14, 2019 9:36 am

Re: Common things covered in MSVC4(win9x ages)

Post by ProMiNick »

I dramaticaly confused with ComModule_RegisterTypeLib code - there are very more code, but it again do trivial things
I declare them in pseudocode: (And where I confused is the fact that tlib of ComModule_RegisterTypeLib share same param slot as occupied with pComModule of ComModule_RegisterServer)

Code: Select all

ComModule_RegisterTypeLib: procedure (tlib, lpSpecialPath)
        if lpSpecialPath
                name = catstr(lpSpecialPath,ModuleName)
        else
                name = ModuleName
        end if
        LoadTypeLib(name,tlib)
        if error(result)
                patched_name_with_tlb_ext = name with replaced ext
                name = patched_name_with_tlb_ext
                LoadTypeLib(name,tlib)
        end if

        if error(result) return result
        path = name with zeroed content after last (any of '\','/',':')
        RegisterTypeLib(tlib,name,path)
        if OK(result) return
        else ITypeLib(tlib).Release,tlib
endp
In files I analized this function body never called - so it will stay in a bit of mistery.
Maybe because of call to ComModule_RegisterTypeLib is in unexecuted branch - compiler left params passed to it messed.
Sorry, but I can`t refer to single person by You. My soul & spirit require acronim thou instead.

Post Reply