
Miscellanous notes for programming under DOS
Michael Brutman, May 2011


Many of us are familiar with 32 bit flat address space environments.
Well, things are a little bit different in this environment.  While
C code is generally portable there are a few things you need to
be aware of.



The DOS 16 bit programming environment

  Welcome to old school programming ...

  Here are the downsides:

    - The operating system is more of a program loader.
    - There is no virtual memory or memory protection.
    - Space is very limited.
    - You basically have complete control of the machine
    - The older machines can be quite slow (4.77Mhz)

  It basically looks like embedded programming does today.  But
  as a reward you get total control of the machine.



Memory models

  This is a 16 bit environment!  Registers are 8 or 16 bits in size.
  It is obvious that we can't fit everything we need (ROM, program code,
  program data, stack, and heap) in a 16 bit address space.  Intel
  x86 architecture lets us get around the 16 bit nature of our pointers
  by using segments and combining pointers together.

  The compiler generally does a good job of hiding the fact that code
  lives in segments; it will generate the right series of instructions
  and pointer fixups to do whatever you need.  The compiler is also
  fairly good at managing your data pointers, giving you the illusion
  of a larger address space.  But if you use large data structures or
  do a lot of pointer manipulation and arithmetic you need to be aware
  of the segmented memory architecture.  (Pointers wrap at 64K boundaries
  unless you take precautions!)

  My general method of getting around pointer problems is to limit
  each instance of a data structure to 64KB or less.  That allows me
  to do whatever pointer math I need to without fear of running over
  a segment.  If I need to do larger manipulations I will normalize
  the pointers instead of turning on "huge" pointers in the compiler.

  Here are the standard memory models we can choose from:

    small:   near pointers for data and near pointers for code
    compact: far pointers for data and near pointers for code
    medium:  near pointers for data and far pointers for code
    large:   far pointers for data and far pointers for code
    huge:    far pointers for everything, and the pointers get
             normalized during runtime operations.  (Very slow!)

  The applications are generally compiled using the "compact" or "large"
  memory models.  I try to use the smaller memory models where possible;
  near pointers give you better performance.  The buffer sizes
  required by the applications generally require far pointers
  for data, although some of the smaller applications get away with
  the small memory model where both data and code use near pointers.
  
  See the Open Watcom documentation and any decent Intel assembly
  language book for a discussion of memory models, near pointers and
  far pointers.


Data types

  Although registers are 16 bits in size the compiler can handle data
  types up to 32 bits in size with no problems.

  I generally use typedefs so that it is very easy to see how wide
  a field is just from looking at the declaration.  For example,
  I use "uint16_t" instead of "unsigned int".  This helps when you
  are constantly transitioning between different architectures.

  x86 is a Little-endian architecture.  "Network byte order" is used
  by the TCP/IP protocols, and it is Big-endian.  You will see usages
  of ntohs, ntohl, htons, htonl in the code where we need to do
  conversions.  Use these macros correctly and religiously, and keep
  in mind that if something looks correct in memory if it gets loaded
  into a register as it might be byte swapped during the load.

  If I need to look at a section of memory in two more more different
  ways I'll generally use a C union to map the memory out instead of
  doing pointer casting.  The compiler likes this approach better
  and you are less likely to make a coding mistake.  (Casting has
  its uses but you are generally overriding any type checking the
  compiler is doing.)


Stack space usage

  Stack space is a finite resource.  It gets allocated at link time and
  can not be grown at run time.  Overflowing your stack means that you
  are corrupting something.

  Be sensitive to the number of objects you are putting on the stack,
  your call depth, and the overall stack size.  My coding style tends
  to put large objects in static (global) variables to avoid bloating
  the stack.

  Using multiple functions to perform work adds to call overhead but
  allows you to grow and shrink the call stack as needed.  In contrast
  large functions with lots of variables generally consume a lot of
  stack space all of the time, and they only release their stack
  storage when the function exits.  (A good example - never put
  a large stack variable in main( ) because unless you are constantly
  using it you will never be able to return to recycle the stack
  space.)

  Even though you may have allocated enough stack space in your
  environment, other versions of DOS running on different machines
  will behave differently.  I ran into this problem on some machines
  where DOS did not have its own interrupt stacks; the combination
  of DOS and packet drivers in use caused stack overflows.  All of
  the stack sizes have been adjusted since then to be overly generous.

  Stack bounds checking is normally turned off in the makefiles.
  This is for performance and code size reasons.  You can turn it
  on during development but after you have correctly sized your stack
  and are done with your testing it is probably not providing value.



Memory pooling

  We don't have a sophisticated memory allocator to work with so
  you need to pay attention to your usage of the heap.

  The biggest threat to long running programs that use heap is heap
  fragmentation.  Unless you are working with fixed length data
  structures where all of the allocations are the same size or you
  impose strict rules on how you allocate memory, you run the risk
  of heap fragmentation.  If the heap gets fragmented your request
  for a memory allocation will fail, and your program will probably
  die.

  I minimize my run time use of the heap by allocating most of the
  memory that I need up front and then managing that memory myself.
  For example, there is a fixed pool of buffers used for incoming
  packets.  One large memory allocation for incoming buffers gets
  done at program startup.  The list of used and free buffers is
  maintained by the packet handling code, which uses a simple stack
  to maintain the free list.  Once the initial allocation is made
  the buffer management code never goes back to the heap.

  This is in contrast to going to the heap when you need a buffer
  and returning the buffer to the heap when you are done with it.
  Using the heap like that increases the overhead, risks
  fragmentation, and is not safe if you do it from within an
  interrupt handler.

