This chapter deals with the following topics:
This section describes the internal or machine representation of the basic types supported by Watcom C/C++:
An item of type char occupies 1 byte of storage. Its value is in the following range:
0 <= n <= 255
Note that char is, by default, unsigned. The Watcom C/C++ compiler option j can be used to change the default from unsigned to signed. If char is signed, an item of type char is in the following range:
-128 <= n <= 127
You can force an item of type char to be unsigned or signed regardless of the default by defining it to be of type unsigned char or signed char, respectively.
An item of type short int occupies 2 bytes of storage. Its value is in the following range:
-32768 <= n <= 32767
Note that short int is signed, and hence short int and signed short int are equivalent. If an item of type short int is to be unsigned, it must be defined as unsigned short int. In this case, its value is in the following range:
0 <= n <= 65535
An item of type long int occupies 4 bytes of storage. Its value is in the following range:
-2147483648 <= n <= 2147483647
Note that long int is signed, and hence long int and signed long int are equivalent. If an item of type long int is to be unsigned, it must be defined as unsigned long int. In this case, its value is in the following range:
0 <= n <= 4294967295
An item of type int occupies 4 bytes of storage. Its value is in the following range:
-2147483648<= n <= 2147483647
Note that int is signed, and hence int and signed int are equivalent. If an item of type int is to be unsigned, it must be defined as unsigned int. In this case its value is in the following range:
0 <= n <= 4294967295
If you are generating code that executes in 32-bit mode, long int and int are equivalent, unsigned long int and unsigned int are equivalent, and signed long int and signed int are equivalent. This may not be the case in other environments where int and short int are 2 bytes.
An item of type float is an approximate representation of a real number. Each item of type float occupies 4 bytes. If m is the magnitude of x (an item of type float), then x can be approximated if
2**-126 <= m < 2**128
or, in more approximate terms, if
1.175494e-38 <= m <= 3.402823e38
Data of type float are represented internally as follows. Note that bytes are stored in memory with the least significant byte first and the most significant byte last.
When the exponent field is all 0 bits, and the significand field is non-zero, then the quantity is a special value called a denormal or nonnormal number.
An item of type double is an approximate representation of a real number. The precision of an item of type double is greater than or equal to one of type float. Each item of type double occupies 8 bytes. If m is the magnitude of x (an item of type double), then x can be approximated if
2**-1022 <= m < 2**1024
or, in more approximate terms, if
2.2250738585072e-308 <= m <= 1.79769313486232e308
Data of type double are represented internally as follows. Note that bytes are stored in memory with the least significant byte first and the most significant byte last.
Note the following:
When the exponent field is all 0 bits, and the significand field is non-zero then the quantity is a special value called a denormal or nonnormal number.
The following sections describe the calling conventions used when compiling with the fpc compiler option:
How arguments are passed to a function with register-based calling conventions is determined by the size (in bytes) of the argument, and where in the argument list the argument appears:
The registers used to pass arguments to a function are EAX, EBX, ECX and EDX. The following algorithm describes how arguments are passed to functions.
Initially, the EAX, EDX, EBX and ECX registers are available for passing arguments. Note that registers are selected from this list in the order in which they appear. That is, the first register selected is EAX, and the last is ECX. For each argument Ai, starting with the leftmost argument, perform the following steps.
Note the following:
The following table lists the predefined types, their size as returned by the sizeof() function, the size of an argument of that type and the registers used to pass that argument if it's the only argument in the argument list.
Basic Type | sizeof() | Argument size | Registers used |
---|---|---|---|
char | 1 | 4 | [EAX] |
short int | 2 | 4 | [EAX] |
int | 4 | 4 | [EAX] |
long int | 4 | 4 | [EAX] |
float | 4 | 8 | [EDX EAX] |
double | 8 | 8 | [EDX EAX] |
near pointer | 4 | 4 | [EAX] |
far pointer | 6 | 8 | [EDX EAX] |
Provided no function prototypes exist, an argument is converted to a default type as described in the following table.
Argument Type | Passed As |
---|---|
char | unsigned int |
signed char | signed int |
unsigned char | unsigned int |
short | unsigned int |
signed short | signed int |
unsigned short | unsigned int |
float | double |
The integral type of an enumerated type is determined by the values of the enumeration constants. In strict ANSI C mode, all enumerated constants are of type int. In the extensions mode, the compiler will use the smallest integral type possible (excluding long ints) that can represent all values of the enumerated type. For instance, if the minimum and maximum values of the enumeration constants are in the range -128 through 127, the enumerated type is equivalent to a signed char (size = 1 byte). All references to enumerated constants in this instance will have type signed char. An enumerated constant is always promoted to an int when passed as an argument.
Function prototypes define the types of the formal parameters of a function. Their appearance affects the way in which arguments are passed. An argument is converted to the type of the corresponding formal parameter in the function prototype. Consider the following example:
void prototype( float x, int i ); void main() { float x; int i; x = 3.14; i = 314; prototype( x, i ); rtn( x, i ); }
The function prototype for prototype() specifies that the first argument is to be passed as a float and the second argument is to be passed as an int. This means that the first argument is passed in register EAX, and the second argument in register EDX.
If no function prototype is given, as is the case for the function rtn(), the first argument is passed as a double, and the second argument would be passed as an int. This means that the first argument is passed in registers EDX and EAX, and the second argument is passed in register EBX.
Note that even though both prototype() and rtn() were called with identical argument lists, the way in which the arguments were passed was completely different, simply because a function prototype for prototype() was specified. Function prototyping is an excellent way to guarantee that arguments are passed as expected to your assembly language function.
Consider the following example:
void main() { double x; int i; double y; x = 7; i = 77; y = 777; myrtn( x, i, y ); }
myrtn() is an assembly language function that requires three arguments:
Using the rules for register-based calling conventions, these arguments are passed to myrtn() in the following way:
Let's look at the stack upon entry to myrtn():
Register EBP is normally used to address arguments on the stack. Upon entry to the function, register EBP is set to point to the stack, but before doing so we must save its contents. The following two instructions achieve this.
push EBP ; save current value of EBP mov EBP,ESP ; get access to arguments
After executing these instructions, the stack looks like this:
As the above diagrams show, the third argument is at offset 8 from register EBP in a small code model, and offset 12 in a big code model.
Upon exit from myrtn(), we must restore the value of EBP. The following two instructions achieve this.
mov ESP,EBP ; restore stack pointer pop EBP ; restore EBP
The following is a sample assembly language function that implements myrtn() for the small memory model (small code, small data):
DGROUP group _DATA, _BSS _TEXT segment byte public 'CODE' assume CS:_TEXT assume DS:DGROUP public myrtn_ myrtn_ proc near push EBP ; save EBP mov EBP,ESP ; get access to arguments ; ; body of function ; mov ESP,EBP ; restore ESP pop EBP ; restore EBP ret 8 ; return and pop last arg myrtn_ endp _TEXT ends
The same function for a large memory model (big code, big data) is as follows:
DGROUP group _DATA, _BSS MYRTN_TEXT segment byte public 'CODE' assume CS:MYRTN_TEXT public myrtn_ myrtn_ proc far push EBP ; save EBP mov EBP,ESP ; get access to arguments ; ; body of function ; mov ESP,EBP ; restore ESP pop EBP ; restore EBP ret 8 ; return and pop last arg myrtn_ endp MYRTN_TEXT ends
Note the following:
Let's now consider the example in the previous section, except this time we'll use the stack-based calling convention. The most significant difference between the stack-based calling convention and the register-based calling convention is the way the arguments are passed. When using the stack-based calling conventions, no registers are used to pass arguments. Instead, all arguments are passed on the stack.
Let's look at the stack on entry to myrtn():
Register EBP is normally used to address arguments on the stack. Upon entry to the function, register EBP is set to point to the stack, but before doing so we must save its contents. The following two instructions achieve this.
push EBP ; save current value of EBP mov EBP,ESP ; get access to arguments
After executing these instructions, the stack looks like this:
As the above diagrams show, the arguments are all on the stack, and are referenced by specifying an offset from register EBP.
Upon exit from myrtn(), we must restore the value of EBP. The following two instructions achieve this:
mov ESP,EBP ; restore stack pointer pop EBP ; restore EBP
The following is a sample assembly language function that implements myrtn() for the small memory model (small code, small data):
DGROUP group _DATA, _BSS _TEXT segment byte public 'CODE' assume CS:_TEXT assume DS:DGROUP public myrtn myrtn proc near push EBP ; save EBP mov EBP,ESP ; get access to arguments ; ; body of function ; mov ESP,EBP ; restore ESP pop EBP ; restore EBP ret ; return myrtn endp _TEXT ends
The same function for the large memory model (big code, big data) is as follows:
DGROUP group _DATA, _BSS MYRTN_TEXT segment byte public 'CODE' assume CS:MYRTN_TEXT public myrtn myrtn proc far push EBP ; save EBP mov EBP,ESP ; get access to arguments ; ; body of function ; mov ESP,EBP ; restore ESP pop EBP ; restore EBP ret ; return myrtn endp MYRTN_TEXT ends
Note the following:
A function prototype with a parameter list that ends with ``,...'' has a variable number of arguments. In this case, all arguments are passed on the stack. Since no prototyping information exists for arguments represented by ``,...'', those arguments are passed as described in the section ``Using Stack-Based Calling Conventions.''
The way in which function values are returned depends on the size of the return value. The following examples describe how function values are to be returned. They are coded for a small code model.
_TEXT segment byte public 'CODE' assume CS:_TEXT public Ret1_ Ret1_ proc near ; char Ret1() mov AL,'G' ret Ret1_ endp _TEXT ends end
_TEXT segment byte public 'CODE' assume CS:_TEXT public Ret2_ Ret2_ proc near ; short int Ret2() mov AX,77 ret Ret2_ endp _TEXT ends end
_TEXT segment byte public 'CODE' assume CS:_TEXT public Ret4_ Ret4_ proc near ; int Ret4() mov EAX,7777777 ret Ret4_ endp _TEXT ends end
.8087 _TEXT segment byte public 'CODE' assume CS:_TEXT public Ret8_ Ret8_ proc near ; double Ret8() mov EDX,dword ptr CS:Val8+4 mov EAX,dword ptr CS:Val8 ret Val8: dq 7.7 Ret8_ endp _TEXT ends end
The .8087 pseudo-op must be specified so that all floating-point constants are generated in 8087 format.
_TEXT segment byte public 'CODE' assume CS:_TEXT public RetX_ ; ; struct int_values { ; int value1, value2, value3, value4, value5; ; }; ; RetX_ proc near ; struct int_values RetX() mov dwordptr SS:0[ESI],71 mov dwordptr SS:4[ESI],72 mov dwordptr SS:8[ESI],73 mov dwordptr SS:12[ESI],74 mov dwordptr SS:16[ESI],75 ret RetX_ endp _TEXT ends end
When returning values on the stack, remember to use a segment override to the stack segment (SS).
The following is an example of a Watcom C/C++ program that calls the above assembly language subprograms.
#include <stdio.h> struct int_values { int value1; int value2; int value3; int value4; int value5; }; extern char Ret1(void); extern short int Ret2(void); extern long int Ret4(void); extern double Ret8(void); extern struct int_values RetX(void); void main() { struct int_values x; printf( "Ret1 = %c\n", Ret1() ); printf( "Ret2 = %d\n", Ret2() ); printf( "Ret4 = %ld\n", Ret4() ); printf( "Ret8 = %f\n", Ret8() ); x = RetX(); printf( "RetX1 = %d\n", x.value1 ); printf( "RetX2 = %d\n", x.value2 ); printf( "RetX3 = %d\n", x.value3 ); printf( "RetX4 = %d\n", x.value4 ); printf( "RetX5 = %d\n", x.value5 ); }
The above function should be compiled for a small code model (use the mf, ms or mc compiler option).
When a source file is compiled by Watcom C/C++ with one of the fpi or fpi87 options, all floating-point arguments are passed on the 80x86 stack. The rules for passing arguments are as follows.
Consider the following example:
extern void myrtn(int,float,double,long int); void main() { float x; double y; int i; long int j; x = 7.7; i = 7; y = 77.77 j = 77; myrtn( i, x, y, j ); }
myrtn() is an assembly language function that requires four arguments:
When using the stack-based calling conventions, all of the arguments are passed on the stack. When using the register-based calling conventions, the above arguments are passed to myrtn() in the following way:
Remember, arguments are pushed on the stack from right to left. That is, the rightmost argument is pushed first.
All arguments passed on the stack must be removed by the called function.
The following is a sample assembly language function that implements myrtn().
.8087 _TEXT segment byte public 'CODE' assume CS:_TEXT public myrtn_ myrtn_ proc near ; ; body of function ; ret 16 ; return and pop arguments myrtn_ endp _TEXT ends end
Note the following:
When using the stack-based calling conventions with fpi or fpi87, floating-point values are returned in registers. Single precision values are returned in EAX, and double precision values are returned in EDX:EAX.
When using the register-based calling conventions with fpi or fpi87, floating-point values are returned in ST(0). All other values are returned in the manner described earlier in this chapter in the section ``Returning Values from Functions.''