Joe has a BSc and MS in electrical engineering and worked for Zortech for several years writing C, C++, and assembly code for DOS, UNIX, 16-bit, and 32-bit DOS extenders. He then founded FlashTek, a 32-bit DOS extender vendor, where he can be contacted at 121 Sweet Ave., Moscow, ID 83843.
Many of today's applications have outgrown MS-DOS's 640K limit, and programmers are turning to DOS extenders for more speed, greater data capacity, and the code size required by enhanced feature sets. While 16-bit DOS extenders are relatively easy to port to because of the minimal number of required code changes, they don't provide all of the advantages of 32-bit code. Such advantages include the elimination of the 64-Kbyte segment limits (it's a pleasure to call malloc() with a request for a megabyte and have it comply!), the availability of programmer-transparent virtual memory (your apparent RAM size is limited by the amount of free disk space), 32-bit integers, and 32-bit near pointers.
If you've programmed exclusively for 16-bit DOS, 32-bit integers and near pointers are both a blessing and curse. 32-bit integers mean a single instruction acting on a 32-bit register instead of slow, multi-instruction long arithmetic (that frequently involves a compiler-known function call). This gives you a new tool to speed up your program and, at the same time, reduce executable size. Since many of the more subtle pitfalls of converting 16-bit applications to 32-bit are related to these two differences, I'll discuss them in depth, insofar as they apply to 32-bit DOS extenders.
When converting 16-bit code to 32-bit DOS, you'll inevitably make some changes in any "real" application. Intercepting hardware interrupts, writing directly to video memory, catching DOS critical errors, and many other details will mean consulting the DOS-extender documentation. You'll have to modify your code to utilize the extender's built-in hooks so you can gracefully access all the normally accessible resources. These hooks vary from vendor to vendor, but vendors usually support all the normal things you do in 16-bit DOS to access the hardware and services. Still, most porting problems are related to memory protection, integer size, or structure size and padding.
Memory-protection problems usually involve uninitialized pointers and over-writing the end of an array. The symptoms are something many 16-bit real-mode DOS programmers have never seen before--register dumps. The DOS extender detects an out-of-bounds memory access, aborts the program, and returns an error message, along with the contents of all the registers at the time the violation occurred. A good debugger generally makes these problems easy to find. Most debuggers will allow you to run the program to recreate the fault and, instead of generating the register dump, the debugger will position the cursor on the offending source line in your code.
Under 16-bit DOS, you might find that a stray pointer had made some rather "creative" changes to DOS you didn't have any clues about until after the program exited. I've spent days tracking down bugs that required a reboot of the machine after every attempt to find it. DOS extenders provide the memory protection to help catch many of these bugs long before they cause any problems.
Integer-size problems can show up when you start doing bit twiddling like that in Figure 1. In this example, the code will work properly for 16-bit code and in many circumstances for 32-bit code. The problem will only occur when the array being searched is larger than 64K, and then only sometimes. These type of bugs are some of the most frustrating and difficult to find. Of course your test suite (which was designed for testing 16-bit code) will pass just fine. Your customer with the killer data set will find the problem, but won't be able to reproduce it on demand--and you can't fix it until you can duplicate it. The solution is to either put in the code some conditional compilation that tests for 32-bit compilation or change the algorithm such that it doesn't depend on hardcoded bit twiddling.
/***** Use shifts and logical operators to speed up the binary search
routine. Avoids the use of slow divides. *****/
unsigned int binary_search(const char **array_p, const char *find_p,
int size)
{
unsigned int mask = 1 << 15; /* Assumes 16-bit integers! */
unsigned int index = mask;
int comp_val = 1; /* Initialize the compare value to decrease */
/* the index until index < size. */
while(index >= size ||
mask && (comp_val = strcmp(array_p[index], find_p)) ! = 0)
{
if(comp_val > 0) /* If the value is too high, clear this bit. */
index ^= mask;
mask >>= 1;
index |= mask;
}
return index;
}
Other integer-size problems can occur when transferring data via a serial port or a binary file. If you're transferring data via a serial port, you are (at some level) breaking down the data into bytes. Make sure the high word of the integer is transferred on output and initialized properly on input. In general, any place your code interfaces with the outside world needs to be examined for potential integer-size problems. This includes disk files, serial ports, parallel ports, shared direct memory access (including things like video memory), sound cards, and game and I/O ports.
Structure size and padding issues are by far the most subtle problems you'll encounter when porting code from 16-bit to 32-bit DOS. One of our customers had to reformat his hard disk at least three times before we found problems related to assumptions about structure size and layout made by a third-party library vendor. That third-party libraries were causing such serious problems convinced us the problem was much more serious than a few novice programmers fumbling in the dark. If experienced third-party vendors were having problems, then there's a serious knowledge gap, and 32-bit DOS extenders could unfairly receive a bad rap for being unreliable.
Different compilers have different default rules for structure padding. The first structure in Figure 2, for example, can have a size varying from 3 to 8 depending on the compiler and memory model. You can even add more members to a structure without changing the size--depending on the compiler and memory model. The compiler may place padding between various members in the structure to put the larger members on 2- or 4-byte boundaries. In 32-bit code, integers are 4 bytes and may be placed on a 4-byte boundary (which can speed up memory access). Short ints are generally 2 bytes and may be put on 2-byte boundaries. By grouping different structure members together, you can pack a structure more tightly, nearly halving the size of a structure; see Figure 3. Not only does this have implications for your program's memory consumption, but, more importantly, it can mean that code access (perhaps 16-bit code that reads binary files or assembly language) to the structure and structure members will not function correctly.
struct fig_2a
{
char c;
int i;
};
struct fig_2b
{
char c1, c2;
int i;
};
Compiler sizeof(struct fig_2a) sizeof(struct fig_2b)
Borland C/C++ 3.1
all models 3 4
Microsoft C/C++ 7.0
all models 4 4
Watcom 386/9.0
(32-bit code) 5 6
Zortech 3.0
T,S,C,M,L,Z models 4 4
Zortech 3.0 X model 8 8
struct fig_3a
{
char c1;
int i1;
char c2;
int i2;
char c3;
int i3;
char c4;
};
struct fig_3b
{
char c1;
char c2;
char c3;
char c4;
int i1;
int i2;
int i3;
};
Compiler sizeof(struct fig_3a) sizeof(struct fig_3b)
Borland C/C++ 3.1
all models 10 10
Microsoft C/C++
7.0 all models 14 10
Watcom 386/9.0
(32-bit code) 16 16
Zortech 3.0
T,S,C,M,L,Z models 14 10
Zortech 3.0 X model 28 16
When code is recompiled for 32 bit, even functional C code (compiled under 16 bit) can break when assumptions about padding are violated. For example, a structure similar to that in Figure 4 was used to store some attributes for an array of graphical objects. This structure was initialized from a text file, not a binary file, which normally would have made the code above suspicion. But the programmer took a shortcut that cost his customer time, money, and a trashed hard disk before the problem was found. The offending code looked something like that in Figure 5.
struct attribute
{
char fg_color, bg_color;
};
#define BYTES_PER_ATTR 2
#define TABLE_SIZE 10
struct attribute attr_table[TABLE_SIZE];
void read_attr_table(FILE *fp)
{
int i;
char *p = (char *)&attr_table[0];
for(i = 0; i <TABLE_SIZE; i++)
{
int j;
for(j = 0; j < BYTES_PER_ATTR; j++)
{
int tmp;
scanf(fp, "%d", &tmp);
*p++ = tmp;
}
}
}
The size of the structure was a multiple of 2, but not a multiple of 4. This meant that for 16-bit code there was no padding, but for 32-bit code there were two bytes of padding at the end of the structure. As the pointer p was advanced past the end of the last member of the first structure, it did not then point to the first member of the next structure; it pointed to the padding instead. Only the first structure received the proper contents; all the others received what amounted to garbage. Yet the 16-bit code worked perfectly and avoided the need to address each of the members of the structure by name, which would have made for much larger code. (There were about ten members instead of two, as in the example.) Once we found the problem, we fixed it by taking the address of the ith element in the array and assigning it to p, thus realigning p at the start of every structure in the array. Even this was somewhat risky, since there are no ANSI standards that specify the padding, nor (I believe) even the order of the members in a structure.
There are other solutions to alignment and padding problems. Most compilers have command-line flags and pragmas that can be used to control how structure members are aligned. You can even mix alignment in the same module. One structure can have 1-byte alignment, another can have 2-byte alignment, and still another the default alignment. Be careful when using command-line flags or making the entire project use an alignment other than the default. It's almost certain that you'll be using run-time library structures, such as FILE, or the time-related structures. If the default padding (with which the runtime library was most likely built) results in a mis-aligned access to the structure members, you could again be facing some obscure bugs. The solution in this case is to bracket the include files that define the structures with the proper pragmas to set the alignment type to that with which the runtime library was built. The best solution is for the runtime library implementors to do this in the header file or to lay out the structure so that padding is not required for the alignment options.
Copyright © 1993, Dr. Dobb's Journal