The chapters 16-bit Pragmas (in the 16-bit Topics supplement), and 32-bit Pragmas briefly describe the use of the auxiliary pragma to create a sequence of assembly language instructions that can be placed anywhere executable C/C++ statements can appear in your source code. This chapter is devoted to an in-depth look at in-line assembly language programming.
The reasons for resorting to in-line assembly code are varied:
There are also some reasons for not resorting to in-line assembly code:
This chapter includes the following sections:
Doing in-line assembly is reasonably straight-forward with Watcom C/C++, although you must be careful. You can generate a sequence of in-line assembly anywhere in your C/C++ code stream. The first step is to define the sequence of instructions that you wish to place in-line. The auxiliary pragma is used to do this. Here's a simple example based on a DOS function call that returns a far pointer to the Double-Byte Character Set (DBCS) encoding table.
extern unsigned short far *dbcs_table( void ); #pragma aux dbcs_table = \ "mov ax,6300h" \ "int 21h" \ value [ds si] \ modify [ax];
To set up the DOS call, the AH register must contain the hexadecimal value 63 (63h). A DOS function call is invoked by interrupt 21h. DOS returns a far pointer in DS:SI to a table of byte pairs in the form (start of range, end of range). On a non-DBCS system, the first pair is (0,0). On a Japanese DBCS system, the first pair is (81h,9Fh).
With each pragma, we define a corresponding function prototype that explains the behaviour of the function in terms of C/C++. Essentially, it's a function that doesn't take any arguments, and that returns a far pointer to a unsigned short item.
The pragma indicates that the result of this ``function'' is returned in DS:SI (value [ds si]). The pragma also indicates that the AX register is modified by the sequence of in-line assembly code (modify [ax]).
Having defined our in-line assembly code, let's see how it's used in actual C code.
#include <stdio.h> extern unsigned short far *dbcs_table( void ); #pragma aux dbcs_table = \ "mov ax,6300h" \ "int 21h" \ value [ds si] \ modify [ax]; void main() { if( *dbcs_table() != 0 ) { /* we are running on a DOS system that supports double-byte characters */ printf( "DBCS supported\n" ); } }
Before you attempt to compile and run this example, consider this: The program won't work! At least, it won't work in most 16-bit memory models. And it doesn't work at all in 32-bit protected mode using a DOS extender. What's wrong with it?
We can examine the disassembled code for this program in order to see why it doesn't always work in 16-bit real-mode applications.
if( *dbcs_table() != 0 ) { /* we are running on a DOS system that supports double-byte characters */ 0007 b8 00 63 mov ax,6300H 000a cd 21 int 21H 000c 83 3c 00 cmp word ptr [si],0000H 000f 74 0a je L1 printf( "DBCS supported\n" ); } 0011 be 00 00 mov si,offset L2 0014 56 push si 0015 e8 00 00 call printf_ 0018 83 c4 02 add sp,0002H }
After the DOS interrupt call, the DS register has been altered and the code generator does nothing to recover the previous value. In the small memory model, the contents of the DS register never change (and any code that causes a change to DS must save and restore its value).
So we must make a small change to the pragma:
extern unsigned short far *dbcs_table( void ); #pragma aux dbcs_table = \ "push ds" \ "mov ax,6300h" \ "int 21h" \ "mov di,ds" \ "pop ds" \ value [di si] \ modify [ax];
If we compile and run this example with a 16-bit compiler, it works properly. We can examine the disassembled code for this revised program:
if( *dbcs_table() != 0 ) { /* we are running on a DOS system that supports double-byte characters */ 0008 1e push ds 0009 b8 00 63 mov ax,6300H 000c cd 21 int 21H 000e 8c df mov di,ds 0010 1f pop ds 0011 8e c7 mov es,di 0013 26 83 3c 00 cmp word ptr es:[si],0000H 0017 74 0a je L1 printf( "DBCS supported\n" ); } 0019 be 00 00 mov si,offset L2 001c 56 push si 001d e8 00 00 call printf_ 0020 83 c4 02 add sp,0002H
If you examine this code, you can see that the DS register is saved and restored by the in-line assembly code. The code generator, having been informed that the far pointer is returned in (DI:SI), loads up the ES register from DI in order to reference the far data correctly.
That takes care of the 16-bit real-mode case. What about 32-bit protected mode? When using a DOS extender, you must examine the accompanying documentation to see if the system call that you wish to make is supported by the DOS extender.
One of the reasons that this particular DOS call isn't so clear-cut is that it returns a 16-bit real-mode segment:offset pointer. A real-mode pointer must be converted by the DOS extender into a protected-mode pointer in order to make it useful. As it turns out, neither the Tenberry Software DOS/4G(W) nor Phar Lap DOS extenders support this particular DOS call (although others may).
The issues with each DOS extender are complex enough that the relative merits of using in-line assembly code aren't worth it. We present an excerpt from the final solution to this problem:
#ifndef __386__ extern unsigned short far *dbcs_table( void ); #pragma aux dbcs_table = \ "push ds" \ "mov ax,6300h" \ "int 21h" \ "mov di,ds" \ "pop ds" \ value [di si] \ modify [ax]; #else unsigned short far * dbcs_table( void ) { union REGPACK regs; static short dbcs_dummy = 0; memset( ®s, 0, sizeof( regs ) ); if( _IsPharLap() ) { PHARLAP_block pblock; memset( &pblock, 0, sizeof( pblock ) ); pblock.real_eax = 0x6300; /* get DBCS vector table */ pblock.int_num = 0x21; /* DOS call */ regs.x.eax = 0x2511; /* issue real-mode interrupt */ regs.x.edx = FP_OFF( &); /* DS:EDX -> parameter block */ regs.w.ds = FP_SEG( &); intr( 0x21, &); return( firstmeg( pblock.real_ds, regs.w.si ) ); } else if( _IsDOS4G() ) { DPMI_block dblock; memset( &dblock, 0, sizeof( dblock ) ); dblock.eax = 0x6300; /* get DBCS vector table */ regs.w.ax = 0x300; /* DPMI Simulate R-M intr */ regs.h.bl = 0x21; /* DOS call */ regs.h.bh = 0; /* flags */ regs.w.cx = 0; /* # bytes from stack */ regs.x.edi = FP_OFF( &); regs.x.es = FP_SEG( &); intr( 0x31, &); return( firstmeg( dblock.ds, dblock.esi ) ); } else { return( &); } } #endif
The 16-bit version uses in-line assembly code, but the 32-bit version uses a C function that's been crafted to work with both Tenberry Software DOS/4G(W) and Phar Lap DOS extenders. The firstmeg() function used in the example is shown below.
#define REAL_SEGMENT 0x34 void far *firstmeg( unsigned segment, unsigned offset ) { void far *meg1; if( _IsDOS4G() ) { meg1 = MK_FP( FP_SEG( &), ( segment << 4 ) + offset ); } else { meg1 = MK_FP( REAL_SEGMENT, ( segment << 4 ) + offset ); } return( meg1 ); }
We have taken a brief look at two features of the auxiliary pragma, the modify and value attributes.
The modify attribute describes those registers that are modified by the execution of the sequence of in-line code. You usually have two choices here:
The value attribute describes the register or registers in which a value is returned (we use the term ``returned'', not in the sense that a function returns a value, but in the sense that a result is available after execution of the code sequence).
This leads the discussion into the third feature of the auxiliary pragma, the feature that allows us to place the results of C expressions into specific registers as part of the ``setup'' for the sequence of in-line code. To illustrate this, let's look at another example:
extern void BIOSSetCurPos( unsigned short __rowcol, unsigned char __page ); #pragma aux BIOSSetCurPos = \ "push bp" \ "mov ah,2" \ "int 10h" \ "pop bp" \ parm [dx] [bh] \ modify [ah];
The parm attribute specifies the list of registers into which values are to be placed as part of the prologue to the in-line code sequence. In the above example, the ``set cursor position'' function requires three pieces of information. It requires that:
In this example, we have decided to combine the row and column information into a single ``argument'' to the function. Note that the function prototype for BIOSSetCurPos() is important. It describes the types and number of arguments to be set up for the in-line code. It also describes the type of the return value (in this case there is none).
Once again, having defined our in-line assembly code, let's see how it's used in actual C code:
#include <stdio.h> extern void BIOSSetCurPos( unsigned short __rowcol, unsigned char __page ); #pragma aux BIOSSetCurPos = \ "push bp" \ "mov ah,2" \ "int 10h" \ "pop bp" \ parm [dx] [bh] \ modify [ah]; void main() { BIOSSetCurPos( (5 << 8) | 20, 0 ); printf( "Hello world\n" ); }
To see how the code generator set up the register values for the in-line code, let's take a look at the disassembled code:
BIOSSetCurPos( (5 << 8) | 20, 0 ); 0008 ba 14 05 mov dx,0514H 000b 30 ff xor bh,bh 000d 55 push bp 000e b4 02 mov ah,02H 0010 cd 10 int 10H 0012 5d pop bp
As we expected, the result of the expression for the row and column is placed in the DX register, and the page number is placed in the BH register. The remaining instructions are our in-line code sequence.
Although our examples have been simple, you should be able to generalize them to your situation.
To review, the parm, value and modify attributes are used to:
Labels can be used in in-line assembly code. Here's an example:
extern void _disable_video( unsigned ); #pragma aux _disable_video = \ "again: in al,dx" \ "test al,8" \ "jz again" \ "mov dx,03c0" \ "mov al,11" \ "out dx,al" \ "mov al,0" \ "out dx,al" \ parm [dx] \ modify [al dx];
To finish our discussion, we provide examples that illustrate the use of variables in the in-line assembly code. The following example illustrates the use of static variable references in the auxiliary pragma:
#include <stdio.h> static short _rowcol; static unsigned char _page; extern void BIOSSetCurPos( void ); #pragma aux BIOSSetCurPos = \ "mov dx,_rowcol" \ "mov bh,_page" \ "push bp" \ "mov ah,2" \ "int 10h" \ "pop bp" \ modify [ah bx dx]; void main() { _rowcol = (5 << 8) | 20; _page = 0; BIOSSetCurPos(); printf( "Hello world\n" ); }
The only rule to follow here is that the auxiliary pragma must be defined after the variables are defined. The in-line assembler is passed information regarding the sizes of variables, so they must be defined first.
If we look at a fragment of the disassembled code, we can see the result:
_rowcol = (5 << 8) | 20; 0008 c7 06 00 00 14 05 mov word ptr __rowcol,0514H _page = 0; 000e c6 06 00 00 00 mov byte ptr __page,00H BIOSSetCurPos(); 0013 8b 16 00 00 mov dx,__rowcol 0017 8a 3e 00 00 mov bh,__page 001b 55 push bp 001c b4 02 mov ah,02H 001e cd 10 int 10H 0020 5d pop bp
The following example illustrates the use of automatic variable references in the auxiliary pragma. Again, the auxiliary pragma must be defined after the variables are defined so the pragma is placed in-line with the function.
#include <stdio.h> void main() { short _rowcol; unsigned char _page; extern void BIOSSetCurPos( void ); # pragma aux BIOSSetCurPos = \ "mov dx,_rowcol" \ "mov bh,_page" \ "push bp" \ "mov ah,2" \ "int 10h" \ "pop bp" \ modify [ah bx dx]; _rowcol = (5 << 8) | 20; _page = 0; BIOSSetCurPos(); printf( "Hello world\n" ); }
If we look at a fragment of the disassembled code, we can see the result:
_rowcol = (5 << 8) | 20; 000e c7 46 fc 14 05 mov word ptr -4H[bp],0514H _page = 0; 0013 c6 46 fe 00 mov byte ptr -2H[bp],00H BIOSSetCurPos(); 0017 8b 96 fc ff mov dx,-4H[bp] 001b 8a be fe ff mov bh,-2H[bp] 001f 55 push bp 0020 b4 02 mov ah,02H 0022 cd 10 int 10H 0024 5d pop bp
It isn't the intention of this chapter to describe assembly-language programming in any detail. You should consult a book that deals with this topic. However, we present a list of the directives, opcodes and register names that are recognized by the assembler built into the compiler's auxiliary pragma processor.
.8086aaa
aad
aam
aas
adc
add
ah
al
and
arpl
ax
bh
bl
bound
bp
bsf
bsr
bswap
bt
btc
btr
bts
bx
byte
call
callf
cbw
cdq
ch
cl
clc
cld
cli
clts
cmc
cmp
cmps
cmpsb
cmpsd
cmpsw
cmpxchg
cmpxchg8b
cpuid
cr0
cr2
cr3
cr4
cs
cwd
cwde
cx
daa
das
db
dd
dec
df
dh
di
div
dl
dp
dr0
dr1
dr2
dr3
dr6
dr7
ds
dup
dw
dword
dx
eax
ebp
ebx
ecx
edi
edx
enter
es
esi
esp
f2xm1
fabs
fadd
faddp
far
fbld
fbstp
fchs
fclex
fcom
fcomp
fcompp
fcos
fdecstp
fdisi
fdiv
fdivp
fdivr
fdivrp
feni
ffree
fiadd
ficom
ficomp
fidiv
fidivr
fild
fimul
fincstp
finit
fist
fistp
fisub
fisubr
fld
fld1
fldcw
fldenv
fldenvd
fldenvw
fldl2e
fldl2t
fldlg2
fldln2
fldpi
fldz
fmul
fmulp
fnclex
fndisi
fneni
fninit
fnop
fnrstor
fnrstord
fnrstorw
fnsave
fnsaved
fnsavew
fnstcw
fnstenv
fnstenvd
fnstenvw
fnstsw
fpatan
fprem
fprem1
fptan
frndint
frstor
frstord
frstorw
fs
fsave
fsaved
fsavew
fscale
fsetpm
fsin
fsincos
fsqrt
fst
fstcw
fstenv
fstenvd
fstenvw
fstp
fstsw
fsub
fsubp
fsubr
fsubrp
ftst
fucom
fucomp
fucompp
fwait
fword
fxam
fxch
fxtract
fyl2x
fyl2xp1
gs
hlt
idiv
imul
in
inc
ins
insb
insd
insw
int
into
invd
invlpg
iret
iretd
ja
jae
jb
jbe
jc
jcxz
je
jecxz
jg
jge
jl
jle
jmp
jmpf
jna
jnae
jnb
jnbe
jnc
jne
jng
jnge
jnl
jnle
jno
jnp
jns
jnz
jo
jp
jpe
jpo
js
jz
lahf
lar
lds
lea
leave
les
lfs
lgdt
lgs
lidt
lldt
lmsw
lock
lods
lodsb
lodsd
lodsw
loop
loope
loopne
loopnz
loopz
lsl
lss
ltr
mov
movs
movsb
movsd
movsw
movsx
movzx
mul
near
neg
no87
nop
not
offset
or
out
outs
outsb
outsd
outsw
pop
popa
popad
popf
popfd
ptr
push
pusha
pushad
pushf
pushfd
pword
qword
rcl
rcr
rdmsr
rdtsc
rep
repe
repne
repnz
repz
ret
retf
retn
rol
ror
rsm
sahf
sal
sar
sbb
scas
scasb
scasd
scasw
seg
seta
setae
setb
setbe
setc
sete
setg
setge
setl
setle
setna
setnae
setnb
setnbe
setnc
setne
setng
setnge
setnl
setnle
setno
setnp
setns
setnz
seto
setp
setpe
setpo
sets
setz
sgdt
shl
shld
short
shr
shrd
si
sidt
sldt
smsw
sp
ss
st
stc
std
sti
stos
stosb
stosd
stosw
str
sub
tbyte
test
tr3
tr4
tr5
tr6
tr7
verr
verw
wait
wbinvd
word
wrmsr
xadd
xchg
xlat
xlatb
xor
A separate assembler is also included with this product; it's described in the chapter The Assembler.