Introduction to IOPL programming - or how
                to display physical memory size under OS/2


OS/2 provides a number of functions which return useful information
about the physical configuration of the machine.

For example DosDevConfig(), which can be used to obtain such things as:
the number of floppy disk drives, whether the display adapter is colour,
or the PC model number.

The Kbd and Vio subsystems also have calls to obtain information about
the underlying hardware, such as KbdGetHWID and VioGetConfig.

However one API which was missing under OS/2 1.x was the one to get the
amount of physical memory in the machine.  This meant that when you wanted
to know how much memory is installed often the only way to answer the
question was to turn the PC off and on and watch the 'count' of memory done
during the POST.

Under OS/2 2.0 one of the information types returned by the DosQuerySysInfo()
API is the total physical memory, so this particular problem has gone away -
for those of you who have no further need of OS/2 1.x at any rate!

However the techniques used to get this information directly are still
valid and I think provide a useful exercise in simple manipulation of
the low level hardware interfaces of the PC.

In this example we want to access the machine configuration, which
is contained in non-volatile memory called the CMOS RAM.

   CMOS RAM - what is it ?

The PC saves away configuration information such as the amount of memory
and the number and type of disks in special memory which is retained when
the machine is turned off.  In the original PCs this memory was CMOS
(Complementary Metal Oxide Semiconductor) and so is usually referred to

The first few bytes of CMOS RAM are standard in IBM PCs and compatibles
and the addresses we need for memory sizes are:

        15h - low byte of base memory in Kb
        16h - high byte of base memory in Kb
        17h - low byte of expansion memory in Kb
        18h - high byte of expansion memory in Kb

Although it is memory it is NOT in the usual address space of the PC.  Data
is read/written from the memory by writing the address to be accessed to
I/O port 70h and then reading/writing the data from/to I/O port 71h.

Under OS/2 this involves using code running at a higher privilege level
than the usual application - so called IOPL code.  This stands for
"Input Output Privilege Level" and such segments are between the privilege
levels of usual application code and that of the operating system kernel
or device drivers.

To understand why this is so needs a brief excursion into the Intel
microprocessors' privilege mechanism for those whose knowledge of it
is either missing, or rusty!

    Overview of IOPL and the 80x86 protection mechanism

The 80286 and above chips have two basic modes of operation: 'real' mode
which is the mode used by standard DOS and 'protected' mode which is
the mode generally used by OS/2.

In real mode the system is simple: any program can perform any instruction
on any valid area of memory or any valid I/O address.  Hence the problems
of DOS programs hanging the machine by overwriting bits of the operating

In protected mode the microprocessor assigns privilege levels to various
instructions, memory addresses and I/O ports.  These levels are also known
as 'rings' and are numbers from lowest privilege (ring 3) to highest
(ring 0).  [Under OS/2 there is no use of ring 1.]

Firstly all accesses to memory are done using an indirect pointer - a
segment description - thus restricting the amount of physical memory that
a program can actually access, and further to this each segment has a
privilege level and can only be used by programs running at the same
(or higher) privilege level.

Instructions are also assigned privilege levels, and so instructions which
manipulate system tables, etc. are of highest privilege whereas those such
as MOV or ADD are lowest - anyone is allowed to do them!

The microprocessor allows transition between privilege levels in a very
controlled fashion by use of special segments descriptors named 'gates'.
The microprocessor handles the change of privilege level, swapping the
stack segment and copying parameters from the lower to the higher privilege
based on the information contained by the gate descriptor.

The instructions which perform I/O such as IN (which reads a byte or bytes
from an I/O port address) or CLI (which disables interrupts during non-
interruptible sequences of instructions) are of the lowest but one privilege
level - level 2.

For this reason privilege level 2 is known as IOPL level, and it is
typically used when you wish to access the machine hardware directly
but do not want to get involved with device drivers or the OS/2 kernel.

So in order to safely access the CMOS RAM under OS/2 the code must run
at IOPL for two reasons: (a) to perform the actual IN and OUT instruction
and (b) to disable interrupts between selecting the byte to access and
actually reading it. IOPL privilege is also sufficient for this program
so there is no good reason to use any higher privilege level.

OS/2 by default builds and loads programs at the lowest level - ring 3 -
also known as application level code, but user programs may also create
code (and data) segments which will be loaded at IOPL privilege.  The
programs must also define the 'gates' to be used to access these IOPL

To protect the system against potential 'rogue' usage of this higher
privilege the loading of programs requiring IOPL segments is controlled
by the IOPL command in CONFIG.SYS.  The default is to prevent programs
containing IOPL segments to be run, the statement "IOPL=YES" allows ANY
program containing IOPL segments to run, and the statement "IOPL=proga,progb"
would allow IOPL access only for programs 'proga' and 'progb'.

NOTE that in order to use IOPL code under OS/2 2.0 you cannot use the default
'flat' model for code and data - despite being perceived as a simple linear
address mode the 80x86 is still requiring a segment descriptor.  The standard
descriptor for an appplication program's code and data is a ring 3 segment
descriptor and so the program MUST change segment to use an IOPL instructions.

The easiest way to do this with the IBM Set C/2 compiler is to write the
IOPL code in 16bit mode since the compiler supports calls from 32bit 'flat'
code to 'segmented' 16bit code.  This is the method I use here.


So the task is simple.  We need to write a program to read the CMOS RAM
locations 15h, 16h, 17h amd 18h; combine the bytes into two words and then
display the values and the total.

The first problem is the mechanics of creating a segment which will be
loaded with IOPL privilege, and of accessing it from our application code.

The key to this process is the linker.  It is told via the linker definition
file about the segment(s) which are to be loaded with IOPL privilege and
about which procedure entry points are to be provided as 'gates' from the
normal application code the IOPL code.

The linker creates the correct flags in the segment definitions and creates
an exported function definition for each gates.  This allows the OS/2 loader
to make the correct type of segment and gate descriptors.

The second problem is one on which many people come to grief - you MUST
reset interrupts BEFORE returning to application level code.  In planning
this sort of program the first draft of the program often has four IOPL
procedures - two to turn interrupts on and off, and two to read/write a
byte to a port. The application level code may then look like:

        WriteByte( 0x70, CmosAddress );
        ReadByte( 0x71, &ByteRead );

Unfortunately OS/2 detects the attempt to return with interrupts disabled
from the DisableInts() function and the program is terminated!
This is because it violates the privilege level mechanism to allow
application code to run with an IOPL attribute, is this case with
interrupts disabled.

You MUST (unfortunately) ensure that the whole operation is done in one call
to IOPL code.  Failing this you will get a SYS1924 error message for the
reason stated above.

So the example has a C program containing the application code, and
an assembler module containing a function CMOSRead() which reads one byte
from a specified address in CMOS RAM.

Program description

The application code (memcnt.c) is very simple.  The IOPL function is
declared as:

    extern BYTE APIENTRY16 CMOSRead( USHORT );

The program simply calls it four times to get the four bytes of data required
and then prints out the amount of memory below and above the 1M boundary and
the total amount.

For compatability with OS/2 1.x I have added the lines:

    #ifndef APIENTRY16

which define APIENTRY16 to be a regular APIENTRY when the compiler is 16bit
by default.

The IOPL code (which is in the assembler file CMOSRead.asm) consists of a
code segment named CMOS_TEXT, in which is a procedure CMOSREAD to read
one byte of CMOS RAM.

Note that the procedure follows the so-called 'pascal' calling convention.
This means that the name is declared in upper case without the leading
underscore more usual for C callable functions, and more particularly
that the procedure is responsible for clearing up its OWN stack.

This point is vital to program IOPL code correctly.  The reason is that the
Intel call gate mechanism itself will copy the parameters passed from the
application's stack to the IOPL stack and will automatically restore the
application stack properly provided the IOPL procedure uses the correct
form of return instruction to tell the microprocessor how many bytes of
parameters there were.  This restriction means that:

   IOPL procedures MUST be declared as APIENTRY16 (APIENTRY for OS/2 1.x)
   and they cannot have a variable number of parameters.

The third file which is required for this example is the linker definition
file (MEMCNT.DEF) which does the interesting part.

I will go through this file item by item for those who maybe have little
experience of them.

    Tells the linker to create a program which can be run in a window as
    well as full screen.  The actual name of the program is defaulted to
    the name of the output file - in this case it will be MEMCNT.

    This notifies the linker that the code segment named CMOS_TEXT must
    be loaded at IOPL privilege level.  All other segments will be loaded
    with the default, ring 3, privilege.

"CMOSREAD        1"
   The exports section lists procedures to be known outside the module.
   This section is most often used to define the entry points for DLL
   (dynamic link library) but in this case we must tell OS/2 that the
   CMOSREAD procedure is the call gate used to access the IOPL segment.
   The number '1' does this by telling OS/2 that CMOSREAD requires one
   word (or two bytes) of parameter space.  This number MUST of course
   be the same number of bytes as the actual function expects - it is
   a shame that the information has to be duplicated like this.

The program is compiled as follows.

First assemble CMOSRead.asm ( I am using Microsft MASM 5.1 ):

        masm /MX/ML CMOSRead.asm ;

then either use IBM Set C/2:

        icc memcnt.c memcnt.def cmosread.obj

or Microsoft C6.00:

        cl memcnt.c memcnt.def cmosread.obj

Then execute the program "memcnt.exe" and you should get output like:

    Conventional memory:   640 KB
    Extended memory:      7552 KB
    Total memory:         8192 KB (8.0 MB)

    Use of C in IOPL segments

An alternative for those without MASM but with Microsoft C6.00 is to use
the CMOSRead.c program listing to replace CMOSRead.asm.  This is simply
a rewrite of the assembler using the _asm keyword, but does provide
a very simple example of using a C procedure in an IOPL segment, even
if all the actual code is really assembler!  In a real example the
assembler code could be hidden inside separate procedures and the
IOPL procedure would perform more actual work.

The difficulty with using C at IOPL is the environment set up by the compiler.
By default the C module uses segments with names like TEXT and DATA which all
get linked together under the same name, and DATA segments are also grouped
in the default group called DGROUP.  This provides some problems, especially
when linking a 16 bit IOPL procedure with 32 bit application code since the
group DGROUP is doubly defined with incompatible attributes!

Further to this the compiler generates calls to internal procedures to, for
example, check the stack.  These calls will FAIL from the IOPL procedure
because they are attempts to call FROM a higher privilege TO a lower privilege
and this not allowed.

The same applies to calls to the C runtime - it is basically very hard to do
these successfully and so you are best advised to write 'raw' C and to
do as much of the work in the application level code as possible.

To make CMOSRead.c work the following command can be used:

    cl /Zl /Gs /NT CMOS_TEXT /ND CMOS_DATA /c CMOSRead.c

The options used are as follows:
    /Zl - disable searching default library to prevent conflicts when
          linking with 32bit code
    /Gs - prevent call to C runtime stack check routine which will (a) be
          at the wrong privilege level and (b) check the WRONG stack since
          we will be using a special OS/2 provided ring 2 stack.
    /NT - force the compiler to use the supplied name (CMOS_TEXT) for code
          segments.  This is the same name used by the assembler example and
          picked up in the linker definition file.
    /ND - force the compiler to use the supplied name (CMOS_DATA) for data -
          prevent problems with DGROUP, though in this case we have no local

The compiled CMOSRead.obj can now be used instead of the assembled one for
BOTH the 16 and 32 bit programs.


Although usually programs under OS/2 can perform all the hardware operations
they require using standard hardware independent APIs there are occasions
when more that this is required.

Before leaping in to write a fully fledged device driver it may be possible
in certain cases to do all (or even some) of the work using IOPL segments
which are easier to debug and less likely to do damage to other programs
should they contain bugs.   A few simple examples of IOPL code such as this
one can help get the mechanics of the approach correct first time and may
remove some of the mystique surrounding the way IOPL segments are defined
and used.

------------------------- MEMCNT.C --------------------------


#include        <os2.h>
#include        <stdio.h>

#ifndef APIENTRY16
#define APIENTRY16 APIENTRY       /* make code work for MSC 6.00             */

/* external function to read 1 byte of CMOS memory                           */


/* GetPhysMem: get the values of conventional and extended memory            */

   *pReal = MAKEUSHORT( CMOSRead( 0x15 ), CMOSRead( 0x16 ) );
   *pExt  = MAKEUSHORT( CMOSRead( 0x17 ), CMOSRead( 0x18 ) );

   return 0;

/* ShowPhysMem: display the values                                           */

void ShowPhysMem( USHORT usReal, USHORT usExt )
   float fTotal;

   fTotal = (float) (usReal + usExt);

   printf( "Conventional memory: %5i KB\n", usReal );
   printf( "Extended memory:     %5i KB\n", usExt );

   printf( "Total memory:        %5.0f KB (%.1f MB)\n",
           fTotal, fTotal / 1024.0 );


/* M A I N   P R O G R A M                                                   */

int main (int argc, char **argv)
   USHORT usReal, usExt;          /* memory of each sort                     */

   if ( GetPhysMem( &usReal, &usExt ) == 0 )
      ShowPhysMem( usReal, usExt );


------------------------- CMOSREAD.ASM ----------------------

; CMOSREAD - provide CMOS memory read of 1 byte at given offset
; BYTE _pascal CMOSRead( USHORT usOffset );

.286c                                   ; assemble 286 instructions

         ASSUME  cs:CMOS_TEXT

;  BYTE _pascal CMOSRead( USHORT usOffset )
usOffset equ     word ptr [bp+6]

        enter   0,0

        mov     ax,usOffset

        cli                     ; disable interrupts

        out     70h,al          ; select CMOS byte to read
        jmp     $+2

        in      al,71h          ; and read it
        jmp     $+2


        ret     2




------------------------- MEMCNT.DEF ------------------------


SEGMENTS CMOS_TEXT CLASS 'CODE' IOPL    ; allow I/O in CMOS code segment

CMOSREAD        1                       ; # words of parameters reqd.

------------------------- CMOSREAD.C ------------------------

 * CMOSREAD - Microsoft C6.00 'wrapper' for the assembler file CMOSRead.ASM

#include <os2.h>

   BYTE result;

      mov     ax, usOffset

      cli                       ; disable interrupts

      out     70h,al            ; select CMOS byte to read
      jmp     $+2

      in      al,71h            ; and read it
      jmp     $+2


      mov     result, al

   return result;

                                                                   Roger Orr

Creative Commons License
This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License