This page describes five examples that illustrate 80386 protected mode concepts. They will be best understood if you have previously read the series Embedded 80386 Programming, which explains the advanced features of the Pentium CPU family.

These examples have been tested and (hopefully) don't contain bugs. They have been built as "ready-to-run" examples. The source code is fully documented and is the best place to look at for explanations. If you intend to build your own system, I would suggest to start with the example that fits the best your requirements and build on it. Once you have resolve all technical issues, you may re-implement your system in a way that suits best your needs.

An utility to build a bootable floppy disk containing the application.

Example1.asm

The source code for example1.

Example1.lst

The assembled source code listing.

Makefile

Makefile to build example 1

Example2\

Example 2 (same structure as Example1\)

Example3\

Example 3 (same structure as Example1\)

Example4\

Example 4 (same structure as Example1\)

Portx86p\

Port of mC/OS to a 32-bit, flat memory model.

Bin\

BootSctr.img

As already described.

ExeToImg.exe

As already described.

Ucos.obj

The object file of mC/OS. The source of it is available from the book.

Dev\

MyTask.dsw

The Visual C++ (VC++) project file.

MyTask.c

Source file of the test application.

Includes.h Os_cfg.h Ucos.h

m

C/OS and application header files.

Ix86p.c Ix86p.h Ix86p_a.asm

The 80x86 protected mode port of mC/OS.

Build.bat

As described earlier.

Debug.txt

As described earlier.

Entry\

Entry.asm

The entry point, in assembly. This is where the protected mode is enabled, and is very similar to Example 2.

Entry.lst

The assembled listing of Entry.asm.

Entry.obj

The assembled application entry point.

Makefile

The Makefile to build Entry.obj.

DEVELOPMENT TOOLS

Each example is a stand-alone system. It does not depend on any other software to run. All these examples are harm-free: they do not alter any of the hardware component. Specifically, they don’t make any access to any disk (doing so would require some drivers, none of them being present in the code).

The following tools have been used to build these examples:

Microsoft Macro Assembler® 6.11 (MASM)

Microsoft Visual C++® 5.0 (VC++)

Microsoft 32-bit Incremental Linker® Version 5.00.7022 (provided with Visual C++ 5.0). This linker is used for all examples, since it produces flat memory model images, whereas MASM’s linker produces DOS executables, which are inadequate.

Also, you will need the following tools:

Microsoft DEBUG® (provided with MS-DOS® and Windows 95®).

Microsoft NMAKE® 1.62.7022 (provided with Visual C++ 5.0).

You may use other tools (assembler, C compiler and linker) that satisfy these requirements:

This section applies to examples 1 to 4; the fifth example is implemented differently.

The procedure to build the examples is the same: invoke the makefile with NMAKE. The result is an .IMG file, which is the exact memory image of the example. The last example is a Visual C++ project, which also produces an .IMG file. The procedure to run an .IMG file consists in copying the file on a bootable disk and booting with that disk.

4

To build an example:

Start a DOS window.

Create a working directory and copy the complete example files into it (the source files, but also Makefile and Built.bat)

If you change the source filenames, update Makefile and Build.bat accordingly.

Build the project by typing NMAKE. You will have to substitute the calls to the assembler and linker I used (both from Microsoft) to yours if they differ. Each example is compiled and linked in order to produce a Common Object File Format (COFF) file. This file is then expanded into a file containing the exact memory (via the ExeToImg.exe utility).

For example, a typical build session is as follows:

C:\EX386>md MyExample

C:\EX386>cd MyExample

C:\EX386\MyExample>xcopy /E ..\Example1\*.* .

Files that are copied are shown

C:\EX386\MyExample>nmake

The nmake commands are shown as they example is built

4

To run an example:

If not already done, start a DOS window and go to the example directory.

Insert a formatted 1.44MB floppy disk into the drive A. If you use another drive than A, edit the Debug.txt file, and change the line:

w 0100 0 0 1

to

w 0100 n 0 1

where n is 0 for A:, 1 for B:, etc.

Type the command BUILD. This copies ..\Bin\BootSctr.img (the bootstrap loader) and the image (Examplex.img) on the floppy disk. If the example directory is not at the same level that the Bin directory, Build.bat must be updated accordingly.

To load and run the application, simply reboot the machine – or – insert the floppy disk into the boot drive of another machine and reboot that machine. The application will load and run.

For example, a typical run session is as follows:

C:\EX386\MyExample>Build

build commands are shown as the system image is built

(reboot)

You don’t need another computer to test (only one x86 computer – a PC – is sufficient), although using two makes the development and testing much easier. Finally, Intel's documentation is the best reference I found so far regarding the CPU features. I recommend you to order it (at www.intel.com) if you don't already have it at hand.

The following programming conventions have been retained for clarity, simplicity and consistency:

No special MASM features

The examples are written with a minimum of dependencies on Microsoft Macro Assembler 6.11a. No special feature, such as the simplified segment directives, has been retained.

Use of procedures

Procedure directives (PROC, ENDP) are used for clarity, although simple labels would be enough.

Optimization

Some considerations have been given to optimization, either in the form of slightly optimized code (but still readable) and aligned data (using the ALIGN directive). WORDs aligned on a 2-byte boundary and DWORDs aligned on a 4-byte boundary save 1 cycle per instruction that accesses them. Strings do not require alignment (in the examples), since they are accessed byte by byte.

NEAR/FAR convention

The NEAR/FAR distinction has to do with the way a function is called and the return it must execute to properly restore the stack. Since the kernel and each task run in their own segment in each example, NEAR calls and procedures are used.

C Calling convention

Procedures are called using a C calling convention, where the caller (not the callee) must clean the stack by removing the parameters. The only exception has to do with the procedures called from a call gate: these procedures are always declared as FAR and must execute a FAR return that has to pop all parameters (call gates are always called with a fixed, known number of parameters).

Style

Directives are written in uppercase (PROC, MACRO, etc.). Macro identifiers and equivalencies are also written in uppercase to prevent confusing them with mnemonics. The rest is written in lowercase (mnemonics, identifiers, etc.). Variables names are written in lowercase with the first letter of a word in uppercase (TaskCount). Underscores are used to separate a structure name or a module from its member (NextTss_Selector, NextTss_Offset, Task1_Code, Task1_Data).

Comments

Each non-trivial line is commented, as well as blocks with a certain complexity.

Makefile

Whenever a source file is arbitrarily too long, it is split into one ..ASM file and multiple .INC files, the latter included into the former. At the end, a single .ASM file has to be compiled, simplifying the makefile. Example 5 is an exception as it has been built with Visual C++ 5.0.

EXAMPLE OVERVIEW

There are five examples:

Switching into protected mode;

Basic segmentation;

Advanced segmentation;

Activating paging;

A port of mC/OS to the protected mode.

All examples but the last (port of mC/OS) are implemented in one file (Examplex.asm) for simplicity (the file may include one or many ..inc files). If you intend to develop your own system, I recommend to keep all CPU-dependent functions apart from the CPU-independent ones, for clarity and easier maintainability.

EXAMPLE 1: SWITCHING INTO PROTECTED-MODE

This example activates the protected-mode and displays the message "Now in protected-mode" in the upper-half corner. It is basically a repetition of the example provided in the first article "The Protected Mode".

EXAMPLE 2: BASIC SEGMENTATION

This example demonstrates a system with a basic kernel and one task that shows a series of '*' at line 8. The kernel simply performs initialization operations and jumps into the task. Protection is not required at that level, so the task and the kernel run with full privileges (CPL 0). One segment is used and it covers the entire physical memory. Two descriptors are required: the code descriptor and an alias data descriptor; both describing the same address space. If many tasks were present, they would share the same address space (no protection). Such a model is adequate for a real-time application that entirely fits in memory (a micro-controller for instance).

EXAMPLE 3: ADVANCED SEGMENTATION

This example demonstrates a system with a micro-kernel and two untrusted tasks. Several concepts are demonstrated in this example:

The micro-kernel contains basic task services (creation and destruction), round-robin scheduling, timer & exception interrupt handlers and two system calls. The first system calls is implemented via an interrupt gate and returns the number of ticks since the system startup. The second call, implemented via a call gate, allows display strings on screen – this service is required since the tasks cannot access the video buffer, outside their address space. The kernel, as usual, runs with all privileges.

The tasks run concurrently (no one has priority over the other) and a task switch occurs at every timer tick, in a round-robin fashion (one after the other). These tasks don’t have any special privilege (CPL 3), protecting the kernel against erroneous access.

EXAMPLE 4: ACTIVATING PAGING

This example builds on Example 1 and activates paging. This initialization code then maps itself at F0000000h (a typical kernel location) and displays ‘Now in protected-mode with paging enabled’ on the upper-left corner of the screen. Note that the video frame buffer, located at physical address B8000h, is accessed at address F00B8000h.

C/OS (TODO: add link to MFI doc) is a portable, ROMable, preemptive, real-time, multitasking kernel for microprocessors. Originally written in C and x86 (real mode) assembly languages, it has been ported to a variety of microprocessors. Because of its accessibility and its price (it comes for free with the book), it is a good choice to demonstrate protected mode concepts.

Porting this system to the x86 is a practical example. The details of the port are beyond the scope of this document (which focuses on protected mode issues only).

This example is a Visual C++ project, although the entry point (the subject of this example) has been assembled apart. The implementation resembles to Example 2. Essentially, the protected mode is activated first and a simple 4GB flat memory model is enabled. The GDT contains three entries: the NULL entry, the code segment (covering 4GB) and the data segment, also covering 4GB. All segment registers are initialized, as well as the stack pointer before calling the application main entry point: main(). From that point and on, it is up to the application and mC/OS to take control. If main() returns, an infinite loop is executed. The application runs at CPL 0. The IDT contains 64 entries:

Entries

Description

0x00-0x1F

These 32 entries are required by the CPU exceptions.

0x20-0x2F

These 16 entries correspond to the IRQ. It is expected that the application will remap the IRQ interrupts to that range.

0x30-0x3F

These 16 entries are available for software interrupts to the applications.

Entry.obj must be the very first object file in the resulting image. Having the source file part of the project does not ensure his position in the resulting image; but specifying the file as the first object file to the linker does.

ucos.c cannot be redistributed (you must purchase the book to obtain it). ucos.obj is obtained by first compiling the file in the project, and then later removing the file from the project and adding ucos.obj to the link options.

The linker options are set to produce a code whose base address is 0h. However, this linker always relocates the code 1000h bytes above the specified based address, making the code relocated at 1000h. The bootstrap loader takes this address into account by loading the code at address 1000h instead of 0.

Default libraries are not ignored, providing access to the standard C library. However, the functions in that library may or may not work (no test have been done at this point).

Finally, the project contains a Custom Build step, which runs ExeToImg over the EXE file. Since the EXE file is targeted for Windows, it cannot be used in that format in a stand-alone mode. Instead, the exact memory image is generated from it, into an IMG file, which can be loaded and run.

With the bootstrap copied on an otherwise empty floppy disk, the image (MyTask.img) can simply be copied on the diskette. Booting a PC with the floppy will load the bootstrap, which in turn will load the application and run it.

To build the project under Windows:

Go to the Dev sub-directory (for instance, C:\EX386\Portx86p\Dev).

Double-click on the MyTask.dsw file to open the project in Visual C++ 5.0.

Press F7 to build the application. The final image will be in C:\EX386\Portx86p\Dev\Release\MyTask.img.

Insert a formatted 1.44MB floppy disk in your drive A:. If you use another drive, update the file C:\EX386\Portx86p\Dev\Debug.txt as explained in the section