Introduction to IOPL programming

From EDM2
Jump to: navigation, search

By Roger Orr

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

Introduction

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 as CMOS RAM.

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 system...

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 segments.

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 application 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 16-bit mode since the compiler supports calls from 32bit 'flat' code to 'segmented' 16bit code. This is the method I use here.

Implementation

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:

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

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 compatibility with OS/2 1.x I have added the lines:

#ifndef APIENTRY16
#define APIENTRY16 APIENTRY
#endif

which define APIENTRY16 to be a regular APIENTRY when the compiler is 16-bit 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 may be have little experience of them.

"NAME WINDOWCOMPAT"
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.
"SEGMENTS CMOS_TEXT CLASS 'CODE' IOPL"
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.
"EXPORTS"
"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 Microsoft 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
      data.

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

Conclusion

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.

Source Code

MEMCNT.C

/*
  IOPL MUST be enabled in CONFIG.SYS: IOPL=YES or IOPL=MEMCNT
*/

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

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

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

extern BYTE APIENTRY16 CMOSRead( USHORT );


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

USHORT GetPhysMem( PUSHORT pReal, PUSHORT pExt )
  {
  *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 );

  return;
  }

/*****************************************************************************/
/* 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 );

  return(0);
  }

CMOSREAD.ASM

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

.286c                                   ; assemble 286 instructions

CMOS_TEXT  SEGMENT BYTE PUBLIC 'CODE'
        ASSUME  cs:CMOS_TEXT


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

CMOSREAD PROC    FAR
       enter   0,0

       mov     ax,usOffset

       pushf
       cli                     ; disable interrupts

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

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

       popf

       leave
       ret     2

CMOSREAD ENDP

CMOS_TEXT ENDS

        END

MEMCNT.DEF

NAME WINDOWCOMPAT 

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

EXPORTS
CMOSREAD        1                       ; # words of parameters reqd.

CMOSREAD.C

/*
 * CMOSREAD - Microsoft C6.00 'wrapper' for the assembler file CMOSRead.ASM
 *
 * BYTE EXPENTRY CMOSRead( USHORT usOffset );
 */

#include <os2.h>

BYTE EXPENTRY CMOSRead( USHORT usOffset )
  {
  BYTE result;

  _asm
     {
     mov     ax, usOffset

     pushf
     cli                       ; disable interrupts

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

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

     popf

     mov     result, al
     }

  return result;
  }

Roger Orr 28-Dec-1992