Embedded 80386 Programming: Examples
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.
The remaining of this document consists of:
- The installation procedure
- The development tools
- The building procedure
- The programming conventions
- The examples
- The references
The installation procedure is straight-forward and requires less than 500KB of disk space.
- In a DOS window, create a directory (C:\EX386 for instance) and go to that directory.
- Download ex386.exe (a self-extracting ZIP file) into that directory.
- Type the following command to extract the files:
The distribution is as follows:
A bootstrap loader that loads a file up to 64KB.
A file used by Build.bat.
An utility to expand a COFF image into its exact memory image.
An utility to build a bootable floppy disk containing the application.
The source code for example1.
The assembled source code listing.
Makefile to build example 1
Example 2 (same structure as Example1\)
Example 3 (same structure as Example1\)
Example 4 (same structure as Example1\)
Port of mC/OS to a 32-bit, flat memory model.
As already described.
As already described.
The object file of mC/OS. The source of it is available from the book.
The Visual C++ (VC++) project file.
Source file of the test application.
Includes.h Os_cfg.h Ucos.h
mC/OS and application header files.
Ix86p.c Ix86p.h Ix86p_a.asm
The 80x86 protected mode port of mC/OS.
As described earlier.
As described earlier.
The entry point, in assembly. This is where the protected mode is enabled, and is very similar to Example 2.
The assembled listing of Entry.asm.
The assembled application entry point.
The Makefile to build Entry.obj.
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:
- All the x86 32-bit instructions are supported (assembler).
- The flat memory model is supported (assembler & linker).
- The final base address is relocatable (linker).
BUILDING AND RUNNING THE EXAMPLES
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.
4To 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\MyExample>xcopy /E ..\Example1\*.* .
Files that are copied are shown
The nmake commands are shown as they example is built
4To 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
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:
build commands are shown as the system image is built
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.
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.
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).
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).
Each non-trivial line is commented, as well as blocks with a certain complexity.
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.
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:
- Separate task address space through segmentation.
- Protected kernel space.
- Local Descriptor Tables (LDT).
- Task State Segments (TSS).
- Interrupt, trap and exception handling through the Interrupt Descriptor Table (IDT).
- Call gates.
- Trap interface.
- Programming the Intel 8259 interrupt controllers.
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.
EXAMPLE 5: A PORT OF mC/OS TO THE PROTECTED MODE
mC/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:
These 32 entries are required by the CPU exceptions.
These 16 entries correspond to the IRQ. It is expected that the application will remap the IRQ interrupts to that range.
These 16 entries are available for software interrupts to the applications.
The port is tested through a Visual C++ 5.0 application, called MyTask. Within Visual C++, the tool options have been set as follows:
Options (within Visual C++ 5.0)
/nologo /ML /W3 /GX /O2 /Fo"Release/" /Fd"Release/" /FD /c
.\Entry\Entry.obj ..\bin\ucos.obj /nologo /base:"0x00000000" /entry:"main" /pdb:none /map:"Release/MyTask.map" /machine:I386 /out:"Release/MyTask.exe" /fixed
ASM file (Custom Build)
ml /c /coff /FlRelease\$(InputName).lst /FoRelease\$(InputName).obj $(InputName).asm
Project (Custom Build)
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:
Loading the Application.
- 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
Double-click on (or execute) the file C:\EX386\Portx86p\Dev\Build.bat to build a bootable floppy disk. Reboot with the diskette in drive A: in place, or transfer the diskette to another machine and reboot that machine.
- Gareau Jean L., "Advanced Embedded 80386 Programming: The Protected Mode", (TODO: add complete reference)
- Gareau Jean L., " Advanced Embedded 80386 Programming: Protection and Segmentation", (TODO: add complete reference)
- Gareau Jean L., " Advanced Embedded 80386 Programming: Paging ", (TODO: add complete reference)
- Labrosse Jean J., "uC/OS The Real-Time Kernel", R&D Publications, Fourth Printing, 1992
mC/OS is a trademark of Jean J. Labrosse.