PortingmC/OS to the x86 Protected Mode
This page describes the port ofmC/OS to a protected mode, 32-bit, flat memory model of the 80386, 80486, Pentium and Pentium II CPUs.
mC/OS 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.
The port is based on ix86s.c and ix86s_a.asm, a x86 small memory model port provided on the standardmC/OS distribution. Although a large memory model port is also provided, the small memory model closely resembles to the current port, as no segments are required. But the small memory implementation of mC/OS has some real mode dependencies:
- It is loaded and runs as a real-mode DOS application.
- The underlying system is already initialized and is in a known state (thanks to DOS).
- The implementation calls the default DOS clock interrupt handler upon completing its internal handling of that interrupt.
These assumptions do not hold in protected mode since DOS only runs in real mode. Consequently the following steps, described in the following sections, are required to complete the port:
The application cannot be loaded by DOS (since there is no DOS) and requires its own loader.
The hardware must be initialized and brought to a known and stable state. Interrupts – particularly the clock interrupt – must be fully processed.
Converting 16-bit, real mode code to 32-bit, flat code.
Compiling, linking and preparing a bootable disk.
Let’s recall thatmC/OS is implemented as a library, or a set of function calls, bundled with the target application. These steps are consequently application-centered.
Loading the Application
The application can be loaded in various ways:
- It can be loaded from DOS. But switching into protected mode and fully bypassing DOS and the BIOS may create some inconsistencies inside DOS, preventing a normal return to it. Thus, the PC would have to be rebooted in order to get back to DOS. Also, the application would have to be built as a DOS application, requiring a 16-bit compiler.
- It can be loaded from a floppy disk upon boot-up. By providing a bootstrap loader, the application would be the first thing loaded and would be in full control. The PC also has to be rebooted back to DOS (or Windows) if required.
- It can be burned into PROM, into a stand-alone system.
Since I did not have a 16-bit compiler (I am using Microsoft Visual C++ 5.0) and I already had a bootstrap loader at hand, I decided to choose method #2. I also used a second PC to test the application, in order not to have to reboot my workstation after each test.
The bootstrap loader (BootSctr.img) can easily be installed on the very first sector of a floppy disk (e.g. the boot sector) by using the DEBUG program, distributed with DOS and Windows. The DEBUG’s write command has the following format:
-w offset disk track sector_count
By executing the following commands:
-w 0100 0 0 1
DEBUG loads the file in memory at offset 0100h (the segment address is irrelevant) and copies 512 bytes from offset 100h (where the file is) onto drive A, on track 0, for 1 sector (512 bytes).
When loaded, this bootstrap loader:
- Loads the first 64k of the floppy disk (the first file in fact) at the physical address 1000h (we will see why this address is important later).
- Disables the interrupts.
- Jumps at 1000h to start executing the application.
Initializing the Hardware
mC/OS is normally initialized within the application, by calling OsInit(). This assumes that the application has already been loaded and started. But in our case, the protected mode must first be activated in order to start the application in a 32-bit, flat memory model mode. Thus, a special application entry point is required, which must be run before main(). Such an entry point is normally provided with a compiler and is operating system-dependent. In our case, since there is no underlying operating system, the entry point must perform itself any initialization tasks required to run the application. Luckily, there are only a few things to care about.
The entry point of the application is coded in Entry.asm. With the interrupts still disabled, the following actions are performed:
- The protected mode is activated with a flat memory model that spans 4GB of physical memory.
- All segment registers are initialized to default values in order to "forget" about them (hence obtaining a flat, non-segmented memory model).
- A temporary 32-bit stack is set to address 8000h (an arbitrary address).
- A call is done to main(), which is the application’s entry point.
- In case main() returns, an infinite loop is executed. This is not really required, since main() is not expected to return.
The file also contains the system tables required by the processor in protected mode:
- Global Descriptor Table (GDT). Since all tasks are given full privileges (CPL 0) and share the same address space (e.g. the entire physical memory), only one code and data descriptors are required.
Unused (set to 0)
Code segment, with the following attributes:
- code segment
- base address of 0
- limit of 4GB (FFFFFh, with the G bit set)
- D-bit set (to run 32-bit code by default)
Data and Stack segment, with the following attributes:
- data segment
- base address of 0
- limit of 4GB (FFFFFh, with the G bit set)
- B-bit set (to execute 32-bit stack instructions by default)
Interrupt Descriptor Table (IDT). Only 64 entries are reserved, among them 16 formC/OS and the application (the number can be increased as needed).
CPU interrupts and exceptions
Hardware Interrupt Request Lines (IRQs)
Available formC/OS and the application
- There is no Local Descriptor Table (LDT) in use.
The rest of the hardware initialization is performed in the application. Once in main(), a call is done to OsCpuInit(), in Ix86p.c, in order to perform the following:
- Enable the address line 20, normally disabled for some real mode considerations. The line is enabled by sending a few commands to the Intel 8042 keyboard controller. See InitA20() for details (Ix86p.c).
- Relocate the IRQ interrupts, since the overlap the CPU interrupts and exceptions (for instance, it is not possible to know if the interrupt 0 has been triggered by the clock or a division by zero). The IRQ are relocated in the range 20h-2Fh by sending a few commands to the Intel 8259 interrupt controllers. See InitPIC() for details (Ix86p.c).
- The interrupt table is initialized by using SetIntVector() and SetIDTGate(). The 64 entries are set to point to a default interrupt handler (DefIntHandler(), in Ix86p_a.asm), which simply performs an interrupt return.
- The General Protection Fault (interrupt 13h), triggered by the CPU when a fault is detected (assigning invalid values to segment registers, executing an interrupt above 40h, etc.), is assigned a dump stack handler (DumpStackHandler(), in x86p_a.asm). This handler displays the address of the faulty instruction on the upper-left corner of the screen and enters an infinite loop. This is useful to identify the source of a problem.
- The clock handler (OsTickISR(), in Ix86p_a.asm) is installed as the interrupt 20h handler.
- The mC/OS context switch handler (OSCtxSw(), in Ix86p_a.asm), is installed as the interrupt uCOS handler (uCOS is defined as 0x30 in os_cfg.h).
Back in main(), OsInit() is then called to initialize mC/OS. Two tasks are then created by calling OsTaskCreate() twice. Finally, multi-tasking is started by executing OSStart(), which never returns.
Note that the entire initialization takes place with the interrupts disabled; they will be re-enabled when the first task is executed.
Converting 16-Bit, Real Mode Code to a 32-Bit, Flat Memory Model
- The OS_FAR symbol (Ix86p.h) is defined as nothing, since there is no far pointers in use.
- A few typedefs and definitions are added in Ix86p.h:
- The format of an interrupt gate (IDT_GATE);
- Interrupt gate types (IDTGATE_xxx);
- The default code selector (CS_SELECTOR), required to initialize the interrupt gates.
- A part of OSTaskCreate() (in Ix86p.c) is updated to take into account:
- The return address (wich is TaskLoop()), should main() return.
- The initial flag register. Note that the IF bit (interrupt enabled) must be set, otherwise interrupts will be disabled for that task.
- The code selector, which is always 08h (the second GDT entry).
- 32-bit offset registers.
- Converting the 16-bit real mode assembly functions (OSTickISR(), OSStartHighRdy(), OSCtxSw(),OSIntCtxSw(), all in Ix86p_a.asm), which consist of:
- Removing segment registers.
- Using 32-bit offset registers (EAX instead of AX for instance).
- Using 32-bit instructions (iretd instead of iret, for instance).
- OSTickISR()(Ix86p_a.asm) makes no longer a call to the previous clock interrupt handler, but it sends an end-of-interupt to the interrupt controllers (i8259). This would also be required for each hardware interrupt (IRQ) handler.
- Some C library functions are added: inportb() and outportb(), implemented in assembly.
Additionally, minor changes have been done in the include files:
- In Includes.h, a directive to include Ix86p.h has been added.
- In Os_cfg.h, the maximum number of tasks (OS_MAX_TASKS) is defined as 2 and the mC/OS interrupt (uCOS) is defined as 0x30.
Building the Application
A test application has been developed for Visual C++ 5.0 as an example of 80386 protected mode programming.
- Labrosse, Jean J., "mC/OS The Real-Time Kernel", Fourth Printing, R&D Publications, 1992
mC/OS is a trademark of Jean J. Labrosse.