Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

.NET

The Microsoft Flash File System


FEB95: The Microsoft Flash File System

The Microsoft Flash File System

Flash file system mechanics

Peter Torelli

Peter is a student at Rensselaer Polytechnic Institute and can be contacted through the DDJ offices.


A file system stores and manipulates named objects in a hierarchical manner. Named objects, called "files," can be organized and stored in sets, or directories. In turn, directories can store other directories as well, leading to a file-directory hierarchy. The Microsoft MS-DOS and Intel RMX operating systems use a File Allocation Table (FAT) file system to maintain a file-directory hierarchy. A flash file system implements this basic file-directory scheme while exploiting the benefits of flash media. Microsoft has implemented a flash file system for DOS, known as "FFS."

In his article, "Flash File Systems" (DDJ, May 1993), Drew Gislason described an implementation for a basic flash translation layer. In this article, I'll continue his discussion by examining a byte-oriented file system for flash media, focusing on the mechanics behind a flash file system based on the Microsoft data structures.

FTL and FFS

As he himself acknowledged, Drew's device driver isn't really a flash file system, but rather a layer of code that translates the DOS FAT file system requests from sectors to physical flash addresses. This approach to using flash media as a DOS disk is commonly known as a "flash translation layer" (FTL). Under this scheme, DOS still uses its FAT file system to process file and directory operations, but the FTL block device driver accesses the media. The device driver maintains the organization of the data within the media by means of special headers that describe the arrangement of the sectors in each block. When the DOS FAT file system needs to read a sector, the FTL processes the request by looking up that sector's physical address in its headers and returning the requested data to DOS. The same process occurs for writes, unless no clean flash space exists. In this case, the FTL driver performs a cleanup to reclaim deallocated space and then performs the write.

FFS departs from the sector-oriented scheme by organizing and storing byte-sized data within various structures. Unlike an FTL device driver, the DOS FAT file system plays no part in FFS; instead, an "installable file system" enters the picture. DOS provides an interface for non-FAT file systems commonly known as the "INT2F Network Redirector Interface;" see Figure 1. The redirector accepts requests from DOS at a level above that of a specific file system, making it possible for any type of storage device or network computer to interface with DOS. The FFS interface to DOS works the same way.

Unlike the device driver, FFS performs no low-level I/O on the flash media. Instead, a PCMCIA-compliant memory card device driver is needed to perform certain functions through the DOS generic IOCTL call. BIOS vendors such as SystemSoft, AMI, Award, Phoenix, and Intel provide these PCMCIA drivers.

Understanding FFS

Three concepts need to be understood before implementing an FFS: FFS data structures, their arrangement in the media, and the basic manipulation of the data structures inherent to a file system. (A fourth element, the interface to the OS, requires a separate discussion. See Andrew Schulman's Undocumented DOS, Addison-Wesley, 1993, for a description of the DOS INT2F redirector interface.)

Linked lists form the basis of FFS. Files, directories, and data are all stored in linked lists. The different types of data structures described here serve as the links in these lists. Microsoft defines four different structures for storing and arranging data in the FFS format: file entry, directory entry, file info, and boot record. The boot record structure contains data describing the media's geometry (size, number of blocks, erase block size, and so on), as well as FFS version information. (Since only one copy of the boot record exists, I'll exclude it when I refer to "data structures.")

The file-directory hierarchy exists in the file-entry directory-entry list (FEDE). In an FFS-formatted flash card, the information displayed by typing a DIR command corresponds to the file and directory entries in that directory's FEDE chain, as shown in Figure 2. All files or subdirectory entries at the same level are part of one FEDE chain and are referred to as "siblings." If a subdirectory exists in that FEDE chain, it points to another FEDE chain. If more subdirectories exist in that FEDE chain, the tree continues.

Actual file-entry file data resembles the FEDE list, except that each entry in the file's list is a file-info structure. This list of file-info structures points sequentially to the regions of the card that contain the file's data. FFS performs a read-file request by locating the proper file entry, traversing its file-info chain, and returning the requested data.

File-entry and directory-entry structures contain three pointers: sibling, primary, and secondary (see Listing One). The sibling pointer always points to the next entry in the same level as that structure. The primary pointer of a directory entry points to the first entry in that directory's FEDE chain. The primary pointer of a file entry points to its file-info chain. The secondary pointers of both structures point to files or directories of the same name that supersede the existing structures.

File-info structures point to "extents," regions of the card that contain file data. The maximum size of an extent is 65,535 bytes. Like file and directory entries, file-info structures contain sibling and secondary pointers, but the primary pointer has been replaced by an extent pointer. The extent pointer points to the first extent of that file's data. The sibling pointer addresses the next file-info structure in the chain, and the secondary pointer indicates where to find updated or superseded extent data.

Allocation of Flash Media Space for Structures and File Data

The pointers I've just discussed don't explicitly reference the physical address of a structure or extent within a block; instead, they point to a block-allocation member, which in turn points to the physical location of that particular structure. Which brings us to the second detail of FFS: block-allocation structures and members.

Each erase block contains one block-allocation structure (BAS). The BAS in Listing Two exists at the topmost address range of every erase block. It contains the block's logical number and erase count, whether or not it is a spare, and other block-specific fields. It also marks the starting point to the chain of block-allocation members (BAMs). FFS uses the BASs to determine the logical block ordering (that is, by locating the boot record or finding a spare block).

To maintain organization of the data structures and file extents, FFS uses BAMs; see Listing Two. These 6-byte fields begin at the top of each erase block, just below the BAS, and grow downward as more structures and extents are written to that block. They contain the length of the pointed-to data region, the beginning offset of that data relative to address zero of that block, and a status field indicating whether or not the data being pointed to is valid or deleted. FFS uses BAMs to locate the physical offset of a data structure or extent by determining if the structure or extent has been deleted via the status field and assisting FFS in performing reclaim.

As FFS writes files or directories to a block, the data structures and extents grow from address zero of the block toward the top and allocation members grow downward from top to bottom; see Figure 3.

Spare Blocks and the BAM: Reclaim Explained

Once a flash cell has been programmed (changed from 1 to 0), it must be erased (changed from 0 to 1) before it can be reprogrammed. Intel Series 2 and 2+ Flash Cards contain multiple 128-Kbyte blocks that require one second to change all of the 0s back to 1s.

When an FFS user deletes a file or directory, FFS zeros the uppermost bit in all of the BAM's status fields associated with that file or directory. The data has been marked as deleted, but it still exists and the flash space it takes up cannot be used again until it's erased. Deleted space is commonly referred to as "dirty" or "deallocated" space. Since the rest of the block may contain valid data, FFS uses a process called "reclaim" to erase the deleted file's space while preserving the valid data.

FFS requires at least one block in an FFS partition to be reserved as a spare. A spare block is indicated in that block's BAS by both the BlockSequence and the BlockSequenceChecksum equaling FFFFH. A spare block is always clean (all 1s).

FFS performs a reclaim when the array becomes filled with used and dirty space. Before it can write another byte, some of the dirty space must be reclaimed. FFS begins a reclaim by selecting a block with the most dirty space. It copies that block's BAS into the spare block and then begins reading the BAMs of that block. If the status bit indicates a valid structure or extent, it gets copied into the spare block and the new BAM's physical offset is adjusted to reference its new address. If the structure or extent has been deleted, that BAM is skipped. The old block gets erased and becomes the new spare; the old spare now has the logical number and valid data of the old block, plus clean flash space. FFS can now write to this space. If it runs out, it performs further reclamation until the partition is full of valid data; see Figure 4.

Reclamation is the sole reason for FFS. BASs, BAMs, and spare blocks make reclaim possible. A robust reclaim mechanism is the key to an efficient flash file system. The more efficiently an FFS implementation handles reclamation, the better it performs.

FFS Mechanics

Before FFS can use a flash array, it must be formatted by a special formatting program. The formatter places a BAS at the top of every block, plus a boot record, root-directory entry, and volume label. It also designates up to eight blocks as spares.

Moving through the FFS file-directory hierarchy requires several basic functions:

  • Adding entries to the FEDE chain.
  • Locating the boot record and root directory.
  • Finding a matching filename in a FEDE chain.
Finding a particular structure in a list begins with a pointer. Primary, secondary, sibling, boot-record, and root-directory pointers all reference a BAM. The pointer is a double word, but not as a physical offset. The high word contains the logical block number, the low word references the logical BAM number.

For example, if the sibling pointer of the root-directory entry equals 00000001H, that translates into logical block 0, logical BAM 1. Reading the BAM's offset (24 bits) gives the physical offset to the sibling structure of the root-directory entry in that block.

To find the physical location of a structure, the necessary data must be obtained from the BAM. Listing Three takes a pointer from a structure (sibling, primary, or whatever) and returns the BAM associated with it. The calling program can then compute where in that block the structure exists. Listing Three then returns the BAS from the given physical block, letting you translate a logical-block address to a physical one so that an absolute flash address may be accessed. With the two procedures in Listing Four , you can read the BAM associated with a structure pointer.

Listing Five presents two procedures used in conjunction with Listing Four to perform the translations necessary to locate the physical address of a structure or extent within a flash array. Listing Six simplifies the process of translating the BAM's 3-byte offset into an address.

The next step in traversing a FEDE chain is to obtain the structure referenced by the previous pointer. Listing Seven reads the FE or DE structure associated with a BAM and returns the data it found in the FEDE structure.

Now that you can read a BAM and a FEDE entry, you can traverse the list (Listing Eight) and find the BAM associated with the last structure. Passing the BAM associated with any FEDE entry in a chain to this function causes it to return the BAM of the last structure. Adding a file or directory requires the procedures in Listing Nine . Assuming you have already written your new structure and its BAM to flash, you append it to the current FEDE chain. LinkBAMptr refers to the new structure, while CurrentBAMptr refers to the current place in the chain.

The conventional heuristic for locating the root directory begins by searching each BAS, first for a boot record, then for the root-directory entry. Qualifying long pathnames often requires traversing several FEDE chains, beginning with the root.

You find the boot record by reading each block's BAS; see Listing Ten . You can then use Listing Eleven to read the boot record using the BAM found in Listing Ten. Finally, you use Listing Twelve to combine the functions in Listings Ten and Eleven to obtain the root directory's BAM.

Another important feature of a File System is the ability to locate a filename. Listing Thirteen searches a FEDE chain from its topmost entry until it finds a file or directory entry whose name matches the 11-character DOS name. It returns a BAM to the matching entry if it is found, or ERROR (FNULL) if not.

Conclusion

The first FFS implementation did not function smoothly: FEDE chains grew out of control, reclaim would occur at the wrong moments, and so on. Today, FFS implementations by SystemSoft and Microsoft have addressed these issues and shown that FFS can achieve reasonable performance.

Additionally, competition between the FTL and FFS approaches has caught the attention of systems designers. SCM Microsystems and M Systems both supply FTL solutions for DOS-based PCs. Still, using an FTL requires a file system, and exchanging data between other FTL platforms does not guarantee compatibility unless they use the same file system. The procedures I've presented in this article cover the basic elements of flash file system mechanics so that you can format a card, follow FEDE chains, add files or directories, and so on. The header file for the procedures I've presented is in Listing Fourteen .

If standards begin to solidify and enough resources are devoted to the development of other operating-system FFS drivers, flash cards could become a dominant form of data exchange for computer users.

Figure 1 DOS model for using flash cards. Figure 2 FEDE chain examples. Figure 3 FFS usage of an erase block. Figure 4 Cleaning up dirty blocks.

Listing One


struct FileOrDirectoryEntry {   
    word    Status;
    dword   SiblingPtr;
    dword   PrimaryPtr;
    dword   SecondaryPtr;
    byte    Attributes;
    word    Time;
    word    Date;
    word    VarStructLen;
    byte    NameLen;
    byte    Name[8];
    byte    Ext[3];
};

struct FileInfoStructure {
    word    Status
    dword   SiblingPtr;
    dword   ExtentPtr;
    dword   SecondaryPtr;
    byte    Attributes;
    word    Time;
    word    Date;
    word    VarStructLen
    word    UncompressedExtentLen
    word    CompressedExtentLen
};


Listing Two


struct BlockAllocationStructure {
    dword   BootRecordPtr;
    dword   EraseCount;
    word    BlockSeq;
    word    BlockSeqChecksum;
    word    Status;
};

struct BlockAllocationMember {
    byte    Status;
    byte    Offset[3];
    word    Len;
};


Listing Three


struct BlockAllocStruct read_BAS( UINT block ) {
  struct BlockAllocStruct BAS;
  dword  address=0UL;
  /* Calculate the top of the physical block address. */
  address = (ULONG) ( block * (ULONG) block_size );
  /* Subtract back the size of a BlockAllocStruct. */
  address -= sizeof( struct BlockAllocStruct );
  /* Read it. */
  read_memory( (UCHAR *) &BAS, address, sizeof( struct BlockAllocStruct ) );
  /* Return the BAS. */
  return BAS;
}


Listing Four


UINT log2phy( UINT lblock ) {
  struct BlockAllocStruct CurBAS;
  UINT   block=0;
  ULONG  address=0UL;

  /* Start at physical block 1. */
  block = 1;
  /* Read each BAS. */
  while( block <= NumBlocks ) {
    CurBAS = read_BAS( block );
    /* Look for the sequence that matches our requested lblock. */
    if( CurBAS.BlockSeq == lblock ) break;
    block++;
  }
  return block;
}
struct BlockAllocMember get_BAM_data( ULONG BAMptr ) {
  struct BlockAllocMember BAM;
  UINT   logical_BAM=0;
  UINT   logical_block=0;
  UINT   physical_block=0;
  ULONG  address=0UL;
  /* Determine the logical Block and BAM number. */
  logical_block  = (ULONG) BAMptr >> 16;
  logical_BAM    = BAMptr & 0xFFFF;
  /* Call the routine that determines the physical block number.  This
     might actually parse each BAS, or it may be read from a look-up table
     that FFS creates for every card insertion and maintains thereafter.  For
     now, translate by reading the BASs. */
  physical_block = log2phy( logical_block );
  /* Determine the top of the physical blocks address. */
  address  = (ULONG) ( physical_block * block_size );
  /* Subtract from it the size of a BAS plus the Number of BAMs
     preceeding the BAM we wish to read. */
  address -= ( sizeof( BlockAllocStruct ) + 
               ( ( logical_BAM + 1 ) * sizeof( struct BlockAllocMember ) ) );
  /* Read the BAM. */
  read_memory( (UCHAR *) &BAM, address, sizeof( struct BlockAllocMember ) );
  /* Return the BAM. */
  return BAM;
}


Listing Five


ULONG get_block_base( ULONG BAMptr ) {
  UINT lblock=0;
  UINT block=0;

  /* Get the logical block # from the BAM. */
  lblock = ( (ULONG) BAMptr ) >> 16;
  /* Translate it to physical. */
  block = log2phy( lblock );
  /* Return its physical base address. */
  return ( ( block - 1 ) * block_size );
}


Listing Six


ULONG translate_BAM_offset( ULONG BAM ) {
  ULONG address=0UL;

  /* Sum the BAM offset components. */
  address = (   ( (ULONG) BAM.Offset[0] ) + 
              ( ( (ULONG) BAM.Offset[1] ) << 8  ) + 
              ( ( (ULONG) BAM.Offset[2] ) << 16 ) );
  return address;
}


Listing Seven


struct FEDE_Entry get_FEDE( ULONG CurrentBAMptr ) {
  struct BlockAllocMember BAM;
  struct FEDE_Entry FEDE;
  ULONG  address=0UL;
  UINT   length=0;
  
  /* Read the BAM data. */
  BAM = get_BAM_data( CurrentBAMptr );
  /* Determine the base absolute address of the Boot Record's block. */
  address = get_block_base( CurrentBAMptr );
  /* Calculate the offset (in that block) of the Boot Record. */
  address += translate_BAM_offset( BAM );
  /* Reat the structure. */
  read_memory( (UCHAR *) &FEDE, address, sizeof( struct FEDE_Entry ) );
  /* And return it. */
  return FEDE;
}


Listing Eight


ULONG go_to_end_of_FEDE( ULONG CurrentBAMptr ) {
  struct FEDE_Entry FEDE;
  UCHAR  buffer[12];

  /* Start traversing at the given point in list specified by CurrentBAMptr. */
  do {
    /* Read the FEDE structure. */
    FEDE = get_FEDE( CurrentBAMptr );
    /* Un-comment the following lines and this code will print out 
       the filenames as it goes.  Passing it the PrimaryPtr of the parent
       directory would list all of the files/subdirs in that directory. */
     /* strncpy( buffer, FEDE.Name, 11 ); */
     /* buffer[11]=0; */
     /* printf("%s\n", buffer); */
    /* If the this isn't the last element, then save the next pointer. */
    if( FEDE.SiblingPtr != FNULL ) CurrentBAMptr = FEDE.SiblingPtr;
    /* Otherwise exit the loop. */
  } while( FEDE.SiblingPtr != FNULL );
  /* The CurrentBAMptr now references the last structure of the chain. */
  return CurrentBAMptr;
}


Listing Nine


void add_FEDE_sibling( ULONG CurrentBAMptr, ULONG LinkBAMptr ) {
  struct BlockAllocMember BAM;
  struct FEDE_Entry FEDE;
  ULONG  LastBAMptr=0UL;
  UINT   length=0;
  ULONG  address=0UL;

  /* Go to the end of current FEDE chain. */
  LastBAMptr = go_to_end_of_FEDE( CurrentBAMptr );
  /* Read the FEDE. */
  FEDE = get_FEDE( LastBAMptr );
  /* Connect the link and update the status bits. */
  FEDE.SiblingPtr = LinkBAMptr;
  /* Zero bit 6 of the status word, indicating the SiblingPtr is valid. */
  FEDE.Status &= 0xFFCF;
  /* Since only the Sibling pointer and status get programmed (the other data
     remains unchanged) re-write the new data to the same location. */
  address = get_block_base( CurrentBAMptr );
  address += translate_BAM_offset( BAM );
  write_memory( (UCHAR *) &FEDE, address, sizeof( struct FEDE_Entry ) );
}


Listing Ten


ULONG find_Boot_Record( void ) {
  struct BlockAllocStruct BAS;
  UINT   current_block=0;
  UCHAR  got_BR_flag=FALSE;

  /* Start at physical block one. */
  current_block = 1;
  /* Loop through physical blocks until we find the Boot Record. */
  do {
    /* Read the BAS of the current BAS. */
    BAS = read_BAS( current_block );
    /* No Boot Record, go to next block. */
    if( BAS.Status & 0x70 != 0x30 ) current_block++;
    /* Otherwise set our flag and break with the correct block. */
    else {
      got_BR_flag = TRUE;
      break;
    }
    /* Make sure we don't run out of blocks (external variable) just
       in case this card was not formatted previously. */
  } while( current_block <= number_of_blocks );
  /* If the flag wasn't set then we didn't find the Boot Record. */
  if( !got_BR_flag ) return ERROR; 
  /* Otherwise, return the Boot Record Pointer. */
  return BAS.BootRecordPtr;
}


Listing Eleven


struct BootRecord read_Boot_Record( ULONG BootRecBAMptr ) {
  struct BootRecord BR;
  struct BlockAllocMember BAM;
  ULONG address=0UL;
  UINT length=0;

  /* Given the root directory BAM pointer, read the BAM to find structure. */
  BAM = get_BAM_data( BootRecBAMptr );
  /* Determine the base absolute address of the Boot Record's block. */
  address = get_block_base( BootRecBAMptr );
  /* Calculate the offset (in that block) of the Boot Record. */
  address += translate_BAM_offset( BAM );
  /* Read it. */
  read_memory( (UCHAR *) &BR, address, sizeof( BootRecord ) );
  /* Return the Boot Record. */
  return BR;  
}


Listing Twelve


ULONG get_ROOT_BAM( void ) {
  struct BootRecord BR;
  ULONG  BAMptr=0UL;

  /* Search for the Boot Record BAM. */
  BAMptr = find_Boot_Record();
  /* Read the Boot Record. */
  BR = read_Boot_Record( BAMptr );
  /* Return the ROOT BAM Pointer;
  return BR.RootDirectoryPtr;
}

ULONG find_in_FEDE( char *DOSname, ULONG topBAMptr ) {
  struct FEDE_Entry FEDE;
  UCHAR  index=0;
  UCHAR  error=FALSE;
  char   buffer[12];
  
  /* Start traversing the FEDE chain starting with topBAMptr. */
  do {
    error=FALSE;
    index = 0;
    /* Read each FEDE element. */
    FEDE = get_FEDE( topBAMptr );
    /* Compare the first 8 characters of the 11 char. DOS name. */
    do {
      /* If one character doesnt match, set the error flag and break. */
      if( DOSname[index] != FEDE.Name[index] ) {
        error = TRUE;
        break;
      }
      index++;
    } while( index < 8 );
    /* If the name matched, check the extension. */
    if( !error ) {
      do {
        if( DOSname[index] != FEDE.Ext[index-8] ) {
          error = TRUE;
          break;
        }
        index++;
      } while( index < 11 );
    }
    /* If we've finished the matching loop WITHOUT an error, than we found
       our entry. */
    if( !error ) break;
    topBAMptr = FEDE.SiblingPtr;
  } while( topBAMptr != FNULL );

  /* There's No point in checking for the topBAMptr to see if we reached
     the end, we defined ERROR as -1 which is a 32 bit FNULL. */
  return topBAMptr;
}


Listing Thirteen

Listing Thirteen

Listing Fourteen


#define TRUE   0
#define FALSE  1
#define ERROR -1

/*  The following assumptions about machine data size are:
      ULONG = dword = 32 bits
      UINT  = word  = 16 bits
      UCHAR = byte  =  8 bits
*/ 

typedef unsigned long ULONG;
typedef unsigned int  UINT;
typedef unsigned char UCHAR;

struct BlockAllocStruct {
  ULONG BootRecordPtr;
  ULONG EraseCount;
  UINT  BlockSeq;
  UINT  BlockSeqChecksum;
  UINT  Status;
};
struct BlockAllocMember {
  UCHAR Status;
  UCHAR Offset[3];
  UINT  Len;
};
struct FEDE_Entry {
  UINT  Status;
  ULONG SiblingPtr;
  ULONG PrimaryPtr;
  ULONG SecondaryPtr;
  UCHAR Attributes;
  UINT  Time;
  UINT  Date;
  UINT  VarStructLen;
  UCHAR NameLen;
  UCHAR Name[8];
  UCHAR Ext[3];
}
/* Function prototypes for our sample code. */
struct BlockAllocStruct read_BAS( UINT block );
UINT   log2phy( UINT lblock );
struct BlockAllocMember get_BAM_data( ULONG BAMptr );
ULONG  get_block_base( ULONG BAMptr );
ULONG  translate_BAM_offset( ULONG BAM );
struct FEDE_Entry get_FEDE( ULONG CurrentBAMptr );
ULONG  go_to_end_of_FEDE( ULONG CurrentBAMptr );
void   add_FEDE_sibling( ULONG CurrentBAMptr, ULONG LinkBAMptr );
ULONG  find_Boot_Record( void );
struct BootRecord read_Boot_Record( ULONG BootRecBAMptr );
ULONG  get_ROOT_BAM( void );
ULONG  find_in_FEDE( char *DOSname, ULONG topBAMptr );

/* The following two procedures are hardware dependent and must be 
included by the OEM. */
UINT read_memory( UCHAR *to_buffer, ULONG from_address, UINT size );
UINT write_memory( UCHAR *from_buffer, ULONG to_address, UINT size );

/* The following external data describe the flash array. */
const extern ULONG block_size;
const extern UINT  number_of_blocks;


Copyright © 1995, 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.