THE DS != SS ISSUE

The segmented architecture of the Intel 8086 family of microprocessors has been giving programmers grief for many years now. But nowhere does segmented architecture cause more problems than in Windows libraries. If you skipped the first half of Chapter 7, thinking that you'd never need to know about segmented architecture and the intricacies of near and far pointers, now is the time to read it. And even if you've read it carefully, you might still benefit from this quick review.

The Intel 8086 family of microprocessors operating in real mode can address 1 megabyte of memory. This memory is addressed by a combination of a 16-bit segment address and a 16-bit offset address. The 16-bit segment address marks the beginning of a 64-KB area of memory. The offset address is relative to the beginning of the segment. In protected mode, the segment address references a 24-bit base address in a descriptor table. The offset address is added to this.

The 8086-family microprocessors have four registers that contain segment addresses: the code segment register (CS), the data segment register (DS), the stack segment register (SS), and the extra segment register (ES). The instruction pointer (IP) always addresses code within the code segment. The stack pointer (SP) always addresses the stack within the stack segment. Registers that address data can do so relative to any of the four current segments. When programming in Microsoft C, 16-bit pointers that specify only an offset address are called near or short pointers, and 32-bit pointers that contain both a segment address and an offset address are called far or long pointers.

In C, all variables defined as outside functions (on the external level) and all variables defined as static within functions are stored in static memory. The compiler uses near pointers relative to the 8086 data segment (DS) to address variables stored in static memory.

All parameters to functions and all variables within functions that are not defined as static are stored on the stack. The compiler uses near pointers relative to the 8086 stack segment (SS) to address the stack.

When you use a near pointer in a C program, the pointer can reference a variable either in static memory or on the stack. The compiler has no way to determine whether the near pointer is an offset to DS or SS. For this reason, C programs are normally con- structed to use the same segment for data and the stack. Simply put, DS == SS. This is almost required for a C implementation on 8086-family microprocessors, because C does not differentiate between pointers to static variables and pointers to stack variables.

Let's take an example. In a small-model or medium-model program, you can use the normal C strlen function to find the length of a string. The parameter to strlen is a near pointer to the string:

wLength = strlen (pString) ;

The string itself could be stored either in static memory or on the stack. You could define pString like this:

char *pString = "This is a string" ;

In this case, the string ”This is a string“ is stored in static memory, and the near pointer is relative to the beginning of the data segment. However, you could do something like this within a function:

char szString [20] ;

[other program lines]

wLength = strlen (szString) ;

In this case, the szString array takes up 20 bytes on the stack. When you refer to szString, you're actually referring to a near pointer relative to the stack segment.

How does the strlen function know whether the near pointer is an offset in the stack segment or in the data segment? It doesn't. If you take a look at the assembly-language code for strlen in the SLIBCEW.LIB library, this is what you'll find:

_strlen NEAR PROC

PUSH BP ; Prologue

MOV BP, SP

MOV DX, DI ; Save DI

MOV AX, DS ; Set ES equal to DS

POP ES, AX

MOV DI,[BP+4] ; Get DI ptr off stack

XOR AX, AX

MOV CX, -1

REPNZ SCASB ; Search for zero in ES:DI

NOT CX ; Calculate length

DEC CX

XCHG AX, CX

MOV DI, DX ; Restore DI

MOV SP, BP ; Epilogue

POP BP

RET

_strlen ENDP

The strlen function assumes that the near pointer is an offset in the data segment. It sets ES equal to DS using this code:

MOV AX, DS

MOV ES, AX

It then uses ES to scan the string for a terminating 0. To write a strlen function that would work with a near pointer in the stack segment, you would need to replace these lines with:

MOV AX, SS

MOV ES, AX

But you've never had to worry about this little problem in Windows programs, because DS equals SS.

Windows dynamic libraries are another story. The data segment is the library's own data segment, but the stack segment is the stack of the caller. That is, DS != SS. If you call strlen in a Windows library for a string that is stored on the stack, the function won't work correctly, because the strlen function assumes that the near pointer is relative to the data segment. When you first realize the implications of this, you're likely to assume that programming Windows dynamic libraries is very difficult. Let's just say that it's not quite as carefree a process as writing a Windows program, but the job certainly isn't impossible. After all, the bulk of Windows consists of dynamic libraries—the KERNEL, USER, and GDI modules.

At one time, the recommended practice was to use no normal C library functions within a dynamic library and to instead write your own functions. This restriction has now been loosened, and information is available to let you use C library functions intelligently. The conventions followed in the strlen function hold in most of the functions in the normal C library distributed with the Microsoft C Compiler: Most functions that accept pointers assume that the pointer is relative to the data segment; these functions do not assume that DS is equal to SS. Any C run time function that cannot be used in a dynamic link library is not included in SDLLCEW.LIB.

When you compile C source code for a small-model Windows library, include the switch -ASw, and for a medium-model Windows library, include the switch -AMw. These switches tell the compiler to assume that DS is not equal to SS. Nothing very magical happens here. The primary purpose of these switches is to alert you to possible problems in your code. For instance, within a function, you might define an array and a pointer:

int array [3] ;

int *ptr ;

If you say:

array [0] = array [1] + array [2] ;

the compiler uses SS to reference the elements of array, because the compiler knows that array is on the stack. However, you might have code like this:

ptr = array ;

*ptr = *(ptr + 1) + *(ptr + 2) ;

In the first statement, the compiler assigns the near address of array (which happens to be referenced from the stack segment) to the near pointer ptr. But when generating code for the second line, the compiler assumes that ptr references a variable in the data segment. That's wrong.

If you have a program with a construction like this and you compile with the -ASw switch and a warning level of 1 or 2, you'll get a warning message for the assignment of the array address to ptr:

warning C4058: address of automatic (local) variable taken, DS != SS

You can translate this message as: ”You've assigned the address of a local variable on the stack to a near pointer. Future use of this near pointer will involve the data segment. You've specified that you want the compiler to assume DS is not equal to SS. This assignment statement contradicts your intentions.“

You can fix this by making ptr a far pointer, as follows:

int array [3] ;

int far *ptr ;

ptr = array ;

Now the compiler assigns the full 32-bit address of array (the stack segment and the offset) to ptr. You're safe in using ptr. Or you can make array a static variable:

static int array [3] ;

int *ptr ;

ptr = array ;

The compiler now assumes that ptr references data in the data segment; this is correct, because array is defined as static.

Don't assume that you'll always be alerted to problems like this. Here's another example. You have a function that sums up the first 100 elements of an integer array:

int sumup (int array [])

{

int i, n = 0 ;

for (i = 0 ; i < 100 ; i++)

n += array [i] ;

return n ;

}

If you call this function and the array happens to be on the stack, you're in trouble:

int array [100] ;

[other program lines]

sumup (array) ; /* A problem here */

This won't even generate a warning message, but it's obviously incorrect. How do you get around it? Use far pointers. Define the function like this:

int sumup (int far array [])

and call the function like this:

sumup ((int far *) array) ;

Or make array a static variable:

static int array [100] ;

[other program lines]

sumup (array) ; /* No problem here */

This is a better solution for an array of this size anyway, because it avoids putting 200 bytes on the calling program's stack.

You can avoid many of the DS != SS problems simply by not using stack variables and instead defining all your local variables as static. If you want to use stack variables for some items to save space in the data segment, you should avoid using the stack either for arrays or for any variables that require pointers. Finally, if you use pointers with stack variables, make them far pointers.

The parameters to a library function are always on the stack. If you need to use a pointer to reference a function parameter, use a far pointer.