SQL and SQL only: Identifying Page Splits

by Josh Patterson 7. September 2010 10:37

Today, I read a comprehensive article by Nagaraj Venkatesan related to getting a handle on SQL Server Page Splits. If you manage any SQL servers than I strongly recommend that you read this article... simply fantastic!

 

SQL and SQL only: Identifying Page Splits - by Nagaraj Venkatesan

Tags: , ,

Miscellaneous

A tribute to Jacob Navia and my favorite compiler: LCC-Win32

by Josh Patterson 3. August 2010 12:14

It seems like more than a decade that I've been going back to that Virginia University page for a copy of lcc-win32. What's more is the fact that I have totally read and nearly memorized the copy of the "Windows API documentation" whose link is now defunct.


The simplicity of the build process was beautiful, the size of the created executables remains still to this day the smallest I've ever seen. If only the compiler and linker utilized better optimizing algorithms, I'd probably still be using it to this day for production coding.

Batch Build process:

lcc -O -unused -c Test.C
lcclnk -s -subsystem:console -o Test.Exe Test.obj

It should be noted, that I still use this compiler for hobbyist applications and still have hundreds of applications that compile flawlessly using both GCC and lcc-Win32!

Tags: , , ,

Miscellaneous

Exploited antique code warning, not for the faint of heart!!

by Josh Patterson 2. August 2010 12:12

In case you are not following, I’ll reiterate: This is old code so don’t give me grief. How old is the code?? I’m not sure exactly, but I was in Jr. High at the time. So again, no jabbing me for "crap code"!

Ok, the main() function has been fudged to show the purpose and functionality of the vulnerable procedure: "ParseRequest(...)"

The idea was to search for different parameters that were being passed over from an anonymous TCP/IP connection and then parse off the parameter text which would always be terminated by a line feed character (or optional carriage return character).

Can you spot the vulnerabilities? This code makes many assumptions about the "perfectly trustworthy" and "completely bug free" remote peer. This is a prime example of bad code.

We have potential underflows, overflows, and injection possibilities, potential out-of-bounds memory reading and writing, ect, ect... or to put it in layman's terms: this application would only have been "safe" if it were run on a machine with no networking capabilities period!

//-----------------------------------------------------------------------
char *ParseRequest(const char *sInput, char *sOutput)
{
 size_t iLen = strlen(sInput);
 size_t r = 0; //Read position.
 size_t w = 0; //Write position.
 
 //Skip the parameter marker.
 while(r == 0 || sInput[(r == 0 ? 1 : r) - 1] != ' ')
 {
  r++;
 }
 
 //Parse the parameter value.
 while(r < iLen && sInput[r] != '\r' && sInput[r] != '\n')
 {
  sOutput[w++] = sInput[r++];
 }
 
 sOutput[w] = 0; //NULL terminator.
 
 return sOutput;
}
 
//-----------------------------------------------------------------------
 
int main(int argc, char *argv[])
{
 char input[1024];
 char output[255];
 
 strcpy(input,
  "/P1 Param text 1\n"
  "/P2 Param text 2\n"
  "/P3 Param text 3\n"
  "/P4 Param text 4\n");
 
 for(size_t i = 0; i < strlen(input); i++)
 {
  if(input[i] == '/')
  {
   ParseRequest(input + i, output);
   printf("%s\n", output);
  }
 }
 
 return 0;
}
 
//-----------------------------------------------------------------------

 

Hints:

  1. What would happen if the input wasn’t null nor carriage-return / line feed terminated?
  2. What would happen if the input or any one of the parameter values were greater than 255 characters?
  3. What would happen if the input contained no spaces between the parameter name (/P1) and its value (Param Text 1).
  4. What if there were no spaces in the param text in conjunction with a missing space between the parameter name and its value?

The answer to all four questions? Hopefully a program crash! Otherwise malicious code could easily be injected for your happy application to execute!

Tags: , , ,

Miscellaneous

What to hack, what to crack? I said we'd go over exploits!

by Josh Patterson 1. August 2010 12:10

Interestingly enough, just a week after I posted to my blog about the possibility of an entry about exploits, I receive an email about an exploit of one of my oldest (and most embarrassing) socket applications: "Finger Server". If you're under 20 years old at the time of this posting then you may need to perform a search for the term "finger Protocol".

I do not really care much for this application anymore but I'm not going to leave a utility with a known vulnerability available via a security website. That wouldn't seem right.

So, what's the exploit? What might one gain from exploiting the server? How can I fix it?

Fortunately, these are all pretty easy to answer when it comes to this application, mainly, because I have the source code and secondly because the heart of the program is only roughly 50 lines of C code. Yea, that'll be easy to patch up!

Tags:

Miscellaneous

Updates... Updates... Updates...

by Josh Patterson 26. June 2010 12:09

The replication issues may be comming to a close. Errors are now being skipped as well as erroneous LSNs.

I have already started on my next technical post so it shouldn't be long now.

Tags: ,

Miscellaneous

Sockets: The foundation of a computer's social life

by Josh Patterson 17. June 2010 12:05
From experience, sockets in C/C++ can be very off-putting. This is "not so" in the case of (near post-modern) languages with a greater level of abstraction (Python, C#, VB.NET, ect, ect) but for good ol' Win32 – you've got your work cut out for you.
 
All this work is not without its rewards! You can create blocking mode clients, asynchronous load-balanced services, super-high throughput applications and seriously reliable programs using only a few simple functions in WinSock.
 
As a side note, it should be known that "WinSock" should seriously be called BSDSock as it was an outright copy from BSD – the windows source code even bears the BSD copyright notices (though I've heard through the grape-vine that Microsoft has rewritten the IP-Stack while in the process of building up support for IPv6 – but that is only speculation on my end).
 
I've highly commented and trimmed down this not-so-fully featured web server for your enjoyment!
#include "windows.h"
#include "winsock.h"
#include "stdio.h"

//be sure to include the linker library: wsock32.lib

#define MAX_CONNECTIONS 25
#define MAX_RECV_SIZE 8120

int main(int argc, char *argv[])
{
 SOCKET listenSocket = NULL; //The socket that will listen for new connections.
 SOCKET peerSockets[MAX_CONNECTIONS];
 SOCKET peerBytesSent[MAX_CONNECTIONS];

 struct timeval tTimeOut; // The TimeOut structure.
 tTimeOut.tv_sec = 1;
 tTimeOut.tv_usec = 0;

 //These are used to identify if a socket is ready
 // for reading, writing or is experiencing an exception.
 fd_set fdRead;
 fd_set fdWrite;
 fd_set fdExcept;

 WORD WSARequiredVersion = 0x0101; // Version 1.1
 WSADATA WSAData;
 WSADATA *WSAPointerData = &WSAData;
 if(WSAStartup(WSARequiredVersion, WSAPointerData) != 0)
 {
  printf("Failed to initialize WinSock: %d\n", WSAGetLastError());
  return 1;
 }

 // Fill In The Address Structure For Local Server
 SOCKADDR_IN localSockAddress; // Socket address of local server
 localSockAddress.sin_family      = AF_INET;    // Address Family.
 localSockAddress.sin_addr.s_addr = INADDR_ANY; // Library assigned address.
 localSockAddress.sin_port   = htons(80);  // Port Number.

 // Create A TCP/IP Stream Socket To "Listen" with.
 if((listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == INVALID_SOCKET)
 {
  printf("Failed to create socket: %d\n", WSAGetLastError());
  WSACleanup();
  return 1;
 }

 // Bind The Name To The Socket.
 if(bind(listenSocket, (SOCKADDR*)&localSockAddress, sizeof(struct sockaddr)) == SOCKET_ERROR)
 {
  printf("Bind failed: %d\n", WSAGetLastError());
  closesocket(listenSocket);
  WSACleanup();
  return 1;
 }

 // Set The Socket To Listen.
 if(listen(listenSocket, MAX_CONNECTIONS) == SOCKET_ERROR)
 {
  printf("Listen failed: %d\n", WSAGetLastError());
  closesocket(listenSocket);
  WSACleanup();
  return 1;
 }

 memset(&peerSockets, 0, sizeof(SOCKET) * MAX_CONNECTIONS);
 memset(&peerBytesSent, 0, sizeof(int) * MAX_CONNECTIONS);

 //---------------------------------------------------------

 while(true)
 {
  // Zero the Read, Write and Except descriptors.
  FD_ZERO(&fdRead);
  FD_ZERO(&fdWrite);
  FD_ZERO(&fdExcept);

  //Add the listen socket to the descriptor array.
  FD_SET(listenSocket, &fdRead);
  FD_SET(listenSocket, &fdExcept);

  //Add all connected sockets to the descriptor array.
  for(int i = 0; i < MAX_CONNECTIONS; i++)
  {
   if(peerSockets[i])
   {
    FD_SET(peerSockets[i], &fdRead);
    FD_SET(peerSockets[i], &fdWrite);
    FD_SET(peerSockets[i], &fdExcept);
   }
  }

  //Socket Select. This will figure out the disposition
  // of all the sockets in the descriptor array.
  if(select(-1, &fdRead, &fdWrite, &fdExcept, &tTimeOut) == SOCKET_ERROR)
  {
   printf("Select failed: %d\n", WSAGetLastError());
   closesocket(listenSocket);
   break;
  }

  //Are there any waiting connections?
  if(FD_ISSET(listenSocket, &fdRead))
  {
   int iLength = 0;
   SOCKET peerSocket = 0;

   SOCKADDR_IN remoteSockAddr;
   int iAddressLength = sizeof(SOCKADDR);

   //Accept the connection.
   peerSocket = accept(listenSocket, (SOCKADDR*)&remoteSockAddr, &iAddressLength);

   if(peerSocket == SOCKET_ERROR)
   {
    printf("Accept failed: %d\n", WSAGetLastError());
    closesocket(listenSocket);
    break;
   }
   else if(peerSocket == 0)
   {
    printf("Accept failed (gracefully): %d\n", WSAGetLastError());
    closesocket(listenSocket);
    break;
   }

   bool bFoundFreeSocket = false;

   //Find a free socket in the socket array.
   for(int i = 0; i < MAX_CONNECTIONS; i++)
   {
    if(peerSockets[i] == NULL)
    {
     peerSockets[i] = peerSocket;
     bFoundFreeSocket = true;
     break;
    }
   }

   if(!bFoundFreeSocket)
   {
    printf("No free sockets\n");
    closesocket(listenSocket);
   }
  } //if(FD_ISSET(listenSocket, &fdRead))
  
  //Check the disposition of each connected socket.
  for(int i = 0; i < MAX_CONNECTIONS; i++)
  {
   if(peerSockets[i])
   {
    //Is data waiting to be received?
    if(FD_ISSET(peerSockets[i], &fdRead))
    {
     char recvBuffer[MAX_RECV_SIZE];
     int recvLength = recv(peerSockets[i],
      recvBuffer, sizeof(recvBuffer), 0);

     if(recvLength == SOCKET_ERROR)
     {
      //Disconnect the socket.
      closesocket(peerSockets[i]);
      peerSockets[i] = NULL;
      peerBytesSent[i] = 0;
     }
     else if(recvLength == 0) // Gracefull disconnect.
     {
      //Disconnect the socket.
      closesocket(peerSockets[i]);
      peerSockets[i] = NULL;
      peerBytesSent[i] = 0;
     }
     else{
      recvBuffer[recvLength] = '\0';
      printf("Received %d bytes: \"%s\"\n", recvLength, recvBuffer);
     }
    }

    //Is the socket ready to accept data?
    if(FD_ISSET(peerSockets[i], &fdWrite))
    {
     //Send some data back to the connected peer.

     char *sendBuffer = "Hello World";

     int bytesSent = send(peerSockets[i], sendBuffer, strlen(sendBuffer), 0);
     if(bytesSent == SOCKET_ERROR)
     {
      int iLastError = WSAGetLastError();
      if(iLastError == WSAEWOULDBLOCK || iLastError == WSAEINPROGRESS)
      {
       //Do nothing, try to send again on the the next go-round.
      }
      else{
       //Disconnect the socket.
       closesocket(peerSockets[i]);
       peerSockets[i] = NULL;
       peerBytesSent[i] = 0;
      }
     }
     else if(bytesSent == 0)
     {
      //Disconnect the socket.
      closesocket(peerSockets[i]);
      peerSockets[i] = NULL;
      peerBytesSent[i] = 0;
     }
     else {
      //Data was sent successfully!

      //Keep track of how much data we sent to each socket.
      peerBytesSent[i] += bytesSent;


      //Disconnect after sending more than 1KB od data.
      if(peerBytesSent[i] > 1024)
      {
       //Disconnect the socket.
       closesocket(peerSockets[i]);
       peerSockets[i] = NULL;
       peerBytesSent[i] = 0;
      }
     }
    }

    //Is the socket in an error state?
    if(FD_ISSET(peerSockets[i], &fdExcept))
    {
     //Disconnect the socket.
     closesocket(peerSockets[i]);
     peerSockets[i] = NULL;
     peerBytesSent[i] = 0;
    }
   }
  }

  Sleep(1); //Give the CPU some breathing time.
 }

 //---------------------------------------------------------

 //Stop listening.
 closesocket(listenSocket);

 if(WSACleanup() == SOCKET_ERROR)
 {
  printf("Failed to cleanup WinSock: %d\n",
   WSAGetLastError());
  return 1;
 }

 return 0;
}

Now clearly there are more to sockets than this post covers (such as non-blocking asynchronous sockets) but trust me, this is 10 fold more than my first socket application.

Tags: , , , ,

Miscellaneous

Memory leaks? Track those allocations and plug the hole(s)!

by Josh Patterson 16. June 2010 12:05

I've written applications that were bad and I’ve written some that were perfect, though it took me years to figure out why a few of the perfect applications were chewing 75% of my RAM. No matter, their still perfect to me...
 
On a serious note, have you ever written any C or C++ code? Yes? Good - Then you are already familiar with memory leaks!
 
Memory leaks occur when you allocate RAM and do not free it. On modern OS's these memory leaks are only potentially detrimental for the lifetime of the running application, that is, the badly managed resources are cleaned up after the leaky application is closed (or unexpectedly terminated). That said, memory leaks really begin to haunt you when writing long running applications or more specifically: services.
 
These leaks can be more than frustrating, I've turned my back on applications that I've dedicated years to because the manor of the memory's escaped was just too elusive (e.g. I've written bad applications).
 
How can we solve such a problem? Well, you can either attempt to be perfect and (on near certain failure) dump five times the development time into debugging or… use some manner of memory tracker.
 
I developed one of these "memory trackers" that I now include in all of my C++ projects – yes, even the small ones – you never know what they’ll grow into.
 
I call this magic tool: CMemoryPool (though, it's not really a memory pool).
 
How's it work? Well it’s simple really - it has 4 modes which are set by defining preprocessors.
 
Modes of Operation:

Disabled
In disabled mode, the memory tracker code is collapsed down to calls to your native malloc(), calloc(), and realloc(). Fully optimized - this mode is for the final release of your product.
 
General
In general mode, the memory tracker will keep track of the allocations that your program makes and raise a debug break when the class is deconstructed if all of the allocations that it witnessed were not cleaned up properly. This mode is faster than the following modes and that's pretty much the end of its advantages.
 
Advanced
In advanced mode (which should have been called "default mode"), the memory tracker will keep track of each allocation that your application makes as well as the number of bytes per allocation and the source code file name and the line number in the source file that the allocation originated from. Much like the previous mode, the class will trigger a debug break when the class is deconstructed if your application has not been well behaved.
 
Verbose
Verbose mode is slow. It's actually more than slow but I don’t want to appear dramatic by telling you that it's "insanely slow" or "near unusable" - despite the fact that these statements would be true. This mode functions exactly like the previous mode (Advanced) but it additionally opens a console (if you didn’t already open one) and prints verbose information about every memory allocation, reallocation and free to the console. Yea, you think memory allocation is slow now – try Verbose mode!
 
One last thing that should be noted about the memory tracker is that it is multi-thread safe – very multi-tread safe. It’s even safe to use with non-multi-thread safe standard libraries. This functionality can be disabled when running in the final "disabled" mode for your final release by ensuring that the "Sequential When Optimized" preprocessor is not defined in the class header.
 
You're not still reading are you? ....I call B.S.
 
Well, if your that interested, grab a copy of the CMemPool and start saving time!!

 

Tags: , , ,

Miscellaneous

Fortitude from scratch and 11 years in the making.

by Josh Patterson 15. June 2010 12:04

This month, 11 years ago I began working on a web server called "The NetworkDLS HTTP Server". The basic idea was to copy the functionality of a little-known web server of the time written by a company called iMatix – their product name was "Xitami". I liked this little server - it was clunky, riddled with holes and could be "hacked" by any elementary school kid with a working knowledge of batch files – but its weaknesses aside – it was my first web server and worked very well over my dial-up connection (anyone remember FreeWWWEB?).
 
Back to my point!
 
Along-side the release of one the shining products of NetworkDLS (the long-awaited "Fortitude HTTP Server" for windows Vista, Windows 7 and Windows 2008 Server x64) – I am releasing the oldest stable version of the server software here, far, far, away from the business end of the NetworkDLS home page.
 
For you emboldened few, I present to you: NetworkDLS HTTP Server 1.0.0.2
 
Please enjoy, and feel free to play with this antique server off-line! It was a modeled after Xitami after all...

Tags: , ,

Miscellaneous

It's not volatile, this is critical

by Josh Patterson 13. June 2010 12:03

For far too long I have listened to the likes of those who believe that the volatile modifier is the end-all answer to multi-threaded development.
 
Parallelism is complex! Given its complexity, it should not be "screwed with" by any person not understanding the difference between "mutual-exclusion synchronization" and "volatile variables" (CPU cache optimization exclusion).
 
Take the example below, read it, study it, compile it and run it. Once you have satisfied yourself that it will always print the number 5,000,000 to the console as its only output, I would then like you to remove the [EnterCriticalSection] / [LeaveCriticalSection] pair and give the application another run - amazed yet?
 
How can this be, I mean I'm using the volatile modifier? (please note the knee deep puddle of sarcasm)

//-------------------------------------------------------------------------------
#include "windows.h"
#include "stdio.h"
//-------------------------------------------------------------------------------

volatile unsigned int iGlobalCounter = 0;
CRITICAL_SECTION CS;

//-------------------------------------------------------------------------------

DWORD WINAPI threadProc(LPVOID pData)
{
 HANDLE hEvent = (HANDLE) pData; //The synchronization object handle.

 //Increment [iGlobalCounter] 1 million times.
 for(unsigned int i = 0; i < 1000000; i++)
 {
  //Comment out [EnterCriticalSection] and [LeaveCriticalSection],
  // which control access to iGlobalCounter to see this
  // multi-threaded example go awry.

  EnterCriticalSection(&CS);
  iGlobalCounter++;
  LeaveCriticalSection(&CS);
 }

 //Set the synchronization object to let the calling
 // thread know that this workload is complete.
 SetEvent(hEvent);

 return 0;
}

//-------------------------------------------------------------------------------

void IncrementGlobalCounter(void)
{
 int iThreadCount = 5; //Create 5 threads.
 char sEventName[255];
 HANDLE *hEvent = NULL;

 //Allocate enough RAM to hold "wait event" handles for each thread.
 if(!(hEvent = (HANDLE *) calloc(sizeof(HANDLE), iThreadCount)))
 {
  printf("Memory allocation error.\n");
  return;
 }

 //Initialize a critical section object.
 memset(&CS, 0, sizeof(CS));
 InitializeCriticalSection(&CS);

 for(int iThread = 0; iThread < iThreadCount; iThread++)
 {
  //Create a unique event name for a synchronization object.
  sprintf(sEventName, "Thread_Event_%d_%d", GetTickCount(), iThread);

  //Create a synchronization object.
  hEvent[iThread] = CreateEvent(NULL, FALSE, FALSE, sEventName);

  //Create a thread, passing it the handle to the synchronization object.
  CreateThread(NULL, 0, threadProc, hEvent[iThread], 0, NULL);
 }

 //Wait on all threads to complete.
 WaitForMultipleObjects(iThreadCount, hEvent, TRUE, INFINITE);

 //Clean up the allocated RAM and the critical section object.
 free(hEvent);
 DeleteCriticalSection(&CS);
}

//-------------------------------------------------------------------------------

int main(int argc, char *argv[])
{
 //CPU Count and CPU Affinity validation
 SYSTEM_INFO SI;
 memset(&SI, 0, sizeof(SI));
 GetSystemInfo(&SI);

 if(SI.dwNumberOfProcessors < 0)
 {
  printf("Your must have more than one CPU core for this example.\n");
  return 1;
 }

 //Lets perform the whole test 10 times.
 for(int iTestNumber = 0; iTestNumber < 10; iTestNumber++)
 {
  iGlobalCounter = 0; //Reset the counter to zero.
 
  IncrementGlobalCounter(); //Increment the global counter.

  //Output the totaled global counter. It should
  // be 5,000,000 ([5 threads] * [1 million increments]).
  printf("iGlobalCounter: %d\n", iGlobalCounter);
 }

 system("Pause");
}

//-------------------------------------------------------------------------------

Tags: , , ,

Miscellaneous

64-bit development… it's no fad.

by Josh Patterson 12. June 2010 12:02

Have you been thinking about porting that existing 32-bit application to a native 64-bit application?
 
There are things to consider before jumping in head first and I’ll go over some of my experiences in this seemingly daunting endeavor.
 
First things first, mental preparation! In the spirit of preparation and a sprinkling of encouragement, I’ll say this despite information found elsewhere: "Porting existing applications to 64-bits really isn’t all that difficult as long as you start your journey with a project that at least loosely followed nearly any recognized coding practice".
 Things to keep in mind:
 
Registry:
 
On a 32-bit machine your registry entries should go into "HKEY_LOCAL_MACHINE\Software\" or "HKEY_CURRENT_USER\Software\". However, if the same 64-bit application is installed on a 64-bit machine (running a 64-bit version of Windows) then your registry entries will automaticly be placed into "HKEY_LOCAL_MACHINE\Software\ Wow6432Node\" or "HKEY_CURRENT_USER\Software\Wow6432Node\". In addition, your application will also be directed at this key when it reads from the registry.
 
This is all handled for you automatically by the underlying windows API and is (for the lack of a better term)... child’s-play. Unless you happen to have an product which contains multiple executables of which are a mix of 32 and 64-bit components - accessing the same common registry key can become challenging in this case.
 
Program Files:
 
The same principal that applies to the registry also applies to the "C:\Program Files\" directory with the exception that the naming convention what no adhered. On a 64-bit version of windows 64-bit applications will access the "C:\Program Files\" directory, while 32-bit applications will automatically be directed to "C:\Program Files (x86)\".
 
This also applies to the "Common Files" location which is traditionally stored under the program files directory. 64-bit versions of Windows will contain two "Common Files" in two separate locations: One under "C:\Program Files\" and one under "C:\Program Files (x86)\" – respectfully.
 
Other Folders:
 
You find this method of directlry redirection being applied across the OS in many different locations, such as "C:\Windows\System32" to "C:\Windows\SysWOW64". Yes! Thats right, the 64bit files are under "C:\Windows\System32", while the 32-bit files are under "C:\Windows\SysWOW64". - It makes perfect sense to me too!
 
Integers:
 
Or size_t to be specific. Once you switch your compiler over to 64-bit, you find that any standard function that returns the size of anything is now 64-bits. This includes functions like strlen(). However, while you can now pass strings with 18 quintillion characters to strlen(),I highly recommend that you keep track of the length of excessively long strings using other methods.
 Think about this for a minute. Are you going to change that existing use of strlen() to now set a 64-bit integer or are you going to typecast the return value to a 32 bit integer.
 
Pointers:
 
Pointers are now 64-bits. This means larger addressable spaces but could also mean that you have to change some existing bad code. For example, a dialog call back as follows works greate in 32-bit environments but is catastrophic in 64-bit environments.


//Works great in 32-bit but explodes when compiled as 64-bit
// because it returns a 32-bit value and the result can be truncated.
int CALLBACK dialogProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
. . .
}

DialogBox(GetAppInstance(), MAKEINTRESOURCE(IDD_TEMPLATE), NULL, dialogProc);

//This works great in both 32-bit and 64-bit
// because it returns a 64-bit value.
INT_PTR CALLBACK dialogProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
. . .
}

DialogBox(GetAppInstance(), MAKEINTRESOURCE(IDD_TEMPLATE), NULL, dialogProc);

 Shell Integration and Explorer Context Menus:
 
You cannot load a 64-bit shell extension into Windows Explorer using a 32-bit OS (for painfully obvious reasons), but you also cannot load that 32-bit shell extension into Windows Explorer when using a 64-bit OS. Why? Because the explorer executable is 64-bits and cannot load a 32-bit DLL.

Tags: , , , ,

Miscellaneous

About the author

My goal is to expand my horizons and to seek knowledge wherever it may lie. I am motivated by sheer yearning and interest in the technical & complex.

Please note however; that "interest" does not necessarily imply understanding.

Month List

Page List