Channels ▼
RSS

.NET

Inside Windows NT System Data

Source Code Accompanies This Article. Download It Now.


Nov99: Inside Windows NT System Data

Sven is a developer in Herzogenaurach, Germany, and be contacted at sbs@orgon.com.


If you have ever written debugging software for Windows NT, you know it doesn't take long before you can get stuck. Things as simple as enumerating processes or querying system performance are difficult due to the lack of detailed documentation. In an interesting Microsoft Systems Journal (MSJ) article series, Matt Pietrek presented two methods for getting at some internal system data. The first one involves querying the so-called Performance Counters stored in the registry (see "Under The Hood," MSJ, March/April, 1996). The other method uses a special-purpose system module named psapi.dll ("Under The Hood," MSJ, August 1996). Both methods work well, but give just a tiny glimpse of the wealth of data used internally by Windows NT.

NtQuerySystemInformation()

While trying to figure out how the kill.exe utility (from the NT Resource Kit) works, I discovered it uses the mysterious NtQuerySystemInformation() function exported by ntdll.dll to get a list of processes currently running. With this list, kill.exe is able to force termination of a process by name, simply by looking up the name in the process list and retrieving the associated process ID, which is stored in this list, too. This looked interesting enough to warrant a closer look. To my surprise, that NtQuerySystemInformation() is apparently the general front door from user-mode code into internal kernel-mode data structures.

One of the things that puzzled me most was the complete lack of documentation on this function. Even the NT Device Driver Kit (DDK) header files ntddk.h and ntdef.h, that usually provide answers to many questions about NT internals, are amazingly ignorant about any aspects of NtQuerySystemInformation(). No API prototype, no info class definitions -- just a single mention of the name in the comments to the definition of the structure CONFIGURATION_INFORMATION. Searching for "NtQuerySystemInformation" in the DDK help yields no hits at all. That's about as undocumented as a system API ever could be! In this article, I'll reveal what kind of data this API gives you access to, and provide a DLL and demo program that lets you display most of that data on the screen (both available electronically; see "Resource Center," page 5).

It's simple to find out what parameters have to be passed to this function -- you simply check how the NT core modules make use of it. Example 1 is the prototype of NtQuerySystemInformation(), as well as that of its counterpart, NtSetSystemInformation(). Actually, they look similar to other (documented) NT info query APIs, such as NtQueryInformationProcess() or ZwQueryValueKey(). All of them use an info class parameter, which is just an index number that selects the kind of information to be returned. pData points to a data buffer allocated by the caller, the size of which is indicated by dSize. On return, pdSize contains the number of bytes actually copied to the buffer. Like its friends, NtQuerySystemInformation() returns an NTSTATUS value. If STATUS_INFO_LENGTH_MISMATCH (0xC0000004) is returned, the buffer was too small to hold the data.

A Simple System Info Spy

To find out more about the data supplied by NtQuerySystemInformation(), I wrote the ntqsi.exe utility (available electronically), a simple console-mode application that outputs a hex dump of the data returned by NtQuerySystemInformation(). The problem with this function is that it returns several types of information, dependent on the specified info class. I've observed the following variations:

  • A fixed-size data block (usually a scalar value or some structure).

  • A variable-size list of fixed-size items.

  • A variable-size list of variable-size items.

  • A list of lists.

As you see, almost any kind of data you can think of is included. This makes it difficult to write a general-purpose spy utility that properly dumps out any data returned by NtQuerySystemInformation() without knowing in advance what this data is like. Unfortunately, the function does not tell you how large your buffer should be if it returns with STATUS_INFO_LENGTH_MISMATCH. (Closer examination of the implementation in ntoskrnl.exe reveals that the kernel also doesn't know beforehand how much data will be returned -- it usually bails out as soon as it's about to write beyond the end of the buffer and discards any waiting data.)

Microsoft programs that have to deal with lists of the variable-size type (like, for instance, process lists) use a trial-and-error strategy to find out the correct buffer size: They start off with a reasonable initial size (32 or 64 KB, for instance), and increase the size by a fixed increment if NtQuerySystemInformation() fails with a length mismatch status. In the ntlib.c/ntinfo.c demo programs (available online), I use multiples of 64 KB, which seem to be a good choice based on my empirical tests.

The situation is further complicated by some info classes accepting larger buffers than required, while others insist on receiving a properly sized buffer. Some might report success although the buffer is actually too small, and still others fail to report the number of returned bytes (for example, set *pdSize = 0). Therefore, ntqsi.c uses a somewhat clumsy, but successful, approach to determine the correct size:

  • It starts with an initial size that is chosen large enough to hold even very large lists (256 KB) and calls NtQuerySystemInformation() repeatedly, decrementing the size by 1 until the buffer size is 0 or a status code other than STATUS_INFO_LENGTH_MISMATCH is detected.

  • If the status code is STATUS_SUCCESS and the reported size is nonzero, this size is assumed to be the correct buffer size.

  • If the status code signaled success and the reported size is 0, the program starts with a 1-byte buffer and keeps on growing it while calling NtQuerySystemInformation() until a status other than STATUS_INFO_LENGTH_MISMATCH is returned. If it's STATUS_SUCCESS, the current buffer size is the desired value.

All info classes returning variable-size data succeed after the first call to NtQuerySystemInformation() and set the result size properly. Listing One is a sample implementation. Many info classes returning fixed-size data blocks succeed as soon as the buffer size is decremented to the correct size, and set the result size to this value. The second pass is only needed for some odd info classes that behave unlike the standard.

Of course, this heuristic is time consuming for info classes that return just a couple of bytes and require an exact size specification. However, the latency is in the range of just a few seconds on Pentium machines. (On a 400-MHz Pentium II, it takes 16 seconds to dump the entire currently valid info class range of 0 through 45.) Anyway, this is the only general approach I know of that returns correct results for virtually all info classes defined for NT 4.0 SP5. Of course, you can optimize the algorithm by using a priori knowledge about the info classes, but I wanted to create a program that's expected to work reliably even on future versions of NT without change.

The general command-line format of ntqsi.exe is: ntqsi { [+p|-p] [/<error limit>] [ <class> | [<class#1>]-[<class#n>] ] }.

Pointer Relocation

In the most simple case, you can enter, say, ntqsi 18, and get a hex dump containing some familiar looking Unicode text. Obviously, this is some information about your page-file configuration. Example 2 is a sample printout. Now try the same command with +p inserted before the number, and you should get something like the hex dump in Example 3. There's not much difference, except for the 4 bytes at offset 0x14 (marked by "=" characters in between). As you'll see, this is the "Buffer" member of a UNICODE_STRING structure (that is, a pointer to a Unicode string) holding the path of a page file.

If NtQuerySystemInformation() returns pointers inside the result data, they always point to addresses inside the caller's buffer. This ensures that all pointers remain valid as long as the caller is working with the returned data, and that deallocating the data buffer also frees all data blocks referenced by those pointers. ntqsi.exe is clever enough to detect pointers inside the returned data stream and marks them with "=" characters between the 4 bytes forming the pointer (see Examples 2 and 3, offset 0x14). Moreover, it subtracts the address of its internal buffer from the pointer, thus normalizing it to become a 0-based offset into the data. If you read out the value at offset 0x14 in Example 3, you get 0x00000018, and that's exactly where you'll find a sequence of Unicode characters making up the file path "D:\pagefile.sys."

Of course, ntqsi.exe lets you specify ranges of info classes, like ntqsi 0-45, or sets like ntqsi 1 2 5 3. In a range specification, you can omit the starting or ending class, or even both. Omitting the first part of the pair defaults to 0, omitting the second part means "go on forever." (There is an upper limit, imposed by the number of values a DWORD variable can take.) Because the latter case might produce tons of garbage as soon as the last info class is transcended, it's wise to use the /<error limit> option here. For instance, using ntqsi /7 37- will dump all info classes starting at 37, and stop after seven consecutive errors have occurred.

ntqsi.exe attempts to find out which members of a structure are actually used by the system. Therefore, it fetches the data twice after presetting the buffer with certain bit patterns. On the first run, it uses 0x55 for every byte, and 0xAA on the second run. Every byte position that contains 0x55 after the first and 0xAA after the second call is considered to be unused. Those bytes are marked by a double dash in the hex output.

The NT Kernel Debugger

Although it's interesting to study hex dumps of NtQuerySystemInformation(), it would be more exciting to know what all those bytes are about. That's really difficult, and it took me several weeks to compile enough information to get a basic understanding of the returned data. If you ever try to gain information about undocumented NT interfaces and don't happen to have access to the source code, there's probably no way out other than using the NT Kernel Debugger (KD), along with the symbol files on your NT setup CD (look for \support\debug\i386\symbols).

The KD comes in two flavors: the GUI version windbg.exe shipping with the Platform SDK, and the console-mode version i386kd.exe (or alphakd,exe, mipskd.exe, or ppckd.exe, if you happen to use a platform other than Intel x86) included on the NT setup CD (\support\debug\<platform>). I recommend using i386kd.exe, because it features a much more powerful instruction set than windbg.exe. In Inside Windows NT, Second Edition (Microsoft Press, 1998), David A. Solomon gives detailed instructions on how to set up i386kd.exe and the symbol files, using the information from a forced crash dump system image. The basic steps are:

1. Enable creation of a crash dump image in the Control Panel (System applet, "Startup/Shutdown" tab). Also, be sure to increase your page file size (twice the amount of physical memory is a good value) so you won't run into a virtual memory problem later.

2. Force a crash dump by killing the "Client Server Runtime Process" csrss.exe, which is the module containing the Win32 subsystem. (You need kill.exe from the NT Resource Kit or an equivalent utility that is able to grab enough privileges to terminate a system service.)

3. Reboot so the crash dump image is copied from the page file to the file specified in the Control Panel (%systemroot%\memory.dmp by default). Note that your system will run quite low on virtual memory after doing that, so you should reboot once more. (That's why I recommended increasing the page file size in the first step.)

4. Set the _NT_SYMBOL_PATH environment variable to the base directory of the symbol files. This directory contains a couple of subdirectories, like "dll," "drv," "exe," and the like.

5. Issue the command i386kd -z <crash dump image>, where "<crash dump image>" is the full path of the crash dump file created before.

Having done this, you can explore a snapshot of the system that was valid at the time you were killing csrss.exe. You can't access some system data structures in user-mode memory, like the process and thread environment blocks. You need a live system to explore those. Now, at the kd> prompt, you can enter the debugger commands of your choice.

There are both built-in commands and external bang commands prefixed by a "!" character, which are processed by an extension DLL. i386kd.exe uses the extension DLL kdextx86.dll by default (kdextalp.dll, kdextmip.dll, and kdextppc on the other platforms). Those DLLs export a couple of APIs that are named like the corresponding bang commands they handle. Enter !help at the kd> prompt to get a list of bang commands supported by the extension DLL currently in use.

Retrieving Internal NT Symbols

Probably the most useful kernel debugger command is ln (list nearest symbol). You can specify either a hex address or a symbol name after the ln token, and you'll get the name and address of the symbol whose address is below the specified one and has the least distance from it. It also displays the address and name of the next symbol, so you get an idea of how large the data addressed by the symbol actually is. (Some symbols are obviously missing, so the distance between two symbols can be misleading.)

When you're exploring NT internals, you usually don't have more than a disassembly listing at hand, which will not contain more symbolic information than is exported from the NT system modules. The ln debugger command helps you convert opaque binary addresses into meaningful symbols that might suggest what this address is used for. Specifically, you can look up the names of all internal functions used by the code you're currently examining. (If you're still looking for a good Win32 disassembler, you might want to contact Jean-Louis Seigne at jls@chez.com who has written a great one.)

Another useful KD command, x nt!*, yields a complete list of symbols defined inside the NT kernel. And finally, the commands db (dump BYTEs), dw (dump WORDs), and dd (dump DWORDs) let you see hex dumps of memory regions in the appropriate number format.

Although the APIs NtQuerySystemInformation() and NtSetSystemInformation() are exported by ntdll.dll, the actual implementation is located in a different module: ntoskrnl.exe. As Example 4 shows, the implementation of those APIs inside ntdll.dll is trivial. The secret of using INT 2Eh is that it serves as a gate from the processor's user mode to kernel mode, so the handlers can execute with enough privileges to access kernel-mode code and data. (On other CPU platforms, different but equivalent means are provided to achieve this effect.)

On the other side of the INT 2Eh gate, the NT operating system kernel (ntoskrnl.exe) is waiting. It uses the index value in register EAX to look up an entry in the kernel's service descriptor table. This entry contains the information that's needed to copy the required number of bytes from the caller's stack (addressed by EDX) to a system stack, and to call the associated handler. Under Windows NT 4.0 SP3, the handlers of NtQuerySystemInformation() and NtSetSystemInformation() happen to be located in the .PAGE section of ntoskrnl.exe at 0x801558c2 and 0x80156a48, respectively. You can use the kernel debugger's ln command to verify that.

System Information Classes

In Windows NT Version 4.0 SP5, NtQuerySystemInformation() and NtSetSystemInformation() recognize info class values in the range of 0 up to 45. However, not all info classes will return a success status. The info classes can be put in one or more categories of the following:

  • Read-only info classes (not to be used with NtSetSystemInformation()).

  • Write-only info classes (not to be used with NtQuerySystemInformation()).

  • Info classes only available in the "Checked Build" of Windows NT.

  • Invalid info classes (either defunct or superseded by other info classes or system APIs).

To keep things simple, I deal almost exclusively with the first category. Table 1 lists all info classes I've been able to identify.

A Custom Header File for ntdll.dll

To save you from the hassle of directly interfacing to ntdll.dll, I've designed the library ntlib.dll, which contains high-level APIs to load and parse the more complex data sets returned by NtQuerySystemInformation(), as well as several utility functions for device-driver management, memory size, and time conversion. It also features a powerful command-line parser with full-blown option management and command help support. The associated utility program ntinfo.exe demonstrates how to make use of it. The source code of both programs is available electronically.

The main problem with ntdll.dll is that neither the Win32 Platform SDK nor the NT DDK provide appropriate header files that can be used in user mode applications. The main DDK header file ntddk.h is designed for kernel-mode drivers, which don't rely on the windows.h file required for Win32 applications. Hence, Microsoft didn't care to make them compatible. If you ever try to include both of them in a single project, the compiler will emit tons of errors and warnings. To remedy this, I'm using the same approach as Matt Pietrek in his MSJ articles: Create my own header file that contains just enough definitions to be able to interface to ntdll.dll, while not sacrificing windows.h compatibility. Now, what name would be most appropriate for a file like that? Yes, of course: NtDll.h.

NtDll.h is designed to be modular. It defines macros, constants, and structures that are used in almost any part of the NT system. Near the end of the file, it yanks in the files NtObj.h, NtTeb.h, and NtSys.h. Only the latter is included in the online package and contains all definitions related to NtQuerySystemInformation() and NtSetSystemInformation(). The others are just stubs. The real things contain so much undocumented stuff about NT objects in general and thread/process objects in particular, that I decided to hold them back for a separate article on NT objects, hopefully to be available soon.

Displaying Structured NT System Information

The general command-line format of ntinfo.exe is: ntinfo { <option> { [<parameter>] } }. A command line may contain several options, which consist of a prefix character ("-" or "/") and a mnemonic name, such as -pagefile or -process. The option name can be abbreviated, provided that the remaining characters still identify it unambiguously. Hence, ntinfo -p is invalid, because it matches both -pagefile and -process. However, ntinfo -pa and ntinfo -pr are okay. Please note that ntinfo.exe cares to list all matching options in the case of an ambiguous specification, using the command-line parser included in ntlib.dll.

Some options don't expect additional parameters, while others can take one or more optional parameters; see Table 2 for a summary of options. For instance, the -pagefile option takes no parameters and always lists all installed page files, while the output of the -process option can be filtered by specifying one or more process IDs.

Some options display just a subset of the available information by default, and you have to specify "*" or "all" to get the complete data. The -object option belongs to this category. Still others accept a this parameter that defaults to the current process, and which is, of course, ntinfo.exe itself. For example, ntinfo -handle this lists all handles currently held by ntinfo.exe. You can also specify a negative filter: If "*" or "all" is specified with "this" or an individual ID or pattern, it means that the result should contain all items except those specified in addition to "*" or "all."

It's remarkable that this undocumented system interface hasn't changed very much in Windows 2000. The latest Beta available at this writing (Release Candidate 1) adds two more info classes, and introduces minor changes in a few data structures. For example, the ntinfo option "-thread" displays garbage, "-lookaside" fails on some entries, and "-object" might throw an exception while trying to display the new "WaitablePort" object list. However, the remaining options appear to work correctly.

Table 3: Object type IDs used by the SystemHandleInformation class.

Conclusion

The system information interface discussed here and exposed by the ntdll.dll API pair NtQuerySystemInformation() and NtSetSystemInformation() offers functions -- querying system configuration items and process/thread or system module lists -- no system utility developer can live without.

DDJ

Listing One

NTSTATUS WINAPI QuerySystemInformation (SYSTEMINFOCLASS sic,
                                        PPVOID          ppData,
                                        PDWORD          pdData)
    {
    PVOID    pData;
    DWORD    dData, n;
    NTSTATUS ns = STATUS_INVALID_PARAMETER;
    dData = 0;
    if (ppData != NULL)
        {
        n = 0;
        while ((pData = LocalAlloc (LMEM_FIXED, dData += 0x10000))
               != NULL)
            {
            ns = NtQuerySystemInformation (sic, pData, dData, &n);
            if (ns != STATUS_SUCCESS) n = 0;
            if (ns != STATUS_INFO_LENGTH_MISMATCH) break;
            LocalFree (pData);
            }
        dData = n;
        if (pData != NULL)
            {
            if (ns != STATUS_SUCCESS)
                {
                LocalFree (pData);
                pData = NULL;
                dData = 0;
                }
            }
        else
            {
            ns = STATUS_NO_MEMORY;
            }
        *ppData = pData;
        }
    if (pdData != NULL) *pdData = dData;
    return ns;
    }

Back to Article

Listing Two

// 05: SystemProcessInformation
//     see ExpGetProcessInformation()
//     see also ExpCopyProcessInfo(), ExpCopyThreadInfo()
typedef struct _SYSTEM_THREAD
    {
    QWORD        qKernelTime;       // 100 nsec units
    QWORD        qUserTime;         // 100 nsec units
    QWORD        qCreateTime;       // relative to 01-01-1601
    DWORD        d18;
    PVOID        pStartAddress;
    CLIENT_ID    Cid;               // process/thread ids
    DWORD        dPriority;
    DWORD        dBasePriority;
    DWORD        dContextSwitches;
    DWORD        dThreadState;      // 2=running, 5=waiting
    KWAIT_REASON WaitReason;
    DWORD        dReserved01;
    }
    SYSTEM_THREAD;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
typedef struct _SYSTEM_PROCESS_INFORMATION
    {
    DWORD          dNext;           // relative offset
    DWORD          dThreadCount;
    DWORD          dReserved01;
    DWORD          dReserved02;
    DWORD          dReserved03;
    DWORD          dReserved04;
    DWORD          dReserved05;
    DWORD          dReserved06;
    QWORD          qCreateTime;     // relative to 01-01-1601

    QWORD          qUserTime;       // 100 nsec units
    QWORD          qKernelTime;     // 100 nsec units
    UNICODE_STRING usName;
    KPRIORITY      BasePriority;
    DWORD          dUniqueProcessId;
    DWORD          dInheritedFromUniqueProcessId;
    DWORD          dHandleCount;
    DWORD          dReserved07;
    DWORD          dReserved08;
    VM_COUNTERS    VmCounters;
    DWORD          dCommitCharge;   // bytes
    SYSTEM_THREAD  ast [];
    }
    SYSTEM_PROCESS_INFORMATION;

Back to Article

Listing Three

void WINAPI DisplayProcesses (void)
    {
    NTL_TABLE                   nt;
    PSYSTEM_PROCESS_INFORMATION pspi;
    TIME_FIELDS                 tf;
    DWORD                       i;
    if (NtlTableProcess (&nt) == STATUS_SUCCESS)
        {
        printf (T("PID Org BP Th Hdls CommChrg WSetSize ")
                T("PFCount Start date and time Name\r\n"));
        pspi = NtlTableFirst (&nt);
        for (i = 0; i < nt.dCount; i++)
            {
            NtlTimeUnpack (&pspi->qCreateTime, &tf);
            printf (T("\r\n%3lu %3lu %2lu %2lu %4lu %8lu %8lu ")
                    T("%7lu %02u-%02u-%04u %02u:%02u:%02u ")
                    T("\"%ls\""),
                    pspi->dUniqueProcessId,
                    pspi->dInheritedFromUniqueProcessId,
                    pspi->BasePriority,
                    pspi->dThreadCount,
                    pspi->dHandleCount,
                    pspi->dCommitCharge,
                    pspi->VmCounters.WorkingSetSize,
                    pspi->VmCounters.PageFaultCount,
                    tf.Month, tf.Day,    tf.Year,
                    tf.Hour,  tf.Minute, tf.Second,
                    (pspi->usName.Buffer != NULL
                     ? pspi->usName.Buffer
                     : L"Idle"));
            pspi = NtlTableNext (&nt, pspi);
            }
        NtlTableUnload (&nt);

        }
    return;
    }

Back to Article

Listing Four

// 11: SystemModuleInformation
//     see ExpQueryModuleInformation
typedef struct _SYSTEM_MODULE
    {
    DWORD dReserved01;
    DWORD d04;
    PVOID pAddress;
    DWORD dSize;                // bytes
    DWORD dFlags;
    WORD  wId;                  // zero based
    WORD  wRank;                // 0 if not assigned
    WORD  w18;
    WORD  wNameOffset;
    BYTE  abName [MAXIMUM_FILENAME_LENGTH];
    }
    SYSTEM_MODULE;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
typedef struct _SYSTEM_MODULE_INFORMATION
    {
    DWORD         dCount;
    SYSTEM_MODULE asm [];
    }
    SYSTEM_MODULE_INFORMATION;

Back to Article

Listing Five

// 16: SystemHandleInformation
//     see ExpGetHandleInformation ()
typedef struct _SYSTEM_HANDLE
    {
    DWORD       dIdProcess;
    BYTE        bObjectType;    // OB_TYPE_*
    BYTE        bFlags;         // bits 0..2 HANDLE_FLAG_*
    WORD        wValue;         // multiple of 4
    POBJECT     pObject;
    ACCESS_MASK GrantedAccess;
    }
    SYSTEM_HANDLE;

Back to Article

Listing Six

// 18: SystemPageFileInformation
//     see MmGetPageFileInformation()
typedef struct _SYSTEM_PAGE_FILE_INFORMATION
    {
    DWORD          dNext;       // relative offset
    DWORD          dTotal;      // pages
    DWORD          dInUse;      // pages
    DWORD          dPeak;       // pages
    UNICODE_STRING usName;
    }
    SYSTEM_PAGE_FILE_INFORMATION;

Back to Article

Listing Seven

// 26: SystemLoadDriver (set mode only)
//     see MmLoadSystemImage()
//     user mode: STATUS_PRIVILEGE_NOT_HELD returned
typedef struct _SYSTEM_LOAD_DRIVER
    {
    UNICODE_STRING usImageFile;     // input
    PVOID          pBaseAddress;    // output
    HANDLE         hSystemImage;    // output
    PVOID          pEntryPoint;     // output
    PVOID          pDirectoryEntry; // output
    }
    SYSTEM_LOAD_DRIVER;
// 27: SystemUnloadDriver (set mode only)
//     see MmUnloadSystemImage()
//     user mode: STATUS_PRIVILEGE_NOT_HELD returned
typedef struct _SYSTEM_UNLOAD_DRIVER
    {
    HANDLE hSystemImage;            // received via SystemLoadDriver
    }
    SYSTEM_UNLOAD_DRIVER;
// 38: SystemAddDriver (set mode only)
//     see MmLoadSystemImage(), MmUnloadSystemImage()
//     user mode: SeLoadDriverPrivilege required
typedef struct _SYSTEM_ADD_DRIVER
    {
    UNICODE_STRING usImageFile;
    }
    SYSTEM_ADD_DRIVER;


Back to Article


Copyright © 1999, Dr. Dobb's Journal

Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video