Win32 Programming Features

by Marshall Brain

Return to Home Page

 

The following is an index of the material presented in this series:

To compile the code in these tutorials under Visual C++, take the following steps:

  1. Start Visual C++. Close all files and projects if any are open.
  2. Choose the New option in the File menu. Create a new Project.
  3. In the Project Type combo box choose Console Application. Type in the name of the project and create a new directory if you like. Click the OK button to create the project.
  4. You will see a "Project Files" dialog. Close it.
  5. Select the New option in the Files menu again. This time create a new code/text file. Type the source code into this file. Save the file and make sure you give it a CPP extension.
  6. Select the Files option in the Project menu. Add the new code file to the project.
  7. Choose the Build option in the Project menu, and then Execute.

Threads

Threads are one of the most exciting and useful features in the Win32 API. Threads let you break up a single program into multiple threads of execution. In this article you will learn the basic concepts used to create threads in any Win32 program.

The Possibilities

There are a number of ways that you might use threads in your own programs. Here are some ideas:

Introduction

If you are moving to NT from UNIX, VMS, or a mainframe, then multi-processing is a familiar concept. Multi-threading may be new to you however. If you are moving from MS-DOS, then both multi-processing and multi-threading are probably new to you. Let's start by looking at what multi-processing and multi-threading actually mean from an operating system standpoint.

The MS-DOS operating system is a single-process operating system. It can run one program at a time. You load a program, work with it, quit it, and then run another. TSRs can, in certain situations, give an impression of multi-processing, but the problems that TSRs normally cause show that they are at best an illusion that MS-DOS was never intended to support.

Microsoft Windows 3.1, as well as Apple's Macintosh system, are cooperative multi-tasking operating environments. Both can run multiple programs (processes), at the same time. For example, you can run a word processor in one window, a spreadsheet in another window, and download a file from a BBS in a third window. The word cooperative is used because it is up to each program to properly relinquish control at appropriate times so that all of the processes appear to be working simultaneously. Cooperative multi-tasking works to some degree. However, a lengthy disk access or other undividable task performed by one program will tend to monopolize the entire system for a moment and the cooperation breaks down. This makes cooperative multi-tasking systems seem jerky in many cases. If one program locks up, the whole system often dies with it. As soon as one program locks, it cannot relinquish control to the others and everything stops.

UNIX is a preemptive multi-tasking operating system. The operating system, rather than individual applications, is in charge of giving CPU time to all of the running processes and it does so as it best sees fit. UNIX gives a process a time slice of CPU time--perhaps 20 milliseconds or so--and when that amount of time expires the operating preempts the process and gives the next slice of CPU time to another process. A UNIX machine can therefore have literally hundreds of processes running at one time and still feel very smooth to the user. If one process locks it has no effect on the others because the operating system is still in control of slicing up the CPU time.

Windows NT (and Windows 95) is a preemptively multi-tasking, multi-threaded operating system. Because it uses preemptive multi-tasking, it shares with UNIX the same smoothness of operation and process independence. Multi-threading goes one step further however. An individual program by default contains one thread, but it can break itself into several independent threads of execution so that, for example, one thread of an application can send a file to the printer while another is responding to user input. This simple change in the a program's design can significantly reduce any waiting that the user normally has to worry about during lengthy recalculations, screen painting, file reading and writing, etc.

Multi-threading also lets you take advantage of multiple CPUs available in many high-end NT machines. Say, for example, that you purchase an advanced RISC machine capable of using up to 10 CPU chips, but initially you purchase only one CPU for it. As part of your learning cycle you write a simple Mandelbrot set program, and you find that for a window of a certain size it takes 15 seconds to redraw the image of the Mandelbrot set.

Now you add nine more CPU chips to the machine. When you rerun the Mandelbrot program, you will find that it still takes almost 15 seconds to execute. NT has the ability to run different threads on different CPUs, but it cannot do anything with a single threaded program but devote one CPU to it. There is no way for NT to divide a single thread across CPUs. Since NT is itself multi-threaded, the Mandelbrot program will speed up slightly because it is not competing with NT's system threads for CPU time. However, any one program cannot harness more than one tenth of the CPU power in a 10-CPU machine unless it is multi-threaded.

If you multi-thread your Mandelbrot program, NT can run the separate threads on separate CPUs, and this allows it to take full advantage of all of the available CPU power. For example, if the Mandelbrot program breaks itself into 10 threads, then one thread will run on each CPU and the program will run ten times faster. There is no reason to use more than ten threads on a 10-CPU machine, because each thread incurs a very slight amount of overhead and it is therefore wasteful to have more than 10. However, you could break the program into 100 threads if you like, or use one thread for each scan-line of the drawing, if that makes things conceptually easier for you in a certain application. There are many cases where breaking an application into multiple threads actually makes the whole program much easier to understand, and threads turn out to be remarkably easy to create.

Processes in NT are not nearly as interesting as threads. A program can create a separate process, but the new process is completely and totally separate from the original program. Unlike UNIX, where a new process obtains a complete copy of the variable space of the original process, a new process in NT can at most inherit copies of specifically indicated handles. Generally you use a process when you want to start another independent program from within a running program. For example, if you wanted to write your own version of the Program Manager or File Manager, you would use processes to launch other applications from your program.

When any process starts in NT, it by default contains one thread of execution. For example, when you type "notepad" on the command line or double click on notepad's icon in the Program Manager, NT creates a process, and that process has one thread that "runs" notepad's code. The process is essentially a container for the global variables, environment strings, the heap owned by the application, and the thread. The thread is what actually executes the code.

All threads in one process share the variable space of their parent process. Each thread also has its own stack. When you create a new thread within a process, it has access to all of the global variables and the heap of the parent process. All of the problems that arise from the careless use of globals in a normal program are therefore compounded in a multi-threaded program, because now several different threads can modify the same global variables independently of one another. To solve the problems that tend to arise in such situations, there are synchronization mechanisms built in to the Win32 API that help you to guarantee exclusive access to global values.

Simple Example 1

For many people, the whole idea of creating multiple threads within a single process requires some mental adjustment. Let's start by looking at several extremely simple examples to see how threads fundamentally work in the Win32 API.

The code below contains an extremely simple single-thread program. In this program, the code prints the value of the global variable named count each time the user presses the return key. Nothing in the program changes count, so the program always prints 0. There is no magic here:


#include <windows.h>
#include <iostream.h>

UINT count;

void main(void)
{
  CHAR retStr[100];

  count=0;
  while(1)
  { 

    cout

     <<  "Press <ENTER>  to display the count... ";

    cin.getline(retStr, 100);

    cout <<  "The count is: " << count <<  endl

      <<  endl;

  }

}

 

Now let's add a thread to the code, as shown below. A thread in NT is simply a function that executes in the background. The function CountThread in the code below increments the global variable named count and then sleeps for 100 milliseconds. When you run the program, you will find that each time you press the enter key, count has increased. The thread runs in the background incrementing count, while the original thread is responding to user input. The program is doing two things at once.


#include <windows.h>
#include <iostream.h>
volatile UINT count;
void CountThread()
{
  while(1)
  {
    count++;
    Sleep(100);
  }
}
void main(void)
{
  HANDLE countHandle;
  DWORD threadID;
  CHAR retStr[100];
  count=0;
  // create a thread which
  // executes the "CountThread" function
  countHandle=CreateThread(0, 0, (LPTHREAD_START_ROUTINE) CountThread, 0, 0, &threadID);

  if (countHandle==0)
    cout <<  "Cannot create thread: "
      <<  GetLastError() <<  endl;

  while(1)
  {
    cout
      <<  "Press <ENTER>  to display the count... ";
    cin.getline(retStr, 100);
    cout <<  "The count is: " <<  count <<  endl
      <<  endl;
  }
}

The arrangement of the main thread, the counting thread, and the global variable in the previous listing is shown below:

The code starts by creating the thread using the CreateThread function (see the Win32 on-line help files in VC++ version 2.0 or the NT SDK for a description of this function). The CreateThread function accepts the name of the thread function, the function to execute in the new thread. Here, the thread function is CountThread. The thread function can optionally accept one four-byte parameter. In the listing above the thread function uses no parameters, but if it did the parameter that the thread function receives is passed to CreateThread in the param parameter, and CreateThread in turn passes it on to the thread function. CreateThread returns both a thread ID and a handle to the thread. The thread ID is used to uniquely identify the thread system-wide, and is also accepted by a few functions such as AttachThreadInput.

You can control the initial size of the thread's stack using the stack parameter. Setting stack parameter to 0 causes the thread to start with a stack of the same size as its parent thread. The stack will grow as necessary, but growth is a wasteful activity so if you know that the thread will need a certain amount of space in its stack because of the size of its local variables, you should try to accommodate that from the start. It is also possible to start a thread so that it is originally suspended. In this case, the thread will consume no CPU time until some other thread unsuspends it with ResumeThread.

The Sleep function in the above listing provides an efficient way for a thread to delay itself. In a previous life you may have used a for loop or some similar mechanism to cause a delay, but as you know, for loops are unreliable and also extremely wasteful. As the for loop is spinning it consumes CPU cycles that could be put to better use. The Sleep function lets a thread delay itself without consuming any CPU cycles. A thread is totally suspended during a sleep.

Note the use of the volatile modifier on the count global variable. If you comment out the Sleep function in the thread and remove the volatile modifier, count may always be zero when you press the enter key. This phenomenon occurs because of strange side effects resulting from compiler optimizations. The compiler, for example, may use a register to store the count variable in the main thread, so changes to the value by the second thread are ignored. The volatile modifier is a way of telling the compiler that you do not want any optimizations applied to the variable, that you do not want it placed in a register, and that the value may change due to outside influences during evaluation. You will find it to be a very important modifier for global variables referenced by multiple threads.

Simple Example 2

The code shown below is another simple example of a background thread running separately from the main thread. This thread beeps in the background while the main thread waits for it to finish. The code shows how to pass an integer parameter to a thread, and also how to wait for a thread to complete.


// thread1.cpp
#include <windows.h>
#include <stdlib.h>
#include <iostream.h>

// The function to run in a thread
void HonkThread(DWORD iter)
{
  DWORD i;

  for (i=0; i < iter; i++)
  {
    Beep(200, 50);
    Sleep(1000);
  }
}

void main(void)
{
  HANDLE honkHandle;
  DWORD threadID;
  DWORD iterations;
  CHAR iterStr[100];
  cout <<  "Enter the number of beeps to produce: ";

  cin.getline(iterStr, 100);

  // convert string into integer
  iterations=atoi(iterStr);

  // create a thread which
  // executes the "HonkThread" function
  honkHandle=CreateThread(0, 0, (LPTHREAD_START_ROUTINE) HonkThread, (VOID *) iterations, 0, &threadID);

  // wait until the thread has finished
  int count=0;

  while ( WaitForSingleObject(honkHandle, 0) == WAIT_TIMEOUT)
  {
    cout
      <<  "waiting for the thread to finish "
      <<  count++

      <<  endl;
  }
}

When you run the above code, you should enter an integer value such as 5 when the program requests it. The main program will start the thread, passing it the value 5 through the parameter. The thread will run in the background, beep five times, and quit. Meanwhile, the main program is waiting for the thread to quit using the WaitForSingleObject function in a loop. Each time through the loop it increments an integer and prints it to stdout.

The WaitForSingleObject function is waiting on the thread handle for the thread to terminate. The value 0 passed in as the timeout value causes WaitForSingleObject to return immediately and indicate that either the thread has or has not completed. If the thread has not completed, WaitForSingleObject returns the value WAIT_TIMEOUT. In this configuration, we are using WaitForSingleObject simply to detect whether or not the background thread has finished.

It is possible to pass structures into a thread function by passing a pointer to the structure through the parameter, as shown below. The structure should be stable. That is, it should be a global variable, a static local variable, or allocated from the heap. The structure should not be a local variable of a function that might cease to exist during the run of the thread.

  
#include <windows.h>
#include <stdlib.h>
#include <iostream.h>

typedef struct
{
  DWORD frequency;
  DWORD duration;
  DWORD iterations;
} honkParams;

void HonkThread(honkParams *params)
{
  DWORD i;
  for (i=0; i < params->iterations; i++)
  {
    Beep(params->frequency, params->duration);
    Sleep(1000);
  }
}

void main(void)
{
  HANDLE honkHandle;
  DWORD threadID;
  honkParams params;
  CHAR freqStr[100];
  CHAR durStr[100];
  CHAR iterStr[100];

  cout <<  "Enter the beep frequency to produce: ";
  cin.getline(freqStr, 100);
  params.frequency=atoi(freqStr);
  cout <<  "Enter the beep duration to produce: ";
  cin.getline(durStr, 100);
  params.duration=atoi(durStr);
  cout <<  "Enter the number of beeps to produce: ";
  cin.getline(iterStr, 100);
  params.iterations=atoi(iterStr);

  // create a thread and pass it the address of
  //the "params" structure
  honkHandle=CreateThread(0, 0,(LPTHREAD_START_ROUTINE) HonkThread, &params, 0, &threadID);
  WaitForSingleObject(honkHandle, INFINITE);
}

In the above code, the three values entered by the user are placed in a structure that is then passed to the thread. The main function calls WaitForSingleObject to keep from terminating before the thread has completed. Without this call, the main function would return immediately, killing off both the main process and the threads.

Simple Example 3

The code below goes step further, and shows that it is possible to create multiple background threads using either multiple thread functions or a single thread function that is called several times.

  
#include <windows.h>
#include <stdlib.h>
#include <iostream.h>

typedef struct
{
  DWORD frequency;
  DWORD duration;
  DWORD iterations;
} honkParams;

void HonkThread(honkParams *params)
{
  DWORD i;

  for (i=0; i < params->iterations; i++)
  {
    Beep(params->frequency, params->duration);
    Sleep(1000);
  }

  GlobalFree(params);
}


void main(void)


{


  HANDLE honkHandles[3];


  DWORD threadID;


  honkParams *params;


  DWORD count;


  CHAR freqStr[100];


  CHAR durStr[100];


  CHAR iterStr[100];





  for (count=0; count < 3; count++)


  {


    // allocate memory for a "params" structure


    params=(honkParams *) GlobalAlloc(GPTR,


      sizeof(honkParams));





    cout <<  "Enter the beep frequency: ";


    cin.getline(freqStr, 100);


    params->frequency=atoi(freqStr);





    cout <<  "Enter the beep duration: ";


    cin.getline(durStr, 100);


    params->duration=atoi(durStr);





    cout <<  "Enter the number of beeps: ";


    cin.getline(iterStr, 100);


    params->iterations=atoi(iterStr);





    // create a thread and pass it the pointer


    // to its "params" struct


    honkHandles[count]=CreateThread(0, 0,


      (LPTHREAD_START_ROUTINE) HonkThread,


      params, 0, &threadID);


  }

  // wait for all threads to finish execution


  WaitForMultipleObjects(3, honkHandles,


    TRUE, INFINITE);


}


When you run the above code, the program asks you to enter a frequency and duration as well as the number of beeps. You can do this three times, so you will hear beeps from all three threads simultaneously if you set the number of beeps for each thread high enough to cause an overlap.

In the above code, a wait function is again used to keep the main function, and therefore the process, from completing before all three threads have finished. The WaitForMultipleObjects function does the same thing that WaitForSingleObject does, but it waits for all of the specified events to occur. WaitForMultipleObjects accepts an array of object handles, in this case handles from the three threads.

Conclusion

You can see from the examples given above that the use of threads, at least at a simple level, is not much more difficult than calling a normal function. The thread function executes as you expect, but it returns immediately and executes in the background, in parallel with the main thread of the application.

Return to Top


Services

Every operating system needs a way to execute background tasks that run continuously regardless of who is using the machine. These background tasks can perform various services important to the system or its users. For example, a messaging system might monitor the network and display a dialog box whenever it receives a message from another machine. An application that sends and receives faxes needs to start up at boot time and then continuously monitor the fax modem for fax machines dialing in. A home or office security program, or code that controls a piece of test equipment, may need to poll sensors periodically and respond to them when appropriate. All of these tasks require CPU time to perform their jobs, but should not affect a user working at the keyboard because they require so little of the total CPU power available.

In MS-DOS, background tasks like these are handled by Terminate and Stay Resident (TSR) programs. These programs are started in the autoexec.bat file. In UNIX, background tasks are handled by Daemons. At the very end of a UNIX machine's boot process you can see the operating system start up things like the Cron and Finger daemons before the system lets the first user log in. In Windows NT, background tasks are called services. Services start automatically when NT boots and remain running in the background regardless of who is logged in.

Windows NT services are implemented as normal executables, but follow a very specific protocol internally that allows them to interact properly with the Service Control Manager (SCM). In this article, you will learn how to create and install simple Win32 services in Windows NT. Once you understand simple services, it is easy to build your own because all services, no matter how complicated, must contain the same basic SCM interface code. Once the requirements of the SCM are met however, there is no real difference between the executable for a service and a regular program.

A good working knowledge of NT services is important to both programmers and system administrators. Programmers obviously benefit because they can create their own services. The benefit to administrators is more subtle, but equally important. Background tasks, in general, can be dangerous. Both MS-DOS and Macintosh systems make such good viral hosts because, through their lack of security, they allow any person or program to create background tasks at any time. Windows NT and UNIX systems are secure, so only an administrator can add background tasks to the system. However, if the administrator adds a destructive background task, then it is free to do its damage. When administrators understand the mechanisms and privileges available to Windows NT services, it is possible for them to be more selective in installing potentially harmful background tasks.

Basic Concepts

Services come in two different varieties. Driver services use device driver protocols to interface NT to specific pieces of hardware. Win32 services, on the other hand, implement general background tasks using the normal Win32 API. This article focuses on Win32 services because of their general utility and ease of creation. Any NT programmer with the normal NT SDK (or Visual C++) and administrative access to an NT machine can implement and install his or her own Win32 services. Any time that you want to create any type of program that starts at boot time and runs continuously as a background task in Windows NT, you will want to use a Win32 service.

Services expose themselves in NT's user interface through the Control Panel. There you will find a Services Applet that displays a list of all available Win32 services. This applet lets you start, stop, pause and resume services. A second dialog, accessed by pressing the Startup button in the Services Applet, lets you change the startup behavior as well as the default account used by the service. A service can start automatically at boot time, it can be totally disabled, or it can be set to start manually. When starting a service manually, a user can supply startup parameters. You need to be logged in as the administrator or a power user to do anything with the Services applet.

Windows NT ships with a number of pre-installed services that handle such things as network messaging, command scheduling with the "at" command, and distributed RPC naming. When you create your own services, you must perform a separate installation step to insert them into the list managed by the services applet. The installation process adds information about a new service--its name, the name of its executable, its startup type, etc.--into the registry so that the SCM knows about the new service the next time the machine boots.

Creating a New Service

A program that acts as a service is a normal EXE file, but it must meet special requirements so that it interfaces properly with the SCM. Microsoft has carefully choreographed the flow of function calls, and you must follow that plan closely or the service will not work. The requirements are listed below. You will find descriptions of the functions discussed here in the Win32 programmer reference manuals, or in the on-line help files for Win32 in the SDK or Visual C++:

  • The service's code must have a normal main or WinMain function. This function should immediately call the StartServiceCrtlDispatcher function. By calling this function, you give the SCM a pointer to a ServiceMain function to call when it wants to start the service.
  • The SCM calls the ServiceMain function when it wants to start the service. For example, if the administrator presses the Start button in the Services applet, then the SCM will execute the ServiceMain function in a separate thread. ServiceMain should call the RegisterServiceCtrlHandler function, which registers a Handler function with the SCM for it to call with control requests. You can name the Handler function anything you like, but it is listed in the documentation under Handler. The RegisterServiceCtrlHandler function returns a handle that the service can use when it wants to send status messages to the SCM.
  • The ServiceMain function must also start the thread that does the actual work of the service itself. The ServiceMain function should not return until it is time for the service to stop. When it returns, the service has stopped.
  • The Handler function contains a switch statement that parses control requests received from the SCM. By default the SCM can send any of the following control constants:
  •  
    • SERVICE_CONTROL_STOP - Tells the service to stop.
    • SERVICE_CONTROL_PAUSE - Tells the service to pause.
    • SERVICE_CONTROL_CONTINUE - Tells the service to resume.
    • SERVICE_CONTROL_INTERROGATE - Tells the service to immediately report its status.
    • SERVICE_CONTROL_SHUTDOWN - Tells the service that shutdown is imminent.

It is also possible to create custom constants (with values between 128 and 255) and send them through the SCM to the service.

When you create an EXE that contains the main, ServiceMain, and Handler functions described above, as well as a function that contains the thread for the service itself, you have a complete service. The figure below summarizes the interactions between these different functions and the SCM:

Listing 1 shows the simplest service possible. This service simply beeps. By default it beeps every two seconds. Optionally you can modify the beep interval with startup parameters. This service is complete in that it will appropriately respond to the SCM for every control signal possible. Because of that, this program can act as a good template for creating your own services.

The main function calls StartServiceCtrlDispatcher to register the ServiceMain function. The registration is performed using an array of SERVICE_TABLE_ENTRY structures. In this case, the program contains just one service, so there is only one entry in the table. However, it is possible for there to be several services in a single EXE file, and in that case the table identifies the appropriate ServiceMain function for each. It is possible to put initialization code in the main function prior to the call to StartServiceCtrlDispatcher, but this code must complete in less than 30 seconds. If it takes longer, the SCM aborts the service on the assumption that something went wrong.

The ServiceMain function gets called when the SCM wants to start the service either during the boot process or because of a manual start. ServiceMain always contains the following steps:

  1. It immediately calls RegisterServiceCtrlHandler to register the Handler function with the SCM as this service's Handler function.
  2. It then calls the SendStatusToSCM function. It does this to notify the SCM of progress. The fourth parameter is a "click count" value, which increments each time the program updates the status. The SCM and other programs can look at the click count and see that progress is being made during initialization. The last parameter is a "wait hint", which tells the SCM how long (in milliseconds) it should expect to wait before the click count next gets updated.
  3. ServiceMain next creates an event that will be used at the bottom of the function to prevent it from returning until the SCM issues a STOP request.
  4. Next, ServiceMain checks for startup parameters. These parameters can be passed in by the user during a manual start, using the Startup Parameters line in the Service applet. Any parameters come into the ServiceMain function in an argv-style array.
  5. If your service needs to perform other initialization tasks, they should go here just prior to the call to InitService.
  6. The ServiceMain function next calls the InitService function, which will start the thread that does the actual work of the service. If this call succeeds, then ServiceMain lets the SCM know that the service has successfully started.
  7. ServiceMain now calls WaitForSingleObject, which waits efficiently for the terminateEvent event object to be set. This object gets set in the Handler function. Once it does get set, ServiceMain calls the terminate function to clean up, and then returns to stop the service.

As you can see, there is not a lot of flexibility in this function. With the exception of step 5, you must perform each of the tasks mentioned in the order mentioned for the service to start properly.

The terminate function cleans up any open handles, and sends a status message to the SCM to tell it that the service is stopped.

The SCM calls the Handler function whenever it wants to pause, resume, interrogate or stop the service. To stop the service, the handler sets terminateEvent. By doing this, it causes ServiceMain, which is executing as a separate thread, to terminate and return. Once ServiceMain returns the service is stopped.

The SendStatusToSCM function consolidates all of the statements necessary to send the service's current status to the SCM.

The InitService function gets called by ServiceMain when it needs to start the service's thread. This function calls CreateThread to create a new thread for the service.

The ServiceThread function contains the actual work that is to be performed by the service. In this case, the thread consists of an infinite loop that beeps and then sleeps for a pre-determined interval. When creating your own services, you can place any code that you like in this thread, calling either Win32 functions or your own functions.

Installing and Removing Services

In order to use the beep service described in the previous section, you have to install it. Installation makes the SCM aware of the service, and causes the SCM to add it to the list of services that appears in the Services applet of the Control Panel. The code shown in Listing 2 demonstrates how to install a service.

Listing 2 starts by opening a connection to the SCM using the OpenSCManager function. In the call to OpenSCManager, you must specify what you want to do so that the SCM can validate that activity. If the account you are logged in under does not have sufficient privilege, then the call will return NULL.

The call to CreateService actually installs the new service. It uses the pointer to the SCM returned by OpenSCManager, the name, label and EXE file specified on the command line, along with a set of standard parameters to fill in all of the other values. The use of SERVICE_WIN32_OWN_PROCESS indicates that the service's EXE file contains just one service, and SERVICE_DEMAND_START indicates that the service is manual start rather than automatic start. A typical invocation of the install program at the command line is shown below:

install BeepService "Beeper" c:\winnt\beep.exe

The first parameter specifies the name of the service used internally by the SCM. This name is later used to remove the service. The second parameter specifies the label used to display the service in the Services applet. The third parameter gives the fully qualified path to the service's executable. After you install the service, start it using the Services applet in the Control Panel. Look up any error codes in the on-line help file for the Win32 API.

To remove a service, you follow the steps shown in Listing 3. It starts by opening a connection to the SCM, and then opens a connection to the service using the OpenService function. Listing 3 next queries the service to find out if it is currently stopped. If it is not, it stops it. The DeleteService function removes the service from the Services applet in the Control Panel. A typical invocation of the removal program shown in Listing 3 would look like this:

remove BeepService

If desired, you can immediately reinstall the service.

Conclusion

Services are an essential part of Windows NT because they allow you to extend the operating system. Using the code in Listing 1 as a template, you will find that it is extremely easy to create new services of your own.

Listing 1

Code that implements the simplest possible NT service




//***************************************************************


// From the book "Win32 System Services: The Heart of Windows NT"


// by Marshall Brain


// Published by Prentice Hall


//


// This code implements the simplest possible service.


// It beeps every 2 seconds, or at a user specified interval.


//***************************************************************


// beepserv.cpp





#include <windows.h>


#include <stdio.h>


#include <iostream.h>


#include <stdlib.h>





#define DEFAULT_BEEP_DELAY 2000





// Global variables





// The name of the service


char *SERVICE_NAME = "BeepService";


// Event used to hold ServiceMain from completing


HANDLE terminateEvent = NULL;


// Handle used to communicate status info with


// the SCM. Created by RegisterServiceCtrlHandler


SERVICE_STATUS_HANDLE serviceStatusHandle;


// The beep interval in ms.


int beepDelay = DEFAULT_BEEP_DELAY;


// Flags holding current state of service


BOOL pauseService = FALSE;


BOOL runningService = FALSE;


// Thread for the actual work


HANDLE threadHandle = 0;





void ErrorHandler(char *s, DWORD err)


{


	cout <<  s <<  endl;


	cout <<  "Error number: " <<  err <<  endl;


	ExitProcess(err);


}





// This function consolidates the activities of


// updating the service status with


// SetServiceStatus


BOOL SendStatusToSCM (DWORD dwCurrentState,


	DWORD dwWin32ExitCode,


	DWORD dwServiceSpecificExitCode,


	DWORD dwCheckPoint,


	DWORD dwWaitHint)


{


	BOOL success;


	SERVICE_STATUS serviceStatus;





	// Fill in all of the SERVICE_STATUS fields


	serviceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;


	serviceStatus.dwCurrentState = dwCurrentState;





	// If in the process of doing something, then accept


	// no control events, else accept anything


	if (dwCurrentState == SERVICE_START_PENDING)


		serviceStatus.dwControlsAccepted = 0;


	else


		serviceStatus.dwControlsAccepted =


			SERVICE_ACCEPT_STOP |


			SERVICE_ACCEPT_PAUSE_CONTINUE |


			SERVICE_ACCEPT_SHUTDOWN;





	// if a specific exit code is defined, set up


	// the win32 exit code properly


	if (dwServiceSpecificExitCode == 0)


		serviceStatus.dwWin32ExitCode = dwWin32ExitCode;


	else


		serviceStatus.dwWin32ExitCode =


			ERROR_SERVICE_SPECIFIC_ERROR;


	serviceStatus.dwServiceSpecificExitCode =


		dwServiceSpecificExitCode;





	serviceStatus.dwCheckPoint = dwCheckPoint;


	serviceStatus.dwWaitHint = dwWaitHint;





	// Pass the status record to the SCM


	success = SetServiceStatus (serviceStatusHandle,


		&serviceStatus);


	return success;


}





DWORD ServiceThread(LPDWORD param)


{


	while (1)


	{


		Beep(200,200);


		Sleep(beepDelay);


	}


	return 0;


}





// Initializes the service by starting its thread


BOOL InitService()


{


	DWORD id;





	// Start the service's thread


	threadHandle = CreateThread(0, 0,


		(LPTHREAD_START_ROUTINE) ServiceThread,


		0, 0, &id);





	if (threadHandle==0)


		return FALSE;


	else


	{


		runningService = TRUE;


		return TRUE;


	}


}





// Dispatches events received from the SCM


VOID Handler (DWORD controlCode)


{


	DWORD currentState = 0;


	BOOL success;





	switch(controlCode)


	{


		// There is no START option because


		// ServiceMain gets called on a start





		// Stop the service


		case SERVICE_CONTROL_STOP:


		  // Tell the SCM what's happening


		  success = SendStatusToSCM(SERVICE_STOP_PENDING,


		  	NO_ERROR, 0, 1, 5000);


		  runningService=FALSE;


		  // Set the event that is holding ServiceMain


		  // so that ServiceMain can return


		  SetEvent(terminateEvent);


		  return;





		// Pause the service


		case SERVICE_CONTROL_PAUSE:


		  if (runningService && !pauseService)


		  {


			  // Tell the SCM what's happening


			  success = SendStatusToSCM(


			  SERVICE_PAUSE_PENDING,


			  NO_ERROR, 0, 1, 1000);


			  pauseService = TRUE;


			  SuspendThread(threadHandle);


			  currentState = SERVICE_PAUSED;


			}


			break;





		// Resume from a pause


		case SERVICE_CONTROL_CONTINUE:


			if (runningService && pauseService)


			{


			  // Tell the SCM what's happening


			  success = SendStatusToSCM(


			  SERVICE_CONTINUE_PENDING,


			  NO_ERROR, 0, 1, 1000);


			  pauseService=FALSE;


			  ResumeThread(threadHandle);


			  currentState = SERVICE_RUNNING;


			}


			break;





		// Update current status


		case SERVICE_CONTROL_INTERROGATE:


		// it will fall to bottom and send status


			break;





		// Do nothing in a shutdown. Could do cleanup


		// here but it must be very quick.


		case SERVICE_CONTROL_SHUTDOWN:


			return;





		default:


 			break;


	}


	SendStatusToSCM(currentState, NO_ERROR, 0, 0, 0);


}





// Handle an error from ServiceMain by cleaning up


// and telling SCM that the service didn't start.


VOID terminate(DWORD error)


{


	// if terminateEvent has been created, close it.


	if (terminateEvent) CloseHandle(terminateEvent);





	// Send a message to the scm to tell about stopage


	if (serviceStatusHandle)


		SendStatusToSCM(SERVICE_STOPPED, error,


			0, 0, 0);





	// If the thread has started, kill it off


	if (threadHandle) CloseHandle(threadHandle);





	// Do not need to close serviceStatusHandle


}





// ServiceMain is called when the SCM wants to


// start the service. When it returns, the service


// has stopped. It therefore waits on an event


// just before the end of the function, and


// that event gets set when it is time to stop.


// It also returns on any error because the


// service cannot start if there is an eror.


VOID ServiceMain(DWORD argc, LPTSTR *argv)


{


	BOOL success;





	// immediately call Registration function


	serviceStatusHandle =


	RegisterServiceCtrlHandler(


	SERVICE_NAME, (LPHANDLER_FUNCTION)Handler);


	if (!serviceStatusHandle) {terminate(GetLastError()); return;}





	// Notify SCM of progress


	success = SendStatusToSCM(SERVICE_START_PENDING,


		NO_ERROR, 0, 1, 5000);


	if (!success) {terminate(GetLastError()); return;}





	// create the termination event


	terminateEvent = CreateEvent (0, TRUE, FALSE, 0);


	if (!terminateEvent) {terminate(GetLastError()); return;}





	// Notify SCM of progress


	success = SendStatusToSCM(SERVICE_START_PENDING,


		NO_ERROR, 0, 2, 1000);


	if (!success) {terminate(GetLastError()); return;}





	// Check for startup params


	if (argc == 2)


	{


		int temp = atoi(argv[1]);


		if (temp < 1000)


			beepDelay = DEFAULT_BEEP_DELAY;


		else


			beepDelay = temp;


	}





	// Notify SCM of progress


	success = SendStatusToSCM(SERVICE_START_PENDING,


		NO_ERROR, 0, 3, 5000);


	if (!success) {terminate(GetLastError()); return;}





	// Start the service itself


	success = InitService();


	if (!success) {terminate(GetLastError()); return;}





	// The service is now running.


	// Notify SCM of progress


	success = SendStatusToSCM(SERVICE_RUNNING,


		NO_ERROR, 0, 0, 0);


	if (!success) {terminate(GetLastError()); return;}





	// Wait for stop signal, and then terminate


	WaitForSingleObject (terminateEvent, INFINITE);





	terminate(0);


}





VOID main(VOID)


{


	SERVICE_TABLE_ENTRY serviceTable[] =


	{


	{ SERVICE_NAME,


		(LPSERVICE_MAIN_FUNCTION) ServiceMain},


	{ NULL, NULL }


	};


	BOOL success;





	// Register with the SCM


	success =


		StartServiceCtrlDispatcher(serviceTable);


	if (!success)


		ErrorHandler("In StartServiceCtrlDispatcher",


			GetLastError());


}


Back to Top

Listing 2

Code that installs an NT service




//***************************************************************


// From the book "Win32 System Services: The Heart of Windows NT"


// by Marshall Brain


// Published by Prentice Hall


//


// This code installs a service.


//***************************************************************





// install.cpp





#include <windows.h>


#include <iostream.h>





void ErrorHandler(char *s, DWORD err)


{


	cout <<  s <<  endl;


	cout <<  "Error number: " <<  err <<  endl;


	ExitProcess(err);


}





void main(int argc, char *argv[])


{


	SC_HANDLE newService, scm;





	if (argc != 4)


	{


		cout <<  "Usage:\n";


		cout <<  "   install service_name \


service_label executable\n";


		cout <<  "           service_name is the \


name used internally by the SCM\n";


		cout <<  "           service_label is the \


name that appears in the Services applet\n";


		cout <<  "              (for multiple \


words, put them in double quotes)\n";


		cout <<  "           executable is the \


full path to the EXE\n\n";


		return;


	}





	// open a connection to the SCM


	scm = OpenSCManager(0, 0, SC_MANAGER_CREATE_SERVICE);


	if (!scm) ErrorHandler("In OpenScManager",


		GetLastError());





	// Install the new service


	newService = CreateService(


		scm, argv[1], // eg "beep_srv"


		argv[2],      // eg "Beep Service"


		SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS,


		SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL,


		argv[3],      // eg "c:\winnt\xxx.exe"


		0, 0, 0, 0, 0);


	if (!newService) ErrorHandler("In CreateService",


		GetLastError());


	else cout <<  "Service installed\n";





	// clean up


	CloseServiceHandle(newService);


	CloseServiceHandle(scm);


}


Back to Top

Listing 3

Code that removes an NT service




//***************************************************************


// From the book "Win32 System Services: The Heart of Windows NT"


// by Marshall Brain


// Published by Prentice Hall


//


// This code removes a service from the Services applet in the


// Control Panel.


//***************************************************************





// remove.cpp





#include <windows.h>


#include <iostream.h>





void ErrorHandler(char *s, DWORD err)


{


	cout << s <<  endl;


	cout <<  "Error number: " <<  err <<  endl;


	ExitProcess(err);


}





void main(int argc, char *argv[])


{


	SC_HANDLE service, scm;


	BOOL success;


	SERVICE_STATUS status;





	if (argc != 2)


	{


		cout <<  "Usage:\n   remove service_name\n";


		return;


	}





	// Open a connection to the SCM


	scm = OpenSCManager(0, 0, SC_MANAGER_CREATE_SERVICE);


	if (!scm) ErrorHandler("In OpenScManager",


		GetLastError());





	// Get the service's handle


	service = OpenService(scm, argv[1],


		SERVICE_ALL_ACCESS | DELETE);


	if (!service) ErrorHandler("In OpenService",


		GetLastError());





	// Stop the service if necessary


	success = QueryServiceStatus(service, &status);


	if (!success) ErrorHandler("In QueryServiceStatus",


		GetLastError());


	if (status.dwCurrentState != SERVICE_STOPPED)


	{


		cout <<  "Stopping service...\n";


		success = ControlService(service,


			SERVICE_CONTROL_STOP, &status);


		if (!success) ErrorHandler("In ControlService",


			GetLastError());


	}





	// Remove the service


	success = DeleteService(service);


	if (success) cout <<  "Service removed\n";


	else ErrorHandler("In DeleteService",


		GetLastError());





	// Clean up


	CloseServiceHandle(service);


	CloseServiceHandle(scm);


}


Back to Top

Network Communications

This article describes the Win32 functions that exist to support network communications, and shows how to apply them in your own applications. Two different facilities in the API support network communications: mailslots and named pipes. This article discusses both techniques as well as the advantages and disadvantages of each.

Because the Win32 API directly supports network communications, it is remarkably easy to create applications that use the network in many different ways. For example, you might want to create a multi-user conferencing system for your network, similar to the "CB" systems you find on Compuserve and other BBSs. In a system like this, users each run the conferencing program on their machines, and any messages they type get broadcast to all of the other users on the network. You generally use mailslots for the implementation because mailslots make it easy to broadcast information. Any multi-player game that uses the network employs similar techniques.

When you want to stream large quantities of data between two machines, you normally use point-to-point named pipe connections. For example, you would use named pipes for a digitized phone or video system implemented on a network. Any client/server configuration also uses named pipes. One central machine acts as the server, and then all of the clients connect to it individually with named pipes.

Network basics

In order to feel comfortable the examples that follow, it is helpful to understand basic network terminology. The figure below demonstrates a simple network that you might find in a small business. Each machine has a network adapter that connects it to the network, as well as a name that uniquely identifies it. The network adapter determines the type of network, generally either Ethernet or Token Ring. The adapter also controls the media used for the network: coax, twisted pair, etc. The important thing to recognize is that all of the machines in a simple network like this can communicate with all of the others equally.

A Network Example 

There are two ways for the machines to communicate using the native Win32 API functions. With a mailslot, one machine can broadcast a message that is received by all of the other machines on the network. With a named pipe, one machine chooses another and forms a specific connection to it. The advantage of a named pipe is that the connection is reliable. If the connection breaks, for example because a network card or cable malfunctions, both ends of the connection receive notification of the break immediately. Mailslots are unreliable in the sense that the sender has no way to confirm receipt of its messages. The advantage of a mailslot is that it is easy to get information to many machines simultaneously.

The figure above shows one network segment. A segment is defined as a group of machines directly connected to one another. There is a limit to the number of machines that can exist on one segment because network traffic grows with the number of machines. Generally the limit is about 100 machines. In a large company, each department might have a single segment consisting of 20 to 30 machines. All of the segments are then connected to one another with a router so that they can intercommunicate, as shown below. This distinction is important because, in general, a mailslot message is confined to a single segment, while named pipe messages can pass through the router to another segment.

Network segments 

Using mailslots and named pipes, three different communication architectures are possible: broadcast, point-to-point, and client/server. In broadcast mode using a mailslot, one machine sends a message to all others on the segment. In point-to-point communications, one machine forms a specific connection with another and data passes back and forth using a named pipe. In a client/server relationship, one machine acts as the server, and all clients connect to it with point-to-point named pipe connections. To emulate a broadcast operation with a client/server architecture, one machine sends a message to the server, and then the server sends duplicates of the message individually to each client.

Mailslot Connections

Mailslots are the simplest way to perform network communications in the Win32 API. Mailslots provide a one-directional communication path from a sender to one or more recipients on the same network segment. You generally use mailslots when you want to send data to many recipients at once.

Mailslots are extremely easy to create, and reading and writing are done using the API's normal ReadFile and WriteFile functions. When creating the mailslot, a special path name passed to the CreateMailslot function causes the system to create a mailslot rather than a normal file. [You will find descriptions of the functions discussed here in the Win32 programmer reference manuals, or in the Win32 help file that comes with the SDK or Visual C++ v2.]

The programs shown in Listings 1 and 2 are as simple as possible so that you can easily see the steps necessary to transmit and receive data through a mailslot. Listing 1 shows how to create a mailslot server with the CreateMailslot function. The server is a queue that holds messages received until you read them using the ReadFile function. Messages are stored in the queue in the order of their arrival.

The name of the mailslot must be of the form "\\.\mailslot\[path]name". This looks just like a file name, and it acts like a file name in the ReadFile function. However, no actual file is created by the function: The mailslot is held in memory. A typical mailslot name, "\\.\mailslot\sms", is used in Listing 1. It is also possible to add "subdirectories" to the path to further categorize mailslots.

When you create the mailslot, you can specify the maximum message length, as well as the read timeout. Mailslots can send no more than 400 bytes over the network in a single message. If you set the timeout value to 0, then any call to ReadFile will return immediately whether or not there is anything in the buffer. If you set the timeout value to a specific number of milliseconds, any read operation will fail if that amount of time elapses before a message arrives. You can also use the MAILSLOT_WAIT_FOREVER constant to create a blocking read.

Listing 1 takes the non-blocking approach, and uses the GetMailslotInfo function to make sure that messages exist in the mailslot queue before performing a read. This function returns the maximum length of messages in the queue, the length of the next message in the queue, and the number of messages waiting. Listing 1 continuously checks to determine if messages exist in the mailslot. If there are, then it reads the first one. Reading from a mailslot is just like reading from a file.

Any message sent from any computer on the network to a machine running Listing 1 will be received provided that the mailslot names of the sender and the receiver match. Listing 2 shows how to send messages to a mailslot. It starts by using the normal CreateFile function to open a writeable connection to the mailslot. The program is referred to as a mailslot client because it writes to mailslot servers already running on the network. The CreateFile function understands, because of the use of the special mailslot file name, that you are not creating a file but instead wish to communicate with a mailslot. Four different formats for the file name are possible:




\\.\mailslot\[path]name


\\*\mailslot\[path]name


\\domain\mailslot\[path]name


\\machine\mailslot\[path]name

In the first and last cases, the name specifies the local machine or a specific machine on the net, respectively. The second form specifies a broadcast operation to all machines in the local machine's primary domain. The third form specifies all machines in the indicated domain. See the book "Windows NT Administration: From Single Machines to Heterogeneous Networks", by Marshall Brain and Shay Woodard, for more information on domains and domain controllers.

After opening the mailslot, Listing 2 gets the local computer's name using GetComputerName, and then broadcasts the name to all mailslots in the current domain every five seconds.

Listing 1 uses a polling technique to check for messages. Every half second it calls GetMailslotInfo and checks to see if any messages are waiting in the slot. In general, polling is not a good technique to use in a multi-threaded environment because it is inefficient. You can eliminate the polling by setting the timeout value in CreateMailslot to an appropriate value, and then calling ReadFile with a buffer length of zero to wait for a message to arrive. Once this call to ReadFile returns you know a message exists, so call GetMailslotInfo and ReadFile as shown in Listing 1.

When you run Listing 2, it broadcasts to all machines on the network. If you run multiple copies of the reader on the same or different machines, all of them will see the messages produced by the writer. Alternatively, you can run multiple writers on the net and any copies of the reader will see the messages from all of them. In both Listing 1 and 2, also note the presumption that the program will be terminated externally. You can formally close either a mailslot server or client using the CloseHandle function.

Named Pipes

Named pipes provide a guaranteed delivery mechanism. Instead of broadcasting the packet onto the network, you form a distinct connection to another machine with a named pipe. If the connection breaks, for example because a machine goes down or a portion of the network fails, both parties to the connection find out as soon as they try to send or receive anything. Packets are also guaranteed to arrive in sequence through a named pipe. The only problem with named pipes is that you lose the ability to broadcast packets. To broadcast anything, all of the target machines must have a connection to a central server, and the server must separately transmit the message to each one.

Named pipes are only slightly more difficult to create than mailslots. Listings 4 and 5 show how to create a simple point-to-point connection between two applications using named pipes. Run the receiving program shown in Listing 4 first, and then on the same machine run the sending program in Listing 5. The program will query you for the name of the machine to connect to. Since you are running the sender and the receiver on the same machine, type "." or enter your machine name. You will see a message sent from sender to receiver every five seconds or so. When you kill off the sender, notice that you immediately see a message in the receiver indicating that it has detected the break in the pipe. If you try to start up the sender without the receiver running, the sender will fail immediately because it cannot connect. Unlike mailslots, pipes can tell when the other end is not working properly.

A named pipe connection can occur across the network as simply as it occurs on the same machine. For example, if the server program in Listing 4 is running on a machine named "orion", log in to a different machine using an account with the exact same login ID and password as the one you are using on "orion". Run Listing 5 on the new machine and enter the name "orion" when it asks for the machine name. The connection will occur properly. Note that, with named pipes, you must know the name of the machine running the server.

Also note that if you have a different user try to connect to the receiver, then the connection fails. For example, if the user "jones" is running the receiver on the machine "orion" and the user "smith" tries to connect from another machine, the connection fails with an "access denied" error. This is the NT security system at work. See the Security chapter of "Win32 System Services: The Heart of Windows NT" for explanations and solutions.

In Listing 4, the program starts by creating a named pipe server using the CreateNamedPipe function. The name used with the CreateNamedPipe function will always have the form:

\\.\pipe\[path]name

As with mailslots, you can specify a path before the name of the pipe to clearly distinguish it from other pipes on the system.

The openMode parameter passed to CreateNamedPipe lets you determine the direction of the pipe. Named pipes can be one directional or bi-directional, depending on the following constants used with the openMode parameter:




PIPE_ACCESS_DUPLEX


PIPE_ACCESS_INBOUND


PIPE_ACCESS_OUTBOUND


The pipeMode parameter of CreateNamedPipe determines whether the pipe works with a pure stream of bytes, or with packets of bytes called messages. A stream of bytes has no logical boundaries. Messages contain a group of bytes perceived as a unit. You can declare byte or message behavior in both the read and write directions:




PIPE_TYPE_MESSAGE


PIPE_TYPE_BYTE


PIPE_READMODE_MESSAGE


PIPE_READMODE_BYTE


A pipe can have more than one instance on a single machine. This capability allows an application to handle multiple clients, each in different threads, and is required to create a named pipe server. Since the example in Listings 4 and 5 is a simple point-to-point connection where only one instance is necessary, a maximum of one instance is specified in the call to CreateNamedPipe.

Listing 4 next waits for a connection on the named pipe using the ConnectNamedPipe function. A connection is formed in the server when a client program calls CreateFile with the proper machine and named pipe specified as its destination. Upon connection, the ConnectNamedPipe function returns. Alternatively you can specify an overlapped structure and ConnectNamedPipe will return immediately and later signal the event upon connection.

Listing 4 then enters a loop, waiting for data to arrive. The ReadFile function behaves slightly differently here than it does with files. Because this named pipe is in message mode, the ReadFile function will return as soon as it receives a complete message, regardless of how many bytes the message contains. It is possible to use a blocking read as shown, or to use an overlapped read.

Listing 5 is a simple client for Listing 4. Listing 5 starts by creating a connection to the named pipe with the CreateFile function. It then writes messages using the WriteFile function. Each individual call to WriteFile constitutes a message at the receiving end of the named pipe, so the receiver's ReadFile function will unblock when it receives the message. Each time the client writes, the server produces a message on the screen.

If two copies of the client try to connect to Listing 4 at the same time, then the server will reject the second client. If you terminate either the client or the server, then the other half of the pair will immediately terminate when it detects the broken connection.

Conclusion

Named pipes are often used in client/server systems, where the server uses a multi-threaded approach to handle multiple connections simultaneously.

Listing 1

A program that creates a mailslot server and reads from it




//***************************************************************


// From the book "Win32 System Services: The Heart of Windows NT"


// by Marshall Brain


// Published by Prentice Hall


//


// This code implements a simple mailslot server (receiver) that


// uses polling.


//***************************************************************





// sms_recv.cpp





#include <windows.h>


#include <iostream.h>





int main()


{


  char toDisptxt[80];


  HANDLE hSMS_Slot;


  DWORD nextSize;


  DWORD Msgs;


  DWORD NumBytesRead;


  BOOL Status;





  /* Create a mailslot for receiving messages */


  hSMS_Slot=CreateMailslot("\\\\.\\mailslot\\sms",


    0, 0, (LPSECURITY_ATTRIBUTES) NULL);





  /* Check and see if the mailslot was created */


  if (hSMS_Slot == INVALID_HANDLE_VALUE)


  {


    cerr <<  "ERROR: Unable to create mailslot "


      <<  GetLastError() <<  endl;


    return (1);


  }





  /* Repeatedly check for messages until the


     program is terminated */


  while(1)


  {


    Status=GetMailslotInfo(hSMS_Slot,


      (LPDWORD) NULL, &nextSize, &Msgs,


      (LPDWORD) NULL);


    if (!Status)


    {


      cerr <<  "ERROR: Unable to get status. "


        <<  GetLastError() <<  endl;


      CloseHandle(hSMS_Slot);


      return (1);


    }





    /* If messages are available, then get them */


    if (Msgs)


    {





      /* Read the message and check to see if


         read was successful */


      if (!ReadFile(hSMS_Slot, toDisptxt, nextSize,


        &NumBytesRead, (LPOVERLAPPED) NULL))


      {


        cerr


          <<  "ERROR: Unable to read from mailslot "


          <<  GetLastError() <<  endl;


        CloseHandle(hSMS_Slot);


        return (1);


      }





      /* Display the Message */


      cout <<  toDisptxt <<  endl;


    }


    else


      /* Check for new messages twice a second */


      Sleep(500);


  } /* while */


}


Back to Top

Listing 2

A program writes to a mailslot every five seconds




//***************************************************************


// From the book "Win32 System Services: The Heart of Windows NT"


// by Marshall Brain


// Published by Prentice Hall


//


// This code implements a simple mailslot sender.


//***************************************************************





// sms_send.c





// Usage: sms_send





#include <windows.h>


#include <iostream.h>


#include <string.h>





int main()


{


  char toSendTxt[100], buffer[100];


  DWORD bufferLen=100;


  HANDLE hSMS_Slot;


  BOOL Status;


  DWORD NumBytesWritten;





  /* Create the mailslot file handle for


     sending messages */


  hSMS_Slot=CreateFile("\\\\*\\mailslot\\sms",


    GENERIC_WRITE, FILE_SHARE_READ,


    (LPSECURITY_ATTRIBUTES) NULL,


    OPEN_EXISTING,


    FILE_ATTRIBUTE_NORMAL,


    (HANDLE) NULL);





  /* Check and see if the mailslot file was


     opened, if not terminate program */


  if (hSMS_Slot == INVALID_HANDLE_VALUE)


  {


    cerr <<  "ERROR: Unable to create mailslot "


      <<  GetLastError() <<  endl;


    return (1);


  }





  /* form string to send */


  GetComputerName(buffer, &bufferLen);


  strcpy(toSendTxt, "Test string from ");


  strcat(toSendTxt, buffer);





  /* Repeatedly send message until program


     is terminated */


  while(1)


  {


    cout <<  "Sending..." <<  endl;


    /* Write message to mailslot */


    Status=WriteFile(hSMS_Slot,


      toSendTxt, (DWORD) strlen(toSendTxt)+1,


      &NumBytesWritten, (LPOVERLAPPED) NULL);





    /* If error occurs when writing to mailslot,


       terminate program */


    if (!Status)


    {


      cerr <<  "ERROR: Unable to write to mailslot "


        <<  GetLastError() <<  endl;


      CloseHandle(hSMS_Slot);


      return (1);


    }





    /* Wait sending the message again */


    Sleep(4800);


  } /* while*/


}


Back to Top

Listing 4

A simple program that creates a named pipe server. The server will wait and accept one connection, and then receive messages from it.




//***************************************************************


// From the book "Win32 System Services: The Heart of Windows NT"


// by Marshall Brain


// Published by Prentice Hall


//


// This code implements a simple named pipe server (receiver).


//***************************************************************





// ssnprecv.cpp





// Usage: ssnprecv





#include <windows.h>


#include <iostream.h>





int main()


{


  char toDisptxt[80];


  HANDLE ssnpPipe;


  DWORD NumBytesRead;





  /* Create a named pipe for receiving messages */


  ssnpPipe=CreateNamedPipe("\\\\.\\pipe\\ssnp",


    PIPE_ACCESS_INBOUND,


    PIPE_TYPE_MESSAGE | PIPE_WAIT,


    1, 0, 0, 150,


    (LPSECURITY_ATTRIBUTES) NULL);





  /* Check and see if the named pipe was created */


  if (ssnpPipe == INVALID_HANDLE_VALUE)


  {


    cerr <<  "ERROR: Unable to create a named pipe. "


      <<  endl;


    return (1);


  }





  /* Allow a client to connect to the name pipe,


     terminate if unsuccessful */


  cout <<  "Waiting for connection... " <<  endl;


  if(!ConnectNamedPipe(ssnpPipe,


    (LPOVERLAPPED) NULL))


  {


    cerr <<  "ERROR: Unable to connect a named pipe "


      <<  GetLastError() <<  endl;


    CloseHandle(ssnpPipe);


    return (1);


  }





  /* Repeatedly check for messages until the program


     is terminated */


  while(1)


  {


    /* Read the message and check to see if read


       was successful */


    if (!ReadFile(ssnpPipe, toDisptxt,


      sizeof(toDisptxt),


      &NumBytesRead, (LPOVERLAPPED) NULL))


    {


      cerr


        <<  "ERROR: Unable to read from named pipe "


        <<  GetLastError() <<  endl;


      CloseHandle(ssnpPipe);


      return (1);


    }





    /* Display the Message */


    cout <<  toDisptxt <<  endl;





  } /* while */


}


Back to Top

Listing 5

A named pipe client able to connect to Listing 4 and send it messages




//***************************************************************


// From the book "Win32 System Services: The Heart of Windows NT"


// by Marshall Brain


// Published by Prentice Hall


//


// Copyright 1994, by Prentice Hall.


//


// This code implements a simple named pipe sender.


//***************************************************************


// ssnpsend.cpp


// Usage: ssnpsend


#include <windows.h>


#include <iostream.h>


int main()
{


  char *toSendtxt="Test String";


  HANDLE ssnpPipe;


  DWORD NumBytesWritten;


  char machineName[80];


  char pipeName[80];





  cout <<  "Enter name of server machine: ";


  cin >> machineName;


  wsprintf(pipeName, "\\\\%s\\pipe\\ssnp",


    machineName);





  /* Create the named pipe file handle for sending


     messages */


  ssnpPipe=CreateFile(pipeName,


    GENERIC_WRITE, FILE_SHARE_READ,


    (LPSECURITY_ATTRIBUTES) NULL,


    OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,


    (HANDLE) NULL);





  /* Check and see if the named pipe file was


     opened, if not terminate program */


  if (ssnpPipe == INVALID_HANDLE_VALUE)


  {


    cerr <<  "ERROR: Unable to create a named pipe "


      <<  endl;


    cerr <<  GetLastError() <<  endl;


    return (1);


  }





  /* Repeatedly send message until program is


     terminated */


  while(1)


  {


    cout <<  "Sending..." <<  endl;


    /* Write message to the pipe */


    if (!WriteFile(ssnpPipe,


          toSendtxt, (DWORD) strlen(toSendtxt)+1,


          &NumBytesWritten, (LPOVERLAPPED) NULL))


    {


      /* If error occurs when writing to named


         pipe, terminate program */


      cerr <<  "ERROR: Unable to write to named pipe "

        <<  GetLastError() <<  endl;

      CloseHandle(ssnpPipe);

      return (1);

    }

    /* Wait before sending the message again */

    Sleep(4800);

  } /* while*/
}


Back to Top

Security 

One of the most fascinating parts of Windows NT is its security system. It is possible to almost completely ignore this part of the Win32 API if you don't care to secure things. However, there are many situations where the security features built into NT can help to solve some very interesting problems. In this article you will learn about the intent of the NT's security system, the concepts that help you to understand what is going on, and the techniques used to manipulate it.

Please note that some of the code in this chapter will not work as described unless you log in as the administrator. Only administrators have the rights necessary to perform some of the system security tasks demonstrated here.

THE POSSIBILITIES

Most people have never seen or worked with a security system as complete and intricate as NT's. Experienced UNIX programmers are familiar with UNIX's file security system and with the idea of root privileges, but these systems are primitive compared to NT's. The following examples give you some feeling for the scope and preciseness of NT's security system.

People using the NT File System can see the security features built into it because of the Security menu and security editor in the File Manager. You can write your own code that modifies the security information in an NT File System volume at that same level of detail. From programs that you write, you can control who has what type of access to any file on an NTFS volume, just like the File Manager does.

  • You can create a named pipe server on one system that, for example, only administrators on other systems can access. When people try to connect to the server, the security system will check to make sure they have the proper access authority and reject users who do not have administrative privileges. You might do this so that administrators on different machines can send information to one another in a way that guarantees authenticity. You can write code so that it selectively allows or denies access to any user or group of users, or to all users.
  • You can create a mutex with security attributes that allow only certain users or groups to access it. This capability applies to any object in the system: files, semaphores, threads, events, shared memory in file mappings, etc. In the case of a mutex, you can use this facility to guard against unauthorized access to the synchronization mechanism of an application.
  • You can give any object (for example, a thread, mutex, pipe, file or registry key) the ability to create an entry in the event log whenever it is accessed. You can specify individual types of access, and individual users, that generate an event log entry. For example, you can cause a specific file to generate an event log entry whenever the user smith successfully reads that file.

The above examples show that NT allows you to give precise security access to specific objects in the system, and it also allows you to monitor and record how objects are used.

UNDERSTANDING THE TERMINOLOGY AND CONCEPTS OF THE NT SECURITY SYSTEM 

One of the most interesting and frustrating parts of the NT security system is the huge number of new concepts that it contains, along with the vocabulary that describes these concepts. The goal of this section is to put those concepts into simple terms so that you can understand them.

By now you are already familiar with the most visible part of the security system: passwords. You can think of the password system as the lock on the front door of a building. The password system keeps people out of an NT system just like a lock on a building keeps people from wandering around in the lobby. Your password gives you a key to the building that is an NT system.

If one of your hard disks is formatted with the NT File System, then you know that there is a second layer of security in NT as well. You can think of this second layer as locks on individual rooms inside of the building. Some rooms are wide open and anyone can walk in, while others have locks on them. The locks on an individual room can be very specific in NT. For example, you can create a file on a volume formatted with the NT File System and you can let any combination of people access it:

  • You can let no one but the owner access it 
  • You can let only a single user access it 
  • You can let several individual users access it 
  • You can name an NT group and let any member of that group access it 
  • You can name an NT group and let any member of that group access it, 
        but then specifically deny access to individual members of the group 
  • You can name multiple groups that can access it 
  • You can let anyone access it 

Think of a secured file as a room. The security permissions that you put on the file are like a lock that determines who is allowed into the room. You can also control what a person is allowed to do once they get into a room. For example, with a file you can give one user read access, another user write access, and so on. See the Permissions dialog in the Security menu of the File Manager for examples.

Think of NT as a building full of rooms. Your password gives you a key that lets you into the building. Once inside, you can then use your key to enter individual rooms. Some rooms have no locks, while other are locked and will let you in, and still others will refuse you entry. The rooms are individual objects in the NT system: files, registry keys, thread and mutex objects, etc.

You can also put a sentry at the door of any of the rooms. The sentry's job is to monitor who uses the room, and how they use it. This is called auditing in NT. When you attach an auditing request to an NT object, the object writes entries in the security portion of the event log whenever certain users access the object in specified ways. For example, if you have access to an NT file system volume and have administrative privileges, open the File Manager and select a file that you own. Then select the Auditing option in the Security menu. For any individual user or group, you can detect when they successfully or unsuccessfully (or both) read the file, write to the file, execute the file, and so on.

The most obvious place to see these security features is in the NT File System. The security editor in the File Manager lets you easily and graphically manipulate all of the different security features available for files. This same level of security detail applies to many different objects in NT. For example, you can secure items in the Registry with this level of detail, and in fact the registry editor contains the same sort of visual security editor that the File Manager contains. You can also specify security attributes on many internal system objects. For example, you can limit access to a named pipe (see Section 10.9) in this way.

Here is a brief summary of the concepts discussed above:

  • An NT system is like a building. Your password gives you a key into the building.
  • Once you are inside you find potentially thousands of rooms. A file is like a room, 
        and so is a registry entry, a named pipe, a thread, and so on. 
  • Each room can be secured by its owner with a lock that is keyed for user and group IDs. 
        That is, the owner of the room can let just one person use the room, or a collection of users and groups, or everyone. 
  • Each room can also be manned with a sentry who detects and records who enters the room 
        and what each person does once they are inside. 

Now that you understand the general concepts, let's look at some of NT's specific vocabulary for discussing security.

Back to Top

NT SECURITY VOCABULARY

When you log into an NT system, the system gives you an access token. The access token is your key to all of the locks found in an NT system. Your key will turn some locks, but it will not work in all of them. Each process that you create contains a copy of your access token.

The access token does two different things. First, it identifies who you are. For example, if you logged in as the user "smith", then your access token contains your identity as that user. The access token also identifies you by all of the groups you belong to. For example, in the User Manager you may be a member of the Power User group, the Backup Operator group, and a custom group named Programmers. Your access token identifies you as a member of these different groups.

Your access token also contains all of your user rights. Each group you belong can have user rights, or privileges, associated with it. If you open the User Manager you can look at the list of rights with the User Rights menu option. For example, power users have the right to set the system time, while normal users do not. Some users can shut down the system while others cannot, and so on. The system builds up the list of rights in your access token by combining all of the individual rights found in each group you belong to. Individual users can also have special rights granted to them specifically by the administrator with the User Manager.

 

An access token contains a user ID, the names of the groups the user belongs to, and the combined user rights from all of those groups

Most of the objects in the system can have locks on them. In NT, a lock is called a security descriptor. If the Create function for an object contains a security parameter, you can lock it. The following objects can have locks:

  • Files (if they exist on an NT File System volume) 
  • Directories (if they exist on an NT File System volume)
  • Registry keys 
  • Processes 
  • Threads 
  • Mutexes 
  • Semaphores 
  • Events 
  • Named Pipes (on the system and over the network) 
  • Anonymous Pipes (on the system only) 
  • Mailslots (on the system only) 
  • Console screen buffers 
  • File mappings 
  • Services 
  • Private objects 

To lock an object, you create a security descriptor and pass it to the object when you create it. In all of the previous sections in this book, the example code has passed a zero in for the security attributes parameter. The zero indicates to the system that it should create a default security descriptor that allows you to access the object. See the book for a description of default security descriptors.

A security descriptor contains four things:

  • An owner identifier that identifies the current owner of the object 
  • A primary group identifier 
  • A system access control list, or SACL, that contains auditing information 
  • A discretionary access control list, or DACL, that determines which users and groups can and cannot access the object. 

The owner of an NT object can always set its security information. For example, if you own a file and accidentally set it so that no one can access it, you can still go back and change its security because you are the owner. It is like breaking the lock to your house: Even though the lock is broken, you can always replace it with a new one because you own the house.

 

The security descriptor for any object contains the owner and group IDs, the discretionary access control list that determines who can and cannot access the object, and the system access control list that controls who gets audited when they use the object

The DACL (Discretionary Access Control List) is the heart of the actual lock. It controls who can and cannot access the object. It is an access control list, or ACL, that contains access control entries, or ACEs. Each ACE indicates one user or group, along with what they can or cannot do to the object. For example, if the object is a file and the user "smith" is allowed to read from the file, then there will be an ACE that indicates that user "smith" has read access. This is called an access allowed ACE because it allows a user or group to do something. There is one access allowed ace for each person or group allowed to access the object. Similarly, there are access denied ACEs which deny specific users or groups access. For example, if you have given the group Power Users access to an object, but "smith" is a power user and you don't want him to have access, an access denied ACE keeps him out.

The SACL (System Access Control List) also contains ACEs, but these ACEs determine who will be audited and why. An ACE in a SACL is called an audit access ACE. For example, if the system is supposed to create an audit entry whenever the user "smith" successfully reads a file, one ACE in the SACL states that.

 

An Access Control Entry (ACE) specifies one user or group (the SID identifies the user or group) and the type of access allowed. ACEs are stored in ACLs (see Figure 10.3).

To convey its information, each ACE in an ACL consists of three parts: a Security Identifier, or SID, an access mask, and an ACE header. The SID is a value stored in the registry (and also available through function calls) that uniquely identifies each user or group that exists in the User Manager. The ACE header determines the type of ACE: access allowed, access denied, etc. An access mask is a 32-bit mask that determines what the user can do with the object. There are standard rights that apply to all objects on the system. Some objects also have a variety of special access rights that apply only to them. For example, you can apply the following specific and standard access rights to a file object:

specific:

  • FILE_READ_DATA 
  • FILE_WRITE_DATA 
  • FILE_APPEND_DATA 
  • FILE_READ_EA 
  • FILE_WRITE_EA 
  • FILE_EXECUTE 
  • FILE_READ_ATTRIBUTES 
  • FILE_WRITE_ATTRIBUTES 
  • FILE_ALL_ACCESS 

standard:

  • DELETE 
  • READ_CONTROL 
  • STANDARD_RIGHTS_ALL 
  • STANDARD_RIGHTS_EXECUTE 
  • STANDARD_RIGHTS_READ 
  • STANDARD_RIGHTS_REQUIRED 
  • STANDARD_RIGHTS_WRITE 
  • SYNCHRONIZE 
  • WRITE_DAC 
  • WRITE_OWNER 

generic:

  • GENERIC_ALL 
  • GENERIC_EXECUTE 
  • GENERIC_READ 
  • GENERIC_WRITE 

The generic rights are pre-defined combinations of standard and specific rights and change from object to object. See Section 10.5 for more information on these securable objects and their access rights.

An access mask is simply a 32-bit bit-mask. Each of the standard, specific and generic rights have a bit associated with them in the access mask. The first 16 bits hold specific rights and are keyed to the object that the access mask refers to (for example, the FILE_ constants above). The next 8 bits hold standard rights. The high-order four bits hold generic rights.

 

The bit layout of an access mask

As you can see, there is a whole cornucopia of new terminology used to describe NT's security features. However, you will become intimately familiar with all of these words as you work through the sample code, and all of the concepts are easy to understand. Here is a quick summary:

  • When users log in, they receive access tokens. An access token contains the user's ID, the user's groups, and the user's privileges culled from the groups. 
  • Each object has a security descriptor that acts as its lock. A security descriptor contains an owner and group identifier, a System ACL, and a Discretionary ACL. 
  • A DACL controls who is allowed to do what to the object 
  • A SACL controls who is audited for doing what to an object 
  • ACLs consist of ACEs. Each ACE contains a SID that identifies the user or group, an access mask that determines the actions allowed to the user or group, and an ACE header that determines the type of ACE. 

The following section presents two simple example to show how the security functions and all of this vocabulary fit together.


Return to Top


SIMPLE EXAMPLES

The purpose of this section is to walk through the creation and application of a security descriptor for a file object. A file object is chosen for this example because it is common and easily understood, and also because it is easy to view the results with the security editor in the File Manager to prove that the code works. However, if your hard disk is not formatted with the NT File System, this code will not do anything and the security editor is disabled. You have three options if this is the case:

  1. Reformat your hard disk with NTFS and reload NT, or use the convert command to convert your hard disk to the NT file system.
  2. Install a new hard disk and format it with the NT File System
  3. Partition off a small section (say 10 megabytes) of your existing hard disk and format it with NTFS. This means that you will have to reformat and reload the original partition as well.

Alternatively, you can wait a moment and we will apply this same code to a registry key. You will be able to see the security editor in the registry editor regardless of the file system that you are using. The code   below contains a program that creates a new file. The security descriptor for the new file is set up so that only the user "guest"   can use the file, and the user can do nothing but read it. The program contains a bsolutely no error checking so that the essentials are easy   to see. When you run the code, it should create a file on c:\ named testfile. Select the file in the File Manager and then   choose the Permissions option in the Security menu. You will find that there is one entry in the list: The user "guest" can read   the file. Feel free to change the name of the file or the name of the user when you run the code yourself.


#include <windows.h>
#include <iostream.h>

SECURITY_ATTRIBUTES sa;
SECURITY_DESCRIPTOR sd;
BYTE aclBuffer[1024];
PACL pacl=(PACL)&aclBuffer;
BYTE sidBuffer[100];
PSID psid=(PSID) &sidBuffer;
DWORD sidBufferSize = 100;
char domainBuffer[80];
DWORD domainBufferSize = 80;
SID_NAME_USE snu;
HANDLE file;

void main(void)
{
  InitializeSecurityDescriptor(&sd,
    SECURITY_DESCRIPTOR_REVISION);
  InitializeAcl(pacl, 1024, ACL_REVISION);
  LookupAccountName(0, "guest", psid,
    &sidBufferSize, domainBuffer,
    &domainBufferSize, &snu);
  AddAccessAllowedAce(pacl, ACL_REVISION,
    GENERIC_READ, psid);
  SetSecurityDescriptorDacl(&sd, TRUE, pacl,
    FALSE);

  sa.nLength= sizeof(SECURITY_ATTRIBUTES);
  sa.bInheritHandle = FALSE;
  sa.lpSecurityDescriptor = &sd;

  file = CreateFile("c:\\testfile",
    GENERIC_READ | GENERIC_WRITE,
    0, &sa, CREATE_NEW,
    FILE_ATTRIBUTE_NORMAL, 0);
  CloseHandle(file);
}


Start by looking at the bottom of the program. Here you will find a call to CreateFile that creates a file named c:\testfile. However, this call has a security parameter at location four in the parameter list. In all NT code you have probably seen, you have seen a zero used in this location. The zero tells the operating system to use a default security descriptor when it creates the object (see Section 10.8 of the book). The zero value also disables inheritance (see Chapter 5). This code code instead creates a SECURITY_ATTRIBUTES structure that contains a valid security descriptor.

The creation of a security descriptor starts with a call to the InitializeSecurityDescriptor function seen at the first line of the listing. [See the Win32 help file in the SDK or Visual C++ help files for more information on this and the following functions.] This step creates a security descriptor in absolute format (there is also a second format called the self-relative format, and the differences are described in Section 10.6 of the book). The new security descriptor initially contains no information besides the revision level: no owner identifier, no group identifier, no SACL, and no DACL.

The next line calls InitializeAcl to create the ACL that will become the DACL for the security descriptor. When the InitializeAcl function returns, pacl points to an empty ACL. That is, the ACL contains no ACEs. If you were to comment out the next two lines so that this empty ACL was placed into the security descriptor, and that security descriptor was applied to the file, then no one would be able to access the file. In the absence of any ACEs in the discretionary ACL, no one has access. Alternatively, if you were to create no DACL at all and pass NULL in during the DACL installation step below, then everyone could access the file.

The next two lines create an ACE and add it to the ACL. The LookupAccountName function returns a SID for the specified account name. The LookupAccountName function looks up the account name specified on the system specified or on the local system. If it is not found locally the function looks on the domain controller or trusted domain controllers if appropriate. The function returns a SID for the account, the domain where it was found if the SID came from a domain controller, and an enumerated value that indicates the type of account:

  • SidTypeUser
  • SidTypeGroup
  • SidTypeDomain
  • SidTypeAlias
  • SidTypeWellKnownGroup
  • SidTypeDeletedAccount
  • SidTypeInvalid
  • SidTypeUnknown

SID is a security identifier. It uniquely identifies a user or a group to the system.

The SID returned by the LookupAccountName function is used in a call to the AddAccessAllowedAce function to create an access allowed (as opposed to access denied)

ACE and add it to the currently-empty ACL.

The AddAccessAllowedAce function creates the ACE with the SID and access mask specified and adds it to the ACL specified. The GENERIC_READ access mask grants

read access to the file and its attributes.See Section 10.5 for a list of the specific and standard rights that GENERIC_READ adds to the access mask.

Now the code in the listing has an ACL containing one ACE that specifies that the user "guest" should have read access. This ACL needs to be placed into the Discretionary ACL

of the security descriptor using the SetSecurityDescriptorDacl function.

Now the security descriptor contains a valid DACL with one ACE in it. The code places this security descriptor in a security attributes structure and then passes it to the CreateFile function (see Chapter 2).

If you were to comment out the previous three lines in the code, and replace pacl with NULL for the DACL in the call to SetSecurityDescriptorDacl, everyone would have a ccess to the file. The absence of a DACL implies that everyone should be able to access the file.

Compile and run the code. Make sure that the file name specified in the program does not already exist when you run it. When you look at the new file with the File Manager's

security editor, you will find that its permissions are set as specified in the code.

You can easily modify the code to experiment with security descriptors. For example, try giving write access to a certain group, or add several ACEs to the DACL, or create an access denied ACE using AddAccessDeniedAce. Be sure that access denied ACEs precede access allowed ACEs. Use the SetSecurityDescriptorOwner function to modify the owner in the security descriptor.

Back to Top

Another Example

This exact same security descriptor code can be applied to any other securable NT object, because the security system is uniform across all objects. For example, you can use it to create a key in the registry, as demonstrated in the code below. See Chapter 15 for information on the registry and registry keys. If you compare this listing with the previous, you will find that they are exactly the same except that this listing uses RegCreateKeyEx to create a registry key instead of a file.


#include <windows.h>
#include <iostream.h>

SECURITY_ATTRIBUTES sa;
SECURITY_DESCRIPTOR sd;
BYTE aclBuffer[1024];


PACL pacl=(PACL)&aclBuffer;


BYTE sidBuffer[100];


PSID psid=(PSID) &sidBuffer;


DWORD sidBufferSize = 100;


char domainBuffer[80];


DWORD domainBufferSize = 80;


SID_NAME_USE snu;


LONG result;


HKEY regKey;


DWORD disposition;





void main(void)


{


  InitializeSecurityDescriptor(&sd,


    SECURITY_DESCRIPTOR_REVISION);


  InitializeAcl(pacl, 1024, ACL_REVISION);


  LookupAccountName(0, "guest", psid,


    &sidBufferSize, domainBuffer,


    &domainBufferSize, &snu);


  AddAccessAllowedAce(pacl, ACL_REVISION,


    GENERIC_READ, psid);


  SetSecurityDescriptorDacl(&sd, TRUE, pacl,


    FALSE);





  sa.nLength= sizeof(SECURITY_ATTRIBUTES);


  sa.bInheritHandle = FALSE;


  sa.lpSecurityDescriptor = &sd;





  result = RegCreateKeyEx(HKEY_CURRENT_USER, "junk", 0, 0, REG_OPTION_VOLATILE,


    KEY_ALL_ACCESS, &sa, &regKey,


    &disposition);


  cout <<  result <<  endl;
  RegCloseKey(regKey);
}

Before running this code, run the registry editor (regedt32.exe) and look at the HKEY_CURRENT_USER window to make sure HKEY_CURRENT_USER does not already contain a key named "junk". If it does, delete it, or modify the code to create a different key.

Now run the code. Refresh the registry editor display if it is not set to automatically refresh, and you will see a new key named "junk".

When you check its permissions using the Permissions option in the Security menu, you will find that you or "administrator" owns the

key and that "guest" is the only user who has access to it. Change the name of the key or the user as you desire. Since the code in c

reates a volatile key (see Chapter 15), the key will disappear when you log off or reboot the system.



Back to Top