Просмотр исходного кода

Add Developing Your Own 32-Bit Operating System workspace

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
master
Daniel Covington 2 недель назад
Родитель
Сommit
9121c7eb8d
36 измененных файлов: 39626 добавлений и 0 удалений
  1. +9
    -0
      MEMORY.md
  2. +2755
    -0
      library/Developing Your Own 32-Bit Operating System/Back-Matter/Back-Matter-source-text.md
  3. +29
    -0
      library/Developing Your Own 32-Bit Operating System/Chapter-01-Overview/Chapter-01-memory.md
  4. +329
    -0
      library/Developing Your Own 32-Bit Operating System/Chapter-01-Overview/Chapter-01-source-text.md
  5. +359
    -0
      library/Developing Your Own 32-Bit Operating System/Chapter-02-General-Discussion-and-Background/Chapter-02-source-text.md
  6. +571
    -0
      library/Developing Your Own 32-Bit Operating System/Chapter-03-The-Tasking-Model/Chapter-03-source-text.md
  7. +510
    -0
      library/Developing Your Own 32-Bit Operating System/Chapter-04-Interprocess-Communications/Chapter-04-source-text.md
  8. +1081
    -0
      library/Developing Your Own 32-Bit Operating System/Chapter-05-Memory-Management/Chapter-05-source-text.md
  9. +999
    -0
      library/Developing Your Own 32-Bit Operating System/Chapter-06-The-Hardware-Interface/Chapter-06-source-text.md
  10. +822
    -0
      library/Developing Your Own 32-Bit Operating System/Chapter-07-OS-Initialization/Chapter-07-source-text.md
  11. +493
    -0
      library/Developing Your Own 32-Bit Operating System/Chapter-08-Programming-Interfaces/Chapter-08-source-text.md
  12. +591
    -0
      library/Developing Your Own 32-Bit Operating System/Chapter-09-Application-Programming/Chapter-09-source-text.md
  13. +738
    -0
      library/Developing Your Own 32-Bit Operating System/Chapter-10-Systems-Programming/Chapter-10-source-text.md
  14. +1081
    -0
      library/Developing Your Own 32-Bit Operating System/Chapter-11-The-MMURTL-Monitor-Program/Chapter-11-source-text.md
  15. +288
    -0
      library/Developing Your Own 32-Bit Operating System/Chapter-12-The-Debugger/Chapter-12-source-text.md
  16. +630
    -0
      library/Developing Your Own 32-Bit Operating System/Chapter-13-Keyboard-Service/Chapter-13-source-text.md
  17. +884
    -0
      library/Developing Your Own 32-Bit Operating System/Chapter-14-File-System-Service/Chapter-14-source-text.md
  18. +1492
    -0
      library/Developing Your Own 32-Bit Operating System/Chapter-15-API-Specification/Chapter-15-source-text.md
  19. +3092
    -0
      library/Developing Your Own 32-Bit Operating System/Chapter-16-MMURTL-Sample-Software/Chapter-16-source-text.md
  20. +275
    -0
      library/Developing Your Own 32-Bit Operating System/Chapter-17-Introduction-to-the-Source-Code/Chapter-17-source-text.md
  21. +2572
    -0
      library/Developing Your Own 32-Bit Operating System/Chapter-18-Kernel-Code/Chapter-18-source-text.md
  22. +1714
    -0
      library/Developing Your Own 32-Bit Operating System/Chapter-19-Memory-Management-Code/Chapter-19-source-text.md
  23. +714
    -0
      library/Developing Your Own 32-Bit Operating System/Chapter-20-Timer-Management-Code/Chapter-20-source-text.md
  24. +1137
    -0
      library/Developing Your Own 32-Bit Operating System/Chapter-21-Initialization-Code/Chapter-21-source-text.md
  25. +1531
    -0
      library/Developing Your Own 32-Bit Operating System/Chapter-22-Job-Management-Code/Chapter-22-source-text.md
  26. +1656
    -0
      library/Developing Your Own 32-Bit Operating System/Chapter-23-Debugger-Code/Chapter-23-source-text.md
  27. +2522
    -0
      library/Developing Your Own 32-Bit Operating System/Chapter-24-Selected-Device-Driver-Code/Chapter-24-source-text.md
  28. +2334
    -0
      library/Developing Your Own 32-Bit Operating System/Chapter-25-Keyboard-Code/Chapter-25-source-text.md
  29. +1185
    -0
      library/Developing Your Own 32-Bit Operating System/Chapter-26-Video-Code/Chapter-26-source-text.md
  30. +3797
    -0
      library/Developing Your Own 32-Bit Operating System/Chapter-27-File-System-Code/Chapter-27-source-text.md
  31. +1434
    -0
      library/Developing Your Own 32-Bit Operating System/Chapter-28-DASM-A-32-Bit-Intel-Based-Assembler/Chapter-28-source-text.md
  32. +1012
    -0
      library/Developing Your Own 32-Bit Operating System/Chapter-29-CM32-A-32-Bit-C-Compiler/Chapter-29-source-text.md
  33. +859
    -0
      library/Developing Your Own 32-Bit Operating System/Front-Matter/Front-Matter-source-text.md
  34. +85
    -0
      library/Developing Your Own 32-Bit Operating System/book-structure.md
  35. Двоичные данные
      library/Developing Your Own 32-Bit Operating System/source-file.pdf
  36. +46
    -0
      tools/split_book.py

+ 9
- 0
MEMORY.md Просмотреть файл

@@ -53,3 +53,12 @@ Status board only. Details live in each chapter's `memory.md`; read the one list
- **Source File**: /library/Inference Engineering/source-file.pdf - **Source File**: /library/Inference Engineering/source-file.pdf
- **Structure**: /library/Inference Engineering/book-structure.md (11 sections; reading number = book chapter + 2; Appendices A/B = Chapters 10/11) - **Structure**: /library/Inference Engineering/book-structure.md (11 sections; reading number = book chapter + 2; Appendices A/B = Chapters 10/11)
- **Last Updated**: 2026-10-05 - **Last Updated**: 2026-10-05

## Developing Your Own 32-Bit Operating System — Richard A. Burgess
- **Status**: In Progress
- **Current Position**: Chapter 1 of 29 — intake complete, awaiting preview
- **Current Chapter Memory**: /library/Developing Your Own 32-Bit Operating System/Chapter-01-Overview/Chapter-01-memory.md
- **Folder**: /library/Developing Your Own 32-Bit Operating System/
- **Source File**: /library/Developing Your Own 32-Bit Operating System/source-file.pdf
- **Structure**: /library/Developing Your Own 32-Bit Operating System/book-structure.md (29 chapters in 5 Parts; scanned OCR text; printed page = PDF page − 22)
- **Last Updated**: 2026-10-03

+ 2755
- 0
library/Developing Your Own 32-Bit Operating System/Back-Matter/Back-Matter-source-text.md
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 29
- 0
library/Developing Your Own 32-Bit Operating System/Chapter-01-Overview/Chapter-01-memory.md Просмотреть файл

@@ -0,0 +1,29 @@
# Developing Your Own 32-Bit Operating System — Chapter 01 Memory: Overview
- **Stage**: Not started — awaiting preview
- **Next Step**: Confirm the starting point with the reader (Chapter 1 or a later chapter), then run the Pre-Reading Briefing for Chapter 1.
- **Reading Span**: PDF pages 23–34 (printed pp 1–12; includes the Part I divider plate on PDF 23–24)
- **Source Text**: /library/Developing Your Own 32-Bit Operating System/Chapter-01-Overview/Chapter-01-source-text.md (the chapter's own words; read instead of the PDF)
- **Full Record**: /library/Developing Your Own 32-Bit Operating System/Chapter-01-Overview/Chapter-01-chapter-notes.md (not yet created)
- **Last Updated**: 2026-10-03

## Carried-in Context (from earlier chapters)
- First chapter — nothing carried in.
- Book-level framing (from the front matter): Burgess (1995) walks through MMURTL, his own message-based, real-time, multitasking micro-kernel OS for 32-bit ISA PCs, guided by Einstein's "as simple as possible, but not simpler." The preface says MMURTL v1.0 is untested and still under construction.

## This Chapter
- **Core Question**: Pending
- **Watch-For Themes**: Pending
- **Core Thesis**: Pending
- **Key Concepts**: Pending
- **Notable Arguments / Evidence Limits**: Pending
- **Action Item**: Pending

## Reader State
- **Pending Questions**: None
- **Reader's Answers (paraphrase)**: —
- **Misconceptions / Feedback Given**: —
- **Personal Threads**: —

## Open Threads
- Cross-book: MMURTL's "simplicity over backward compatibility" stance may parallel KISS/YAGNI in *FOCUS Architecture*.
- Source text is OCR from a scan, so verify exact identifiers and code against the PDF page when it matters.

+ 329
- 0
library/Developing Your Own 32-Bit Operating System/Chapter-01-Overview/Chapter-01-source-text.md Просмотреть файл

@@ -0,0 +1,329 @@
# Developing Your Own 32-Bit Operating System — Chapter-01: Overview
- **Source**: /library/Developing Your Own 32-Bit Operating System/source-file.pdf
- **PDF pages**: 23–34
- **Pages without text**: 23, 24, 25, 34
- **Note**: Includes the Part I divider plate and its blank verso before the chapter.

---

<!-- PDF page 23 -->
Introduction

<!-- PDF page 24 -->


<!-- PDF page 25 -->
Overview

<!-- PDF page 26 -->
ra
Part I 1 1 Introduction
Computer programmers and software engineers work with computer operating systems every
day. They use them, they work with them, they even work "around" them if that's what it
takes to get their jobs done. If you're an experienced programmer, I'm willing to bet you've
pondered changes you would make to the operating systems you use, or you've even thought
about what you would build and design into an operating system if you were to write your
own.
You don't have to be Albert Einstein to figure out that many years of labor can go into writing
a computer operating system. You also don't have to be Einstein to write one, although I'm
sure it would have increased my productivity a little while writing my own. Unfortunately, I
have only Albert's absent-mindedness, not his IQ.
Late in 1989, 1 undertook the task of writing a computer operating system. What I discovered
was that most of the books about operating-system design were very heavy on general theory
and existing designs, but very light on specifics, code, and advice on the seemingly endless tasks
facing someone who really wants to sit down and create one. I'm not knocking the authors of
these books; I've learned a lot from all that I've read. It just seemed there should be a more
"down to earth" book that documents what you need to know to get the job done.
Writing my own operating system has turned into a very personal thing for me. I have been
involved in many large software projects where the deliverables were conceived by others and
I was to turn them into working code and documentation. In the case of my own operating
system, I set the ground rules. I decided what stayed and what went. I determined the specifi-
cations for the final product.
When I started out, I thought the "specs" would be the easy part. I would just make a list, do
some research on each of the desirables, and begin coding. Not so fast there, bucko!
I may not have to tell you this if you've done some serious "code cutting" yourself, but the
simplest items on the wish list for a piece of software can add years to the critical path for re-
search and development, especially if you have a programming team comprising just one per-
son.
My first list of desirables looked like a kid's letter to Santa Claus. It turned out that I hadn't
been a good-enough boy to get all those things (and the average life expectancy of a male hu-
man being wouldn't allow it either). Realizing this, I whittled away to find a basic set of ingre-
dients that constitute the real make-up of the "guts" of operating systems. The most obvious of
these, as viewed by the outside programmer, are the tasking model, the memory model, and
the programming interface. As coding began, certain not-so-obvious factors came into play,
such as the OS-to-hardware interface, portability issues, size, and speed.
One of the most obvious concerns for commercial operating-system developers is compatibil-
ity. If you write something that no one can use, it's unlikely they'll use it. Each of the versions
of MS-DOS that were produced had to be able to run software written for previous versions.
When OS/2 was first developed, the "DOS-BOX" was a bad joke. It didn't provide the needed

<!-- PDF page 27 -->
Chapter 1 M Overview
compatibility to run the programs that business depended on. In the early days, UNIX had a
somewhat more luxurious life, because source-code compatibility was the main objective; you
were pretty much expected to recompile something if you wanted it to run on your particular
flavor of the system. You may have noticed that this has changed substantially.
I did not intend to let compatibility issues drive my design decisions. I was not out to compete
with the likes of Microsoft, IBM, or Sun. You, however, may want compatibility if you intend
to write your own operating system. It's up to you. If you desire compatibility, the existing
interfaces are all well documented, so you don't need to design your own. But I wanted a small
API. That was part of my wish list.
Along my somewhat twisted journey of operating-system design and implementation, I have
documented what it takes to really write one, and I've included the "unfinished" product for
you to use with very few limitations or restrictions. Even though I refer to it as unfinished, it's
a complete system. I believe that when a piece of software is "finished," it's probably time to
replace it because of obsolescence or lack of maintenance.
m
What This Book Is About
In this book I discuss the major topics of designing your own OS: the tasking model, the memory
model, programming interfaces, hardware interface, portability issues, size, and speed. I back
up these discussions by showing you what I decided on (for my own system), and then finally
I discuss my code in great detail. Your wish list may not be exactly like mine. In fact, I'm sure
it won't be. For instance, you'll notice the ever-so-popular term "object oriented" conspicu-
ously missing from any of my discussions. This was completely intentional. This term clouds
far too many real issues these days. Don't get me wrong — I'm an avid C++ and OOP user. But
that's not what this book is about, so I've removed the mystique. Once again, your goals will
be your own.
Throughout this book, I use the operating system I developed (MMURTL) to help explain
some of the topics I discuss. You can use pieces of it, ideas from it, or even all of it if you wish
to write your own.
I have included the complete source code to my 32-bit, message-based, multitasking, real-time,
operating system (MMURTL), which is designed for the Intel 386/486/Pentium processors
on the PC Industry Standard Architecture (ISA) platforms.
The source code, written in 32-bit, Intel-based assembly language and C, along with all the
tools necessary to build, modify, and use the operating system are included on the accompany-
ing CD-ROM. The hardware requirements section below tells you what you need to run
MMURTL and to use the tools I have developed. It's not really required if you decide on a
different platform and simply use this book as a reference.

<!-- PDF page 28 -->
m
Introduction
One thing I didn't put on my wish list was documentation. I always thought that many sys-
tems lacked adequate documentation (and most of it was poorly written). I wanted to know
more about how the system worked internally, which I felt would help me write better appli-
cation software for it. For this reason, I have completely documented every facet of my com-
puter operating system and included it here. You may or may not want to do this for your
system, but it was a "must" for me. I think you will find that this will help you if you intend to
write your own. Well-commented code examples are worth ten times their weight in generic
theory or simple text-based pseudo-algorithms.
The "Architecture and General Theory" part of this book, along with the sections that are specific
to the MMURTL operating system, will provide the necessary information for you to use this
system as is, to redesign it to meet your requirements, or to write your own. If you don't like
the way something in my system is designed or implemented, change it to suit your needs. The
only legal restriction placed on your use of the source code is that you may not sell it or give it
away.
Who Should Use This Book
This book is for you if you are a professional programmer or a serious hobbyist, and you have
an interest in any of the following topics:
H Writing a 32-bit microcomputer operating system.
■ 80386/486/Pentium 32-bit assembly language. (An assembler is included.)
H The C programming language. (A compiler for C is included.)
■ Intel 80386/486/Pentium paged memory operation and management using the
processor paging hardware.
■ Intel 80386/486/Pentium 32-bit hardware and software task management using the
processor-hardware task-management facilities.
■ Embedded or dedicated systems using the 32-bit PC ISA architecture.
■ Real-time, message-based operating systems.
■ PC Industry Standard Architecture hardware management — including: DMA
controllers, hardware timers, priority interrupt controller units,
serial and parallel ports, and hard or floppy disk controllers.
■ File systems. (A DOS File Allocation Table-compatible file system in C is included.)
You may note that throughout this book I refer to the 386, 486, and Pentium processors as if
they were all the same. If you know a fair amount about the Intel 32-bit processors, you know
that a quantum leap was made between the 286 and 386 processors. This was the 16- to 32-bit
jump.

<!-- PDF page 29 -->
Chapter 1 ■ Overview
The 486 and Pentium series have maintained compatibility with the 386 instruction set. Even
though these newer processors have added speed, a few more instructions, and some serious
"turbo" modifications, they are effectively super-386 processors. In my opinion, from a
programmer's standpoint the next quantum leap really hasn't been made, even though the
Pentium provides 64-bit data access.
My Basic Design Goals (Yours May Vary)
A review of my ambitions when I was designing the MMURTL operating system may help
you understand what I faced, my end results, and what you may face. If you're going to write
your own system, I recommend that you avoid an overly extensive wish list. You may start with
my list and modify it, or you may start one from scratch. From my own wish list, you may be
able to get a good handle on your own list. Chapter 2, "General Discussion and Background,"
may also help by explaining in some detail how my list came to be.
Here was my wish list:
True multitasking: Not just task switching between programs, or even sharing the
processor between multiple programs loaded in memory, but real multithreading — the
ability for a single program to create several threads of execution that could communi-
cate and synchronize with each other while carrying out individual tasks. I'll discuss
some of your options, and also what I decided on, in Chapter 3, "The Tasking
Model," and Chapter 4, "Interprocess Communications."
Real-time operation: The ability to react to outside events in real time. This design
goal mandated that MMURTL be message based. The messaging system would be the
heart of the kernel. Synchronization of the messages would be the basis for effective
CPU utilization (the tasking model). Real-time operation may not even be on your list
of requirements, but I think you'll see that the push in the industry is towards real-
time systems, and for a very good reason. We, the humans, can be thought of as
"outside events." We want the system to react to us. This is also discussed in Chapters
3 and 4.
Client/server design: The ability to share services with multiple client applications,
whether on the same machine or across a network. A message-based operating system
seemed the best way to do this. I added the Request and Respond message types to the
more common Send and Wait found on most message-based systems. Message agents
could be added to any platform. If you don't have this requirement, the Request and
Respond primitives could be removed, but I don't recommend it. I discuss this in
Chapter 4.
A common, affordable hardware platform with minimal hardware requirements:
The most common 32-bit platform in the world is the PC ISA platform. How many
do you think are out there? Millions! How much does a 386SX cost these days? A
m

<!-- PDF page 30 -->
ra
Part I ■ Introduction
little more than dirt (or maybe a little less). Of course, you may have a different
platform in mind. The hardware interface may radically change, based on your choice.
I discuss this in Chapter 6, "The Hardware Interface."
Flat 32-bit virtual memory model: Those of you that program on the Intel proces-
sors know all about segmentation and the headaches it can cause (not to mention
Expanded, Extended, LIM, UMBs, HIMEM, LOMEM, YOURMOTHERSMEM,
and who knows what other memory schemes and definitions out there). Computers
using the Intel-compatible 32-bit processors are cheap, and most of them are grossly
underused. MMURTL would use the memory paging capabilities of 386/486 proces-
sors to provide an easy 32-bit flat address space for each application running on the
system. Chapter 5, "Memory Management," covers memory management but not all
of the options. Many detailed books have been written on schemes for memory
management, and I stuck with a very simple paged model that made use of the Intel
hardware. You could go overboard here and throw in the kitchen sink, if desired.
Easy programming: The easiest way to interface to an operating system from a
procedural language, such as C or Pascal, is with a procedural interface. A procedural
interface directly into the operating system (with no intermediate library) is the easiest
there is. Simple descriptive names of procedures and structures are used throughout
the system. I also wanted to maintain a small, uncluttered Applications Programming
Interface (API) specification, adding only the necessary calls to get the job done —
power without bloat, the right mix of capability and simplicity. I would shoot for less
than 200 basic public functions. I discuss some of your options in Chapter 8,
"Programming Interfaces."
Protection from other programs: But not the programmer. I wanted an OS that
would prevent another program from taking the system down, yet allow us the power
to do what we wanted, if we knew how. This could be a function of the hardware as
well as the software, and the techniques may change based on your platform and
processor of choice.
Use of the CPU instruction set as designed: Many languages and operating systems
ported between processors tend to ignore many of the finer capabilities of the target
processor. I didn't want to do this. I use the stack, calling conventions, hardware
paging, and task management that is native to the Intel 32-bit x86 series of processors.
Even with this goal in mind, I have attempted to isolate the API from the hardware as
much as possible (for future source-code portability). You may want your operating
system to run on another platform or even another processor. If it is for another
processor, many of the items that I use and discuss in this book may not apply. I take
a generic look at these issues in Chapters 6 and 8.
Simplicity: Keep the system as simple as possible, yet powerful enough to get the job
done. I also wanted to reduce the "jargon" level and minimize the number of terse,
archaic names and labels so often used in the computer industry.

<!-- PDF page 31 -->
Chapter 1 M Overview
A Brief Description of MMURTL
From the wish list, I put together the MMURTL operating system. I will describe it to you
here so that you can see what I ended up with, based on the list. This may help you with your
own list.
MMURTL (pronounced like the girl's name Myrtle) is a 32-bit, message-based, multitasking,
real-time operating system designed around the Intel 80386 and 80486 processors on the PC
Industry Standard Architecture (ISA) platforms. The name is an acronym for Message-based,
Multitasking, Real-Time kerneL. If you don't like my acronym, make up your own. But I
warn you: Acronyms are in short supply in the computer industry.
MMURTL is designed to run on most 32-bit ISA PCs in existence. Yes, this means it will even
run on an 80386SX with one megabyte of RAM (although I recommend 2MB). Then again,
it runs rather well on a Pentium with 24MB of RAM, too. If you intend to run MMURTL or
to use any of the tools I included, see the "Hardware Requirements" section later in this chap-
ter.
MMURTL is not designed to be compatible with today's popular operating systems, nor is it
intended to directly replace them. It is radically different, in that simplicity was my most im-
portant goal.
If you don't want to start from scratch or you have an embedded application for MMURTL,
the tools, the code, and the information you need are all contained here so you can make
MMURTL into exactly what you want. Sections of the source code will, no doubt, be of value
in your other programming projects as well.
ra
Uses for MMURTL
If you are not interested in writing your own operating system (it can be a serious time-sink)
and you want to use MMURTL as is (or with minor modifications), here's what MMURTL
can be used for:
■ MMURTL can be considered a general-purpose operating system, and dedicated
vertical applications are an ideal use.
■ It is an ideal learning and reference tool for programmers working in 32-bit environ-
ments on the Intel 32-bit, x86/Pentium processors, even if they aren't on ISA plat-
forms or using message-based operating systems.
■ MMURTL can be the foundation for a powerful, dedicated communications system,
or can be used as a dedicated interface between existing systems. The real-time nature
of MMURTL makes it ideal for handling large numbers of interrupts and communi-
cations tasks very efficiently.
■ Dedicated, complex equipment control is not beyond MMURTL's capabilities.

<!-- PDF page 32 -->
H
Part I ■ Introduction
Vertical applications of any kind can use MMURTL where a character-based color or
monochrome VGA text interface is suitable.
MMURTL would also be suitable for ROM-based embedded systems (with a few
minor changes). One of my goals was to keep the basic system under 192K (a maxi-
mum of 128K of code and 64K of data), excluding dynamic allocation of system
structures after loading. The OS could be moved from ROM to RAM, and the
initialization entry point could be jumped to. If you're not worried about memory
consumption, expand it to your heart's desire.
In the educational field, MMURTL can be used to teach multitasking theory. The
heavily commented source code and in-depth theory covered in this book make it
ideal for a learning/teaching tool or for general reference.
And of course, MMURTL can be the basis or a good starting point for your very own
microcomputer operating system.
Similarities to Other Operating Systems
MMURTL is not really like any other OS that I can think of. Its closest relative is CTOS
(Unisys), but that is only a distant cousin, and only so because of the similar kernel-messaging
schemes. Your creation also will take on a personality of its own, I'm sure.
The flat memory-model implementation is a little like that of OS/2 2.x (IBM), but not enough
alike that you could ever confuse the two.
Some of the public and internal calls may resemble UNIX a little (or even MS-DOS), but still,
it is not at all the same.
The file system included with MMURTL uses the MS-DOS disk structures, but only out of
convenience. This certainly wasn't because I liked the design or filename limitations. There
are some 100 million+ disks out there with MS-DOS FAT formats. This makes it easy to use
MMURTL, and more importantly, it eases the development burden. No reformatting or par-
titioning your disks — you simply run the MMURTL OS loader from DOS, and the system
boots. Rebooting back to DOS is a few keystrokes away — if you also want to run DOS, that is.
MMURTL has its own loader to boot from a disk, which eliminates the need for MS-DOS
altogether. If you don't want to write a completely new operating system, some serious fun can
be had by writing a much better file system than the one I have included.
Hardware Requirements
This section describes the hardware required to run MMURTL (as included) and to work with
the code and tools I have provided. If you intend to build on a platform other than that de-
scribed here, you can ignore this section.

<!-- PDF page 33 -->
Chapter 1
The hardware (computer motherboard and bus design) must be PC ISA (Industry Standard
Architecture) or EISA. Other 16 or 32-bit bus designs may also work, but minor changes to
specialized interface hardware may be required.
The processor must be an Intel 80386, 80486, Pentium, or one of the many clones in exist-
ence that execute the full 80386 instruction set. These include Cyrix, AMD, IBM, and other
clone processors.
VGA video text (monochrome or color) is required. MMURTL accesses the VGA text memory
in color text mode (which is the default mode set up in the boot ROM if you have a VGA
adapter).
One megabyte of RAM is required; 2MB (or more) are recommended. MMURTL will handle
up to 64MB of RAM. MMURTL itself uses about 300K after complete initialization.
The floppy disk controller must be compatible with the original IBM AT controller specifica-
tion. (Most are.) Both 5.25 inch and 3.5 inch are supported.
The hard-disk controller must be MFM or IDE (Integrated Drive Electronics). IDE is pre-
ferred. Some RLL controllers will not work properly with the current hard-disk device driver.
A standard 101-key AT keyboard and controller (or equivalent) is required.
The A20 address-gate line must be controllable, as documented in the IBM-PC AT hardware
manual, via the keyboard serial controller. Most are, but a few are not. If yours isn't, you can
change the code as needed, based on your system manufacturer's documentation (if you can
get your hands on it).
8250, 16450, or 16550 serial controllers are required for the serial ports (if used).
The parallel port should follow the standard IBM AT parallel port specification. Most I have
found exceed this specification and are fully compatible.
MMURTL does not use the BIOS code on these machines. Full 32-bit device drivers control
all hardware. Hence, BIOS types and versions do not matter.
m

<!-- PDF page 34 -->


+ 359
- 0
library/Developing Your Own 32-Bit Operating System/Chapter-02-General-Discussion-and-Background/Chapter-02-source-text.md Просмотреть файл

@@ -0,0 +1,359 @@
# Developing Your Own 32-Bit Operating System — Chapter-02: General Discussion and Background
- **Source**: /library/Developing Your Own 32-Bit Operating System/source-file.pdf
- **PDF pages**: 35–44
- **Pages without text**: none

---

<!-- PDF page 35 -->
General Discussion and
Background

<!-- PDF page 36 -->
H
Part I ■ Introduction
This section discusses the actual chore of writing an operating system, as well as what may in-
fluence your design. I wrote an operating system over a period of almost five years. Things change
a lot in the computer industry in five years, but I'm happy to find that many of the concepts I
was interested in have become a little more popular (small tight kernels, simplified memory
management, client/server designs, and so on). Reading this chapter will help you understand
why I did things a certain way; my methods may not seem obvious to the casual observer. I also
give you an idea of the steps involved in writing an operating system.
Where Does One Begin?
Where does one begin when setting out to write a computer operating system? It's not really
an easy question to answer. The history of MMURTL and an explanation of why certain de-
cisions were made, however, can be valuable to you if you intend to dig into the source code,
write your own operating system, or even just use MMURTL the way it is.
A friend of mine and I began by daydreaming over brown-bag lunches in a lab environment.
We bantered back and forth about what we would build and design into a microcomputer
operating system if we had the time and inclination to write one. This lead to a fairly concrete
set of design goals that I whittled down to a size I thought I could accomplish in two years or
so. That was 1989. As you can see, Fud's Law of Software Design Time Estimates came into
play rather heavily here. You know the adage (or a similar one): Take the time estimate, mul-
tiply by two, and add 25 percent for the unexpected. That worked out just about right. Five
years later, here it is. I hope this book will shave several years off of your development effort.
In Chapter 1, "Overview," I covered what my final design goals were. I have tried to stick with
them as closely as possible without scrimping on power or overcomplicating it to the point of
it becoming a maintenance headache. Far too often, a piece of software becomes too complex
to maintain efficiently without an army of programmers. Usually software also tends to ex-
pand to fill all available memory (and more!). You would not let this happen.
You Are Where You Were When
The heading above sounds a little funny (it was the title of a book that was popular a few years
back, and it is borrowed with great respect, I might add). It does, however, make a lot of sense.
MMURTL's design is influenced by all of my experiences with software and hardware, my
schooling, my reading of various publications and periodicals, and what I have been introduced
to by friends and coworkers. Even a course titled Structured Programming with FORTRAN
77, which I took online from the University of Minnesota in 1 982, has had an effect. I have no
doubt that your background will have a major effect on your own design goals for such a sys-
tem. Borrow from mine, add, take away, improve, and just make it better if you have the de-
sire. What I don't know fills the Library of Congress quite nicely, and "where you were when"

<!-- PDF page 37 -->
Chapter 2 ■ General Discussion and Background
will certainly give you a different perspective on what you would want in your own system.
Every little thing you've run across will, no doubt, affect its design.
My first introduction to computers was in 1976, on board a ship in the U.S. Coast Guard. I
was trained and tasked to maintain a Honeywell DDP-516 computer system. It had a "huge"
8K of hand-wound core memory and was as big as a refrigerator (including ice maker). It was
a 32-bit system, and it was very intriguing (even though punching in hundreds of octal codes
on the front panel made my fingers hurt). The term "register" has a whole new meaning when
you are looking for a bit that intermittently drops out in a piece of hardware that size. It whet
my whistle, and from there I did everything I could to get into the "computer science" end of
life. I bought many little computers (remember the Altair 8800 and the TRS-80 Model I? I
know, you might not want to). I spent 20 years in the military and took computer science courses
at schools when time allowed.
In 1981 I was introduced to a computer system made by a small company named Convergent
Technologies (since swallowed by Unisys, a.k.a. Burroughs and Sperry, merged).
That system was an Intel 8086-based system and ran a multitasking, real-time operating sys-
tem from the very start (called CTOS). Imagine an 8086-based system with an eight-inch, 10MB
hard drive costing $20,000. This was a good while before I was ever introduced to PC-DOS or
MS-DOS. In fact, the convergent hardware and the CTOS operating system reached the mar-
ket at about the same time.
After I got involved with the IBM PC, I kept looking for the rich set of kernel primitives I had
become accustomed to for interprocess communications and task management, but, as you
well know, there was no such thing. This lack was disappointing because I wanted a multitasking
OS running on a piece of hardware I could afford to own! In this early introduction to CTOS,
I also tasted "real-time" operation in a multitasking environment. The ability for a program-
mer to determine what was important while the software was running, and also the luxury of
changing tasks based on outside events in a timely manner, was a must. There was no turning
back. Nothing about my operating system is very new and exciting. The messaging is based on
theory that has been around since the early 1970s. It is just now becoming popular. People are
just now seeing the advantages of message-based operating systems. Message-based, modular
micro kernels seem to be coming of age.
The Development Platform and the Tools
My desire to develop an operating system to run on an inexpensive, popular platform, com-
bined with my experience on the Intel x86 processors, clinched my decision about where to
start: It would definitely be ISA 386-based machines (the 486 was in its infancy). The next
major decision was the software development environment. Again, access and familiarity played
a role in deciding the tools that I would use. MS-DOS was everywhere, like a bad virus. I had
two computers at that time (a 386DX and a 386SX) still running MS-DOS and not enough
ra

<!-- PDF page 38 -->
Part I 1 1 Introduction
memory or horsepower to run UNIX or even OS/2, which at the time was still 16 bits anyway.
I thought about other processors. I purchased technical documentation for Motorola's 68000
series, National Semiconductor's N80000 series, and a few others. The popularity of the Intel
processor and the PC-compatible systems was, however, a lure that could not be suppressed.
As you may be aware, one of the first tools required on any new system is the assembler. It's the
place to start. Utilities and compilers are usually the next things to be designed or ported to a
system. Not having to start from scratch was a major advantage. Having assemblers available
for the processor from the start made my job a lot easier. Of course, in the process of using
these other tools, I found some serious bugs in them when used for 32-bit development. The
32-bit capabilities were added to these assemblers to take advantage of the capabilities of the
new processors, but not many people were using them for 32-bit development five or six years
ago on the Intel-based platforms. Operating system developers, people porting UNIX systems,
and those building 32-bit DOS extenders were the bulk of the users.
I began with Microsoft's assembler (version 5.0) and later moved to 5.1. Two years later I ran
into problems with the Microsoft linker and even some of the 32-bit instructions with the as-
sembler; these problems pretty much forced the move to all-Borland tools. Then those were
the 2.x assembler and 2.x linker. I'm not a Borland salesman, but the tools just worked better.
There were actually several advantages to using MS-DOS in real mode during development.
Being in real mode and not having to worry about circumventing protection mechanisms to
try out 32-bit processor instructions actually lead to a reduced development cycle in the early
stages.
The Chicken and the Egg
The egg is first... maybe. Trying to see the exact development path was not always easy. In
fact, at times it was a quite difficult. I wanted the finished product to have its own develop-
ment environment — including assembler, compiler, and utilities — yet I needed to have them
in the MS-DOS environment to build the operating system. I wanted eventually to port them
to the finished operating system. How many of you have heard of or remember ISIS or
PLM-86? These were some of the very first environments for the early Intel processors — all
but ancient history now.
It was definitely a "chicken-and-egg" situation. You need the 32-bit OS to run the 32-bit tools,
yet you need the 32-bit tools to develop the 32-bit OS. It can be a little confusing and frustrat-
ing. Again, MS-DOS and the capability for the Intel processors to execute some 32-bit instruc-
tions in real mode (while still in 16-bit code segments) made life a lot easier. I could actually
experiment with 32-bit instructions without having to move the processor into protected mode
and without having to define 32-bit segments. Memory access was limited, but the tools worked.
I even put a 32-bit C compiler in the public domain for MS-DOS as freeware. It was limited
to the small-memory model, but it worked nonetheless.

<!-- PDF page 39 -->
Chapter 2 ■ General Discussion and Background
I really do hate reinventing the wheel; however, in the case of having the source code to an
assembler I was comfortable with, I really had no choice. Some assemblers were available in the
"public domain" but came with restrictions for use, and they didn't execute all the instructions
I needed anyway. I even called and inquired about purchasing limited source rights to some
tools from the major software vendors. Needless to say, the cost was prohibitive. (They men-
tioned dollar figures with five and six digits in them. . .. Now that's a digital nightmare.) This
lead to the development of my own 32-bit assembler. Developing this assembler was, in itself,
a major task. That set me back four months, at least; but it gave me some serious insight into
the operation of the Intel processor. The source code to this assembler (DASM) is included
with this book on the CD-ROM. The prohibitive costs also led to the development of a 32-bit
C compiler, a disassembler, and a few other minor utilities. The C compiler produces assembly
language, which is fed to the assembler. There is no linker. That's right, no linker. It's simply
not needed. To fully understand this, you will need to read Chapter 28, "DASM: A 32-Bit
Intel-Based Assembler."
Assembly language has its place in the world, but there is nothing like a high-level language to
speed development and make maintenance easier. I actually prefer Pascal and Modula over C
from a maintenance standpoint. I was into Pascal long before I was into C. I think many people
started there. Those who must maintain code or read code produced by others understand the
advantages, if they get past the "popularity concept." C still can be made almost unreadable by
the macros and some of the language constructs, even though ANSI standards have fixed much
of it. C's big strength lies in its capability to be ported easily. Most of the machine-dependent
items are left completely external to the language in libraries that you may, or may not, want or
need. I don't like the calling conventions used by C compilers — for example, parameters passed
from right to left, and the fact that the caller cleans the stack. This is obviously for variable
length parameter blocks, but it leads to some serious trouble on occasion. Nothing is harder to
troubleshoot than a stack gone haywire. MMURTL uses the Pascal conventions (referred to as
PLM calling conventions in the early Intel days). Parameters are passed left to right, and the
called function cleans the stack. I mention this now to explain my need to do some serious C
compiler work.
I started with a public-domain version of Small-C (which has been around quite a while) and
took a good look at Dave Dunfield's excellent Micro-C compiler, but I pretty much built from
scratch. Some very important pieces are still missing from the compiler, but another port is in
the works. The details of CM-32 (C-Minus 32) are discussed in Chapter 29, "CM32: A 32-Bit
C Compiler."
m
Early Hardware Investigation
I taught computer electronics technology for two years at the Computer Learning Center of
Washington, DC. I have worked for years with various forms of hardware, including most of
the digital logic families (ECL, TTL, and so on). I dealt with both the electrical and timing

<!-- PDF page 40 -->
H
Introduction
characteristics, as well as the software control of these devices. The thought of controlling all of
the hardware (timers, DMA, communications controllers, and so on) in the machine was actu-
ally a little exciting. The biggest problem was accumulating all of the technical documentation
in adequate detail to ensure it was done right. If you've ever looked though technical manuals
that are supposed to give you "all" of the information you need to work with these integrated
circuits, you know that it's usually not everything you need.
You will need to accumulate a sizable library of technical documentation for your target hard-
ware platform if you intend to write an operating system or even to port MMURTL to it.
I'm not going to tell you I did some disassembly of some BIOS code; I plead the "Fifth" on
this. Besides, IBM published theirs. BIOS code brings new meaning to the words spaghetti code.
It's not that the programmers who put it together aren't top quality; it's the limitations of size
(memory constraints) and possibly the need to protect trade secrets that create the spaghetti-
code effect. Following superfluous JMP instructions is not my idea of a pleasant afternoon.
I used many different books, documents, articles, and technical manuals throughout the early
development of my operating system. The IBM PC-AT and Personal System/2 Technical
Reference manuals were a must. They give you the overall picture of how everything in the
system ties together. Computer chip manufacturers, such as Chips & Technologies, supply
excellent little documents that give even more specific details on their particular implementa-
tion of the AT series ISA logic devices. These documents are usually free.
All of the ISA-type computer systems use VLSI (Very Large Scale Integration) integrated cir-
cuits that combine and perform most of the functions that were done with smaller, discreet
devices in the IBM AT system. I keep referring to the IBM AT system even though it was only
an 80286 (16-bit architecture). I do so because its bus, interface design, and ancillary logic
design form the basis for the 32-bit ISA internal architecture. In fact, as you may understand,
the external bus interface on the ISA machines is still 16 bits. It is tied to the internal bus, which
is 32 bits for the most part. SX machines still have 16-bit memory access limitations, but this
is transparent to the programmer, even to the operating system writer. Many bus designs now
out take care of this problem (PCI, EISA, and so on).
This made the IBM designs the natural place to start. It was proven that PC-style computer
manufacturers who deviated from this hardware design to the point of any real low-level soft-
ware incompatibility usually weren't around very long. Even machines that were technically
superior in most every respect died horrible deaths. (Remember the Tandy 2000?)
MS-DOS really didn't have to worry about the specific hardware implementation because the
BIOS software/firmware writers and designers took care of those details. As you may know,
however, the BIOS code is not 32-bit (not even on 32-bit machines). It is also not designed to
be of any real value in most true multitasking environments. A variety of more recent operat-
ing systems go directly to much of the hardware just to get things done more efficiently, any-
way (for example, OS/2 Version 2.x, Windows Version 3.x, all implementations of UNIX). I
knew from the start that this would be a necessity. "Thunking" was out of the question.

<!-- PDF page 41 -->
Chapter 2 M General Discussion and Background
Thunking is what occurs when you interface 32- and 16-bit code and data. An
example would be making a call to a 32-bit section of code from a 16-bit code segment.
This generally involves manipulating the stack or registers, depending on the calling
conventions used in the destination code, as well as storage on the stack. Some proces-
sors (such as the Intel 32-bit x86 series) force a specific element size for the stack.
Much of the early development was a series of small test programs to ensure I knew how to
control all of the hardware, such as Direct Memory Access (DMA), timers, and the Priority
Interrupt Controller Unit (PICU). This easily consumed the first six months. During this time,
I was also building the kernel and auxiliary functions that would be required to make the first
executable version of an operating system. The second six months was spent moving in and
out of protected mode and writing small programs that used 32-bit segments. If you use Intel-
based platforms, this is a year you won't have to go through, because of what you get on this
book's CD-ROM.
My initial thought on memory management centered on segmentation, but not on a fully seg-
mented model — two segments per program and two for the OS, all based on their own selec-
tors. This would have given me a zero-based addressable memory area for all code and data
access for every program. It would have appeared as a flat memory space to all the applications,
but it presented several serious complications. The largest complication was that all memory
access would be based on 48-bit (far) pointers. The thought was rather sickening, and some
initial testing showed that it noticeably slowed down program execution. When the 386/486
loads a segment register, a speed penalty is paid because of the hardware overhead (loading
shadow registers, and so on). I was after a "kinder, gentler" environment, anyway. The seg-
mented memory idea was canned, and I went straight to fully paged memory allocation for the
operating system. It has turned out very nicely, as you will see.
One other consideration on the memory model is address aliasing. If two programs in memory
must exchange data and they are based on different selectors, you must create an alias selector
to be used by one of the programs. In a paged system, aliasing is also required, but it's not as
complicated. In Chapter 5, "Memory Management," I discuss several options you have.
To get a simple model up and running took almost two years. This simple model could allo-
cate and manage memory, and it had only the most rudimentary form of messaging (Send and
Wait).
There was no loader, no way to get test programs into the system. There wasn't even a file
system! Test programs were actually built as small parts of the operating system code, and the
entire system was rebuilt for each test. This was time-consuming but necessary to prove some
of my theories and hunches. All of the early test programs were in assembler, of course. If you
start from scratch on a different processor, you will go through similar contortions. If you
H

<!-- PDF page 42 -->
H
Introduction
intend to use the Intel processors and start with MMURTL or the information presented in
this book, you'll be ahead of the game.
The Real Task at Hand (Pun Intended)
All of the hardware details aside, my original goal was to create a multitasking operating system
that was not a huge pig, but instead a lean, powerful, easy-to-use system with a tremendous
amount of documentation. I had to meet my design goals, and the primary emphasis was on
the kernel and all that that implies. If you write your own system, don't lose sight of the forest
for the trees. I touched on what I wanted in Chapter 1, but putting it on paper and getting it
to run are two very different things.
A Starting Point
If you have truly contemplated writing an operating system, you know that it can be a very
daunting task. I wish there would have been some concrete information on really where to start,
but there wasn't. The books I have purchased on operating system design gave me a vast amount
of insight into the major theories and overall concepts concerning operating system design,
but they just didn't say, "You start here, at the beginning of the yellow brick road."
In addition, it is difficult for me to advise you because I don't know exactly where you're com-
ing from, how much knowledge you have, or how much time you really have to invest. But I'll
try.
The theory behind what you want is the key to a good design. Know your desired application.
If you are writing an operating system for the learning experience (and many people do), the
actual application may not be so important, and you may find that you really have something
when you're done with it — or at least when you get it working; you'll never really be done with
it. Your target application will help you determine what is important to you. For example, if
you are interested in embedded applications for equipment control, you may want to concen-
trate on the device and hardware interface in more detail. I was really interested in communi-
cations. You'll probably be able to see that as you read further in this book.
The easy way out would be to take a working operating system and modify it to suit you. I
have provided such an operating system with this book. It is, however, by no means the only
operating system source code available. Of course, I'm not going to recommend any others to
you. If you choose to modify an existing operating system, study what you have and make sure
you completely understand it before making modifications. I say this from experience. Many
times I have taken and "gutted" a piece of software that I didn't fully understand, only to find
out that it had incorporated many of the pieces I needed — I simply hadn't understood them at
the time. If this has never happened to you, I envy you.

<!-- PDF page 43 -->
Chapter 2 ■ General Discussion and Background
The Critical Path
Along your journey, you'll find that there are major items to be accomplished, and there will
also be background noise. If you've ever done any large-scale project management, you know
that certain items in a project must work or that certain must be proved in order for you to
continue along the projected path, also called the critical path. I'll give you a list of these items
in the approximate order that I found were required while I wrote my system.
1 . Decide on your basic models. This includes the tasking model, memory model, and
basic programming interface. You may need to choose a hardware platform before
making your memory model concrete. Chapter 3, "The Tasking Model," and Chapter
4, "Interprocess Communications," will give you plenty of food-for-thought on the
tasking model.
2. Select your hardware platform. It may be RISC, it may be CISC, but whatever you
decide on, get to know it very well. Try to be sure that it will still be in production
when you think you'll be done with your system. This may sound humorous, but I'm
very serious. Things change rapidly in the computer industry.
3. Investigate your tools. Get to know your assemblers, compilers, and utilities. Even
though you may think the programmers who wrote those tools know all there is to
know about them, you will no doubt find bugs. I blamed myself several times for
problems I thought were mine but were not.
4. Play with the hardware to be sure you understand how to control it. This includes
understanding some of the more esoteric processor commands. Operating systems use
processor instructions and hardware commands simply never used by applications and
even device drivers. Document the problems, the discoveries — anything you think
you'll need to remember. Chapter 6, "The Hardware Interface," gives you some hints
on this. With so many platforms out there, hints are about all I can give you, unless
you go with the same hardware platform I did.
5. Go for the kernel. If you have to build a prototype running under another operating
system, do it. Doing so will be worth the time spent. You can then "port" the proto-
type.
6. Memory management is next. This can be tricky because you may be playing equally
with hardware and software. The level of complexity, of course, will depend on the
memory model you choose. In Chapter 5, I give you some options. I tell you the
easiest option, and I also dive into some more challenging possibilities.
7. If you've gotten past the fun parts (kernel and memory), the rest is real work. You can
take these pieces — everything from device drivers, garbage collection, and overall
program management — as you see fit.
H

<!-- PDF page 44 -->
H
Part I 1 1 Introduction
Working Attitude
Set your goals, but don't set them too high. I had my wish list well in advance. Creating a
realistic wish list may be one of the harder jobs. Don't wish for a Porsche if what you need is a
good, dependable pickup truck. The Porsche is nice... but think of the maintenance (and can
you haul firewood in it?).
Make sure your tools work. Make certain you understand their capabilities and limitations. I
spent a good six months playing with assemblers, compilers, and utilities to be sure I was "armed
and dangerous." After I was satisfied, I moved on from there. As I mentioned previously, I
actually had to switch assemblers in midstream. Problems with programming tools were some-
thing I couldn't see in advance. The tools you should have include documentation for the
hardware and plenty of "theory food" for the engine (your brain).
Work with small, easily digestible chunks. Even if you have to write four or five small test pro-
grams to be sure that each added piece functions correctly, it's worth the time. Nothing is worse
than finding out that the foundation of your pyramid was built on marshmallows. When writ-
ing my memory- allocation routines, I must have written 30 small programs (many of which I
did not intend to act correctly) to be sure I had what I thought I had. I'll admit that I had to do
some major backtracking a time or two.
Document as you go. I lay out my notes for what I want, I write the documentation, and then
I write the code. Sounds backward? It is. I wrote major portions of my programming docu-
mentation before the system was even usable, but doing so gave me great insight into what a
programmer would see from the outside.
Good luck. Really, it won't be luck. It will be blood, sweat, and maybe some tears, all the way.

+ 571
- 0
library/Developing Your Own 32-Bit Operating System/Chapter-03-The-Tasking-Model/Chapter-03-source-text.md Просмотреть файл

@@ -0,0 +1,571 @@
# Developing Your Own 32-Bit Operating System — Chapter-03: The Tasking Model
- **Source**: /library/Developing Your Own 32-Bit Operating System/source-file.pdf
- **PDF pages**: 45–62
- **Pages without text**: 45, 46, 47
- **Note**: Includes the Part II divider plate and its blank verso before the chapter.

---

<!-- PDF page 45 -->


<!-- PDF page 46 -->


<!-- PDF page 47 -->
The Tasking Model

<!-- PDF page 48 -->
26 I Part II ■ Architecture and General Theory
This chapter discusses the tasking model and the things that have an effect on it. Most of the
discussion centers on resource management and, more specifically, CPU utilization. After all,
resource management is supposed to be what a computer operating system provides you.
Terms We Should Agree On
One thing the computer industry lacks is a common language, and I'm not talking about pro-
gramming languages. One computer term may mean six different things to six different people.
Much of the problem is caused by hype and deceptive advertising, but some of it also can be
attributed to laziness. (Yes, I admit some guilt here too.) People use terms in everyday conver-
sation without really knowing what they mean or at least without coming to agreement on
them. Before I begin a general discussion or analyze your options for a tasking model, I have to
define some terms just to make sure we're on common ground. You may not agree with the
definitions, but I hope you will accept them while you're reading this book. The terms you
already should be familiar with are as follows:
■ Task
■ Kernel
■ Preemptive multitasking
Tasks
A task'is an independently scheduled thread of execution. You can transform the same 50 pro-
cessor instructions in memory into 20 independent tasks. When a task is suspended from ex-
ecution, its hardware and software states are saved while the next task's state is restored and
then executed. A single computer program can have one or more tasks. Some operating sys-
tems call a task a "process," whereas others call it a "thread." In most instances, I call it a task.
Some documentation, however, may use other words that mean the same thing, such as
"interprocess communications," which means communications between tasks.
Generally, I have seen the term thread applied to additional tasks all belonging to one pro-
gram. But this is not cast in stone either. It really depends on the documentation with the
operating system you are using. If you write your own, be specific about it. Tell the program-
mers what you mean.
For task, I use the same definition as in the Intel 80386 System Software Writer's Guide and the
80386 and 80486 Programmer's Reference manuals. This is convenient if you want to refer to
these documents while reading this book. I literally destroyed two copies of each or these books
while writing MMURTL (pages falling out, coffee and soda spilled on them, more highlighter
and chicken scratching than you can imagine). The Intel-based hardware is by far the most
available and least expensive, so I imagine most of you will be working with it.

<!-- PDF page 49 -->
Chapter 3 ■ The Tasking Model
The Kernel
The term kernelis not disputed very much, but it is used a lot in the computer industry, so it
is important to understand. Apparently a lot of people use the term and they don't even know
what it is. The kernel of an operating system is the code that is directly responsible for the task-
ing model. In other words, it is responsible for the way the CPU's time is allocated. MMURTL
has a very small amount of code that is responsible for the tasking model. In its executable
form, it probably isn't much more than two or three kilobytes of code. This makes it a very
small kernel. This might incline me to use the techno-buzz-word "microkernel" if I were into
buzzwords. The operating system functions that directly affect CPU tasking are called kernel
primitives. This is not because they're from the days of Neanderthal computing, but because
they are the lowest-level entry points into the kernel code of the operating system.
Preemptive Multitasking
Preemptive multitasking is a hotly disputed term (or phrase) in the industry. It shouldn't be,
but it is. In computing, the word preempt simply means to interrupt something that is cur-
rently running, but doesn't expect the interrupt. There are many ways to do this in a computer
operating system. One type of preemption that occurs all the time is hardware interrupts. I'm
not including them in this discussion because they generally return control of the processor
back to the same interrupted task, and non-multitasking systems use them to steal time from
programs and simulate true multitasking (for example, MS-DOS TSRs — terminate-and-stay-
resident programs). The preemptive multitasking I'm talking about and that most people refer
to is the capability for an operating system to equitably share CPU time between several well-
defined tasks currently scheduled to run on the system (even tasks that may be a little greedy
with the CPU).
My definition of preemptive multitasking is as follows: If an operating system has the capabil-
ity to stop a task while running before that task was ready to give up the processor, to save its
state, and then start another task running, it's preemptive. How and when it does this is driven
by its tasking model. An operating system that you use may be preemptive, but its tasking model
may not be the correct one for the job you want to accomplish (or you may not be using it
correctly) — hence, all the grumbling and heated discussions I see on the online services.
When you determine exactly what you want for a tasking model, you will see the rest of your
operating system fit around this decision.
n
Resource Management
An operating system manages resources for applications and services. This is its only real pur-
pose in life. Consider your resources when planning your operating system. The resources in-
clude CPU time sharing (tasking model), memory management, and hardware (timers, DMA,
interrupt controller units, and so on). These are the real resources. Everything else uses these

<!-- PDF page 50 -->
:s
Part II ■ Architecture and General Theory
resources to accomplish their missions. Input/output (I/O) devices, including video, keyboard,
disk, and communications equipment, make use of these basic resources to do their jobs. These
resources must be managed properly for all these things to work effectively in a multitasking
environment.
One of my major goals in designing the MMURTL operating system was simplicity. To para-
phrase a famous scientist (yes, good old Albert), "Everything should be as simple as possible,
but not simpler." (Otherwise it wouldn't work!) I have attempted to live up to that motto while
building my system as a resource manager. One of my friends suggested the motto "Simple
software for simple minds," but, needless to say, it didn't sit very well with me. Managing these
resources in a simple yet effective fashion is paramount to building a system you can maintain
and work with.
Task Management
Tasks begin. Tasks end. Tasks crash. All of these things must be taken into consideration when
designing a task-management scheme. Obviously, keeping track of tasks is no simple "task."
In some operating systems, each task may be considered a completely independent entity. It
may have memory assigned to it, and it may even run after the task that created it is long gone.
On the other hand, it may be completely dependent on its parent task or program. How tasks
are handled and how resources are assigned to these tasks was easy for me to decide, but it may
not be so easy for you.
If you decide that a single task may exist on its own two feet, then you must make sure that
your task-management structures and any interprocess communications mechanisms take this
into account. When you get to Part IV of this book ("The Operating System Source Code"),
take a good look at the TSS (Task State Segment). This is where all of the resources for my
tasks are managed. You may need to expand on this structure quite a bit if you decide that each
and every task is an entity unto itself.
Single User Versus Multiuser
One of the determining factors on how your tasks are managed may be whether or not you
have a true multiuser system. The operating system included with this book is not multiuser. It
is not a UNIX clone by any stretch of the imagination. Nor did I want it to be. In a system that
is designed for multiple terminals/users, tasks may be created to monitor serial ports or even
network connections for logons, execute programs as new users' desire, and then still be there
after all the child processes are long gone (for the next user).
Probably the best way to get this point across is to describe what I decided, and why. From this
information you will be able to judge what's right for you.
I have written many commercial applications that span a wide variety of applications. Some
are communications programs (fax software, comms programs), some deal with equipment
control (interaction with a variety of external hardware), and some are user-oriented software.

<!-- PDF page 51 -->
Chapter 3 H The Tasking Model
In each case, when I look back at the various jobs this software had to handle, each of the func-
tions dealt with a specific well-defined mission. In other words, each program had a set of things
that it had to accomplish to be successful (satisfy the user). When I wrote these programs, the
ability to spawn more tasks to handle requirements for simultaneous operations came in very
handy. But in almost all of these systems, when the mission was accomplished, or the user was
done, there seemed to be no purpose in having additional tasks that continued to run. The
largest factor was that I had a CPU in front of me, and each of the places these programs ex-
ecuted also had a CPU — in other words, single user, multitasking.
My operating system is designed for microcomputers rather than a mini or mainframe envi-
ronment (although this line is getting really blurry with the unbelievable power found in mi-
cro CPUs these days). I deal with one user at a time. You may want to deal with many. Your
decision in this matter will have a large effect on how you manage tasks or whole programs that
are comprised of one or more tasks.
The Hardware State
When a task switch occurs, the task that is running must have its hardware context saved. The
hardware context in its simplest form consists of the values in registers and processor flags at
the time the task stopped executing. In the case of hardware interrupts, this is usually a no-
brainer. The CPU takes care of it for you. If you must do it yourself, there are instructions on
some processors to save all of the necessary registers onto the stack. Then all you have to do is
switch stacks. When you restore a task, you switch to its stack and pop the registers off.
The hardware state may also consist of memory context if you are using some form of hard-
ware paging or memory management. This will depend on the processor you are using and
possibly on external paging circuitry. You will have to determine this from the technical manuals
you accumulate for your system.
The Software State
The software state can be a little more complicated than the hardware state and depends on
how you implement your task management. The software state can consist of almost nothing,
or it may be dozens of things that have to be saved and restored with each task switch.
An example of an item that would be part of the software state is a simple variable in your
operating system that you can check to see which task is running. When you change tasks, you
change this variable to tell you which task is currently executing. The number of things you
have to save may also depend on whether or not the processor assists you in saving and restor-
ing the software state. Some processors (such as the Intel series) let you define software-state
items in a memory structure that is partially managed by the operating system. This is where
the hardware state is saved.
It's actually easier than it sounds. You can define a structure to maintain the items you need for
each task, then change a pointer to the current state structure for the task that is executing.
H

<!-- PDF page 52 -->
30
Part II ■ Architecture and General Theory
CPU Time
CPU time is the single most important resource in a computer system. "It's 1 1:00 p.m. Do
you know where your CPU is?" A funny question, but an important one. What instructions
are executing? Where is it spending all its time? And why?
When an application programmer writes a program, he or she generally assumes that it's the
only thing running on the computer system. My first programs were written with that atti-
tude. I didn't worry about how long the operating system took to do something, unless it seemed
like it wasn't fast enough. Then I asked, "What is that thing doing?" When I used my first
multiuser system, there were many times that I thought the system had died and gone to com-
puter heaven. Yet, it would seem to return at its leisure. What was that thing doing? It was
being shared with a few dozen other people staring at their terminals just like I was. We were
each getting a piece of the pie, albeit not a very big piece. Microcomputers spoiled me. I had
my very own processor, and I could pretty much figure out what it was doing most of the time.
Many of the programs I wrote didn't require multitasking, and a single thread of instructions
suited me just fine. It was in 1980 that I wrote my first time sheer, which was built into a small
program on an 8-bit processor. It was crude, but effective. I knew exactly how long each of my
two tasks were going to run. I also knew exactly how many microseconds it took to switch
between them. There were so few factors to be concerned with on that early system, it was a
breeze.
In a multitasking operating system, ensuring that the CPU time is properly shared is the job of
kernel and scheduling code. This will be the heart and brains of the system.
Single Tasking
What if you don't want a multitasking system? This is entirely possible, and in some cases a
single-threaded system may be the best solution. In such a case, you basically have a hardware
manager. In a single-tasking operating system, management of CPU time is easy. The only
thing for the programmer to worry about is how efficient the operating system is at its other
jobs, such as file handling, interrupt servicing, and basic input/output. Many methods have
been devised to allow single-tasking operating systems to share the CPU among pseudo-tasks.
These have been in the form of special languages (Co-Pascal), and it can also be done with
mini-multitasking kernels you include in your programs. These forms of CPU time sharing
suffer from the disadvantage that they are not in complete control of all resources. They can,
however, do an adequate job in many cases.
You may also be aware that multitasking is simulated quite often in single-tasking systems by
stealing time from hardware interrupts. This is done by tacking your code onto other code that
is executed as a result of an interrupt.

<!-- PDF page 53 -->
Chapter 3 ■ The Tasking Model
Multitasking
I categorize forms of multitasking into two main groups: cooperative and. preemptive. An oper-
ating system's tasking model can be one of these, or a combination of the two. In fact, many
operating-system tasking models are a combination. There are other important factors that apply
to these two types of multitasking, which I cover later in this chapter.
The major point that you must understand here is that there are really only two ways a task
switch will occur on any system: A task that has the processor gives it up, or it is preempted
(that is, the processor is taken away from the task).
H
Cooperative Multitasking
In a solely cooperative environment, each task runs until it decides to give up the processor.
This can be for a number of reasons, but usually it is either to wait for a resource that is not
immediately available or to pause when it has nothing to do, if the system allows this (a sleep
or delay function). An important point here is that in a cooperative system, programs (or, more
precisely, the programmer) may have to be aware of things that must be done to allow a task
switch to happen. This implies that the programmer does part of the scheduling of task execu-
tion. This is in the form of the task saying to the operating system, "Hey — I'm done with the
CPU for now." From there, the operating system decides which of the tasks will run next (if
any are ready to run at all). It may not always be obvious when it happens, however. For in-
stance, a program that is running makes an operating-system call to get the time of day. The
operating system may be written in such a fashion that this (or any call into the operating-
system code) will suspend that task and execute another one.
I'm sure you've run across the need to delay the execution of your program for a short period
of time, and you have used a library or system call with a name such as sleep ( ) or delay ( ).
With these calls, you pass a parameter that indicates how long the program should pause be-
fore resuming execution. This may be expressed in milliseconds, tenths of seconds, or even full
seconds. If the operating system uses this period of time to suspend your task and execute an-
other, you are cooperating with the operating system. It is then making wise use of your re-
quest to delay or sleep. In many single-tasking systems, a call of this type may do nothing more
than loop through a series of instructions or continually check a timer function until the de-
sired time has passed. And yes, that's a waste of good CPU time if something else can be done
with it.
There is a major problem with a fully cooperative system. If a programmer writes code that
doesn't give up the CPU (which means he doesn't make an operating-system call that some-
how gets to the scheduler), he then monopolizes the CPU. If this isn't the task that the user is
involved with, the screen and everything else looks frozen. Meanwhile, there's some task in
there calculating pi to a zillion digits, and the computer looks dead for all practical purposes.

<!-- PDF page 54 -->
B
Part II ■ Architecture and General Theory
Because such "CPU hogs" are not only possible but inevitable, it is a good idea to have some
form of a preemptive system, with the ability to cut a task off at the knees, if necessary.
Preemptive Multitasking
At the opposite end of the spectrum from a fully cooperative system is a preemptive system.
This is an operating system that can stop a task dead in its tracks and start another one. I de-
fined preemptive at the beginning of this chapter. To expand on that definition, when a task is
preempted, it doesn't expect it. It does nothing to prepare for it. This can occur between any
two CPU instructions that it is executing (with some exceptions discussed later).
As I also mentioned earlier, this actually happens every time a hardware interrupt is activated.
However, when a hardware interrupt occurs on most hardware platforms, the entire "state" of
your task is not saved. Most of the time, it's only the hardware state, which includes registers,
and maybe memory context, or a few other hardware-related things. Also, after the interrupt is
finished, the CPU is returned to the task that was interrupted. Generally, hardware interrupts
occur in order to pass information to a device driver, a program, or the operating system from
an external device (hardware timer, disk drive, communications device, and so on).
One of the most important hardware interrupts and one of the most useful to an operating
system designer is a timer interrupt. On most platforms, there is hardware (programmable sup-
port circuitry) that allows you to cause a hardware interrupt either at intervals or after a set
period of time. The timer can help determine when to preempt.
Task Scheduling
Given the only two real ways a task switch can occur, you must now decide which task runs
next. To refresh your memory, these two reasons are as follows:
1 . A task that has the processor decides to give it up.
2. The currently running task is preempted, which means the processor is taken away
from the task without its knowledge.
The next section is the tricky part. When do you switch tasks, to which task do you switch,
and how long does that task get to run?
Who, When, and How Long?
The procedure or code that is responsible for determining which task is next is usually called
the scheduler.

<!-- PDF page 55 -->
Chapter 3 ■ The Tasking Model
In fully cooperative systems, the scheduler simply determines who's next. The programmer (or
the task itself) determines how long a task will run. This is usually until it willingly surrenders
the processor. In a preemptive system, the scheduler also determines how long a task gets to
run.
For the preemptive system, this adds the requirement of timing. How long does the current
task run before you suspend it and start the next one?
One thing that you can't lose sight of when considering scheduling is that computer programs
are generally performing functions for a human (the user). They are also, quite often, connected
to the outside world — communicating, controlling, or monitoring. This means there is often
some desire or necessity to have certain jobs performed in a timely fashion so there is synchro-
nization with outside events. This synchronization may be required for human interaction with
the computer which is not that critical, or it may be for something extremely critical, such as
ensuring your coffee is actually brewed when you walk out for a cup in a maniacal daze looking
like Bill the Cat.
This is where the scheduling fun really begins.
H
Scheduling Techniques
In a multitasking system, only one task is running at a time (excluding the possibility of multi-
processor systems). The rest of the tasks are waiting for their shot at the CPU, or waiting for
outside resources such as I/O.
As you can see, there is often a desire, or even a requirement, for some form of communication
between several parties involved in the scheduling process. These parties include the program-
mer, the task that's running, the scheduler, outside events, and maybe (if you're nice) the user.
I'm going to present several techniques as if they were used alone. This way you can picture
what each technique has to offer.
Time Slicing
Time slicing means you switch tasks based on set periods of execution time for each task — "a
slice of time." This implies the system is capable of preempting tasks without their permission
or knowledge.
In the simplest time-sliced systems, each task will get a fixed amount of time — all tasks will get
the same slice — and they will be executed in the order they were created. A simple queueing
system suffices. For example, each task gets 25 milliseconds. When a task's time is up, its com-
plete hardware and software state is saved, and the next task in line is restored and executed.
Many early multitasking operating systems were like this. They were perhaps not quite that
simple, but they used a time-slicing scheduler based on a hardware timer to divide CPU time
among the tasks that were ready to run.

<!-- PDF page 56 -->
Part II ■ Architecture and General Theory
In some systems, a simple time-slicing scheme may be adequate or at least acceptable. For ex-
ample, a multitasking system that is handling simple point-of-sale and ordering transactions at
Ferdinand's Fast Food and Burger Emporium may be just fine as an equitably time-sliced sys-
tem. The customer (and the greasy guy waiting on you behind the counter) would never see
the slowdown as the CPU in the back room did a simple round-robin between all the tasks for
the terminals. This would even take care of the situation where an employee falls asleep on the
terminal and orders 8,000 HuMonGo Burgers by mistake. The others may see a slowdown,
but it won't be on their terminals. (The others will be flipping 8,000 burgers.)
Simple Cooperative Queueing
The very simplest of cooperative operating-system schedulers could use a single queue system
and execute the tasks on a first-come/first-served basis. When a task indicates it is done by making
certain calls to the operating system, it is suspended and goes to the end of the queue. This is
usually not very effective, and if a user interface is involved, it can make for some pretty choppy
user interaction. But it may be just fine in some circumstances.
Prioritized Cooperative Scheduling
A logical step up from a simple first-in/first-out cooperative queue is to prioritize the tasks.
Each task can be assigned a number (from highest to lowest priority) indicating its importance.
This gives the scheduler something to work with. It can look at the priorities of those tasks that
are ready-to-run and select the best based on what the programmer thinks is important.
In a cooperative system, this means that of all the tasks that are ready to run, the one with the
highest priority will be executed. This also implies that some may not be ready at all. They can
be waiting for something such as I/O.
Variable Time Slicing
In a preemptive system, if you preempted a task without it first telling you it was OK to do so,
you must assume that it is ready to run immediately. This also means you can't just assign simple
priorities in a time-sliced system; otherwise, only the very highest would ever get to run.
In a time-sliced preemptive system, it makes more sense to increase the time allocation for more
important tasks than to execute the tasks in a sequence of equal time segments based solely on
a priority assignment.
The tasks that need more time can get it by asking for it or by the programmer (or user) telling
the scheduler it needs it. Time allocation can even be increased by some form of signaling based
on other outside events.
Time slicing is a an old technique. When combined with an adequate form of interprocess
communications or signaling (as discussed in Chapter 4, "Interprocess Communications"), it
can serve admirably. But it has some major drawbacks. Who determines which task gets more

<!-- PDF page 57 -->
Chapter 3 ■ The Tasking Model
time or which task will run more often? You generally can't ask the user if they need more
time. (They will always say yes!) And the programmer can't even determine in advance how
much time he or she will need, or how much time there is to go around en the system. Because
it is still based on a preset period of time, there is always the possibility of being preempted
before a certain important function is accomplished.
It's pretty simple to calculate how much time you'll need for a communications program run-
ning at full capacity at a known data rate. To know in advance if it will actually be used at that
rate is, however, another story entirely.
E
Other Scheduling Factors
You know as well as I do that things are rarely as simple as they seem. Users may not know how
their computers operate, but they know what they want, including dependable, clean commu-
nications and an interface that is responsive to their needs. To achieve maximum functional-
ity, the machine must perform countless tasks about which the user may have no concept or
interest.
In case you haven't noticed, computers are everywhere. They do so many things for you that
you simply accept them in your everyday life. Many of these things depend on real-time inter-
action between the machine and the world to which it is connected.
Some of the best examples of real-time computing are in robotics and equipment control. A
computer in charge of climate control for a large building may be a good example, but it's not
dramatic enough. Instead, let's look at one that controls a nuclear reactor and the various events
at a nuclear power plant. The programs that run on this computer may have some snap deci-
sions to make based on information from outside sensors or the user. If this computer has to
wait 200 or more milliseconds before performing a critical safety task, it may spell disaster.
Therefore, outside events must be able to force (or at least speed up) a task switch. This implies
that some form of communication is required between the sensors (for example, hardware-
interrupt service routines) and the scheduler. The programmer on this type of system must
also be able to indicate to the operating system that certain programs or tasks are more impor-
tant than others. (Should turning on the coffee pot for the midnight shift take priority over
emergency cooling-water control?)
Not many of us will ever write the software for nuclear power-plant control, but there are ev-
eryday tasks that require real-time computing to operate smoothly. One of the most volatile
reactions I can think of is when a user loses data from a communications program or an impor-
tant fax is garbled because buffers went a little bit too long before being emptied or filled. If
there's big money involved, this reaction can approximate meltdown conditions. This is criti-
cal real-time stuff. Thus, when something that is out of the immediate control of the program-
mer (and the CPU) must be handled within a specific period of time, it requires the ability to
juggle tasks on the system in real-time.

<!-- PDF page 58 -->
H
Part II ■ Architecture and General Theory
In the case of the nuclear-reactor operating system, you definitely want to be able to preempt
a task, and prioritization of these tasks will be critical. If I thought my operating system would
be used in this fashion, I would have spent more time on the tasking model and also on inter-
rupt service routines. But I opted to "get real" instead.
If you have ever had the pleasure of writing communications-interrupt service routines on a
system that has a purely time-sliced model, you know that it can lead to serious stress and maybe
even hair loss.
Operating systems that share the CPU among tasks by just dividing up a period of time and
giving each task a share are the kind that respond poorly to outside events. This is not to say
that time-sliced systems have poor interrupt latency, but they have problems with unbalanced
jobs being performed by the tasks. For instance, take two communications programs that handle
two identical ports with two independent Interrupt Service Routines (ISRs). If the programs
have an equal time slice and equal buffer sizes, but one channel is handling five times the data,
the program servicing the busier port may lose data if it can't get back to the buffer in time to
empty it. The buffer will overflow. This is a simple example (actually a common problem), but
it makes the point. In a message-based system, the ISR can send a message to the program ser-
vicing it to tell it that the buffer is almost full. ("Hey, come do something with this data before
I lose it!")
I think you can see the advantages of a priority-based system, and maybe you can even see where
it would be advantageous to combine a cooperative system (where the programmer can volun-
tarily give up the CPU) with a preemptive time-sliced system.
Mixed Scheduling Techniques
As you can see, you have many options for scheduling techniques, as follows:
■ Cooperative task scheduling
■ Preemptive time slicing
■ Cooperative, prioritized task scheduling
■ Variable/prioritized time slicing
■ Any and all combinations of the above
There are as many ways to mix the two basic types of task scheduling as there are copies of this
book out there. No doubt, you have a few ideas of your own. I invite you to experiment with
your own combinations. See what works for your applications. That's the only way you'll re-
ally know for sure.
In the next sections I'll present a few ways to mix them. All of these ways require some form of
signaling or interprocess communications, and I discuss all of that in the next chapter. I didn't
want to cloud the tasking issue with the required communications methods, even though these

<!-- PDF page 59 -->
Chapter 3 ■ The Tasking Model
methods are very important and even help define the tasking model. In addition, all of the
examples in the next sections have preemptive capabilities. To be honest, I can't picture a sys-
tem without these capabilities, although such systems exist.
Fully Time Sliced, Some Cooperation
In this type of model, all tasks will be scheduled for execution and will execute for a predeter-
mined amount of time. A task may, however, give up the CPU if it's no longer needed. This
would put the task back into the queue to be executed when it surrendered the CPU.
In this type of system, all tasks will get to run. You may opt to give certain tasks a larger execu-
tion time based on external factors or the programmer's desires. But even when the task has
nothing to do, it would still be scheduled to execute. If such were the case (having nothing to
do) the task would simply surrender the CPU again.
In a system that was designed for multiuser application sharing (such as UNIX originally was),
time-slicing all the tasks is suitable (even desirable) in order to accomplish what was initially
intended by the system builders. Users on a larger, solely time-sliced system will see the system
slow down as the time is equitably divided between an increasing number of users or tasks.
Writing communications tasks for these types of systems has always been a challenge (and that's
being polite about it).
Time Sliced, More Cooperative
Building on the previous mix (some cooperation), additional signaling or messaging could be
incorporated in the system to allow a task to indicate that it requires more time to execute.
This could be in the form of a system call to indicate to the scheduler that this task should get
a larger percentage of the available CPU time.
You can probably picture in your head how this could get complicated as several tasks begin to
tell the scheduler they need a larger chunk of time. There would have to be additional mecha-
nisms to prevent poorly written tasks from simply gobbling up the CPU. This could be in the
form of a maximum percentage of CPU time allowed, based on the number of tasks sched-
uled. A very simple, rapid calculation would hold the reins on a CPU hog.
Primarily Cooperative, Limited Time Slicing
In this type of system, tasks are prioritized and generally run until they surrender the CPU.
This means that the programmer sets the priority of the task and the scheduler abides by those
wishes. Keep in mind that on a prioritized cooperative system, the task that is executing is al-
ways the one with the highest priority found on the queue.
Only in circumstances where multiple tasks of the same priority are queued to run would time
slicing be invoked. These tasks would always be the highest priority tasks that are queued on
the system.
H

<!-- PDF page 60 -->
38
Part II ■ Architecture and General Theory
This type of system would also have the capability to preempt a task in order to execute a higher
priority task if one became queued (messaging from an Interrupt Service Routine).
Primarily Cooperative, More Time Slicing
This is similar to the previous model except you can slice a full range of priorities instead of
just the highest priority that is running. For instance, if you have 256 priorities, 0 through 255
(the highest) may be sliced together, giving the lowest numbers (highest priorities) the greatest
time slice.
The Tasking Model I Chose
I'll tell you what I wanted, then you can make your own choice based on my experiences. As I
once read on a sugar packet, "Experience is a wonderful thing. It allows you to recognize a
mistake when you make it again." (No, it wasn't Ferdinand's Burger Emporium.)
I wanted a priority-based, preemptive, multitasking, real-time operating system. My goal was
not just to equitably share the processor among the tasks, but to use the programmer's desires
as the primary factor. In such a system, prioritization of tasks is very important. I made
MMURTL listen to the programmer. Remember the saying: "Be careful what you ask for —
you may get it." Let me repeat that my goal was a real-time system. I never wanted to miss
outside events. To me, the outside events are even more important than placating the user, and
in reality, the user is an outside event! This put my system in the category of "primarily coop-
erative, limited time slicing."
As an illustration of my tasking model, MMURTL task switches occur for only two reasons:
1. The currently running task can't continue because it needs more information from the
outside world (outside its own data area), such as keystrokes, file access, timer services,
or whatever. In such a case, it sends a "request" or non-specific message, goes into a
"waiting" state, and the next task with the highest priority executes.
2. An outside event (an interrupt) has sent a message to a task that has an equal or higher
priority than the currently running task. This in itself does not immediately cause a
task switch.
The timer-interrupt routine, which provides a piece of the scheduling function, monitors the
current task priority, as well as checking for the highest priority task that is queued to run.
When it detects that a higher priority task is in a ready-to-run state, it will initiate a task switch
as soon as it finds this. If it detects that one or more tasks with priorities equal to the current
task are in the ready queue, it will initiate a task switch after a set period of time. This is the
only "time slicing" that MMURTL does. This has the desired effect of ensuring that the high-
est priority task is running when it needs to and also that all tasks of equal priority get a shot at
the CPU. This is the preemptive nature of MMURTL.

<!-- PDF page 61 -->
Chapter 3 U The Tasking Model
In Chapter 18, "Kernel Code," and Chapter 20, "Timer Management Code," I discuss in de-
tail how I melded these two models.
H
Interrupt Handling
Hardware interrupt service routines (ISRs) have always been interesting to me. I have learned
two very valuable lessons concerning ISRs: Keep them short, and limit the functionality if at
all possible. I bring them up in this chapter because how an operating system handles ISRs can
affect the tasking model, and they will also be used for execution timing on some preemptive
systems.
Certain critical functions in the operating-system kernel and even some functions in device
drivers will require you to suspend interrupts for brief periods of time. The less time the better.
This will usually be during the allocation of critical shared operating-system resources and cer-
tain hardware I/O functions.
With the Intel processors, you even have a choice of whether the ISR executes as an indepen-
dent task or executes in the context of a task that it interrupted. The task-based ISR is slower.
A complete processor task change must occur for the ISR to execute, and another one must
occur to return it to the task that was interrupted. Other processors may offer similar options.
Go for the fastest option. Period.
I decided that MMURTL ISRs would execute in the context of the task that they interrupted.
This is a speed issue. It's faster — actually, much faster. This could have presented some protec-
tion problems, and also the possibility of memory-management headaches, but because ISRs
all execute code at the OS level of protection and all programs on the system share this com-
mon OS-base addressing, the problems didn't surface. This was due to MMURTL's memory
management methods. I discuss memory management in Chapter 5, "Memory Management,"
and MMURTL's specific methods in Chapter 19, "Memory Management Code."
An important thing you should understand is that you don't have a big choice as to when an
ISR executes. It is determined by hardware that may be completely out of your control, or even
possibly the user's, and they are surely out of your control most of the time.
For instance, who's to say when Mr. or Ms. User is going to press a key on the keyboard? That's
right — only the user knows. Who will determine how long it actually takes to transfer several
sectors from a disk device? There are too many factors involved to even prepare for it. This
means that your operating system must be prepared at all times to be interrupted. Your only
option (when you need one) is to suspend the interrupts either through the processor (which
stops all of them except those of the non-maskable variety), or to suspend one or more through
the Priority Interrupt Controller Unit.

<!-- PDF page 62 -->
/q Part II ■ Architecture and General Theory
The real effect on the tasking model comes from the fact that some interrupts may convey
information that should cause an eventual task switch. On systems that are primarily time sliced,
the generic clock interrupt may be the basis of most task switches. After a period of time has
elapsed, you switch tasks. In a cooperative environment, it may possibly be some form of intel-
ligent message from an ISR that causes a task switch (if an ISR causes a task switch at all).
When you get to Chapter 4, you'll see how messaging ties into and even helps to define your
tasking model.

+ 510
- 0
library/Developing Your Own 32-Bit Operating System/Chapter-04-Interprocess-Communications/Chapter-04-source-text.md Просмотреть файл

@@ -0,0 +1,510 @@
# Developing Your Own 32-Bit Operating System — Chapter-04: Interprocess Communications
- **Source**: /library/Developing Your Own 32-Bit Operating System/source-file.pdf
- **PDF pages**: 63–76
- **Pages without text**: 63

---

<!-- PDF page 63 -->
Interprocess
Communications

<!-- PDF page 64 -->
H
Architecture and General Theory
This chapter introduces you to some of your options for Interprocess Communications (IPC).
As I mentioned in earlier chapters, you should have a good idea what your operating system
will be used for before you determine such important things as its tasking model and what
forms of interprocess communications it will use. As you will see in this chapter, the IPC mecha-
nisms play a very important pan in the overall makeup of the system. You will also notice that
even though I covered task scheduling in the previous chapter, I can't get away from it here
because it's tied in very closely with interprocess communications.
Messaging and Tasks
As described earlier, a task is a single thread or series of instructions that can be independently
scheduled for execution. How the operating system decides when a task is ready to execute,
where it waits, or is suspended when it can't run, and also how it is scheduled will all depend
on the messaging facilities provided by the system.
Synchronization
Synchronization of the entire system is what IPC all boils down to. On a single CPU, only one
task can execute at a time. Whether a task is ready to run will depend on information it re-
ceives from other tasks (or the system) and also on how many other tasks are ready to run
(multiple tasks competing for CPU time).
Looking at this synchronization from the application programmer's point of view, he or she
doesn't see most of the action occurring in the system, nor should he or she have to. When a
programmer makes a simple system call for a resource or I/O, the task may be suspended until
that resource is available or until another task is done with it. Some resources may be available
to more than one task at a time, while others are only available to a single task at any one point
in time.
An example of a resource that may only be available for one task at time is a system memory-
allocation routine. Of course, everyone can allocate memory in an operating system, but more
than likely it will be managed in such a way that it can be handed out to only one task or pro-
gram at a time. In other words, portions of the code will not be reentrant. I discuss reentrancy
in a later section of this chapter.
In Chapter 3, "The Tasking Model," I mentioned that a task switch can occur at just about
any time. This means that unless interrupts are suspended while one task is in the middle of a
memory-allocation routine, this task could be suspended and another task could be started. If
the memory-allocation routine was in the middle of a critical piece of code (such as updating
a linked list or table) and the second task called this same routine, it could cause some serious
problems. This is one place where the IPC mechanism comes into play and is very important.
You can't suspend interrupts for extended periods of time. I discuss this also in detail later in
this chapter.

<!-- PDF page 65 -->
Chapter 4 ■ Interprocess Communications
Semaphores
One common IPC mechanism is called a semaphore. As the name implies, semaphores are used
for signaling and exchanging information. Semaphores are code routines and associated data
structures that can be accessed by more than one task.
In operating systems that use semaphores, there are public calls that allocate semaphores, per-
form operations on the semaphore structures, and finally destroy (deallocate) the semaphore
and its associated structures when they are no longer required.
A semaphore is usually assigned a system-wide unique identifier that tasks can use to select the
semaphore when they want to perform an operation on it. However, some systems allow pri-
vate semaphores that only related tasks can access. This identifier is generally associated with
the ID of the task that created it, and more than likely it will be used to control access to one
or more functions that the owner task provides. This allows you to lock out tasks when neces-
sary to prevent simultaneous execution on non-reentrant code. The semaphores play traffic
cop. They are used also to signal the beginning or ending of an event.
Typical semaphore system calls are semget( ), which allocates a semaphore; semctl( ), which
controls certain semaphore operations; and semop(), which performs operations on a sema-
phore. The arguments (or parameters) to these calls usually include the semaphore ID (or an
indication of where to return the ID if it's being allocated), what operation you want to per-
form, and a set of flags that indicate specific desires or conditions that your task wants to ad-
here to, such as one that says "Don't suspend me if I can't get by you" or "I'll wait till I can
get it."
In most cases, the application programmer won't even know that a semaphore operation is
being performed. He or she will make a system call for some resource, and the library code or
code contained in the system will perform the semaphore operation that ensures synchronized
access to its code (if that's its purpose) or that ensures some other required condition on the
system is met before the calling task is scheduled to execute.
When a semaphore operation is performed and the calling task is suspended, it is waiting at
that semaphore for a condition to change before proceeding (being restarted to execute). As
you can see, in order to work properly, the semaphore mechanism would have to be coupled
with all of the other kernel code, such as those procedures that create new tasks, destroy tasks,
or schedule tasks for execution.
I opted not to use semaphores in my operating system. I studied how they were used in UNIX
systems and in OS/2, and it was my opinion that they added an unnecessary layer of complex-
ity. Furthermore, all of the functions that they perform are available with simpler messaging
facilities.
Another reason I avoided semaphores is that they are usually associated specifically with tasks
(processes). In my system, tasks are not wholly independent. They rely on a certain data con-
tained in structures that are assigned to a program or job that will consist of one or more tasks.
H

<!-- PDF page 66 -->
Part II ■ Architecture and General Theory
If you intend to implement semaphores, you should study how they are implemented in the
more popular systems that use them (such as UNIX, OS/2, and so on). One thing to note is
that portability may be an issue for you. Semaphore functions on some systems are actually
implemented as an installable device instead of as an integrated pan of the kernel code. In my
humble opinion, I think they are best implemented as tightly integrated pieces of the kernel
code. This is much more efficient.
Pipes
Another popular IPC mechanism is called a pipe. Pipes are pseudo input/output devices that
can be used to send or receive data between tasks. They are normally stream-oriented devices,
and the system calls will look very much like file-system operations.
Pipes can be implemented as public devices, which means they can be given names so that
unrelated tasks can access them. Quite often they are not public and are used to exchange data
between tasks that are part of one program.
Common system calls for pipe operations would include CreatePipe ( ) , OpenPipe ( ) , ClosePipe ( ) ,
ReadPipe( ), and WritePipe( ). The parameters to these calls would be very similar to equiva-
lent file operations. In fact, some systems allow standard I/O to be redirected through pipes.
OS/2 is one such system.
If you intend to implement pipes as part of your IPC repertoire, you should study how they are
used in other systems.
I initially considered public named pipes for MMURTL to allow data transfer between system
services and clients, but I used a second form of messaging instead. If you're not worried about
size or complexity in your system, public pipes are very handy. They also don't have to be so
tightly coupled with the kernel code.
Messages
Messaging methods can be implemented in several different ways (although not in as many
different ways as semaphores), as follows:
Send and Wait
The most rudimentary form of messaging on any system is a pair of system calls generally known
as send ( ) and wait ( ) . You will see these commands, or similar ones, on most real-time operat-
ing systems. It is even possible to implement messaging routines using semaphores and shared
memory. This would provide the needed synchronization for task management on the system.
But once again, you must keep the code-maintenance aspects in mind for whatever implemen-
tation you choose.

<!-- PDF page 67 -->
Chapter 4 ■ Interprocess Communications
The best method to implement messaging (and I don't think I'll get an argument here) is to
make it an integral part of the kernel code in your system. Indeed, the entire kernel should be
built around whatever messaging scheme you decide on.
With the send( ) and wait ( ) types of messaging, the only other resource required is somewhere
to send a message and a place to wait for one (usually the same place).
The best way I can illustrate messaging is to provide you the details of my own messaging
implementation. This will afford an overview of messaging for the novice as well as the details
for the more-experienced systems programmer.
In the system I've included for you (MMURTL), a program is made up of one or more tasks.
I refer to a program as a job. The initial task in a program may create additional tasks as needed.
These new tasks inherit certain attributes from the initial task. I am describing this because
you may decide to implement tasks as wholly independent entities.
In a message-based system such as MMURTL, tasks exchange information and synchron-
ize their execution by sending messages and waiting for them. On many
message-based systems for smaller embedded applications, only the basic form of messaging is
implemented. I have added a second, more-powerful form of messaging, which adds two more
kernel message functions: request ( ) and respond ( ) . This now gives MMURTL two forms of
messaging, one which is a "request" for services, which should receive a response, and a
non-specific "message" that doesn't expect a response. The request/respond concept is what I
consider the key to the client-server architecture that was one of my original goals. Other
operating systems use this type of messaging scheme also.
Sending and receiving messages in any message-based system is not unlike messaging of any
kind (even phone messages). You can send one-way messages to a person, such as, "Tell Mr.
Zork to forget it. His offer is the pits." This is an example of a one-way message, for which you
expect no response. One key element in a message system is where you leave your messages. In
Bob Zork's case, he will get the message from his secretary. In an operating system's case, it is
generally an allocated resource that can be called a mailbox, a bin, or just about any other name
you like. It is a place where you send messages or wait for them to exchange information. In
MMURTL's case, I called it an exchange. In order to send or receive a message, you must have
an exchange. In your system, you will provide a function to allocate a mailbox, an exchange
point, or whatever you call it. MMURTL provides a call to allocate exchanges for jobs. It is
called AllocExch( ) and is defined as follows in C:
unsigned long AllocExch(long *pdExchRet);
The parameter pdExchRet points to a dword (32-bit unsigned variable) where the exchange num-
ber is returned. The function return value is the kernel error if there is one (such as when there
are no more exchanges). It returns 0 if all goes well.
You also need a way to send the message. Operating-system calls to send a message come in
several varieties. The most common is the send ( ) function I described earlier. I implemented
H

<!-- PDF page 68 -->
//- I Part II ■ Architecture and General Theory
this as SendMsg ( ) . It does just what it says — it sends a message. You tell it what exchange, give
it the message, and away it goes. The task that you want to receive the message may be "wait-
ing" at the exchange by calling the wait ( ) function I described earlier. In MMURTL I call this
WaitMsg ( ) . If there is no task waiting at that exchange, the message will wait there until a task
waits at the exchange or checks the exchange with CheckMsg(). This is an added messaging
function to allow you to check an exchange for a message without actually waiting. The C
definitions for these calls and their parameters as I have implemented them are as
follows:
unsigned long SendMsg(long dExch,
long dMsgPartl ,
long dMsgPart2) ;
The dExch is the destination exchange.
The dMsgPartl and dMsgPart2 are the two dwords in the message. Note that my messages are
two 32-bit values — not too large to move around, but large enough for a couple of pointers.
unsigned long WaitMsg(long dExch,
char *pMsgRet) ;
unsigned long CheckMsg(long dExch,
char *pMsgRet) ;
The dExch is the exchange where you will wait to check for a message.
The pMsgRet points to an eight-byte (two dwords) structure where the message will be placed.
Did you notice (from the preceding definitions) that not only messages wait at exchanges, but
tasks can wait there, too? This is an extremely important concept. Consider the example of the
telephone again. The task is the human, the answering machine is the exchange. You can leave
a message on the machine (at the exchange) if no one (no task) is waiting there. If a human is
there waiting (a task is at the exchange, waiting), the message is received right away.
Now consider this: In a single processor system that is executing a multitasking operating sys-
tem, only one task is actually executing instructions. All of the other tasks are waiting some-
where.
There are only two places where a task can wait in MMURTL: at an exchange or on the Ready
queue. The Ready queue is the line-up of tasks that are in a ready-to-run state but are not run-
ning because there's a higher-priority task currently executing.
Here's one more quick topic to round off what I've already covered. Tasks are started with the
kernel primitives SpawnTask or NewTask. You point to a piece of code, provide some other pieces
of information, and voila, a task is born. Yes, it's a little more complicated than that, but you
have enough information to understand the basic theory.
Now I have defined some very important terms and functions — not in detail yet, but enough
that I can talk about them. SendMsg ( ) , CheckMsg ( ) , WaitMsg ( ) , SpawnTask ( ) , and NewTask ( ) are
five very important kernel primitives. AllocExch ( ) is an important auxiliary function. The only

<!-- PDF page 69 -->
Chapter 4 ■ Interprocess Communications H
reason this is discussed with the rest of the kernel primitives is because of its importance. I don't
consider it part of the kernel, because it has no effect on the tasking model (CPU time alloca-
tion). You also know now about exchanges and the Ready queue.
I apologize that none of the items I've introduced here have names that are confusing or based
on buzzwords. I'll try to liven it up later on.
You now have enough information under your belt to provide a scenario of message passing
that will help enlighten you to what actually occurs in a message-based system. Table 4. 1 shows
actions and reactions between a program and the operating system.
You start with the execution of a single task. What it's doing isn't important. Imagine that as
you move down the table in the example, time is passing. In this example, whenever you call a
kernel primitive, you enter The Kernel Zone. (Just kidding. It's not called The Kernel Zone —
just the "kernel.")
Table 4.1. Task and kernel interaction.
Task Action
Kernel Action
Taskl is running.
Taskl allocates Exchl.
Taskl calls SpawnTask (to start Task2).
Task2 is running.
Task2 allocates Exch2.
Task2 sends a message to Exchl.
Task2 is still running.
Task2 calls WaitMsg at Exch2.
Kernel checks priority of new task. Task2
is higher.
Kernel places Taskl on the Ready queue.
Kernel makes Task2 run.
Kernel checks for a task waiting at Exchl.
None are waiting.
Kernel attaches message to Exchl. Kernel
evaluates Ready queue to see what runs
next. It's still Task2.
Kernel checks for a message waiting at
Exch2. None found. Kernel places Task2
on Exch2.

<!-- PDF page 70 -->
48 |
Part II ■ Architecture and General Theory
Table 4.1. continued
Task Action
Kernel Acti
Taskl is running.
Taskl sends a message to Exch2.
Task2 is running.
Kernel evaluates Ready queue.
Taskl is ready to run.
Kernel makes Taskl run.
Kernel checks for task at Exch2 and finds
Task2 there. It gives Task2 the message.
Kernel places Taskl on Ready queue.
Kernel makes Task2 run. (It's a higher
priority.)
From the simple example in Table 4. 1 , you can see that the kernel has its job cut out for it. You
can also see that it is the messaging and the priority of the tasks that determine the sharing of
CPU time (as was described in previous chapters). From the example you can also see that when
you send a message, the kernel attaches the message to an exchange. If there is a task at the
exchange, the message is associated with that task and it is placed on the Ready queue. The
Ready queue is immediately reevaluated, and if the task that just received the message is a higher
priority than the task that sent it, the receiving task is made to run.
What would happen if both Taskl and Task2 waited at their exchanges and no one sent a
message? You guessed it — the processor suddenly finds itself with nothing to do. Actually it
really does absolutely nothing. In some systems, dummy tasks are created with extremely low
priorities, so there is always a task to run. This can simplify the scheduler. This low-priority
task can even be made to do some chores, such as garbage collection or a system-integrity check.
I didn't do this with MMURTL. I actually halt the processor with interrupts enabled. If every-
one is waiting at an exchange, they must be waiting for something. More than likely it is some-
thing from the outside world, such as a keystroke. Each time an interrupt occurs, the processor
is activated and it checks the Ready queue. If the interrupt that caused it to wake up sent a
message, there should be a task sitting on the Ready queue. If there is one, it will be made to
run. This may not work on all types of processors. You will have to dig into the technical as-
pects of the processor you are using to ensure that a scheme like this will work.
RequestQ
I have added two more messaging types in my system. If a very small embedded kernel is your
goal, you will more than likely not use these types in your system. They are dedicated types.

<!-- PDF page 71 -->
Chapter 4 ■ Interprocess Communications
Request ( ) and Respond ( ) form the basis for a client-server system that provides for identifica-
tion of the destination exchange and also allows you to send and receive much more than the
eight-byte message used with the SendMsg ( ) primitive.
The Request ( ) and Respond ( ) messaging primitives are designed so you can install a program
called a system service, which provides shared processing for all applications on the system. The
processing is carried out by the service, and a response (with the results and possibly data) is
returned to the requester.
Message-based services are used to provide shared processing functions that are not time criti-
cal (where a few-hundred-microsecond delay would not make a difference), and also need to
be shared with multiple applications. They are ideal for things such as file systems, keyboard
input, printing services, queued file management, e-mail services, BBS systems, fax services,
the list could go on and on. The MMURTL FAT-file system and keyboard are both handled
with system services. In fact, you could implement named pipes (discussed earlier in this chap-
ter) as a system service.
With MMURTL, each service that is installed is given a name. The name must be unique on
the machine. When the service is first installed, it registers its name with the OS Name Regis-
try and tells the OS what exchange it will be serving. This way, the user of the system service
doesn't need to know the exchange number, only the service name. The exchange number may
be different every time it's installed, but the name will always be the same. A fully loaded sys-
tem may have 5, 10, or even 15 system services. Each service can provide up to 65,533 differ-
ent functions as identified by the service code. The functions and what they do are defined by
the service. The OS knows nothing about the service codes (except for one discussed later). It
doesn't need to, because the Request ( ) interface is identical for all services.
The interface to the Request ( ) primitive is procedural, but it has quite a few more parameters
than SendMsg ( ) . Look at it prototyped in C:
unsigned long Request (char *pSvcName,
unsigned int wSvcCode,
long dRespExch,
long *pdRqHndlRet,
long dnpSend
char *pData1SR,
long dcbDatalSR,
char *pData2SR,
long dcbData2SR,
long dDataO,
long dDatal ,
long dData2 ) ;
At first glance, you'll see that the call for making a request is a little bit more complicated than
SendMsg ( ). But, as the plot unfolds you will see just how powerful this messaging mechanism
can be (and how simple it really is). Consider each of the parameters:
pSvcName Pointer to the service name you are requesting. The service name is eight
characters, all capitals, and space-padded on the right.
H

<!-- PDF page 72 -->
H
cq I Ptfrf 77 ■ Architecture and General Theory
wSvcCode Number of the specific function you are requesting. These are documented
independently by each service.
dRespExch Exchange that the service will respond to with the results (an exchange
you have allocated).
pdRqHndlRet Pointer where the OS will return a handle to you that is used to
identify this request when you receive the response at your exchange. This is needed
because you can make multiple requests and direct all the responses to the same
exchange. If you made more than one request, you'll need to know which one is
responding.
dnpSend The number (0, 1, or 2) of the two data pointers that are moving data from
you to the service. The service already knows this, but network transport mechanisms
do not. If pSendi was a pointer to some data the service was getting from your
memory area and pSend2 was not used or was pointing to your memory area for the
service to fill, dnpSend would be 1. If both data items were being sent to you from the
service, this would be 0.
pDatal Pointer to memory in your address space that the service must access (either
to read or write as defined by the service code). For instance, in the file-system
OpenFile( ) function, pDatal points to the file name (data being sent to the service).
This may even point to a structure or an array, depending on how much information
is needed for this particular function (service code).
dcbDatal How many bytes the pDataSend points to. Using the 0penFile( ) example,
this would be the size (length) of the filename.
pData2 This is a second pointer, exactly like pDatal described previously.
dcbData2 This is the same as dcbDatal .
dDataO, dDatal , dData2 These are three dwords that provide additional information
for the service. In many functions you will not even use pDatal or pData2 to send data
to the service, but will simply fill in a value in one or more of these three dwords.
These can never be pointers to data. This will be explained later, in the discussion of
memory management.
RespondQ
The Respond ( ) primitive is much less complicated than Request ( ). This doesn't mean the sys-
tem service has it easy. There's still a little work to do. Here is the C prototype:
Respond(long dRqHndl, long dStatRet);
The parameters are also a little less intimidating than those for Request ( ) . They are as follows:
dRqHndl The handle to the request block that the service is responding to.
dStatRet The status or error code returned to the requester.

<!-- PDF page 73 -->
Chapter 4 M Interprocess Communications
A job (your program) calls Request ( ) and asks a service to do something, then it calls WaitMsg ( )
and sits at an exchange waiting for a reply. If you remember, the message that comes in to an
exchange is an eight-byte (two dwords) message. Two questions arise:
1 . How do you know if this is a response or just a simple message sent here by another
task?
2. Where is the response data and status code?
First, the content of the eight-byte message tells you if it is a message or a response. The con-
vention is the value of the first dword in the message. If it is 80000000h or above, it is not*
response. Second, this dword should match the request handle you were provided when you
made the Request ( ) call. (Remember pRqHndlRet?) If this is the response, the second dword is
the status code or error from the service. Zero (0) usually indicates no error, although its exact
meaning is left up to the service.
Second, if the request was serviced (with no errors), the data has already been placed into your
memory space where pDatal or pData2 was pointing. This is possible because the kernel pro-
vides alias pointers to the service into your data area to read or write data. Also, if you were
sending data into the service via pDatal or pData2, the kernel has aliased this pointer for the
service as well, and the service has already read your data and used it as needed.
Not as difficult as you expected, right? But let me guess — this aliasing thing with memory ad-
dresses is still a bit muddy to you. A little further into this chapter I'll cover memory manage-
ment as it applies to messaging, which should clear things up considerably.
Link Blocks
I keep referring to how a message or a task just "sits" or "waits" at an exchange. An exchange is
a small structure in memory. You will need some way (a method) to attach messages to your
mailbox. It will no doubt be a linked list of sorts.
I opted to use something called a link block (LB). A link block is a little structure (smaller than
an exchange) that becomes a link in a linked list of items that are connected to an exchange.
Link blocks are not very big, but still very important. (You will find out how important they
are if you run out of them!) There is one link block in use for every outstanding (unanswered)
request and one for every message waiting at an exchange. This can add up to hundreds in a
real hurry.
m
Reentrancy Issues
A multitasking system that lets you simply point to a set of instructions and execute them means
that you can actually be executing the same code at almost the same time. I say "almost" be-
cause really only one task is running at a time.

<!-- PDF page 74 -->
H
Part II ■ Architecture and General Theory
This can lead to serious problems if the code being executed is not designed to be reentrant.
Reentrant means that if a task is executing a series of instructions and gets preempted, or if it
pauses in the middle of them, it's OK for another task to enter and execute the same code be-
fore the first task is done with it.
To really understand this issue, let's look at an example. I'll use a pseudo-text algorithm to
define a series of instructions that are wow-reentrant and show you what can happen. The ex-
ample is a function that allocates buffers and returns a pointer to it. It's called GetMeSomeMem( ).
In this example, the code accesses two variables to carry out its mission.
Variable nBuffersLeft
Variable pNextBuffer
GetMeSomeMem()
If (nBuffersLeft > 0)
(A place between instructions called PointX)
{ (Allocate a Buffer)
Decrement nBuffersLeft
Increment pNextBuffer
Return pNextBuffer
}
else
Return Error
Let's suppose two tasks are designed to call this function. As you know, only one task can really
be running at a time, even in a multitasking system. So, Taskl calls GetMeSomeMem( ) . But when
it reaches PointX, it is preempted. For the sake of understanding this, there was only one buffer
left. Now, Task2 is running and calls GetMeSomeMem ( ) . It successfully makes it all the way through
and gets to that last buffer. Now, Taskl gets to run again. He is restarted by the scheduler and
starts up at PointX. The code has already checked and there was one buffer left. So the code
goes in and whammo! — it's crash time. The buffer is gone; pointers are off the ends of arrays;
it's a mess.
As you can see, some method is required to prevent two tasks from being inside this code at the
same time. Semaphores are used on many systems. Each task that enters the code checks the
semaphore and will be suspended until the last task is done with it. The call to manage and
check the semaphore would actually be pan of the allocation code itself. Messages can also
provide exactly the same functionality. Allocate a mailbox or exchange and send it one mes-
sage. Each task will wait ( ) at the exchange until it has a message. When each task is done with
the allocation routine, it sends back the message so the next task can get in. It's really quite
simple.
What's usually not so simple for the operating-system writer is ensuring that you identify and
protect all of the non-reentrant code.
If the instruction sequence is short enough, you can simply disable interrupts for the critical
period of time in the code. You should always know how long this will be, and it shouldn't be

<!-- PDF page 75 -->
Chapter 4 M Interprocess Communications
too long. If you disable interrupts for too long, you will suffer interrupt latency problems. You
will lose data, as discussed forthwith.
S
Interrupt Latency
Interrupt latency is when interrupts don't get serviced fast enough, and maybe even not often
enough. I mentioned in the preceding section that you shouldn't disable interrupts for too long
a period of time. Just to give you an idea of what periods of time I'm talking about, look at a
real-world example. A nonbufifered communication UART (Universal Asynchronous Receiver
Transmitter) operating at 38,400 bits per second will interrupt every 208 microseconds. This
is 1/38,400 x 8 because they will interrupt for every byte (8 bits). The typical processor run-
ning at 25MHz executes most of its instructions in 2 or 3 system-clock periods. That would be
an average of 120 nanoseconds (1/25,000,000 X 3). In theory, this means you could execute as
many as 1 ,730 instructions in the interrupt interval.
Whoa there — that was only in theory! Now you have to do a reality check. You must take into
consideration that there are more interrupts than just that communications channel. The timer
interrupt will be firing off every so often. The communications interrupt itself will have inter-
rupts disabled for a good period of time, and there is also "the biggie": task switches.
Why are task switches considered "the biggie"? A task switch can consume as many as 30 or 40
microseconds. The actual amount of time will depend on whether you use hardware task switch-
ing or do it yourself, but you will still have interrupts disabled for a rather scary length of time.
Also, if you remember the GetMeSomeMem( ) example, you know you locked out multiple users
with the kernel messaging (or semaphores). You didn't disable interrupts. In the kernel code,
you can't use the kernel code to lock things out of the kernel. You must disable interrupts in
many sections. It will be the only way.
Good, tight code in the kernel is a must. I could probably tighten mine a whole lot, but my
testing shows I did OK. You will have to do some serious testing on your system to ensure
interrupt latency is not going to be a problem. Have you ever run a communications program
for an extended period of time at 19,200 baud in Windows 3.x? You will lose data. Don't blame
Windows, though. It's doing some things I personally consider impossible.
Memory Management Issues
The last hurdle in interprocess communications is memory. It doesn't sound like it would be
a problem, but it can be, depending on how you implement your memory model.
If you use a completely flat model for the entire system, it will simplify IPC greatly — but it will
increase the headaches elsewhere. A huge linear array of memory in your system that is shared
by all the programs and tasks means they all understand a pointer to any memory address. This

<!-- PDF page 76 -->
H
Part II ■ Architecture and General Theory
in turn means you can freely pass pointers in messages back and forth (even between different
programs) and no one gets confused.
If you implement independent linear-memory arrays through paging hardware (as I did with
MMURTL), then you have to find a way to translate (alias) addresses that are contained in
messages that will be passed between different programs.
Segmented memory models will suffer from this aliasing requirement to an even greater ex-
tent. I recommend you avoid a fully segmented model. I started with one but abandoned it
early on. Fully segmented models allow any memory allocation to be zero-based. This means
that if paging is used along with the segmentation, it will be even a greater chore to translate
addresses that are passed around on the system.
You will learn about memory-management options in Chapter 5, "Memory Management."

+ 1081
- 0
library/Developing Your Own 32-Bit Operating System/Chapter-05-Memory-Management/Chapter-05-source-text.md
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 999
- 0
library/Developing Your Own 32-Bit Operating System/Chapter-06-The-Hardware-Interface/Chapter-06-source-text.md
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 822
- 0
library/Developing Your Own 32-Bit Operating System/Chapter-07-OS-Initialization/Chapter-07-source-text.md Просмотреть файл

@@ -0,0 +1,822 @@
# Developing Your Own 32-Bit Operating System — Chapter-07: OS Initialization
- **Source**: /library/Developing Your Own 32-Bit Operating System/source-file.pdf
- **PDF pages**: 121–136
- **Pages without text**: 121

---

<!-- PDF page 121 -->
OS Initialization

<!-- PDF page 122 -->
1
100
Part II ■ Architecture and General Theory
Initializing an operating system is much more complicated than initializing a single program.
In this chapter, we start with loading the operating system, then go through each section of the
basic initialization, including hardware, memory, tasks, and important structures.
Your operating system's tasking and memory models may differ from MMURTL's, but the
basic steps to get your operating system up and running will be very similar.
Getting Booted
If you have ever done any research on how an operating system is loaded from disk, you know
there is no definitive publication you can turn to for all the answers; it's not every day that
someone wants to write an operating system, and it's not exacdy easy.
The only hardware platform I've researched for the purpose of learning how to boot an oper-
ating system is the IBM-PC ISA-compatible system. If you're using another platform, you will
have to do some research. I'm going to give you a good idea of what you need to know, which
you can apply to most systems in a generic fashion. This information gives you a very good
head start on your studies. If you're using a PC ISA-compatible platform, I've done all your
homework.
Boot ROM
When a processor is powered up, it executes from a reset state. Processor designers are very
specific about what state they leave the processor in after they accomplish their internal hard-
ware reset. You can find this information in the processor hardware manual or in the processor
programming manual from the manufacturer.
The processor's purpose is to execute instructions. The question is, what instruction does it
execute first? The instruction pointer register or instruction counter (whatever they call it for your
particular processor) tells the processor which instruction to execute first. (The initial address
that is executed is documented in the processor manual.) The operating system writer may not
really need to know the processor's initial execution address, because this address is executed
by a boot ROM. For this reason, the first executed address usually is somewhere at the high
end of physical memory, where boot ROMs usually reside.
The boot ROMis read-only memory that contains just enough code to perform a basic system
initialization and load (boot) the operating system. When I say "just enough code," that's ex-
actly what I mean. The operating system is located on a disk or other block-oriented device.
This device is, no doubt, organized within a file system.
Consider that the boot ROM code knows little or nothing about the file system. If this is true,
how does it load a file (the operating system)? It generally doesn't — at least not as a file from
the file system.

<!-- PDF page 123 -->
Chapter 7 ■ OS Initialization
The Boot Sector
The boot ROM code knows only enough to retrieve a sector or two from the disk to some
location in RAM, and then execute it. This process amounts to reading a sector into a fixed
address in memory, and then jumping to the first address of the sector. In the PC world, this
first address is known as the boot sector of the disk. It usually is the first logical sector of the
disk.
This small amount of boot sector code has to know how to get the operating system loaded
and executed. This process can be completed in more than one step if required. In other words,
the boot sector code may load another, more complicated, loader program that in turn loads
the operating system. This process is known as a multistage boot. To be honest, 512 bytes (a
standard hardware sector size on many systems) is not a lot of code.
If the operating system you are loading is located in a special place on the disk, such as a fixed
track or cylinder, then life is easy. You only need enough hardware and file-system knowledge
to read the sectors from disk to the location you want them in RAM. But it gets more compli-
cated. When the boot ROM loaded your boot sector, it may have loaded it right where you
want to put your operating system. This means that the boot sector code has to relocate itself
before it can even load the operating system. This dynamic relocation can eat up more of the
precious 512 bytes of code space in the boot sector (unless your boot ROM enables you to load
more than one sector, such as a whole track).
You should consider these factors when planning how your operating system boots:
1 . Where the boot ROM expects your boot sector or sectors to be on disk.
2. Where this boot sector is loaded and executed.
3. Whether you can leave the boot sector code where the boot ROM loaded it (dynamic
relocation may be needed if the boot sector is where your operating system will load).
4. How to find your operating system or first-stage boot program on disk from the boot
sector code after it is executed.
5. How the boot sector code will read the operating system or first-stage boot program
from disk (the hardware commands or BIOS calls needed to load it into memory, for
example).
6. Where your operating system will reside in memory, and whether you have to move it
after you load it.
7. Whether there are any additional set-up items to perform before executing the
operating system.
8. Where your operating system's first instruction will be, so that you can begin
execution.
101

<!-- PDF page 124 -->
102 [
Part II ■ Architecture and General Theory
I don't pretend to know how to boot your system if you're not using the platform I chose. But
I'm sure you can gain some insight on what you will have to do if I give a detailed description
of what it's like on an IBM PC-AT-compatible system. I will go over each of the eight items on
the list to explain how it's handled, and then I'll show you some boot sector code in grueling
detail. Explanations of each step follow:
1 . In an ISA system, the boot ROM expects your boot sector at what it considers is
logical sector number zero for the first drive it finds it can read. Some systems enable
you to change the boot order for drives, but the standard order on ISA systems is drive
A, followed by drive C. Keep in mind that the PC ISA systems have a BIOS (Basic
Input/Output System) in ROM, and they have some knowledge of the drive geometry
from their initialization routines, which they store in a small work area in RAM.
2. A single boot sector (512 bytes) is loaded to address 7C00 hex, which is in the first
64KofRAM.
3. The 7C00 hex address may be fine for your operating system if you intend to load it
into high memory. I wanted mine at the very bottom, and it would have overwritten
this memory address, so I relocated my boot sector code (I moved it and executed the
next instruction at the new address).
4. How you find your operating system on disk depends on the type of file system and
where you stored it. If you intend to load from a wide variety of disk types, your OS
may not always be in the same place if it's located after variable-sized structures on the
first portion of the disk. This was the case with my system, and also with MS-DOS
boot schemes.
5. I was lucky with the PC ISA systems, because they provide the BIOS. Each boot
sector provides a small table with detailed disk and file system parameters that enable
you to update what the BIOS knows about the disk; then you can use the BIOS calls
to load the operating system or first-stage boot program.
6. My operating system data and code must start at address 0 (zero), and runs contigu-
ously for about 160K. This was a double problem. The boot sector was in the way (at
7C00h), as were the active interrupt vector table and RAM work area for the BIOS.
This problem lead to two relocation efforts. First, I moved the boot sector code way
up in memory, and then I executed the next instruction at the new address. Second, I
read in the operating system above the actively used memory and then relocated it
when I didn't need the BIOS anymore.
7. Usually, you should do a few things before you actually execute the operating system.
Just what these things are depends on the hardware platform. In my case, I have to
turn on a hardware gate to access all the available memory, and I also go into 32-bit
protected mode (Intel-specific) before executing the first operating system instruction.
8. I know the address of the first operating system instruction because my design has
only two areas (data and code), and the code segment loads at a fixed address. If your

<!-- PDF page 125 -->
Chapter 7 ■ OS Initialization
operating system's first execution address varies each time you build it, you may need
to store this offset at a fixed data location and allow your boot sector code to read it so
that it knows where to jump.
The following code is a boot sector to load an operating system from a floppy on a PC ISA-
compatible system. It must be assembled with the Borland assembler (TASM) because the as-
sembler I have provided (DASM) doesn't handle 16-bit code or addressing. One thing you
will notice right away is that I take over memory like I own it. I stick the stack anywhere I want
in the first megabyte of RAM. Why not? I do own it. There's no operating system here now.
Examine the following code:
.386P
;This boot sector is STUFFed to the gills to do a single
; stage boot of the MMURTL OS which is about 160K stored as
;a loadable image beginning at cluster 2 on the disk. The OS must
;be stored contiguously in each of the following logical sectors.
;The actual number of sectors is stored in the data param nOSSectors.
;The .386P directive is required because I use protected
; instructions.
CSEG
SEGMENT WORD 'Code' USE16
ASSUME CS:CSEG, DS:CSEG, ES: Nothing
ORG
JMP SHORT Bootup
NOP
Padding to make it 3 bytes
;This 59-byte structure follows the 3 jump bytes above
;and is found on all MS-DOS FAT -compatible
;disks. This contains additional information about the file system
;on this disk that the operating needs to know. The boot sector code also
; needs some of this information if this is a bootable disk.
Herald DB 'MMURTLvl '
nBytesPerSect DW 0200h jnBytes/Sector
;Sect/Cluster
;Resvd sectors
; nFATs
;Root Dir entries max
;nTotal Sectors (0 = <32Mb)
;media desc. (worthless)
;nSect in FAT
;nSectors/track
;nHeads
;nHidden Sectors (first whole track on HD)
;nTotalSectors if > 32Mb
; Drive boot sector came from
;Used for temp storage of Sector to Read
Ext Boot Signature (always 29h)
(140 sectors max) Was Volume ID number
;11 bytes for volume name
;8 bytes for FAT name (Type)
nSectPerClstr
DB
01 h
nRsvdSect
DW
0001 h
nFATS
DB
02
nRootDirEnts
DW
00E0h
nTotalSectors
DW
0B40h
bMedia
DB
0F0h
nSectPerFAT
DW
0009h
nSectPerTrack
DW
0012h
nHeads
DW
0002h
nHidden
DD
000000
nTotalSect32
DD
000000
bBootDrive
DB
00h
ResvdByte
DB
00h
ExtBootSig
DB
29h
nOSSectors
DW
0140h
ResvdWord
DW
0000h
Volname
DB
'RICH
FatType
DB
'FAT12

<!-- PDF page 126 -->
H
i q / Part II ■ Architecture and General Theory
;The following are pointers to my IDT and GDT after my OS loads
; and are not part of the above boot sector data structure.
IDTptr DW 7FFh ; LIMIT 256 IDT Slots
DD 0000h ;BASE (Linear)
GDTptr DW 17FFh ; LIMIT 768 slots
DD 0800h ;BASE (Linear)
;This is where we jump from those first 3 bytes
BootUp:
;This is the Boot block's first instruction from the initial jump
; at the beginning of this code
CLI ;Clear interrupts
;Stick the stack at linear 98000h (an arbitrary location)
MOV AX, 9000 h
MOV SS, AX
MOV SP, 8000h
;Move this boot sector UP to 90000h Linear (dynamic relocation)
;Destination segment
[Source segment
; Now we "jump" UP to where we moved it. It's pseudo jump
; that I calculated after assembling it once. Push the
; return address, then "return" to it. It works.
MOV AX, 09000h ; Segment for new location
PUSH AX
MOV AX, 6Fh .-Offset to new location (Next PUSH)
PUSH AX
RETF
; Now set DS equal to ES which is 9000h
PUSH ES
POP DS
;Now we must update the BIOS drive parameter
; table so it can read the disk for us.
;This is done by finding out where it is and writing
; some of the parameters to it from the boot sector.
;The BIOS left a pointer to it at the 1E hex interrupt
; vector location.
MOV CX, nSectPerTrack ;Get this while DS is correct
XOR AX, AX
MOV DS, AX
MOV
AX,
0900I
MOV
ES,
AX
XOR
DI,
DI
MOV
AX,
7C0h
MOV
DS,
AX
XOR
SI,
SI
MOV
CX,
512
REP
MOVSB

<!-- PDF page 127 -->
Chapter 7 ■ OS Initialization
MOV BX, 0078h ;Int 1E FDC Params!
LDS SI, DS:[BX]
MOV BYTE PTR [SI +4], CL
MOV BYTE PTR [SI+9] , 0Fh
PUSH ES
POP DS
PUSH DS
STI
MOV DL, bBootDrive
XOR AX, AX
;Make DS correct again
;Save DS through the reset
;Must set interrupts for BIOS to work
;Required for Disk System Reset
INT 13h
JC SHORT BadBoot
POP DS
;Reset Disk Controller (DL has drive num)
; Reset failed. . .
;The controller is reset. Now let's read some stuff!!
;We are gonna skip checking to see if the first file
; really IS the OS. We need the space for other code.
MOV SI, OFFSET MsgLoad
CALL PutChars
What we do now is calculate our way to the third cluster
on the disk and read in the total number of OS sectors in
logical sector order. (3rd cluster is really the first allocated
cluster because the first 2 are unused)
The layout of the Disk is:
Hidden Sectors (optional)
Boot Sector (at logical sector 0)
FATS (1 or more)
Additional Reserved sectors (optional)
Root Directory (n Sectors long)
XOR AX, AX
MOV AL, nFATS
MUL WORD PTR nSectPerFAT
ADD AX, WORD PTR nHidden ;
ADC DX, WORD PTR nHidden+2
ADD AX, nRsvdSect
MOV CX, AX ;Save in CX
;CX now has a Word that contains the sector of the Root
;Calculate the size of the root directory and skip past it
;to the first allocated sectors (this is where the OS or
; stage 1 of a 2 stage loader should be).
MOV AX,0020h ;Size of Dir Entry
MUL WORD PTR nRootDirEntS
MOV BX, nBytesPerSect
DIV BX
ADD AX, CX

<!-- PDF page 128 -->
I
106
Part II ■ Architecture and General Theory
;AX is at sector for cluster 0, but cluster 0 and 1 don't exist
;so we are really at cluster 2 like we want to be.
MOV CX, nOSSectors ; Number of OS sectors to read
JMP SHORT ContinueBoot
;Bad boot goes here and displays a message then
;waits for a key to reboot (or tries to) via int 19h
BadBoot:
MOV SI, OFFSET MsgBadDisk
CALL PutChars
XOR
AX, AX
INT
16h
;Wait for keystroke
INT
19h
;Sys Reboot
PutChars
LODSB
OR
AL.AL
JZ
SHORT Done
MOV
AH, 0Eh
MOV
BX,0007
INT
10h
JMP
SHORT PutChars
Done:
RETN
ContinueBoot:
MOV BX, 06000h
MOV ES, BX
;This is segment where we load the OS
; before we move it down to 0000h
NextSector:
PUSH AX
PUSH CX
PUSH DX
PUSH ES
;This is a single sector read loop
; It's slow, but works well.
XOR BX, BX
ES:BX points to sector read address
Read a logical sector to ES:BX
AX has Logical Sector Number
MOV
DIV
INC
MOV
XOR
DIV
MOV
XCHG
MOV
XCHG
SHL
OR
MOV
MOV
INT
JC
SI, nSectPerTrack
SI
DL
ResvdByte, DL
DX, DX
WORD PTR nHeads
DH, bBootDrive
DH
AX
CH
; Divide LogicalSect by nSectPerTrack
;Sector numbering begins at 1 (not 0)
;Sector to read
[Logical Track left in AX
; Leaves Head in DL, Cyl in AX
Head to DH, Drive to DL
Cyl into CX
Low 8 bits of Cyl to CH, Hi 2 bits to CL
shifted to bits 6 and 7
BYTE PTR ResvdByte ;OR with Sector number
1 ; Number of sectors
2 ;Read
;Tell BIOS to do it!
SHORT BadBoot

<!-- PDF page 129 -->
Chapter 7 H OS Initialization H
MOV SI, OFFSET MsgDot
CALL PutChars
POP
ES
POP
DX
POP
cx
POP
AX
MOV
BX, ES
ADD
BX,
20h
MOV
ES,
BX
INC
AX
LOOP NextSector
I increment ES and leave BX 0
512 bytes for segment
BX will be zeroed at top of loop
Next Sector
;At this point we have the OS loaded in a contigous section
;from 60000h linear up to about 80000h linear.
;Now we disable interrupts, turn on the A20 line, move
;the OS down to address 0, set protected mode, and JUMP!
CLI
XOR CX.CX
IBEmm0:
IN AL,64h
TEST AL,02h
LOOPNZ IBEmm0
MOV AL,0D1h
OUT 64h,AL
XOR CX.CX
IBEmml :
IN AL,64h
TEST AL,02h
LOOPNZ IBEmml
MOV AL,0DFh
OUT 60h,AL
XOR CX.CX
IBEmm2:
IN AL,64h
TEST AL,02h
LOOPNZ IBEmm2
;A20 line should be ON Now
;So move the OS
MOV DX, 8000h
; Move 64K data chunk from linear 60000h to linear 0
MOV BX, 06000h
MOV DS, BX
XOR SI, SI
XOR AX, AX
MOV ES.AX
XOR DI.DI
MOV CX, DX
CLD
REP MOVSW
;WORD move

<!-- PDF page 130 -->
I
108
Part II
Architecture and General Theory
Move first 64K code chunk from linear 70000h to 10000h
MOV BX
07000h
MOV DS
BX
XOR SI
SI
MOV AX 1000h
MOV ES AX
XOR DI DI
MOV CX
DX
REP MOVSW
; Move
last code
MOV DS
DX
XOR SI
SI
MOV AX 2000h
MOV ES AX
XOR DI DI
MOV CX
DX
REP MOVSB
MOV BX
9000h
MOV DS
BX
;WORD move
(32K) from linear 80000h to 1
;DX is 8000h anyway
;BYTE move
XOR EDX, EDX
MOV DL, bBootDrive
;OS can find bootdrive in DL on entry
LIDT FWORD PTR IDTptr
LGDT FWORD PTR GDTptr
iLoad Processor ITD Pointer
Load Processor GDT Pointer
MOV EAX.CR0
OR AL,1
MOV CR0 , EAX
JMP
$+5
NOP
NOP
MOV BX
10h
MOV DS BX
MOV ES BX
MOV FS BX
MOV GS BX
MOV SS BX
;Control Register
;Set protected mode bit
; Clear CPU prefetch queue with JMP
Set up segment registers
;We define a far jump with 48-bit pointer manually
;by defining the bytes that would make the FAR jump
;call JMP FAR 08h:10000h. The segment and code size escape
;bytes (66h and 67h) are required to indicate we are
; referencing a 32-bit segment with a 32-bit instruction.
DB 66h
DB 67h
DB 0EAh
DD
DW 8h
MsgNone DB ' This is actually padding!
MsgBadDisk DB 0Dh , 0Ah, 'Bad Boot Disk!', 00h

<!-- PDF page 131 -->
Chapter 7 ■ OS Initialization
MsgLoad DB 0Dh, 0Ah, 'Loading MMURTL', 00h
MsgDot DB ' . ' , 00h
BootSig DW 0AA5Fh ;MS-DOS used this. I left it in.
CSEG ENDS
END
To assemble and link this into a usable boot sector, use Borland's Turbo Assembler:
TASM Bootblok.asm <Enter>
This will make an EXE file exactly 1024 bytes in length. The first 512 bytes are the EXE header.
It's worthless. The last 512 bytes are the boot sector binary code and data in a ready-to-use
format. As you change and experiment with the boot sector, you need to inspect the actual size
of the sector and location of some of the code, such as the dynamic relocation address (if you
needed to relocate). To generate a list file from the assembly (Bootblok.lst) and a map file from
the link (Bootblok.map), use TASM:
TASM Bootblok Bootblok Bootblok <Enter>
You need some additional information in order to experiment with this boot sector if you are
on the PC ISA platforms, such as how to write the boot sector to the disk or read a boot sector
from the disk and inspect it.
Most operating systems provide utilities to make a disk bootable. The systems may do this as
they format the disk, or after the fact. Some systems force you to make a decision as to whether
the disk will be bootable before you format it if they pre-allocate space on the disk for the
operating system.
I used the MS-DOS Debug utility to read and write the disk boot sector while experimenting.
It's quite easy to do with the Load, Write, and Name commands. To read the boot sector from a
disk, find out where Debug's work buffer is by using an Assemble command (just press A while
in Debug). This command shows you a segmented address. Press Enter without entering any
instructions and use the address shown for the Load command with the drive, logical sector
number, and number of sectors following it. The following code loads the boot sector from
the disk to address 219F:0000 (or whatever address you were shown):
A <Enter>
219F:0000 <Enter>
-L 21 9F: 0000 0 0 1 <Enter>
Now you can write this sector to another disk as a file with the Name, Register, and Write com-
mands:
N C:\Bootblok.bin
R BX <Enter>
BX:0 <Enter>
R CX <Enter>
CX:200 <Enter>
W 21 9F: 0000
H

<!-- PDF page 132 -->
110
Part II ■ Architecture and General Theory
The boot sector from drive 0 is now in a file named Bootblok.bin in the root directory of drive
C. If you want the boot sector from your hard disk, you must use the Load command:
-L 219FC0000 2 0 1 <Enter>
The only difference was the drive number that immediately follows the segmented address.
Debug refers to the drives numerically from 0 to n.
After you assemble your boot sector, you can use Debug to load it as an EXE file and write the
single 512 sector directly to the disk. Keep in mind that you should not do this to your active
development drive. The results can be disastrous. From the MS-DOS prompt, type Debug,
followed by the name of your new executable boot sector. Then write it to the disk. To find
where Debug loaded the boot sector, you can use the Dump command to display it:
C:\>Debug BootBlok.exe <Enter>
-D <Enter>
219F:0000 EB 3E 90 (etc.)
The following code dumps the first 128 bytes and provides the address you need to the Write
command:
-W 219F:0000 0 0 1 <Enter>
After the boot sector is on the disk, you only need to put the operating system where it expects
to find it. The boot sector code expects to find the operating system in a ready-to-run format
at the first cluster on the disk and running contiguously in logical sectors until it ends. This is
easy to do. Format a disk with MS-DOS 5.0 or higher using the /U (unconditional) option,
and without the /s (system) option. Then copy the operating system image to the disk. It will
be the first file on the disk and will occupy the correct sectors. After that, you can copy any-
thing you want to the disk. The commands follow:
C:\> Format A: /U <Enter>
C:\> Copy OS. IMG A: \ <Enter>
The new boot sector and the operating system are on the disk and ready to go. Good luck. I
only had to do it 30 or 40 times to get it right. Hopefully, I've saved you some serious hours of
experimentation. If so, I'm happy.
Operating System Boot Image
Depending on the tools you use to build your operating system, it may not be in a form you
can load directly from disk. If your operating system is in an executable format from a linker,
there will be a header and possibly fix-up instructions contained in it. You therefore must pro-
vide a utility to turn the operating system executable into a directly executable image that can
be booted and run by the small amount of code in a boot sector.
With MMURTL, I have provided a utility called Makelmg. This reads the MMURTL execut-
able and turns it into a single contiguous executable image to be placed on the disk. The data
and code are aligned where they would have been had they been loaded with a loader that

<!-- PDF page 133 -->
Chapter 7 ■ OS Initialization
understood my executable format. My Makelmg utility is included on the CD-ROM, and must
be built with the Borland C compiler. Instructions are included in the single source file
(Makelmg.c) to properly build it.
If no address fix-ups are required in your operating system executable, you might be able to
avoid a need for a utility like this. I couldn't.
Other Boot Options
If the operating system you write will be run from another environment, you may face the chore
of booting it from another operating system. This is how I run MMURTL most of the time. I
have a program (also provided on CD-ROM) that reads, understands, and loads the image
into RAM and then executes it. This program is called MMLoader. It has some additional
debugging display code that you easily can eliminate if you want.
This single source file program (MMLoader.C) also should be built with the Borland C com-
piler, because it has imbedded assembly language. This program basically turns MS-DOS into
a $79 loader. It does everything that the boot sector code does, along with reading the operat-
ing system from its native executable format (a RUN file).
$
Basic Hardware Initialization
One of the first things the operating system does after it is loaded and the boot code jumps to
the first address is to set up a simple environment. This process includes setting up the OS
stack. In a segmented processor (such as the Intel series), the boot code already may have set up
the segment registers. In a non-segmented processor (such as the 680x0 series) this wouldn't
even be necessary.
At this point, interrupts usually are disabled because there are no addresses in the interrupt
vector table. Your operating system already may have some of the addresses hard-coded in
advance, but it is not necessary. Disabling interrupts is one of the chores that my initialization
code handles.
All the hardware that is resident in the system — such as the programmable interrupt controller
unit, the direct memory access hardware, and basic timers — must be initialized before you can
set up the rest of the system (the software state). These elements of hardware simply must be
initialized to their default states. Chapter 21, "Initialization Code," contains all the code from
MMURTL that is used to set up the operating system from the very first instruction executed,
up to the point you jump the monitor program. All this code is contained in the source file
Main.asm on the CD-ROM. It should help give you a picture of what's required.
Additionally, some of the hardware-related tasks you may have to accomplish are discussed in
Chapter 6, "The Hardware Interface."
$

<!-- PDF page 134 -->
0
Part II ■ Architecture and General Theory
Static Tables and Structures
How your operating system handles interrupts and memory, and how you designed your basic
operating system API and calling conventions determine how many tables and structures you
must set up before you can do things like task and memory management.
I had to set up the basic interrupt vector table, the array of call gates (how users get to OS
calls), and some basic variables. Chapter 21 shows how I go about this. These first tables are all
static tables and do not require any memory allocation. You will have to think about which
tables are static and which are dynamically allocated, because this determines whether their
initialization occurs before or after memory-management initialization.
After the basic hardware tables are set up, you may realize that most of this code may never be
executed again. This leaves you with a few options — the first of which may be to eliminate the
code from the operating system after it is executed so that it doesn't take up memory. Another
option is just to forget the code if it doesn't take up too much space. A third option is to make
the code reusable by breaking up the code into small segments so that the code can be more
generic and used for other things. What you do simply may depend on how much time you
want to spend. I just left the code alone. It's not that big. If you want to eliminate the code,
you can position it at the end of your code section and simply deallocate the memory for it, or
not even initially allocate it with your memory-initialization code.
Initialization of Task Management
Initialization of task management, setting up for multitasking, is not trivial. The processor you're
working with may provide the task management in the form of structures that the processor
works with, but you also may be performing your own task management. Either way, you've
got your job cut out for you.
Chapter 3, "The Tasking Model," discusses some of your options for a tasking model. In all
the cases discussed, you must understand that the code you are executing on startup can be
considered your first task. Its job is to set up the hardware and to get things ready to create
additional tasks. If you prefer, you can let this first thread of execution just die after you set up
a real first task. The option usually is up to you, but also may depend on hardware task
management if you use it. With Intel processors, for example, you need to set up a task state
segment (TSS) to switch from, as well as one to switch to. Because I used the Intel task-
management hardware, I really had no choice. Being a little conservative, I didn't want to waste
a perfectly good TSS by just ignoring it. I could have gone back and added code to reuse it, but
why not just let it become a real task that can be switched back to?
Chapter 21 shows what I have to do using the Intel hardware task management. Additional
task-management accomplishments may include setting up linked lists for the execution queue
(scheduler) and setting variables to keep track of statistical information.

<!-- PDF page 135 -->
Chapter 7 ■ OS Initialization s
Initialization of Memory
During the basic hardware initialization, you may be required to send a few commands to vari-
ous pieces of hardware to set up physical addressing the way you need it. This depends on the
platform. It may be to turn on or off a range of hardware addresses, or even to set up the physi-
cal range of video memory if your system has internal video hardware.
One particular item that falls into this category on the PC ISA hardware platform is the A20
address line gate. This is one example to help you understand that there may be intricacies on
your platform that will drive you nuts until you find the little piece of obscure documentation
that explains it in enough detail to get a handle on it.
What is the A20 line gate? It all stems back to IBM's initial PC-AT design. As most of you are
aware (if you're programmers), the real mode addresses are 20 bits wide. This is enough to
cover a full one megabyte. These address lines are numbered A0 through A19. If you use one
more bit, you can address above 1MB. While in real mode, the A20 address line serves no
purpose. I suppose that this line even caused some problems because programmers provided a
way in hardware to turn it off (always 0). It seems that memory address calculations wrapped
around in real mode (from 1MB to 0), and the programmers wanted the same problem to exist
in the AT series of processors. I think it's kind of like inheriting baldness from your parents.
You learn to live with it.
My goal was not to discover why the A20 address line gate existed, but to ensure that it was
turned on and left that way. I tried several methods, some of which didn't work on all ma-
chines, and ended up sticking with the way it was documented in the original IBM PC-AT
documentation. This method seems to work on most machines, although some people still
may have problems with the gate. It is controlled through a spare port on the 8042 keyboard
controller (actually a dedicated microprocessor). If your machine doesn't support this method,
you may have to do some tweaking of the code. Meanwhile, back to the initialization (already
in progress).
At this stage of the initialization, you have basic tables set up and tasks ready to go, but you
aren't cognizant of memory yet. You don't know how much you have, and you cannot manage
or allocate it.
This is where you call the routine that initializes memory management. This piece of code may
have a lot to do. The following list is not all-inclusive, but it gives you a good idea of what you
may need to do. Most of the list depends on what memory model you chose for your design.
If you went with a simple flat model, you will have a pretty easy job. If you went with a full-
blown demand paged system, this could be a fairly large chunk of code. Chapter 19, "Memory
Management Code," shows what I did using the Intel paging hardware. It is explained in a fair
amount of detail. You may need to consider the following points:
■ Find out how much physical memory you have.
■ Know where your OS loaded and allocate the memory that it is occupying.

<!-- PDF page 136 -->
1 1 / Part II ■ Architecture and General Theory
■ Allocate other areas of memory for things such as video or I/O arrays if they are used
on your system.
■ Set up the basic linked list structures if you will be using this type of memory manage-
ment; otherwise, set up the basic tables that will manage memory.
■ Make a transition to a paged memory environment, if you used paging hardware and
your processor doesn't start out that way (Intel processors don't).
Something of importance to note about the Intel platform and paged memory-management
hardware is that your physical addresses must match your linear addresses when you make the
transition into and out of paged memory mode. This is important because it forces you to have
a section of code that remains, or is at least loaded, where the address match will occur. I took
the easy way out (totally by accident) when I left the entire static portion of my operating sys-
tem in the low end of RAM. If you chose not to do this, it may mean forced dynamic reloca-
tion of your entire operating system. It's not that much of a headache, but one I'm glad I didn't
face.
Dynamic Tables and Structures
After memory management has been initialized, you can begin to allocate and set up any dy-
namic tables and structures required for your operating system.
These structures or tables may be allocated linked lists for resources needed for interprocess
communications (IPC), memory areas for future tables that cannot be allocated on the fly, or
any number of things. The point is, you couldn't allocate memory until memory-management
functions were available.
After all the dynamic structures are allocated and set up, you're ready to execute a real program
(of sorts). This program generally is part of the operating system, and does things like set up
the program environments and load device drivers or system services.
At this point, you usually load a command-line interpreter, or maybe even load up your
windowing environment.

+ 493
- 0
library/Developing Your Own 32-Bit Operating System/Chapter-08-Programming-Interfaces/Chapter-08-source-text.md Просмотреть файл

@@ -0,0 +1,493 @@
# Developing Your Own 32-Bit Operating System — Chapter-08: Programming Interfaces
- **Source**: /library/Developing Your Own 32-Bit Operating System/source-file.pdf
- **PDF pages**: 137–148
- **Pages without text**: 137, 148

---

<!-- PDF page 137 -->
Programming Interfaces

<!-- PDF page 138 -->
]i^ Part II ■ Architecture and General Theory
For an operating system to be useful, the resources it manages must be accessible to the pro-
grammers who will implement applications for it. The programmers must be able to under-
stand what the system is capable of and how to make the best use of the system resources.
Entire books have been written to describe "undocumented system calls" for some operating
systems. This can happen when operating system developers add a call that they think may not
be useful to most programmers — or even confusing, because the call they add, and leave un-
documented, may be similar to another documented call on the system.
In the following sections, I describe both the application programming interface and the sys-
tems programming interface, including the mechanics behind some of the possible ways to
implement them.
Application Programming Interface
The Application Programming Interface (API) is how a programmer sees your system. The
functions you provide are a foundation for all applications that run on your system. The de-
sign and implementation of an API may seem easy at first, but there are actually many deci-
sions to be made before you begin work.
Documentation, the consistency of the interface design, error handling, and even the mechanical
details of the call itself, all have an effect on how the application programmer views the oper-
ating system. These are covered in detail in the following sections.
Documenting Your API
Your operating system may be for a single application, an embedded system, public distribu-
tion, or your own use and enjoyment. In any of these cases, you need to properly document
the correct use of each call. This may sound like a no-brainer, but it turned out to be a time-
consuming and very important task for me. Even though I designed each of the calls for the
operating system, I found that, on occasion, I had questions about some of the very parameters
I selected when I went back to use them. If this happens to me, I can only imagine what will
happen to someone walking into it from a cold start.
Chapter 15, "API Specification," shows you how I documented my calls. Providing examples
of the calls is very desirable.
Procedural Interfaces
Procedural interfaces are the easiest and most common type of interface for any API. You place
the name of the call and its parameters in a parenthetical list and it's done. The call is made,
the function does its job, and returns. What could be easier? I can't imagine. But there are so
many underlying complications for the system designer it can give you a serious headache. This

<!-- PDF page 139 -->
Chapter 8 ■ Programming Interfaces
is really a layered problem that application programmers shouldn't have to worry about. Quite
often though, they do. I'll go over the complications that have crossed my path. The calling
conventions of the interface were the largest complications.
Calling Conventions
As a programmer, you're aware that not every language and operating system handles the me-
chanics of the procedural call in the same way. These are some of the considerations:
■ Where are the parameters placed (on the stack or in registers)?
■ If the parameters are on the stack, what order are they in?
■ If the stack is used, are there alignment restrictions imposed by hardware?
■ If registers are used, what do you do with the excess parameters if you run out of
register?
■ Who cleans the stack (resets the stack pointer) when the call is completed?
These considerations can get complicated. No kidding! With UNIX, life was easy in this re-
spect. UNIX was written with C; it used the standard defined C calling conventions. With
newer operating systems always looking for more speed (for us power junkies), faster, more
complicated calling conventions have been designed and put into place. It leads to a lot of
confusion if you're not prepared for it when you go use a mixed-language programming model.
OS/2, for example, has three or four calling conventions. Some use registers, some use the stack,
and some use both. Even the IBM C/Set2 compiler has its own unique calling conventions
that only work with CSet/2 (no one else supports it). I even have run across commercial devel-
opment libraries (DLLs) that used this convention, which forced a specific compiler, even though
I preferred another (Borland's, as if you couldn't guess).
When a high-level language has its own calling convention, it will not be a problem if this lan-
guage also provides a method to reach the operating system if its conventions are different.
The following are the two most popular procedural calling conventions:
■ Standard C. Parameters are pushed onto the stack from right to left, and the caller
(the one that made the OS call) is responsible for cleaning the stack.
■ Pascal. (Also known as PLM conventions in some circles.) Parameters are pushed left
to right, and the called function is responsible for making the stack correct.
There is a plethora of not-so-standard conventions out there. I'm sure that you know of sev-
eral. Execution speed, code size, and hardware requirements are the considerations that drive
the need to deviate from a standardized calling convention. I'm fairly opinionated on this is-
sue, especially when it comes to speed. In other words, I don't think the few microseconds
saved (that's all it really is) by redesigning the mechanics of a standardized interface are really
worth it when it comes to an operating system. Not many calls are made recursively or in a
tightly looped repetitive fashion to many operating system functions. Your opinion may vary
on this. It's up to you.
H

<!-- PDF page 140 -->
Part II ■ Architecture and General Theory
The hardware requirements also may be one of the deciding factors. I used an Intel-specific
processor hardware function for my calls (call gates), and it required a fixed number of param-
eters. I had no need for variable-length parameter blocks into the operating system, so the C
calling conventions had no appeal to me. I decided on the Pascal (PLM) conventions. This
makes porting a C compiler a little hectic, but I wasn't going to let that drive my design.
You may not want to go to the lengths that I did to support a specific calling convention on
your system. You may be using your favorite compiler and it will, no doubt, support several
conventions. If you want to use code generated by several compilers or languages, you surely
will want to do some research to ensure that there is full compatibility in the convention that
you choose.
Mechanical Details
Aside from the not-so-simple differences of the calling conventions, there are many additional
details to think about when designing the API.
Will your call go directly into the operating system? In other words, will the call actually cause
the processor to change the instruction pointer to the code you wrote for that particular call?
Your answer will more than likely be no.
Many systems use an intermediate library, which is linked into your code that performs some
translation before the actual operating system code is executed. There are two reasons this li-
brary may be required. First, your code may expect all the data to be in registers, and you may
want to maintain a standard procedural calling convention for the sake of the high-level lan-
guage interface. Second, the actual execution of the call may depend on a hardware mecha-
nism to actually execute the code.
An example of a hardware-calling mechanism is the software interrupt. A software interrupt \s
a method provided by some processors that enables a pseudo-interrupt instruction to trigger a
form of a call. This feature causes the instruction pointer to be loaded with a value from a table
(the interrupt vector table, for example). The stack is used to save the return address just like a
CALL instruction, but the stack is not used for parameters. A special instruction is provided to
return from the software interrupt. MS-DOS uses such a device on the Intel-compatible pro-
cessors. This device is extremely hardware dependent, and forces an intermediate library to
provide a procedural interface.
The advantages of using the software-interrupt function include speed (from assembly language
only), and also the fact that operating system code relocation doesn't affect the address of the
call because it is a value in the interrupt vector table. The address of the operating system calls
may change every time you load it, or at least every time it is rebuilt. This address change is not
sufficient reason to use this type of interface, however. A simple table fixed in memory to use
for indirect call addresses, or some other processor hardware capability can provide this func-
tionality without all the registers directly involved.

<!-- PDF page 141 -->
Chapter 8 ■ Programming Interfaces
When the software-interrupt mechanism is used, you may need only one interrupt for all the
calls in the operating system. This interrupt is accomplished by using a single register to iden-
tify the call. The intermediate library would fill in this value, and then execute the interrupt
instruction.
Portability Considerations
Looking at portability might not seem so important as you write your system for one platform.
Murphy's law, however, says that as soon as you get the system completed, the hardware will
be obsolete. I really hope not, but this is the computer industry of the '90s. I can remember
reading the phrase "30 or 40 megahertz will be the physical limit of CPU clock speeds." Yeah,
right.
The portability I speak of is not the source-code portability of the applications between sys-
tems, but the portability of the operating system source code itself. For application source code
portability, the procedural interface is the key. For the operating system source, how you de-
sign the internal mechanisms of the system calls may save you a lot of time if you want to move
your system to another platform.
I thought about this design consideration before I used the Intel-specific call gates, and the call
gates are really not a problem. They aid in the interface but don't detract from portability be-
cause they were designed to support a high-level calling convention in the first place (proce-
dural interfaces). Things like software interrupts and register usage, however, leave you with
subtle problems to solve on a different platform. If portability is near the top of your wish list,
I recommend that you avoid a register-driven interface if at all possible.
Error Handling and Reporting
So many systems out there make you go on some kind of wild-goose chase to find out what
happened when the function you called didn't work correctly. The systems make you go find
a public variable that you're not sure is named correctly for the language you're using, or they
make you call a second function to find out why the first function failed. This may be a per-
sonal issue for me, but if I could have any influence in this industry at all, it would be to have
all systems return a status or error code directly from the function call that would give me some
intelligent information about why my call didn't work.
I recommend that you consider providing a status or error code as the returned value from
your system functions that really tells the caller what happened. When you need to return data
from the call, have the caller provide a pointer as one of the parameters.
Error reporting has special implications in a multitasking environment, especially with mul-
tiple threads that may share common data areas or a single data segment. A single public vari-
able linked into your data to return an error code doesn't work well anymore. Give this some
serious thought before you begin coding.

<!-- PDF page 142 -->
Part II ■ Architecture and General Theory
Systems Programming Interface
I divide the systems programming interface into two distinct categories. The first category is
the interface seen by programmers that write device drivers or any kind of extension to the
operating system. The second category is how internal calls are made from inside the operating
system to other operating system procedures (public or internal). I never considered the sec-
ond aspect until I wrote my own operating system.
Internal Cooperation
Understanding how internal calls are made from inside the operating system to other operat-
ing system procedures is not as easy as it sounds. This is true especially if you intend to use
assembler and high-level languages at the same time. You more than likely will. Many operat-
ing systems now are written almost entirely with high-level languages, but underneath them
somewhere is some assembly language coding for processor control and status.
Suddenly, naming conventions and register usage can become a nightmare. Select a register-
usage plan and try to stick with it. I had to deviate from my plan quite often as you will see, but
there was at least a plan to go back to when all else failed.
You also need to investigate exactly how public names are modified by the compiler(s) you
intend to use. Is it one underscore before the name? Maybe it was one before and one after, or
maybe it was none if a particular compiler switch was active. Do you see what I mean? Inter-
nally, I ended up with two underscores for the public names that would be accessed by outside
callers, and this wasn't my original plan.
Device Driver Interfaces
For all practical purposes, device drivers actually become part of the operating system. They
work with the hardware, and so much synchronization is required with the tasking and memory-
management code, that it's almost impossible to separate them.
Some device drivers load after the operating system is up and running, so you cannot put the
code entry point addresses into a table in advance. You need a way to dynamically make all the
device drivers accessible to outside callers without them knowing where the code is in advance.
This capability requires some form of indirect calling mechanism. This mechanism may be a
table to place addresses in that the operating system can get to when a standardized entry point
is made by an application.
You also have to consider how to make the device appear at least somewhat generic in your
system. Can the programmer read a sector from a floppy disk drive or a hard disk drive with-
out caring which one it really is, for example? That's something to think about when you ap-
proach your design.

<!-- PDF page 143 -->
Chapter 8 ■ Programming Interfaces
I was familiar with two device driver interface schemes before I started writing my own system.
I knew that there were four basic requirements when it came to device drivers:
■ Installation of the driver
■ Initialization of the driver
■ Controlling (using) the driver
■ Statusing the driver
You need to consider these requirements in your design. The possibility exists that you may be
writing a much more compact system than I did. If this is the case, all your device drivers may
be so deeply embedded in your code that you may not want a separate layered interface.
You also must realize that it isn't just the interface alone that makes things easier for the device
driver programmer. Certain system calls may have to be designed to allow them to concentrate
their programming efforts on the device and not the hardware on the platform. You may want
to satisfy these requirements by adding calls to isolate them from some of the system hardware
such as DMA, timers, and certain memory-management functions.
A Device-Driver Interface Example
With the four basic requirements I mentioned in mind, I set out to design the simplest, fully
functional device driver interface I could imagine. Its purpose is to work generically with exter-
nal hardware, or hardware that appeared external from the operating system's standpoint.
The interface calls I came up with to satisfy my design follow:
InitDevDr(dDevNum, pDCBs, nDevices, fReplace)
DeviceOp(dDevice, dOpNum, dLBA, ndBlocks, pData) :dError
DeviceInit(dDevice, plnitData, sdlnitdata) :dError
DeviceStat(dDevice, pStatRet, sdStatRetmax) :dError
In Chapter 10, "Systems Programming," I provide detail on the use of these calls, so I will not
go over them here. I do want to discuss the underlying code that enables the calls to function
for multiple device drivers in a system, however. This will give you a good place to start in your
design.
A requirement for the operating system to know things about each device driver led me to re-
quire that each driver maintain a common data structure called a device control block (DCB).
This is a common requirement on many systems. This structure is filled out with items com-
mon to all drivers before the driver tells the operating system that it is ready to go into business
serving the applications.
The first call listed in this section (lnitDevDr( )) is called from the driver to provide the oper-
ating system with the information it needs to seamlessly blend it into the system. You will note
that a pointer to its DCB is provided by the driver.
E

<!-- PDF page 144 -->
0
Part II ■ Architecture and General Theory
The other three functions are the calls that the operating system redirects to the proper device
driver as identified by the first parameter (the device number).
In a multitasking system, you also will find that some device drivers may not be re-entrant.
Two simultaneous calls to the driver may not be possible (or desirable). When a user is doing
serial communications, for example, this particular device must be protected from someone
else trying to use it. I provided a mechanism to prevent re-entrant use of the drivers if the flag
was set in the DCB when the driver initialized itself. I use system IPC messaging to block sub-
sequent calls to a driver that indicates it is not re-entrant.
The code example in the following section is from the file DevDrvr.ASM. This is the layer in
MMURTL that initializes device drivers and also redirects the three device driver calls to the
proper entry points. When you look at the code, pay attention to the comments instead of the
details of the code. The comments explain each of the ideas I've discussed here. It's included in
this chapter to emphasize the concepts and not to directly document exactly what I've written.
Announcing the Driver to the OS
This is the call that a driver makes after it is loaded to tell the operating system that it is ready
to work. This code is part of the operating system. You will see certain validity checks, and you
will notice that you allocate an exchange for messaging if the driver is not re-entrant:
InitDevDr(dDevNum,
EBP+24
pDCBs,
EBP+20
nDevices,
EBP+16
dfReplace)
EBP+12
dError
sParam 16
Local vars
dDevX EQU DWORD PTR [EBP -4]
prgDevs EQU DWORD PTR [EBP -8]
nDevs EQU DWORD PTR [EBP -12]
dExchTmp EQU DWORD PTR [EBP -16]
PUBLIC
_InitDevDr:
PUSH EBP
MOV EBP, ESP
SUB ESP, 16
MOV EAX, [EBP+24]
MOV dDevX, EAX
MOV EAX, [EBP+20]
MOV prgDevs, EAX
MOV EAX, [EBP+16]
MOV nDevs, EAX
;Set up local vars
InitDev00:
CMP dDevX, nDevices
JB InitDev01
MOV EAX, ErcBadDevNum
JMP InitDevEnd
;Valid DCB number?
;Not valid DCB number
ImtDev0l : ;Now check to see if device is already installed
;and whether it's to be replaced
LEA EBX, rgpDCBs
; Point EBX to rgpDCB

<!-- PDF page 145 -->
Chapter 8 ■ Programming Interfaces 0
MOV EAX, dDevX ;dDevNum
SHL EAX, 2
ADD EBX, EAX
CMP DWORD PTR [EBX], 0 ; pDCBx = 0 if not used yet
JZ InitDev02 ;Empty, OK to use
CMP DWORD PTR [EBP+12], 0 ;OK to replace existing driver?
JNZ InitDev02 ;Yes
MOV EAX, ErcDCBInUse ;No - error exit
JMP InitDevEnd
InitDev02: ;If we got here, we can check DCB items then move ptr
MOV EAX, prgDevs ;EAX points to DCB
CMP BYTE PTR [EAX+sbDevName] , 12 ;Check Device name size
JA InitDev03
CMP BYTE PTR [EAX+sbDevName], 0 ;is Devname OK?
JA InitDev04
InitDev03:
MOV EAX, ErcBadDevName
JMP InitDevEnd
InitDev04:
;Now see if there are more devices for this driver
DEC nDevs ;Decrement nDevices
JZ InitDev05 ;NONE left
ADD prgDevs, 64 ;Next caller DCB
INC dDevX ;Next devnum
JMP SHORT InitDev00 ;
;A11 error checking on DCB(s) should be done at this point
InitDev05: ;Alloc Exch if driver is NOT re-entrant
MOV EBX, [EBP+20] ;pDCBs
CMP BYTE PTR [EBX+f DevReent] , 0
JNZ InitDev06 ;device IS re-entrant!
LEA EAX, dExchTmp ;Allocate device Exchange
PUSH EAX ;into temp storage
CALL FWORD PTR _AllocExch
CMP EAX, 0
JNZ SHORT InitDevEnd
InitDev06:
;A11 went OK so far, now move the DCB pointer(s) into array
; and assign exchange from temp storage to each DCB
MOV EAX, [EBP+16] ; nDevices
MOV nDevs, EAX ;Set nDev to number of devices again
LEA EBX, rgpDCBs ; Point EBX to OS rgpDCBs
MOV EAX, [EBP+24] jdDevNum
SHL EAX, 2
ADD EBX, EAX ;EBX now points to correct pointer
MOV EAX, [EBP+20] ;EAX points to first DCB
MOV ECX, dExchTmp ;ECX has semaphore exchange
InitDev07:
;Now that EBX, EAX, and ECX are set up, loop through each

<!-- PDF page 146 -->
I
124
Part II
Architecture and General Theory
;DCB (if more than 1) and set up OS pointer to it, and
;also place Exchange into DCB. This is the same exchange
:for all devices that one driver controls.
MOV [EAX+DevSemExch], ECX
MOV [EBX], EAX
ADD EBX, 4
ADD EAX, 64
DEC nDevs
JNZ InitDev07
XOR EAX, EAX
;next p in rgp of DCBs
;next DCB
;Any more DCBs??
;Set up for no error
;If the device driver was NOT re-entrant
;we send a semaphore message to the exchange for
;the first customer to use.
MOV EBX, [EBP+20]
CMP BYTE PTR [EBX+f DevReent]
JNZ InitDev06
PUSH ECX
PUSH 0FFFFFFFEh
PUSH 0FFFFFFFEh
CALL FWORD PTR _SendMsg
InitDevEnd:
MOV ESP, EBP
POP EBP
RETF 16
;pDCBs
;device IS re-entrant!
;ECX is still the exchange
; Dummy message
;Let ere in EAX fall through (Was ISend)
A Call to the Driver
This is the call that the user of the device driver makes to initially set up the device, or to reset
it after a failure. You can see in the comments how you redirect this call to the proper code in
the driver from the address provided in the DCB. The other two calls, DeviceOpO and
DeviceStat ( ), are almost identical to Devicelnit ( ) , and are not presented here.
DeviceInit(dDevNum, plnitData,
EBP+20 EBP+16
PUBLIC Devicelnit:
PUSH EBP
MOV EBP, ESP
CMP DWORD PTR [EBP+20]
JB Devlnit01
MOV EAX, ErcBadDevNum
JMP DevInitEnd
Devlnit01 :
LEA EAX, rgpDCBs
MOV EBX, [EBP+20]
SHL EBX, 2
ADD EAX, EBX
MOV EBX, [EAX]
CMP EBX, 0
JNZ DevInitIA
sdlnitData)
EBP+12
Count = 12
nDevices ;Valid Device number?
; Sorry no valid DCB
now EBX points to DCB (maybe)
Is there a pointer to a DCB?
Yes

<!-- PDF page 147 -->
Chapter 8 ■ Programming Interfaces H
MOV EAX, ErcNoDriver
JMP DevInitEnd
>vInit1A:
CMP BYTE PTR [ EBX+DevType]
JNZ Devlnit02
MOV EAX, ErcNoDevice
JMP DevInitEnd
;NO driver!
;Is there a physical device?
Devlnit02: ;A11 looks good with device number
;so we check to see if driver is re-entrant
;call WAIT to get "semaphore" ticket...
CMP BYTE PTR [EBX+f DevReent ] , 0
JNZ Devlnit03
PUSH EBX
PUSH DWORD PTR [EBX+DevSemExch]
LEA EAX, [EBX+DevSemMsg]
PUSH EAX
CALL FWORD PTR _WaitMsg
POP EBX
CMP EAX, 0
JNE DevInitEnd
If not we
;Device IS re-entrant
;save ptr to DCB
;Push exchange number
;Ptr to message area
;Get semaphore ticket
;Get DCB ptr back
;Serious kernel error!
Devlnit03:
PUSH EBX
PUSH DWORD PTR [EBP+20]
PUSH DWORD PTR [EBP+16]
PUSH DWORD PTR [EBP+12]
CALL DWORD PTR [EBX+pDevInit]
POP EBX
PUSH EAX
CMP BYTE PTR [EBX+f DevReent] ,
JNZ Devlnit04
;Save ptr to DCB
;Push all params for call to DD
;Get ptr to DCB back into EBX
;save error (if any)
[Re-entrant?
;YES
PUSH DWORD PTR [EBX+DevSemExch]
PUSH 0FFFFFFFEh
PUSH 0FFFFFFFEh
CALL FWORD PTR _SendMsg
Devlnit04:
POP EAX
DevInitEnd:
MOV ESP, EBP
POP EBP
RETF 12
No, Send semaphore message to Exch
Bogus Message
Ignore kernel error
Get device error back
dump params
Your interface doesn't have to be in assembly language like the preceding example, but I'm
sure you noticed that I added examples of the procedural interface in the comments. If you use
assembly language and provide code examples, adding the procedural call examples is a good
idea.
This chapter ends the basic theory for system design. The next chapter begins the documenta-
tion of and for an operating system.

<!-- PDF page 148 -->


+ 591
- 0
library/Developing Your Own 32-Bit Operating System/Chapter-09-Application-Programming/Chapter-09-source-text.md Просмотреть файл

@@ -0,0 +1,591 @@
# Developing Your Own 32-Bit Operating System — Chapter-09: Application Programming
- **Source**: /library/Developing Your Own 32-Bit Operating System/source-file.pdf
- **PDF pages**: 149–162
- **Pages without text**: 149, 150, 151
- **Note**: Includes the Part III divider plate and its blank verso before the chapter.

---

<!-- PDF page 149 -->
MMURTL-Specific Details

<!-- PDF page 150 -->


<!-- PDF page 151 -->
Application Programming

<!-- PDF page 152 -->
Part III ■ MMURTL-Specific Details
This section introduces the MMURTL operating-system interface and provides guidance on
programming techniques specific to the MMURTL environment. It also provides informa-
tion on basic video and keyboard usage as well as examples of multithreading with MMURTL
and information on memory-management techniques and requirements. This chapter should
be used with the reference manual for the language you are using.
Before writing your first MMURTL program, you should be familiar with the material cov-
ered in Chapter 4, "Interprocess Communications," and Chapter 5, "Memory Management."
These chapters provide discussions of the concepts behind message-based operating systems
and paged memory management. These are important concepts you should understand before
you attempt to create or port your first application.
Some of the information provided in this section may not actually be needed in order to code
a program for MMURTL. I think you will find it useful, though, as you discover the methods
employed by this operating system. It may even be entertaining.
Terminology
Throughout this section, I use several words that may seem language-specific or that mean
different things to different programmers. The following definitions are provided to prevent
confusion:
Procedure A function in C or Pascal, or a procedure in Pascal, or a procedure in
Assembler. All MMURTL operating system calls return values (called error codes).
Function Same as procedure.
Call Used interchangeably with junction and procedure.
Byte An eight-bit value (signed or unsigned).
Word A 16-bit value (signed or unsigned).
Dword A 32-bit value (signed or unsigned).
Parameter A value passed to a function or procedure, usually passed on the stack.
Argument Same as parameter.
Understanding 32-Bit Software
If you have been programming with a 1 6-bit operating system (for example, MS-DOS), you
will have to get used to the fact that all parameters to calls, and most structures (records) have
many 32-bit components. If you are used to the 16-bit names for assembler variables, you should
be comfortable with MMURTL because I have maintained the Intel and Microsoft conven-
tions of byte, word, and dword Most MMURTL parameters to operating system calls are dwords

<!-- PDF page 153 -->
Access as
byte
01
word
01
02
dword
01
02
Chapter 9 ■ Application Programming
(32-bit values). Ifl were a perfectionist, I would have called a 32-bit value a word, but alas, old
habits are hard to break and one of my key design goals was simplicity for the programmer on
Intel-based ISA systems.
One of the things that make processors different from each other is how data is stored and
accessed in memory. The Intel processors have often been called "BackWords" because they
store the least significant data bytes in lower memory addresses (hence, the byte values in a
word appear reversed when viewed with a program that displays hexidecimal bytes). A byte at
address 0 is accessed at the same memory location as a word at address 0, and a dword is also
accessed at the same address. Look at the Table 9. 1 to visualize what you find in memory:
Table 9.1. Memory alignment.
Address 0 12 3 Hex value
Olh
0201h
03 04 0403020 lh
This alignment serves a very useful purpose. Conversions between types is automatic when read
as a different type at the same address. This also plays a useful role in languages such as C,
Pascal, and assembly language, although it's usually transparent to the programmer using a high-
level language.
When you are running the Intel processors in 32-bit protected mode, this also makes a great
deal of difference when values are passed on the stack. You can push a dword on the stack and
read it as a byte at the same location on the stack from a procedure. MMURTL always uses a
dword stack. (Values are always pushed and popped as dwords — 32-bit values.) This is a hard-
ware requirement of the 386/486 processors when 32-bit segments are used.
Operating-System Calling Conventions
All operating system calls are listed alphabetically in Chapter 15, "API Specification."
Most operating system calls in MMURTL return a dword (32-bit) error code, which is often
abbreviated as ERC, ere, or dError. The descriptions in the API specification (Chapter 1 5) show
it as dError to indicate it is a 32-bit unsigned value. These codes convey information to the
caller about the status of the call. It may indicate an error, or it may simply convey information
that may or may not be an error (depending on what you expect). A list of all documented
status or error codes can be found in header files included with the source code.
H

<!-- PDF page 154 -->
H
Part III M MMURTL-Specific Details
All public operating-system calls are accessible through call gates and can be reached from "out-
side" programs. They are defined as far (this may be language-dependent) and require a 48-bit
call address.
A far call address to the operating system consists of a selector and an offset. The selector is the
call gate in the Global Descriptor Table (GDT), while the offset- is ignored by the 386/486
processor. The GDT entry for the call gate defines the actual address that is called and the
number of dwords of stack parameters to be passed to the call. You can even hard code the far
address of the operating-system call because the system calls are at call gates. It doesn't matter
where the operating system code is in memory. The call gate will never change unless it be-
comes obsolete (in which case it will left in during several major revisions of the operating sys-
tem for compatibility purposes). This means that an assembly-language programmer can make
the far call to what appears to be a hard address (but is really a call gate). The addresses of the
calls are documented with the source code (header or include files).
Stack Usage
Each of the MMURTL operating-system calls is defined as a function with parameters. Most
return a value that is an error or status code. Some do not.
All but a few of the operating-system calls use the stack to pass parameters to the operating-
system functions.
All parameters are pushed from left to right and are expanded to 32-bit values. This will be
done by the compiler, and in some cases, even the assembler or processor itself. This is because
the processor enforces a four-byte aligned stack.
The called operating-system function always cleans the stack. In some C compilers, this is re-
ferred to as the Pascal Calling Convention. The CM32 compiler uses this convention.
It is also very easy to use from assembly language. The following is an example of a call to the
operating system in assembler:
PUSH 20
PUSH 15
CALL FWORD PTR SetXY
In the preceding example, SetXY is actually a far pointer stored in memory that normally would
contain the address of the SetXY function. The address is, however, really a call gate, not the
real address of the SetXY function.
Memory Management
MMURTL uses all of the memory in your system and handles it as it as one contiguous ad-
dress span. If you need a 2MB array, you simply allocate it. Chapter 3, "The Tasking Model,"
and Chapter 4, "Interprocess Communications," describe this in great detail.

<!-- PDF page 155 -->
Chapter 9
Application Programming S
MMURTL has control of all physical or linear memory allocation and hands it out it as pro-
grams ask for it. Application programs all reside at the one-gigabyte memory location inside of
their own address space. The operating system resides at address 0. This doesn't change any
programming techniques you normally use. What does change your techniques is the fact that
you are working with a flat address space that is all referenced to one single selector. There are
no far data pointers. This also means that a pointer to allocated memory will never start at 0.
In fact, when an application allocates pages of memory, they will begin somewhere above one
gigabyte (40000000h).
MMURTL really doesn't provide memory management in the sense that compilers and lan-
guage systems provide a heap or an area that is managed and cleaned up for the caller. MMURTL
is a paged memory system. MMURTL hands out (allocates) pages of memory as they are re-
quested, and returns them to the pool of free pages when they are turned in (deallocated). Heap-
management functions that allow you to allocate less than 4K at a time should be provided by
your high-level language. The library code for the high-level language will allocate pages and
manage them for you. This would let you allocate 30 or 40 bytes at a time for dynamic struc-
tures such as linked lists (for example, malloc( ) in C).
A MMURTL application considers itself to be the only thing running on the system aside from
the operating system and device drivers. Your application sees the memory map described in
Table 9.2.
Table 9.2. Basic memory map.
Address Span
Memory Usage
7FFFFFFFh
Top of user address space
40000000h
3FFFFFFFh
Application base (1GB)
Top of OS address space
OOOOOOOOh
OS base address
Even if six applications are running at once, they all see this same "virtual memory map" of the
system. The operating system and device drivers reside in low linear memory (0), while all
applications and system services begin at the one-gigabyte mark. Paged virtual memory is imple-
mented with the paging capabilities of the 386/486 processor. Multiple applications all load
and run at what seems to be the same address range, which logically looks like all programs are
loaded parallel to one another. The paging features of the 386/486 enable you to assign physi-
cal memory pages to any address ranges you like. Read up on memory management in Chap-
ter 5 if you're really interested.

<!-- PDF page 156 -->
I
134
Part III ■ MMURTL-Specific Details
The operating system provides three basic calls for application memory management. They
are as follows:
AllocPage(nPages, ppMemRet): dError
DeAllocPage(pMem, dnPages): dError
QueryPages(pdnPagesRet) : dError
When your program needs a page of memory, you allocate it. When you are done with it, you
should deallocate it. There are cleanup routines in the operating system that will deallocate it
for you if your program exits without doing so, but remember, it's bad manners not to clean
up after yourself. MMURTL is kind of like your mother. She may pick up your dirty socks,
but she would much rather that you did so yourself.
Memory-page allocation routines always return a pointer to the new memory. The address of
the pointer is always a multiple of the size of a page (modulo 4096).
The memory allocation routines use a first fit algorithm in your memory space. It is up to you
to manage your own addressable memory space. Your program can address up to 1 GB of space.
Of course, this doesn't mean you can allocate a gigabyte of memory, but pointer arithmetic
may get you into trouble if you allocate and deallocate often and lose track of what your active
linear addresses are. The high-level language allocation routine may help you out with this,
but I have to address the issues faced by those that write the library allocation routines for the
languages.
Consider the following scenario:
1. Your program takes exactly 1MB before you allocate.
2. You allocate five pages. Address 40100000h is returned to you. You now have 20K at
the 40 lOOOOOh address.
3. Then you deallocate three of these pages at address 40100000h (12K). You are left
with a hole in your addressable memory area. This is not a problem, as long as you
know it can't be addressed.
4. Now you allocate four pages. The memory- allocation algorithm can't fit it into the
three-page hole you left, so it will return a pointer to four pages just above the original
five you allocated first (address 40105000h). Now you have six pages beginning at
40103000h.
If you attempt to address this "unallocated" area, a Page Fault will occur. This will terminate
your application automatically. (Ouch!)
Operating-System Protection
MMURTL provides protection for the operating system and other jobs using the paging-
hardware protection.

<!-- PDF page 157 -->
Chapter 9 ■ Application Programming
The operating system code, data, and device drivers run and can only be accessed at the sys-
tem-protection level (level 0), while system services and applications run at user-level protec-
tion. The operating system prevents ill-behaved programs from intentionally or accidentally
accessing another program's data or code (from "a pointer in the weeds"). It even prevents
applications from executing code that doesn't belong to them, even if they know where it's
located (that is, its physical or linear address). MMURTL does not, however, protect program-
mers from themselves.
This means that in a multitasking environment, a program that crashes and burns doesn't eat
the rest of the machine in the process of croaking. In this case, the operating system acts more
like a referee than a mother.
Application Messaging
The most basic applications in MMURTL may not even use messaging directly. You may not
have to call an operating-system primitive-message routine throughout the execution of your
entire program. However, when you write multithreaded applications (those with more than
one task), you will probably need a way to synchronize their operations or to "queue up" events
between the threads (tasks). This is where non-specific intertask messaging is employed. This
is done with the SendMsg ( ) and WaitMsg ( ) calls.
You may also require the use of a system service for which there isn't a direct procedural inter-
face, in which case you will use Request ( ) and WaitMsg ( )
.
The following messaging calls are provided by the operating system for applications:
SendMsg (dExch, dMsgl , dMsg2): dError
CheckMsg(dExch, pMsgsRet) : dError
WaitMsg (dExch, pqMsgRet) :dError
Request (pSvcName, wSvcCode, dRespExch, pRqHndlRet, npSend, pDatal ,
cbDatal , pData2, cbData2, dData0, dDatal , dData2 ) : dError
Before you can use messaging, you will need an exchange where you can receive messages, or
you will need to know of one you can send messages to. The following calls are provided to
acquire exchanges and to return them to the operating system when you no longer need them:
AllocExch(pdExchRet) : dError
DeAllocExch(dExch) : dError
Normally, applications will allocate all of the exchanges they require somewhere early in the
program execution and return them (DeAllocExch) before exiting.
Messaging is very powerful, but it can also be destructive (to the entire system). Memory-
management protection provided by MMURTL will help some, but you can easily eat up all
of the system resources and cause the entire system to fail. MMURTL is not a mind reader.
For instance, accidentally sending messages in an endless loop to an exchange where no task is
waiting to receive them causes the system to fail. This eats all of your link blocks. The operat-
ing system is brought to its knees in a hurry.

<!-- PDF page 158 -->
y^n Pan III m MMURTL-Specific Details
Starting a New Thread
Some programs need the capability to concurrently execute two or three things, such as data
communications, printing, and user interaction. A simple example would be keeping the time
on the user interface updated no matter what else you are doing. In these cases, you can "spawn"
a new task to handle the secondary functions while your main program goes about its business.
The operating system provides the SpawnTask ( ) call, which gives you the ability to point to one
of the functions in your program and turn it into a "new thread" of execution. This new func-
tion will be scheduled for execution independently of your main program.
Prior to calling SpawnTask ( ) , you need to allocate or provide memory from your data segment
for the stack that the new task will use. Other than that, you basically have two programs run-
ning in the same memory space. They can even execute the same procedures and functions
(that is, share them). The stack you allocate should be at least 512 bytes (128 dwords) plus
whatever memory you require for your task. You need to ensure you don't overrun this stack.
If you do, only your program will be trashed, but you don't want that, either.
You should be concerned about reentrancy if you have more than one task executing the same
code. Reentrancy considerations include modifications to variables in your data segment (two
tasks sharing variables), operating system messaging (sharing an exchange), and the allocation
or use of system resources such as memory.
Look at the code for the monitor (Monitor.c, provided with the operating-system source code)
for a good example of spawning new tasks. Several tasks are spawned in the monitor.
Job Control Block
Applications may sometimes need to see the data in a Job Control Block. Utilities that display
information about all of the jobs that are running may also need to see this data.
The GetpJCB( ) call will provide a pointer to allow you to read values from a JCB. The pointer
is read-only. Attempts to write directly to the JCB will cause your application to be terminated
with a protection violation.
In order to use the Get pJCB() call, you need to know your job number, which theGetJobNum( )
call will provide.
Header files included with the source code have already defined the following structure for you.
Listing 9.1 . JCB structure definition in C.
struct JCBRec {
long JobNum;
char sbJobName(14] ; /* lstring */

<!-- PDF page 159 -->
Chapter 9 ■ Application Programming H
char
*pJcbPD; /*
char
*pJcbCode; /*
unsigned long
sJcbCode; /*
char
*pJcbData; /*
unsigned long
sJcbData; /*
char
*pJcbStack; /*
unsigned long
sJcbStack; /*
char
sbUserName[30] ; /
char
sbPath[70]; /
char
JcbExitRF[80]; /
char
JcbCmdLine[80] ; /
char
JcbSysIn[50] ; /
char
JcbSysOut[50]; /
long
ExitError; /
char
*pVidMem; /
char
*pVirtVid; /
long
CrntX; /
long
CrntY;
long
nCols; /
long
nLines;
long
VidMode; /
long
NormVid; /
char
fCursOn; /
char
fCursType; /
unsigned char ScrlCnt; /
char
fVidPause; /
long
};
NextJCB; /
Linear add of Job's PD */
Address of code segment */
Size of code segment */
Adresss of data segment */
Size of data segment */
Addrees of primary stack */
Size of primary stack */
* User Name - LString */
* current path (prefix) */
* Exit Run file (if any) */
* Command Line - LString */
* std input - LString */
* std output - LString */
* Error Set by ExitJob */
* Active video buffer */
* Virtual Video Buffer */
* Current cursor position */
* Virtual Screen Size */
* 0 = 80x25 VGA color text */
* 7 = WhiteOnBlack */
* 1 = Cursor is visible */
* 0=UL, 1 = Block */
* Count since last pause */
* Full screen pause */
* OS Uses to allocate JCBs */
The JCB structure is a total of 512 bytes, with unused portions padded. Only the active por-
tions are shown in the preceding listing. All of the names and filenames (strings) are stored
with the first byte containing the active size of the string and with the second byte (offset 1)
actually holding the first byte of string data.
To change data in the JCB, you must use the calls provided by the operating system. These
include the following:
SetJobName(pJobName, dcbJobName): dError
SetExitJob(pFileName, dcbFileName) : dError
SetPath(pPath, dcbPath): dError
SetUserName(pUserName, dcbUserName) : dError
SetSysIn(pFileName, dcbFileName): dError
Some of the more commonly used information can be read from the JCB without defining the
structure by using the following operating-system-provided calls:
GetExitJob(pFileNameRet, pcbFileNameRet) : dError
GetCmdLine(pCmdLineRet, pdcbCmdLineRet) : dError
GetPath(dJobNum, pPathRet, pdcbPathRet) : dError
GetUserName(pUserNameRet, pdcbUserNameRet) : dError

<!-- PDF page 160 -->
jjgl Part III m MMURTL-Specific Details
Basic Keyboard
Internal support is provided for the standard IBM PC AT-compatible 101-key keyboard or
equivalent. The keyboard interface is a system service. Chapter 13, "Keyboard Service," pro-
vides complete details on the keyboard implementation. A system service allows concurrent
access to system-wide resources for applications. The keyboard has a built-in device driver that
the keyboard service accesses and controls.
For most applications, the single procedural ReadKbd ( ) operating-system call will suffice. It
provides access to all standard alphanumeric, editing, and special keys. This call returns a 32-
bit value that contains the entire keyboard state. (All shifted states such as Alt and Ctrl are in-
cluded.) There are tables in Chapter 13 to show all of the possible values that can be returned.
The first of the two parameters to the ReadKbd ( ) call asks you to point to where the 32-bit
keycode value will be returned, while the second parameter is a flag asking if you want to wait
for a keystroke if one isn't available.
Basic Video
Each application and system service in MMURTL is assigned a virtual video buffer that is the
same size as the video memory in standard VGA-color character operation. This is 4000 bytes
(even though an entire page is allocated — 4096 bytes).
The video calls built into the operating system provide TTY (teletype) as well as direct screen
access (via PutVidChars and GetVidChar calls) for all applications.
MMURTL has a basic character device driver built-in, which provides an array of calls to sup-
port screen color, character placement, and cursor positioning.
Positions on the screen are determined in x and y coordinates on a standard 80 x 25 matrix (80
across and 25 down).
X is the position across a row and referenced from 0 to 79. Y is the position down the screen and
referenced as 0 to 24.
The character set used is the standard IBM PC internal font loaded from ROM when the sys-
tem is booted. The colors for TTYOut, PutChars, and PutAttrs comprise 16 foreground colors,
8 background colors, and 1 bit for blinking. Even though the attribute is passed in as a dword,
only the least significant byte is used. In this byte, the high nibble is the background, and low
nibble is the foreground. The high bit of each is the intensity bit.
Tables 9.3 and 9.4 describe the attributes. These names are also defined in standard header
files included with the operating-system code and sample programs.

<!-- PDF page 161 -->
>und colors (low nibble).
Chapter 9 ■
Application Programming
|
139
Table 9.3. Foregr
Normal
Binary
Hex
Intensity Bit Set
Binary
Hex
Black
0000
OOh
Gray
1000
08h
Blue
0001
Olh
Light blue
1001
09h
Green
0010
02h
Light green
1010
OAh
Cyan
0011
03h
Light cyan
1011
OBh
Red
0100
04h
Light red
1100
OCh
Magenta
0101
05h
Light magenta
1101
ODh
Brown
0110
06h
Yellow
1110
OEh
White
0111
07h
Bright white
mi
OFh
Table 9.4. Background colors (high nibble).
Normal Background
Binary
Hex Byte
Black
Blue
Green
Cyan
Red
Magenta
Brown
Gray
0000
OOh
0001
lOh
0010
20h
0011
30h
0100
40h
0101
50h
0110
60h
0111
70h
To specify an attribute to one of the video calls, logically OR the foreground, background, and
blink, if desired to form a single value. Look at the following coding example. (Hex values are
shown for the attributes.)
#def ine
#define
#define
#def ine
#define
#define
#define
#def ine
#def ine
#define
#define
BLACK
BLUE
GREEN
CYAN
RED
WHITE
GRAY
LTBLUE
LTGREEN
LTCYAN
LTRED
0x00
1x07
0x02
0x03
0x04
0x07
0X08
0x09
0X0A
0X0B
0X0C

<!-- PDF page 162 -->
I
140
Part III ■ MMURTL-Specific Details
#define BGBLACK 0x00
#define BGBLUE 1x70
#define BGGREEN 0x20
#define BGCYAN 0x30
dError = PutVidChars(0, 0,
"This is a test.
.
. " , 17,
BGBLUE | WHITE);
Terminating Your Application
The call ExitJob( ) is provided to end the execution of your program. The exit ( ) function in
a high-level language library will call this function.
It is best to deallocate all system resources before calling Exit Job ( ) . This includes any memory,
exchanges, and other things allocated while you were running. You should also wait for re-
sponses to any requests you made or system calls.
MMURTL will clean up after you if you don't, but an application knows what it allocated,
and from an overall system standpoint it is more efficient to do it yourself. MMURTL has to
hunt for them. Remember, MMURTL is more like a referee on the football field. You will be
terminated and sent to the "sidelines" if necessary.
Any task in your job may call ExitJob( ). All tasks in your job will be shut down at that time
and the application will be terminated.
Replacing Your Application
Your application has the option of "chaining" to another application. This is done by calling
the Chain ( ) function.
This terminates your application and loads another one that you specify. Some of the basic
resources are saved for the new application, such as the job-control block and virtual video
memory. You can pass information to the incoming application by using the SetCmdLineO
function.
Another option to chaining is to tell the operating system what application to run when you
exit. This is done with the SetExitJob( ) call. You can specify the name of a run file to execute
when your job exits. If no file is specified, all the resources are recovered from the job and it is
terminated.

+ 738
- 0
library/Developing Your Own 32-Bit Operating System/Chapter-10-Systems-Programming/Chapter-10-source-text.md Просмотреть файл

@@ -0,0 +1,738 @@
# Developing Your Own 32-Bit Operating System — Chapter-10: Systems Programming
- **Source**: /library/Developing Your Own 32-Bit Operating System/source-file.pdf
- **PDF pages**: 163–180
- **Pages without text**: 163

---

<!-- PDF page 163 -->
Systems Programming

<!-- PDF page 164 -->
142 |
Part III ■ MMURTL-Specific Details
This chapter is aimed at the MMURTL systems programmer. It describes writing message-
based system services and device drivers. Use of the associated operating system calls is described
in a generic format that can be understood by C, Pascal, and assembly language programmers.
Systems programming deals with writing programs that work directly with the operating sys-
tem and hardware or that provide services to application programs.
Systems programmers require a better understanding of the operating system. Application pro-
grammers shouldn't have to worry about hardware and operating-system internal operations
to accomplish their tasks. They should have a basic understanding of multitasking and messag-
ing and should be able to concentrate on the application itself.
If you want (or need) to build device drivers or system services, you should make sure you
understand the material covered in the architecture and applications programming chapters.
They contain theory and examples to help you understand accessing services, messaging, and
memory management operations.
Writing Message-Based System Services
System services are installable programs or built-in operating system services that provide
system-wide, message-based services for application programs, as well as for other services.
MMURTL is a message-based operating system and is designed to support programs in a
client/server environment. Many people associate the term client/server only with a separate com-
puter system that is the server for client workstations. MMURTL takes this down to a single
processor level. A program that performs a specific function or group of functions that are shared
by two or more programs is a system service. This is the basis behind the Request ( ) and
Respond ( ) messaging primitives. Client programs make a request; the service does the process-
ing and responds. This type of messaging can be compared to remote procedure calls with a
twist. The file system and keyboard services are examples of message-based system services.
Initializing Your System Service
The basic steps to initialize your service and start serving requests are listed here:
1. Initialize or allocate any resources required, such as:
Additional memory, if required.
Main service exchange (clients send requests here).
Additional exchanges, if needed.
Additional tasks, if required.
Anything else you need to prepare for business (initialize variables, and so on).
2. Call RegisterSVC( ) with your name and main service exchange.
3. Wait for messages, service them, then respond.

<!-- PDF page 165 -->
Chapter 10 ■ Systems Programming
A Simple System Service Example
Listing 10.1 is the world's simplest system service for the MMURTL operating system. Its
purpose is to hand out unique numbers to each request that comes in. It's not very useful, but
it's a good clean example showing the basic "guts" of a system service. The service name is
"NUMBERS " (note that the name is space-padded). Service names are case-sensitive. The service
will get a pointer in the request block (pDatal) that tells it where the user wants the number
returned.
Listing 10.1. A system service.
/* Super Simple System Service */
/* 64 byte request block structure */
struct RqBlk {
long ServiceExch;
long RespExch;
long RqOwnerJob;
long ServiceRoute;
char *pRqHndlRet;
long dDataO;
long dDatal ;
long dData2;
int ServiceCode;
char npSend;
char npRecv;
char *pDatal ;
long cbDatal ;
char *pData2;
long cbData2;
long RQBRsvdl ;
long RQBRsvd2;
long RQBRsvd3;
};
#define ErcOK 0
#define ErcBadSvcCode 32
struct pRqBlk *RqBlk;
unsigned long NextNumber :
unsigned long MainExch;
unsigned long Message[2];
/* A pointer to a Request Block */
/* The number to return */
/* Where we wait for Requests */
/* The Message with the Request */
void main(void)
{
unsigned long OSError, ErrorToUser;
OSError = AllocExch(&MainExch) ;
if (OSError) {
exit (OSError) ;
/* get an exchange */
look for a kernel error */
OSError = RegisterSvc( "NUMBERS ", MainExch);
if (OSError) { /* look for a system error */

<!-- PDF page 166 -->
144 |
Part III ■ MMURTL-Specific Details
Listing 10.1. continued
exit(OSError) ;
while (1) { /* WHILE forever */
OSError = WaitMsg(MainExch, Message); /* Exch & pointer */
if (lOSError) {
pRqBlk = Message[0]; /* First DWORD contains ptr to RqBlk */
if (pRqBlk.ServiceCode ==0) /* Abort request from OS */
ErrorTollser = ErcOK;
else if (pRqBlk.ServiceCode == 1 ) { /* User Asking for Number */
*pRqBlk.pData1 = NextNumber++; /* Give them a number */
ErrorToUser = ErcOK; /* Respond with No error */
}
else
ErrorToUser = ErcBadSvcCode; /* Unknown Service code! */
OSError = Respond(pRqBlk, ErrorToUser): /* Respond to Request */
}
} /* Loop while(1) */
The Request Block
As you can see, the system service interface using Request ( ) and Respond ( ) is not complicated.
As a system service, when you receive a request, Wait returns and the two-dword message is
filled in. The first dword is the request block handle. This value is actually a pointer to the
request block itself in unprotected operating system memory. The memory address of the re-
quest block and the aliased pointer to the user's memory are active and available only while the
request block is being serviced. This gives the system service the opportunity to look at or use
any parts of the request block it needs.
Several items in the request block are important to the service. The number of items needed
depends on the complexity of the service you're writing.
Items in the Request Block
The request block contains certain items that the service needs to do its job. Programs and the
operating system pass information to the service in various request block items. The complex-
ity of your service, and how much information a program must pass to you, will be determined
by how much information you need to carry out the request. As a system service writer, you
define the data items you need, and it is your responsibility to document this information for
application programmers.

<!-- PDF page 167 -->
Chapter 10 ■ Systems Programming
The service must never attempt to write to the request block. In fact, installable services would
find themselves terminated for the attempt. The request block is in operating-system memory
that is read-only to user-level jobs. An installable service will operate at the protection and ac-
cess user levels.
The Service Code
A single word (a 1 6-bit value) is used for the callers to indicate exactly what service they are
requesting from you. This is called the service code. For example, in the file system, one num-
ber represents OpenFile, another CloseFile, and another RenameFile. In the simple example
above, the service only defines one service code that it handles. This is the number 1 (one).
The operating system reserves service code 0 (zero) for itself in all services. You may not use or
define service code 0 to callers in your documentation. When a service receives a request with
service code 0, the operating system is telling you that a job (a user program or another service)
either has exited or has been shut down by the operating system. How you handle this depends
on how complex your service is, and whether or not you hold requests from multiple callers
before responding.
For all other values of the service code, you must describe in your documentation what request
information you expect from the caller and what service you provide. If you receive a service
code you don't handle or haven't defined, you should return the error message Bad Service
Code, which is defined in standard MMURTL header files.
Caller Information in a Request
Three dwords are defined to allow a program to pass information to the service you require to
do your job.
These items are dDatae, dDatal, and dData2. They are 32-bit values, and whether or not they
are signed or unsigned is up to you (the system service writer). They are one-way data values
from the caller to you. They may not contain pointers to the caller's memory area, because the
operating system does not alias them as such. A prime example of their use is in the file system
service, where callers pass in a file handle using these data items.
For larger data movement to the service, or any data movement back to the caller, the request
block items pDatal and pData2 must be used. These two request block items are specifically
designed as pointers to a data area in the caller's memory space. The size of the area is defined
by the accompanying 32-bit values, cbDatal and cbData2. The operating system is "aware" of
this, and it will alias the pointers so the caller's memory space is accessible to the service. As a
service, you must not attempt to access any data in the caller's space other than what is defined
by pDatal , cbDatal and pData2, cbData2. Also, be aware that these pointers are valid only from
the time you receive the request and until you respond.

<!-- PDF page 168 -->
H
Part III ■ MMURTL-Specific Details
Asynchronous Services
Most services will be synchronous. Synchronous services wait, receive a request, and then re-
spond before waiting for the next request. All operations are handled in a serial fashion. This is
the easiest way to handle requests.
A service may receive and hold more than one request before responding. This type of service
is defined as asynchronous. This type of service may even respond to the second request it re-
ceives before responding to the first.
This type of service requires one or more additional exchanges to hold the outstanding requests.
The keyboard service is a good example of an asynchronous service. In the keyboard service,
callers can make a request to be notified of global key codes. These key codes may not occur for
long periods of time, and many normal keystrokes may occur in the meantime. This means the
keyboard service must place the global key code requests on another exchange while it handles
normal requests.
The kernel primitive MoveRequest ( ) takes a request you have received using Wait, and moves it
to another exchange. You can then use WaitMsg or CheckMsg to get the requests from this sec-
ond hold exchange when you know you can respond to them.
While you are holding requests asynchronously, you may receive an abort service code from
the operating system (service code 0). If this happens, you need to ensure that you look at all
the requests you are holding to see if this abort is for one of those requests. If so, you must
respond to the held request identified by the abort immediately, before responding to any other
requests you are holding. This is so that the operating system can reclaim the request block,
which is a valuable system resource. The abort request contains the job number of the aborted
program in dDataG. You would look at each request block in the RqOwnerJob field to see if this
request came from it. If so, you must respond without accessing its memory or acting on its
request.
System Service Error Handling
When the service responds, the error (or status) to the user is passed back as the second dword
parameter in the respond call. The first is the request block handle.
When you write system services, you must be aware of conventions used in error passing and
handling. Some operating systems simply give you a true or false style of response as to whether
or not the function you requested was carried out without errors.
MMURTL uses this concept but carries it one step further. You return a 0 (zero) if all went
well. If an error occurred or if you need to pass some kind of status back to the caller, you
return a number that indicates what you want to tell them. An example of this is how the file
system handles errors. OpenFile( ) returns a 0 error code if everything went fine. However, it

<!-- PDF page 169 -->
Chapter 10 ■ Systems Programming
may return five or six different values for various problems that can occur during the OpenFile ( )
process. The file may already be opened in an incompatible mode by someone else, the file
may not exist, or maybe the file system couldn't allocate an operating system resource needed
to open your file. In each of these cases, you would receive a non-zero number indicating the
error. You don't have to access a global in your program or make another call to determine
what went wrong.
|
147
Writing Device Drivers
Device drivers control or emulate hardware on the system. Disk drives, communications ports
(serial and parallel), tape drives, RAM disks, and network frame handlers are all examples of
devices (or pseudo devices) that require device drivers to control them. MMURTL has stan-
dardized entry points for all devices on the system.
MMURTL is built in layers. The device drivers are the closest layer to the operating system.
You should not confuse device drivers with message-based system services. Device drivers are
accessed via call gates transparent to the applications programmer that allow callers to easily
and rapidly access code with a procedural interface from high-level languages (C and Pascal for
example), or by simply "pushing" parameters on the stack in the assembler and making a call.
MMURTL provides several built-in device drivers including floppy disk, hard disk, basic video,
keyboard, and communication (serial and parallel).
Devices in MMURTL are classified as two different types: random- and sequential-oncnttd.
Random-oriented devices are things such as disk drives and RAM disks that require the caller
to tell the driver how much data to read or write, as well as where the data is on the device.
Network frame handlers may also be random because a link layer address is usually associated
with the read and write functions, although they may be sequential too. Some types of tape
drives allow reading and writing to addresses (sectors) on the tape and therefore may also be
random devices. Sequential devices are things like communications ports, keyboard, and se-
quential video access (ANSI drivers, and so on). Most byte-oriented devices tend to be sequen-
tial devices because they don't have a fixed-size block and are not randomly accessed.
The MMURTL standard device driver interface can handle both sequential and random de-
vices with fixed or variable block and/or sector lengths. The interface is non-specific.
All device drivers use a device control block (DCB). The device driver defines and keeps the
DCB in its memory space, which actually becomes the operating system memory space when
you load it. When a device driver initializes itself with the call initDevDr ( ) , it passes in a pointer
to its DCB. If a device driver controls more than one device, it will pass in a pointer to an array
of DCBs. Things like Device Name, Block Size, and Device Type are examples of the fields in
a DCB. Each device driver must also maintain in its data area all hardware specific data (vari-
ables) needed to control and keep track of the state of the device.

<!-- PDF page 170 -->
H
Part III ■ MMURTL-Specific Details
Device Driver Theory
MMURTL is a protected-mode, paged memory operating system that uses two of the four
possible protection levels of the 386/486 processors. The levels are 0 and 3 and are referred to
as System (0) and User (3). I have made it rather painless to build fast, powerful, device drivers
and have even gone into substantial detail for beginners.
Device drivers must have access to the hardware they control. Device drivers have complete
access to processor I/O ports for their devices without operating system intervention. Only
system code (protection level 0) is allowed processor port I/O access. This means message- or
request-based system services and application code (both at user level 3) cannot access pons at
all; otherwise, a processor protection violation will occur. For example, the file system is at user
level 3. It does not directly access hardware. The device-driver builder must not access ports
other than those required to control its hardware.
Device drivers must not attempt to direcdy control the system DMA, timer hardware, inter-
rupt vectors, PICUs, or any operating system structures. MMURTL provides a complete array
of calls to handle all the hardware, which ensures complete harmony in MMURTL's real-time,
multitasking, multithreaded environment, while providing a fair amount of hardware inde-
pendence. MMURTL takes care of synchronization of system-level hardware access so device-
driver programmers don't have to worry about it. Those who have written device drivers on
other systems will appreciate this concept.
Building Device Drivers
When the device driver is installed by the loader, or included in the operating system code at
build time, it has to make a system call to set itself up as a device driver in the system. Addi-
tional system calls may have to be made to allocate or initialize other system resources required
before it can handle its device.
Device drivers can be doing anything from simple disk device emulation (RAM disk, for ex-
ample), to handling complex devices such as hard drives, SCSI ports, or network frame han-
dlers (network link layer devices).
The call to set up as a device driver is InitDevDr( ). With InitDevDr( ), you provided a pointer
to your device control blocks (DCB) after you filled in the required information. The DCB is
shared by both the device driver and operating system code.
Before calling InitDevDr, you will most likely have to allocate other system resources and pos-
sibly even set up hardware interrupts that you will handle from your device. Additional system
resources available to device drivers are discussed later in this chapter.
The device driver looks, and is programmed, like any application in MMURTL. It has a main
entry point you specify when writing the program. In C this would be main ( ) ; in Pascal it's the
main program block. In assembler, this would be the START entry point specified for the

<!-- PDF page 171 -->
Chapter 10 ■ Systems Programming
program. This code is immediately executed after loading the driver, just like any other pro-
gram. This code will execute only once to allow you to initialize the driver. The main entry
point will never be called again. This will generally be a very small section of code (approxi-
mately 30 lines).
How Callers Reach Your Driver
All device drivers are accessed via one of three public calls defined in the operating system. You
will have functions in your driver to handle each of these calls. When you fill in the DCB, you
will fill in the addresses of the three entry points the operating system will call on behalf of the
original caller.
Your driver program must interface properly with the three public calls. You can name them
anything you want in your program, but they must have the same number and type of param-
eters (arguments) as the PUBLIC calls defined in the operating system for all device drivers. The
operating system defines the three following PUBLIC calls:
Devicelnit()
DeviceOpO
DeviceStat()
The parameter listings and function expectations are discussed in detail later in this chapter.
Your functions may not have to do anything when called depending on the device you are
controlling. They must at least accept the calls and return a status code of 0 (for no error) if
they ignore it. When programs call one of these PUBLIC calls, MMURTL does some house-
keeping and then calls your specified function.
Device Driver Setup and Installation
The initialization section of your device driver must make certain calls to set up and install as
a driver. The generic steps are described here (your actual calls may vary depending on what
system resources you need):
1 . Initialize or allocate any resources required.
Allocate any additional memory, if required.
Allocate exchanges, if needed for messaging from a second task or the ISRs (see the
floppy device driver for an example of a device driver with a separate task to handle
hardware interrupts).
Set up interrupt service routines, if needed.
Check or set up the devices, if needed.
Do anything else you need to before being called.
2. Enter the required data in the DCBs to pass to the InitDevDr( ) call.

<!-- PDF page 172 -->
150
Part III ■ MMURTL-Specific Details
3. Enable (unMaskIRQ( )) any hardware interrupts you are servicing, unless this is done in
one of the procedures (such as Device I nit ( )) as a normal function of your driver.
4. Call InitDevDr().
InitDevDr never returns to you. It terminates your task and leaves your code resident to be
called by the operating system when one of the three device-driver calls is made.
At this point, you have three device-driver routines that are ready to be called by the operating
system. It will be the operating system, not the users of the devices, that calls them. This is
because the operating system has a very small layer of code that provides common entry points
to the three device-driver calls for rf// drivers. This layer of code performs several functions for
you. One very important function is blocking for non-re-entrant device drivers. In a multitasking
environment it is likely that a device-driver function could be called while someone else's code
is already executing it, especially if the driver controls two devices. One of the items you fill out
in the DCB is a flag (f DevReent) to tell the operating system whether your driver is re-entrant.
Most device drivers are not re-entrant, so this will normally be set to FALSE (0).
System Resources for Device Drivers
Because they control the hardware, device drivers are very close to the hardware. They require
special operating system resources to accomplish their missions. Many device drivers need to
use DMA and timer facilities, and they must service interrupts from the devices they control.
MMURTL has a complete array of support calls for such drivers.
Interrupts
Several calls are provided to handle initializing and servicing hardware interrupt service rou-
tines (ISRs) in your code. See the section that deals specifically with ISRs for complete descrip-
tions and examples of simple ISRs. The floppy and hard-disk device drivers also provide good
ISR examples. ISRs should be written in assembly language for speed and complete code con-
trol, although it's not a requirement. The following is a list with brief descriptions of the calls
you may need to set up and use in your ISR:
SetIRQVector( ) is how you tell the operating system what function to call when your
hardware interrupt occurs. This is often called an interrupt vector.
EndOfIRQ() sends an "End of Interrupt" signal to the programmable interrupt control-
ler units (PICU), and should be called as the last thing the ISR does. In other operat-
ing systems, specifically DOS, the ISR usually sends the EOI sequence directly to the
PICU. Do not do this in MMURTL, as other code may be required for execution in
later versions of MMURTL, which will render your device driver obsolete.
MaskIRQ() and UnMaskIRQ() are used to turn on and off your hardware interrupt by
masking and unmasking it directly with the PICU. More detail about each of these
calls can be found in the alphabetical listing of operating system calls in Chapter 1 5i
"API Specification".

<!-- PDF page 173 -->
Chapter 10 ■ Systems Programming
Direct Memory Access Device (DMA)
If your device uses DMA, you will have to use the DMASetUp ( ) call each time before instructing
your device to make the programmed DMA transfer. The DMASetUp ( ) call lets you set a DMA
channel for the type, mode, direction, and size of a DMA move. DMA also has some quirks
you should be aware of, such as its inability to cross-segment physical boundaries (64 K). Your
driver should use the AllocDMAPage( ) call to allocate memory that is guaranteed not to cross a
physical segment boundary and also provide you with the physical address to pass to DMASetUp ( )
.
It is important to understand that the memory addresses your program uses are linear addresses
and do not equal physical addresses. MMURTL uses paged memory, and the addresses you
use will probably never be equal to the physical address! Another thing to consider is that DMA
on the ISA platforms is limited to a 16MB physical address range. Program code can, and will
most likely, be loaded into memory above the 16MB limit if you have that much memory in
your machine. This means all DMA transfers must be buffered into memory that was allo-
cated using AllocDMAPage( ). See the operating system public call descriptions for more infor-
mation on DMASetUp ( ) and how to use AllocDMAPage ( ) . Many users of DMA also need to be
able to query the DMA count register to see if the DMA transfer was complete, or how much
was transferred. This is accomplished with GetDMACount( ).
Timer Facilities
Quite often, device drivers must delay (sleep) while waiting for an action to occur or have the
capability to "time-out" if an expected action doesn't occur in a specific amount of time. Two
calls provide these functions. The Sleep ( ) call actually puts the calling process to sleep (makes
it wait) for a specific number of 10-millisecond periods. The Alarm ( ) call will send a message
to a specified exchange after a number of caller-specified 1 0-millisecond increments. The Alarm ( )
call can be used asynchronously to allow the device code to continue doing something while
the alarm is counting down. If you use Alarm ( ) , you will most likely need to use KillAlarm( ) ,
which stops the alarm function if it hasn't already fired off a message to you. See the call de-
scriptions for more information on the Sleep ( ), Alarm ( ), and KillAlarm( ) functions.
Message Facilities for Device Drivers
Interrupt service routines (if you need them in your driver) sometimes require special messag-
ing features that application programs don't need. A special call that may be required by some
device drivers is ISendMsg ( ) . ISendMsg ( ) is a special form of SendMsg ( ) that can be used inside
an ISR (with interrupts disabled). It doesn't force a task switch even if the destination exchange
has a higher priority process waiting there. ISendMsg ( ) also does wofre-enable interrupts before
returning to the ISR that called it, so it may be called from an ISR while interrupts are dis-
abled. ISendMsg () is used if an ISR must send more than one message from the interrupted
condition. If you must send a message that guarantees a task switch to a higher priority task, or
you only need to send one message from the ISR, you can use SendMsg ( ) . If you use SendMsg ( ) ,
it must be x.\\e final cA\ in your ISR after the EndOf IRQ( ) call has been made and just before the
IRETD (return from interrupt). This will guarantee a task switch if the message is to a higher
priority task.
H

<!-- PDF page 174 -->
152 |
Part III ■ MMURTL-Specific Details
Detailed Device Interface Specification
The following sections describe the device control block (DCB), each of the three PUBLIC de-
vice calls, and how to implement them in your code.
Device Control Block Setup and Use
The DCB structure is shown in the following table with sizes and offsets specified. How the
structure is implemented in each programming language is different, but easy to figure out.
The length of each field in the DCB is shown in bytes, and all values are unsigned. You can
look at some of the included device driver code with MMURTL to help you.
The size of a device control block is 64 bytes. You must ensure there is no padding between the
variables if you use a record in Pascal, a structure in C, or assembler to define the DCB. If your
driver controls more than one device, you will need to have a DCB for each one it controls.
Multiple DCBs must be contiguous in memory. Table 10.1 presents the fields that define a
DCB.
Table 10.1. Device control block definition.
Name
Size Offset Description
DevName
12
0
cbDevName
1
12
DevType
1
13
nBPB
2
14
dLastDevErc
4
16
ndDevBlocks
4
20
pDevOp
4
24
pDevInit
4
28
pDevStat
4
32
fDevReent
1
36
fSingleUser
1
37
wJob
2
38
OSUseONLYl
4
40
0SUse0NLY2
4
44
0Suse0NLY3
4
48
0Suse0NLY4
4
52
0Suse0NLY5
4
56
0Suse0NLY6
4
60
Device name, left justified
Length of device name in bytes
1 = RANDOM device, 2 = SEQUENTIAL
Bytes Per Block (1 to 65535 max.) (0 for variable
block size)
Last error code from an operation
Number of blocks in device (0 for sequential)
Pointer to device operation handler
Pointer to device Init handler
Pointer to device status handler
Is device handler re-entrant?
Is device usable from 1 job only?
If fSingleUser true, this is job
Allocate and 0, but do not modify
Allocate and 0, but do not modify
Allocate and 0, but do not modify
Allocate and 0, but do not modify
Allocate and 0, but do not modify
Allocate and 0, but do not modify

<!-- PDF page 175 -->
Chapter 10 ■ Systems Programming
The fields of the DCB must be filled in with the initial values for the device you control before
calling lnitDevDr( ). Most of the fields are explained satisfactorily in the descriptions next to
them, but the following fields require detailed explanations:
DevName Each driver names the device it serves. Some names are standardized in
MMURTL, but this doesn't prevent the driver from assigning a non-standard name.
The name is used so that callers don't need to know a device's number in order to use
it. For instance, floppy disk drives are usually named FDO and FD1, but a driver for
non-standard floppy devices can name them anything (up to 12 characters). Each
device name must be unique.
cbDevName A single byte containing the number of bytes in the device name.
DevType This indicates whether the device is addressable. If data can be reached at a
specific numbered address as specified in the DeviceOP( ) call parameter dLBA (logical
block address), then it's considered a random device and this byte should contain a 1.
If the device is sequential, and the concept of an address for the data has no meaning
(such as with a communications port), then this should contain a 2.
nBPB The number of bytes per block. Disk drives are divided into sectors. Each
sector is considered a block. If the sectors are 512-byte sectors (MMURTL standard),
then this would be 512. If it is another device, or it's a disk drive that is initialized for
1 024-byte sectors, then this would reflect the true block size of the device. If it is a
single-byte oriented device such as a communications port or sequential video, then
this would be 1 . If the device has variable length blocks, then this should be 0.
dLastDevErc When a driver encounters an error, usually an unexpected one, it
should set this variable with the last error code it received. The driver does not need to
reset this value as MMURTL resets it to zero on entry to each call to DeviceOP for
each device.
ndDevBlocks This is the number of addressable blocks in your device. For example,
on a floppy disk this would be the number of sectors X number of cylinders X number
of heads on the drive. MMURTL expects device drivers to organize access to each
random device by 0 to n logical addresses. An example of how this is accomplished is
with the floppy device driver. It numbers all sectors on the disk from 0 to
nTotalSectors (nTotalSectors is nHeads * nSecPerTrack * nTracks). The head
number takes precedence over track number when calculating the actual position on
the disk. In other words, if you are on track zero, sector zero, head zero, and you read
one sector past the end of that track, we then move to the next head, not the next
track. This reduces disk arm movement, which increases throughput. The hard disk
driver operates the same way. Addressing with 32 bits limits address access to 4GB on
a single-byte device and 4GB X sector size on disk devices, but this is not much of a
limitation. If the device is mountable, or allows multiple media sizes, this value should
be set initially to allow access to any media for identification; this is device dependent.
S

<!-- PDF page 176 -->
154| Part III ■ MMURTL-Specific Details
pDevOp, pDevlnit, and pDevStat These are pointers to the calls in your driver for the
following PUBLIC device functions:
DeviceOp — Performs I/O operations in device
Device I nit — Initializes or resets the device
DeviceStats — Returns device-specific status
MMURTL uses indirect 32-bit near calls to your driver's code for these three calls.
The return instruction you use should be a 32-bit near return. This is usually dictated
by your compiler or assembler and how you define the function or procedure. In C-32
(MMURTL's standard C compiler), defining the function with no additional
attributes or modifiers defaults it to a 32-bit near return. In assembler, RETN is the
proper instruction.
fDevReent This is a 1-byte flag (0 false, non-zero true) that tells MMURTL if the
device driver is re-entrant. MMURTL handles conflicts that occur when more than
one task attempts to access a device. Most device drivers have a single set of variables
that keep track of the current state of the device and the current operation. In a true
multitasking environment more than one task can call a device driver. For devices that
are not re-entrant, MMURTL will queue up tasks that call a device driver currently in
use. The queuing is accomplished with an exchange using the SendMsg( ) and
WaitMsg( ) primitives. A portion of the reserved DCB is used for this purpose. On very
complicated devices such as a SCSI driver, where several devices are controlled though
one access point, the driver may have to tell MMURTL it's re-entrant, and handle
device conflicts internally.
fSingleUser If a device can only be assigned and used by one job (user) at a time,
this flag should be true (non-zero). This applies to devices like communications ports
that are assigned and used for a session.
wJob If the fSingleUser is true, this will be the job that is currently assigned to the
device. If this is zero, no job is currently assigned to the device. If fSingleUser is false,
this field is ignored.
OSUseONLYi ,2,3,4,5,6 These are reserved for operating system use and device drivers
should not alter the values in them after the call to lnitDevDr( ). They must be set to
zero before the call to InitDevDr().
Standard Device Call Definitions
As described previously, all calls to device drivers are accessed through three predefined PUBLIC
calls in MMURTL. They are these:
DeviceOp(dDevice, dOpNum, dLBA, ndBlocks, pData) :dError
DeviceInit(dDevice, plnitData, sdlnitdata) :dError
DeviceStat(dDevice, pStatRet, sdStatRetmax) :dError

<!-- PDF page 177 -->
Chapter 10 M Systems Programming
Detailed information follows for each of these calls. The procedural interfaces are shown with
additional information such as the call frame offset, where the parameters will be found on the
stack after the function has set up its stack frame using Intel standard stack frame entry tech-
niques. Note that your function should also remove these parameters from the stack. All func-
tions in MMURTL that return errors use the EAX register. Your function implementations
must do the same. Returning zero indicates successful completion of the function. A non-zero
value indicates an error or status that the caller should act on. Standard device errors should be
used when they describe well the error or status you wish to convey to the caller. Device-
specific error codes should be included with the documentation for the driver.
DeviceOp Function Implementation
The DeviceOp ( ) function is used by services and programs to carry out normal operations such
as Read and Write on a device. The call is not device specific and allows any device to be inter-
faced through it. The dOpNum parameter tells the driver which operation is to be performed. An
almost unlimited number of operations can be specified for the device operation call (232). The
first 256 operations (dOp number) are pre-defined or reserved. They equate to standard de-
vice operations such as read, write, verify, and format. The rest of the dOp Numbers may be
implemented for any device-specific operations as long as they conform to the call parameters
described here.
dError = DeviceOp(dDevice,dOpNum,dLBA,dnBlocks)pData) ;
The call frame offsets for assembly language programmers are these:
dDevice [EBP+24]
dOpNum [EBP+20]
dLBA [EBP+16]
dnBlocks [EBP+12]
pData [EBP+08]
Parameter descriptions:
dDevice The device number
dOpNum Identifies which operation to perform
0 Null operation
1 Read (receive data from the device)
2 Write (send data to the device)
3 Verify (compare data on the device)
4 Format Block (tape or disk devices)
5 Format Track (disk devices only)
6 Seek Block (tape or disk devices only)

<!-- PDF page 178 -->
\156
Part III U MMURTL-Specific Details
7 Seek Track (disk devices only)
(Communications devices)
10 OpenDevice (communications devices)
1 1 CloseDevice (communications devices)
(RS-232 devices with explicit modem control)
15SetDTR
16 SetCTS
Undefined operation numbers below 255 are reserved
256-n driver defined (driver specific)
dLBA Logical block address for I/O operation.
For sequential devices, this parameter will be ignored.
dnBlocks Number of contiguous blocks for the operation specified. For sequential
devices, this will simply be the number of bytes.
pData Pointer to data (or buffer for reads) for specified operation
DevkeStat Function Implementation
The DeviceStat( ) function provides a way for device-specific status to be returned to a caller
if needed. Not all devices will return status on demand. In cases where the function doesn't, or
can't, return status, you should return 0 to pdStatusRet and return the standard device error
ErcNoStatus.
dError = DeviceStat(dDevice,pStatRet,dStatusMax,pdStatusRet) ;
The call frame offsets for assembly language programmers are these:
dDevice [EBP+20]
pStatusRet [EBP+16]
dStatusMax [EBP+12]
pdStatusRet[EBP+08]
Parameter descriptions:
dDevice Device number to status
pStatBuf Pointer to buffer where status will be returned
dStatusMax Caller sets this to tell you the maximum size of status to return in bytes
pdStatusRet Pointer to dword where you return actual size of status returned in
bytes

<!-- PDF page 179 -->
Chapter 10 ■ Systems Programming icy
Devicelnit Function Implementation
Some devices may require a call to initialize them before use or to reset them after a catastro-
phe. An example of initialization would be a communications port for baud rate, parity, and so
on. The size of the initializing data and its contents are device specific and should be defined
with the documentation for the specific device driver.
dError = Devicelnit (dDevice,pInitData,dInitData) ;
The call frame offsets for assembly language programmers are these:
dDevice [EBP+16]
plnitData [EBP+12]
dlnitData [EBP+08]
Parameter descriptions:
dDevice Dword indicating device number
plnitData Pointer to device specific data for initialization to be returned
dlnitData Dword indicating maximum size of status to return in bytes
Initializing Your Driver
InitDevDr( ) is called from a device driver after it is first loaded to let the operating system in-
tegrate it into the system. After the device driver has been loaded, it should allocate all system
resources it needs to operate and control its devices while providing service through the three
standard entry points. A 64-byte DCB must be filled out for each device the driver controls
before this call is made. When a driver controls more than one device, it must provide the device
control blocks for each device. The DBCs must be contiguous in memory. If the driver is flagged
as not re-entrant, then all devices controlled by the driver will be locked out when the driver is
busy. This is because one controller, such as a disk or SCSI controller, usually handles multiple
devices through a single set of hardware ports, and one DMA channel if applicable, and can't
handle more than one active transfer at a time. If this is not the case, and the driver can handle
two devices simultaneously, the driver should be broken into two separate drivers.
The definition and parameters to lnitDevDr( ) are as follows:
InitDevDr(dDevNum, pDCBs, nDevices, fReplace)
dDevNum This is the device number that the driver is controlling. If the driver
controls more than one device, this is the first number of the devices. This means the
devices are numbered consecutively.

<!-- PDF page 180 -->
s
Part III ■ MMURTI-Specific Details
pDCBs This is a pointer to the DCB for the device. If more than one device is
controlled, this is the pointer to the first in an array of DCBs for the devices. This
means the second DCB must be located at pDCBs + 64, the third at pDCBs + 128, and
so on.
nDevices This is the number of devices that the driver controls. It must equal the
number of contiguous DCBs that the driver has filled out before the InitDevDr call is
made.
f Replace If true, the driver will be substituted for the existing driver functions
already in place. This does not mean that the existing driver will be replaced in
memory, it only means the new driver will be called when the device is accessed. A
replacement driver must specify at least as many devices as the original driver handled.
Operating System Functions for Device Drivers
The following is a list of functions that were specifically designed for device drivers. They per-
form many of the tedious functions that would otherwise require assembly language, and a
very good knowledge of the hardware. Please use the MMURTL API reference for a detailed
description of their use.
AllocDMAMem Allocates memory that will be compatible with DMA operations. It
also returns the physical address of the memory which is required for the DMASetUp
call.
EndOf IRQ Resets the programmable interrupt controller unit (PICU) at the end of
the ISR sequence.
MasklRQ Masks one interrupt (prevents it from interrupting) by programming the
PICU.
SetlRQVector Sets up a vector to your ISR.
DMASetUp Programs the DMA hardware channel specified to move data to or from
your device.
UnMasklRQ Allows interrupts to occur from a specified channel on the PICU.
Standard Device Error Codes
The MMURTL device-driver interface code will return errors for certain conditions such as a
device that's not installed being called. Your device drive will also return error codes for prob-
lems it has honoring the device call. If an error code already defined in MMURTL's standard
header file adequately describes the error or status, then use it.

+ 1081
- 0
library/Developing Your Own 32-Bit Operating System/Chapter-11-The-MMURTL-Monitor-Program/Chapter-11-source-text.md
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 288
- 0
library/Developing Your Own 32-Bit Operating System/Chapter-12-The-Debugger/Chapter-12-source-text.md Просмотреть файл

@@ -0,0 +1,288 @@
# Developing Your Own 32-Bit Operating System — Chapter-12: The Debugger
- **Source**: /library/Developing Your Own 32-Bit Operating System/source-file.pdf
- **PDF pages**: 203–210
- **Pages without text**: 203

---

<!-- PDF page 203 -->
12
The Debugger

<!-- PDF page 204 -->
182 Part III ■ MMURTL-Specific Details
The debugger is built into the operating system. It is not a separate run file. The debugger
provides the following functions:
Display of instructions (disassembled)
Dumping of linear memory as bytes or dwords
Display of exchanges, and waiting messages or tasks
Display of active tasks
Set and clear an instruction breakpoint
Full register display
Selection of linear address to display
Display of important OS structure addresses
Using the Debugger
In its current incarnation, the debugger is an assembly language, nonsymbolic debugger. Ex-
tensive knowledge of the Intel processor instruction set is required to properly use the debugger.
Entering the Debugger
The debugger may be entered using the Debugger function key in the monitor, the Debug
command in the command-line interpreter, or by placing an INT 03 instruction anywhere in
your application's assembly language file. See the "Debugger Theory" section for more infor-
mation on INT 03 instruction usage.
The debugger may also start on its own if certain exceptions occur. Some exceptions are de-
signed to take you directly into the debugger and display an error code indicating the problem.
The most common exceptions are the General Protection Fault (OD hex) and the Page Fault
(OE hex). When the exception occurs, the debugger is entered, and a red banner is displayed,
along with the exception number. The registers can be examined to see where it happened, and
why. This usually provides enough information to correct the offending code.
Exiting the Debugger
Pressing the Esc key will exit the debugger and begin execution at the next instruction.
If the debugger was entered on a fault, pressing Esc will restart the debugger at the offending
address and you will re-enter the debugger. See the "Debugger Theory" section for more infor-
mation.
The debugger will also exit using the Single Step command (Fl function key), and will be re-
entered immediately following the execution of the next complete instruction.

<!-- PDF page 205 -->
Chapter 12
The Debugger
Debugger Display
The debugger display is divided into three sections. The complete general register set is dis-
played along the right side of the screen, function keys are across the bottom, and the left side
is the instruction and data display area.
Certain debugger display functions, such as dumping data, will clear the screen for display.
The registers and function keys will be redisplayed when the function is finished.
Debugger Function Keys
The debugger function key actions are described here:
Fl SStep — Single Step. This returns to the application and executes one instruction, after which
it immediately returns to the debugger where the next active code address and its associated
instruction are displayed.
F2 SetBP — Set Breakpoint. This sets the breakpoint at the currently displayed instruction.
You may move down through the instructions, without executing them, using the down arrow
key. You may also use F8 CrntAdd (Current Address) to select and display a new address to
display before setting the breakpoint.
F3 ClrBP — Clear Breakpoint. This clears the single breakpoint.
F4 CS:EIP — Goto CS:EIP. This redisplays the current instruction pointer address. This be-
comes the active address.
F5 Exch — Display Exchanges. This displays all exchanges and shows messages or tasks that
may be waiting there. Only exchanges that actually have a message or task will be displayed.
F6 Tasks — Display Tasks. This displays all active tasks on the system. The task number, the
associated job number, the address of the task's JCB, the address of the TSS, and the priority
of the task are displayed with each one.
F8 CrntAdd — Change Current Address. This enables you to set the code address the debugger
is displaying (disassembling). This does not change the address scheduled for execution. The
F4 CS:EIP function key can be used to return to the next address scheduled for execution.
F9 DumpB — Dump Bytes. This requests a linear address, and then dumps the bytes located at
that address. The address is displayed on the left, and all the data, including the ASCII, is shown
to the right. The format is the same as the dump command in the CLI.
F 1 0 — Dump Dwords. This requests a linear address, and then dumps the dwords located at
that address. The address is displayed on the left, and all the data, including the ASCII, is shown
to the right. Each byte of a dword is displayed in the proper order (high order, next, next, low
order). This command is useful for dumping structures that are dword oriented, as most of
them are.

<!-- PDF page 206 -->
I
184
Part III ■ MMURTL-Specific Details
F12 Addlnfo — Address Information. This lists names and linear addresses of important struc-
tures used during OS development. These structures can also be very useful during program
debugging. These values are hard coded at system build. The abbreviations are given in the
following list, with the full name of the structure and what use it may be during debugging.
IDT — Interrupt Descriptor Table. This can be dumped to view interrupt vector types
and addresses. This may be useful if you have set up an interrupt and you want to
ensure it was encoded properly and placed in the table.
GDT — Global Descriptor Table. This can be dumped to view all of the GDT entries.
The GDT contains many items, including the TSS descriptor entries for your pro-
gram. TSS descriptors point to your task state segment and also indicate the privilege
level of the task.
RQBs — Request Block Array. This is the address of the first request block in the array
of request blocks allocated during system initialization. The request handle that you
receive from the request primitive is actually a pointer, a linear address, that will be
somewhere in this array.
TSS1 — First TSS. The first two task state segments are static structures and are used
for the operating system Monitor and debugger. This is the address of the monitor
TSS. The debugger TSS is 512 bytes after this address.
TSS3 — Dynamic TSSs. The rest of the task state segments are in several pages of
allocated memory and are initialized during system startup. TSSs are numbered
sequentially, which makes the first of the dynamic TSSs numbered three. Each TSS is
512 bytes.
LBs — Link Blocks. These are 16-byte structures used as links in chains of messages
and tasks waiting at an exchange. The array of link blocks are allocated as a static
array.
RdyQ — Ready Queue. This is the address of the 32 queue structures where tasks that
are ready to run are queued.
JCBs — Job Control Blocks. This the address of the array of Job Control Blocks. This
is an array of 512-byte structures. The JCB is discussed in detail in Chapter 9,
"Application Programming."
S VCs — Services Array. This is the address of the array of currently registered services.
The names of the services and the exchanges they service are contained here.
Exch — Exchanges. This is the address of the array of exchanges in allocated memory.
When you receive an exchange number from the AllocExch( ) function, it is the index
of that exchange in the array. Each exchange is 1 6 bytes.
aTmr — Timer Blocks. This is the address of the array of timer blocks. The Sleep ( )
and Alarm ( ) functions each set up a timer block.

<!-- PDF page 207 -->
Chapter 12 ■ The Debugger H
Debugging Your Application
If your application is "going south" on you (crashing, locking up, entering the debugger, or
whatever), you can set breakpoints in several locations of your program by editing the .ASM
files, listed in your ATF file. You would insert the INT 03 instruction wherever you feel it would
be useful.
In some cases, just knowing how far your program made it is enough to solve the problem.
You can start by searching for main in your primary assembly language file generated by the C
compiler, or for .START in an assembly language program, and placing an INT 03 instruction
there.
It may help you to be able to identify high-level entry and exit code sequences (begin and end
of a C function), and also how local variables are accessed.
The following example shows typical entry and exit code found in a high-level procedure.
PUSH EBP
MOV EBP, ESP
; Lots of code here
MOV ESP, EBP
POP EBP
RETN
When local variables are accessed, they are always referenced below the frame pointer in memory.
A single 4-byte integer is referenced as [EBP-4] or [EBP+FFFFFFFC], and displayed as an
unsigned number.
Stack parameters, or arguments, are always referenced above the frame pointer, such as
[EBP+12]. With near calls like you would make inside your program, the last parameter is al-
ways [EBP+8].
There is a symbolic debugger in MMURTL's near future, but for right now, it's down and
dirty troubleshooting.
Debugger Theory
The Intel 32-bit processors have very powerful debugging features. They have internal debug
registers that allow you to set code and data breakpoints. What this means is that you can set
the processor to execute an interrupt, actually a trap, whenever any linear memory location is
accessed, or at the beginning of any instruction.
The INT 03 instruction may also be placed in your code, like with the older processors, without
having to fill in special registers. There are four debug address registers in the processor, which
means you can have four active breakpoints in your program if you use only the registers. The
MMURTL debugger currently uses only one of these registers and allows only instruction
breakpoints to be set.

<!-- PDF page 208 -->
I
186
Part III ■ MMURTL-Specific Details
The INT 03 interrupt is just like any other on the system. When it occurs, entry 3 in the inter-
rupt table is used to vector to a procedure or task. In MMURTL, it is an interrupt procedure
that will switch to the debugger task after some fix-ups have been completed.
Debugging in a multitasking operating system is complicated to say the least. The 386/486
processors make it much easier because they let you determine if the breakpoint is local to a
single task or global to all tasks. Remember that several tasks may be executing the same piece
of code. If you set a breakpoint using the INT 03 instruction, every task that executes it will
produce the interrupt and enter the debugger. For applications, this really doesn't apply as much
because they each have their own linear address space. The paging hardware translates the lin-
ear address to a physical address for its own use.
MMURTL's debugger is set up as a separate job. It has its own Job Control Block (JCB) and
one task (its own TSS). The debugger is the highest priority task on the system (level 1). No
other task operates at a priority that high. The debugger also has its own virtual screen, just like
applications do.
When writing an operating system, the debugger becomes one of the most important tools
you can have. If you have problems before the debugger is initialized, it can mean hours of
grueling debugging with very little indication of what's going on, usually a blank screen and
nothing else.
Even though the debugger has its own JCB and looks like another application, it has special
privileges that allow it to operate outside the direct control of the kernel. It can remove the
currently running task and make itself run, and do the reverse without the kernel even being
aware of what has happened. The debugger must be able to do this to get its job done.
When the debug interrupt activates, you initially enter an interrupt procedure that places the
running task in a hold status. Hold status simply means that it is not placed on the ready queue
as if it were a normal task switch. Instead, the current pRunTSS, a pointer to the currently run-
ning TSS, is saved by the debugger, and the interrupt procedure does a task switch to the
debugger's task. The debugger becomes the active job. This effectively replaces the running
task with the debugger task.
A few housekeeping chores must be done before the task switch is made. These include copy-
ing the interrupted task's page directory entry into the debugger's JCB and the debugger's TSS.
This is so the debugger is using the task's exact linear memory image. If you are going to debug
a task, you must be able to access all of its memory, including allocated memory.
The debugger cannot debug itself. It's actually an orphan when it comes to being a program in
its own right. It doesn't have its own Page Directory or Page Tables like other jobs (programs).
It almost, but not quite, becomes a task in the job it interrupted.
The INT 03 interrupt is not the only way to enter MMURTL's debugger. The debugger is also
entered when other fatal processor exceptions occur. If you look through the 386/486 docu-
mentation, you will see that there are many interrupts, called exceptions, or faults, that can

<!-- PDF page 209 -->
Chapter 12 M The Debugger
occur. Some of these are used by the OS to indicate something needs to be done, while others
should not happen and are considered fatal for the task that caused them. Until MMURTL
uses demand page virtual memory, a page fault is fatal to a task. It means it tried to access memory
that didn't belong to it. The exceptions that cause entry into the debugger mc\\ide faults, traps,
and aborts (processor fatal) and are shown in Table 12.1.
|
187
Table 12.1. Processor exceptions.
No.
Type
Description
0*
F
Divide by zero
1
T/F
Debug exception (debugger uses for single step)
3
T
Breakpoint (set by debugger, or INT 03 in code)
4
T
Overflow (lNT0 instruction)
5*
F
Bounds check (from bound instruction)
6*
F
Invalid opcodes (reserved instructions)
7*
F
Coprocessor not available (on Esc and wait)
8*
9
10*
A
Double fault (real bad news...)
F
Invalid TSS
11*
F
Segment not present
12*
F
Stack fault
13*
F/T
General protection fault
14*
F
Page fault
16
F
Coprocessor error
The asterisk (*) indicates that the return address points to faulting instruction. This return address is
used by the debugger entry code.
Some of the exceptions even place an error code on the stack after the return address. MMURTL
has a very small interrupt procedure for each of these exceptions. These procedures get the
information from the stack before switching to the debugger. The debugger uses this informa-
tion to help you find the problem. When the debugger is entered, it displays the registers from
the interrupted task. To do this, it uses the values in the task's Task State Segment (TSS). All
the values are the same as when the exception occurred, except the CS and EIP. These contain
the address of the exception handler. To be able to see the proper address of the code that was
interrupted, you must get the return address from the stack and place it back into the TSS.
This also has the effect of starting the task back at the instruction after it was interrupted, which
is the next instruction to execute. This makes the debugger transparent, which is exactly what
you want. This also allows you to restart the application after a breakpoint at the proper place.

<!-- PDF page 210 -->
Part III m MMURTL-Specific Details
If the debugger was entered because of a fault, you should kill the offending job and restart,
setting a breakpoint before the exception occurs, then single step while watching the registers
to see where the problem is.
The debugger exit code is just about the reverse of the code used to enter the debugger. The
debugger removes itself as the running task and returns the interrupted task to the running
state with the values in the TSS. This also includes switching the video back to the rightful
owner.
While you are in the debugger, all the registers from the interrupted task are displayed, along
with any errors that may have been removed from the stack on a fault.

+ 630
- 0
library/Developing Your Own 32-Bit Operating System/Chapter-13-Keyboard-Service/Chapter-13-source-text.md Просмотреть файл

@@ -0,0 +1,630 @@
# Developing Your Own 32-Bit Operating System — Chapter-13: Keyboard Service
- **Source**: /library/Developing Your Own 32-Bit Operating System/source-file.pdf
- **PDF pages**: 211–222
- **Pages without text**: 211, 222

---

<!-- PDF page 211 -->
13
Keyboard Service

<!-- PDF page 212 -->
19Q Part III ■ MMURTL-Specific Details
The keyboard is handled with a system service. As described in Chapter 1 0, "Systems Program-
ming," system services are accessed with the Request primitive. The keyboard is handled with
a system service because it, like the file system, is a shared resource. Several programs can have
outstanding requests to the keyboard service at one time. Only one program (job) will be as-
signed to receive keystrokes at any one time. The exception to this rule is the global keyboard
request to receive Ctrl-Alt keystrokes.
The service name is KEYBOARD (uppercase as always). Each of the functions is identified by its
service code number, here is an example of a keyboard service request:
Request (
"KEYBOARD",
/* Service Name */
1,
/* wSvcCode for ReadKbd */
MyExch,
/* dRespExch */
&MyRqhandle,
/* pRqHndlRet */
0,
/* npSend (no Send Ptrs) */
&KeyCode
/* pDatal */
4,
/* cbDatal (size of KeyCode)*/
0
/* pData2 - not used (0) */
0,
/* Not used (0) */
1 ,
/* dData0 - fWait for Key */
0>
/* dDatal - Not used (0) */
0);
/* dData2 - Not used (0) */
All message-based services use the same request interface. Using the request interface allows for
asynchronous program operation. You can make a request and then go do something else be-
fore you return to wait or to check the function to see if you received a response from your
request. This is true multitasking.
If you don't need asynchronous keyboard access, the public call ReadKbd ( ) is provided. This is
a blocking call with only two parameters. It's easier to use but not as powerful. I'll describe it
after I discuss the following services.
Available Service
Table 13.1 shows the services provided by the service code.
Table 13.1. Available keyboard services.
Service Code Function
1 Read Keyboard
2 Notify On Global Keys
3 Cancel Notify On Global Keys
4 Assign Keyboard
The following sections describe these four functions.

<!-- PDF page 213 -->
Chapter 13 U Keyboard Service
Read Keyboard
The Read Keyboard function ( 1 ) allows a program to request keyboard input from the service.
The first request pointer points to an unsigned dword where the key code will be returned.
The key codes are described in detail in tables later in this chapter. The dDataB determines
whether the service will hold the request until a key is available. A value of 0 means the request
will be sent back to you immediately, even if a key is not available. The error from the service
is ErcNoKeyAvailable (700) if no key was available. The key code is undefined if no key was
available (so you must check the error). A value of 1 in dData0 causes the service to hold your
request until a key is available. If a key was available, the error should be 0.
The following are the Request parameters for Read Keyboard:
wSvcCode = 1
npSend = 0
pDatai = Pointer where the KeyCode will be returned
dcbDatai = 4 - Count of bytes in the code
pData2 = 0 - Not used
dcbData2 = 0 - Not used.
dDataO = fWaitForKey (0 or 1)
0 - Return with error if no key
waiting
1 - Wait for Key
dDatal = 0 - Not used
dData2 = 0 - Not used
H
Notify On Global Keys
The Notify On Global Keys function (2) allows a program to look for any keystroke entered
with the Ctrl and Alt keys depressed. This allows for "hot-key" operation.
Unlike regular keystrokes, keys that are pressed when the Ctrl-Alt keys are pressed are not
buffered. This means that users of the Read Keyboard function (1) will not see these keys. It
also means that your application must have an outstanding Notify request with the keyboard
service to receive these keys. If you have an outstanding Notify request and you no longer want
to receive Global key notifications, you should send a Cancel Notify request (service code 3).
The Global key codes returned are identical to the regular key codes, described in detail later.
These are the parameters for the Notify On Global Keys function:
wSvcCode = 2
npSend = 0
pDatai = Pointer where the KeyCode will be returned
dcbDatai = 4 - Count of bytes in the code

<!-- PDF page 214 -->
I
192
Part III ■ MMURTL-Specific Details
pData2 = 0 - Not used
dcbData2 = 0 - Not used
dData0 = 0 - Not used
dDatal = 0 - Not used
dData2 = 0 - Not used
Cancel Notify On Global Keys
The Cancel Notify On Global Keys function (3) cancels an outstanding Notify On Global
Keys request. If you have an outstanding Notify request, this function cancels it. The Notify
request is sent back to the exchange with an error stating it was canceled. The Cancel Notify
request is returned also. This cancels ^// outstanding Notify requests from the same job num-
ber for your application.
Here are the parameters for Notify On Global Keys function:
wSvcCode = 3
npSend = 0
pDatai = 0 - Not used
dcbDatai = 0 - Not used
pData2 = 0 - Not used
dcbData2 = 0 - Not used
dDataO = 0 - Not used
dDatal = 0 - Not used
dData2 = 0 - Not used
Assign Keyboard
The Assign Keyboard request assigns a new job to receive keys from the service. This request
should be used only by services such as the Monitor or a program that manages multiple jobs
running under the MMURTL OS.
These are the parameters for the Assign Keyboard request:
wSvcCode = 3
npSend = 0
pDatai = 0 - Not used
dcbDatai = 0 - Not used
pData2 = 0 - Not used
dcbData2 = 0 - Not used
dDataO = x - New Job Number (1 to njobs)
dDatal = 0 - Not used
dData2 = 0 - Not used

<!-- PDF page 215 -->
Chapter 13 ■ Keyboard Se
Key Codes and Status
MMURTL supports the standard AT 101-key advanced keyboard. All ASCII text and punc-
tuation is supported directly, while providing complete keyboard status to allow further trans-
lation for all ASCII control codes.
Alphanumeric Key Values
The key code returned by the keyboard service is a 32-bit (4-byte) value. The low-order byte is
the actual key code, as shown in Table 13.4.
To eliminate all the key status from the 4-byte key code to get the keystroke value itself, logi-
cally AND the key code by OFFh (Oxffin C). This leaves you with the 8-bit code for the key. The
key will be properly shifted according to the state of the Shift and Lock keys.
The upper three bytes provide the status of special keys (Shifts, Locks, and so on). The second
byte provides the shift state, which is six bits for Shift, Alt, and Ctrl, as shown in Table 13.2.
The third byte is for lock states (three bits for Caps, Num, and Scroll), as shown in Table 13.3.
The high-order byte is for the Numeric Pad indicator and is described later.
Shift-State Byte (Second Byte)
If you need to know the shift state of any of the shift keys (Ctrl, Alt, or Shift), you can use the
second byte in the 32-bit word returned. Table 13.2 shows how the bits are defined.
Table 13.2. Shift-state bits.
Bit Meaning When Set (1)
0
Left Ctrl key down
1
Right Ctrl key down
2
Left Shift key down
3
Right Shift key down
4
Left Alt key down
5
Right Alt key down
6
Not used (0)
7
Not used (0)
The following masks in assembly language can be used to determine whether the Ctrl, Shift, or
Alt keys were pressed for the key code being read.

<!-- PDF page 216 -->
jcJT] Part III ■ MMURTL-Specific Details
CtrlDownMask EQU 00000011b
ShftDownMask EQU 00001100b
AltDownMask EQU 00110000b
Lock-State Byte (Third Byte)
If you need to know the lock state of any of the lock-capable keys (Caps, Num, or Scroll), you
can use the third byte in the 32-bit word returned. Table 13.3 shows how the bits are defined:
Table 13.3. Lock-state bits.
Bit Meaning When Set (1)
0
Scroll Lock On
1
Num Lock On
2
Caps Lock On
3
Not used (0)
4
Not used (0)
5
Not used (0)
6
Not used (0)
7
Not used (0)
The following masks in assembly language can be used to determine whether one of the lock
keys was active for the key code being read.
CpLockMask EQU 00000100b
NmLockMask EQU 00000010b
ScLockMask EQU 00000001b
Numeric Pad Indicator
Only one bit is used in the high-order byte of the key code. This bit (Bit 0, LSB) will be set if
the keystroke came from the numeric keypad. You need this if you use certain keys from the
numeric pad differently than their equivalent keys on the main keyboard. For example, in your
program, the Enter key on the numeric keypad might do something differently than the type-
writer Enter key.
Key Codes
Table 13.4 shows the hexadecimal value provided in the low-order byte of the key code (U.im
significant) by the keyboard service for each key on a 101 -key keyboard. The Shift Code

<!-- PDF page 217 -->
Chapter 13 M Keyboard Service ra
column shows the value returned if the Shift key is active. The Caps Lock key does not affect
the keys shown with an asterisk (*) in the Shift Code column. A description is given only if
required.
If a shift code is not shown, the same value is provided in all shifted states (Shift, Ctrl, Alt, or
Locks). All codes are 7-bit values (1 to 127 — lh to 7Fh). Zero and values above 127 will not
be returned.
Table 1 3.4. Key values.
Base Key
Desc.
Key Code
Shifted Key
Shift Code
Desc
Esc
Escape
lBh
1
31h
1
21h
exclamation
2
32h
@
40h
at
3
33h
#
23h
pound
4
34h
$
24h
dollar
5
35h
%
25h
percent
6
36h
A
5Fh
carat
7
37h
&
26h
ampersand
8
38h
*
2Ah
asterisk
9
39h
(
28h
open paren
0
30h
)
29h
close paren
-
Minus
2Dh
5Fh
underscore
=
Equal
3Dh
+
2Bh
BS
Backspace
08h
HT
Tab
09h
a
61h
A
4lh
b
62h
B
42h
c
63h
C
43h
d
64h
D
44h
e
65h
E
45h
f
66h
F
46h
g
67h
G
47h
h
68h
H
48h
i
69h
I
49h
J
6Ah
J
4Ah

<!-- PDF page 218 -->
1
196
Part III M MMURTL-Specific i Details
Table 13.4. continued
Base Key
Dae.
Key Code
Shifted Key
Shift Code
Desc
k
6Bh
K
4Bh
1
6Ch
L
4Ch
m
6Dh
M
4Dh
n
6Eh
N
4Eh
o
6Fh
O
4Fh
P
70h
P
50h
q
71h
Q
51h
r
72h
R
52h
s
73h
S
53h
t
74h
T
54h
u
75h
U
55h
V
76h
V
56h
w
77h
W
57h
X
78h
X
58h
y
79h
Y
59h
z
7Ah
Z
5Ah
[
5Bh
{
7Bh
open brace
\
5Ch
I
7Ch
vert, bar
]
5Dh
}
7Dh
close brace
«
Accent
60h
-
7Eh
tilde
CR
Enter
ODh
;
Semicolon
3Bh
3Ah
colon
'
Apostr.
27h
"
22h
quote
,
Comma
2Ch
<
3Ch
less
Period
2Eh
>
3Eh
greater
/
Slash
2Fh
3Fh
question
Space
20h
Function Strip Key Codes
Shift and Lock states do not affect the function key values. Shift and Lock state bytes must be
used to determine program functionality. Table 13.5 shows the key code value returned.

<!-- PDF page 219 -->
Chapter 13 M Keyboard Service
|
197
Table 13.5. Function key strip codes.
Base Key
Key Code
Fl
F2
F3
F4
F5
F6
F7
F8
F9
F10
Fll
F12
OFh
lOh
llh
12h
13h
I4h
15h
16h
17h
18h
19h
lAh
Numeric Pad Key Codes
The Shift values shown in Table 13.6 are returned when the shift keys or Num Lock are active.
Caps Lock does not affect these keys.
Table 13.6. Numeric pad key codes.
Base Key
Key Code
Shift Code
Shifted Key
End
OBh
31h
1
Down
02h
32h
2
PgDn
OCh
33h
3
Left
03h
34h
4
Blank key
lFh
35h
5 (Extra Code)
Right
04h
36h
6
Home
06h
37h
7
Up
Olh
38h
8
PgUp
05h
39h
9
Insert
OEh
30h
0
Delete
7Fh
2Eh
. (Period)
continues

<!-- PDF page 220 -->
19g Part III ■ MMURTL-Specific Details
Table 13.6. continued
Base Key Key Code Shift Code Shifted Key
CR
Dash
2Dh
Asterisk
2Ah
Slash
2Fh
Plus
2Bh
Enter
ODh
Cursor, Edit, and Special Pad Key Codes
None of the keys in these additional keypads are affected by Shift or Lock states. The values
returned as the key code are shown in Table 13.7.
Table 13.7. Additional key codes.
Base Key Key Code
Print Screen
ICh
Pause
lDh
Insert
OEh
Delete
7Fh
Home
06h
End
OBh
PgUp
05h
PgDn
OCh
Up
Olh
Down
02h
Left
03h
Right
04h
Your Keyboard Implementation
You may not want all of the functionality in your system that I provided, or you may wan
more, such as international key translations. The keyboard translation tables could be used to
nationalize this system, if you desire.

<!-- PDF page 221 -->
Chapter 13 M Keyboard Service
You may also want the keyboard more deeply embedded in your operating system code. Chap-
ter 25, "Keyboard Code," contains the source code behind the system service described in this
chapter. Pieces of it may be useful for your system.
I also recommend that you concentrate on the documentation for your keyboard implementa-
tion, as I have tried to do in this chapter. Remember, when all else fails, the programmer will
read the documentation.
[
199

<!-- PDF page 222 -->


+ 884
- 0
library/Developing Your Own 32-Bit Operating System/Chapter-14-File-System-Service/Chapter-14-source-text.md Просмотреть файл

@@ -0,0 +1,884 @@
# Developing Your Own 32-Bit Operating System — Chapter-14: File System Service
- **Source**: /library/Developing Your Own 32-Bit Operating System/source-file.pdf
- **PDF pages**: 223–242
- **Pages without text**: 223

---

<!-- PDF page 223 -->
File System Service

<!-- PDF page 224 -->
I
202
Part III m MMURTL-Specific Details
The file system included with MMURTL is compatible with the MS-DOS FAT (File Alloca-
tion Table) file system. This means that MMURTL can read and write MS-DOS disks (both
hard and floppy). This was done so that you could use MMURTL without reformatting your
hard disks or working from floppies. It certainly wasn't done because I liked the design or the
filename length limitations. MS-DOS has the unenviable task of being compatible with its
previous versions. This ties its hands, so to speak. Better file systems (disk formats) are in use,
but none is as widespread.
The internal management of a file system is no trivial matter. Because you are simply accessing
one that was designed by someone else, there isn't anything new about the format, just how
you access it. I have tried to follow the simplicity motto, and I limited the number of functions
to what is necessary for decent operation. The service name is "FILESYS".
File Specifications
A filename consists of one to eight characters, a period, then up to three more characters, for
example, filename, txt.
A full file specification for the FAT-compatible file system consists of a drive letter identifier
followed by a colon, then the path — which is each directory separated by the backslash charac-
ter— and finally the filename itself. Here's an example:
C:\Dir1\Dir2\Dir3\Filename . txt
Network File Request Routing
File system requests are routed based on the full file specification. A filename that is prefixed
with a node name, and optionally, a network name, will be routed to the service with the same
name.
A full network file specification consists of three parts.
The first part is the network name enclosed in brackets [ ]. The second part is the network's
node name enclosed in braces { }. The third part is the filename as previously described. A com-
plete network filename looks like this:
[network ]{Node}C: \Dir1 \Dir2\Filename . txt
If the network name is omitted from the specification, and a node name is specified, the de-
fault name NETWORK is used.
The network name matches the name of the network service when it was installed, hence the
default system service name for a network routing service is network. This also means that net-
work names are limited to eight characters. Even though service names must be capitalized and
space padded, this is not necessary with network names because the file system fixes them.

<!-- PDF page 225 -->
Chapter 14 ■ File System Service
When a network file is opened, the network service receives the request from the file system.
The file handle returned will be unique on the system that originated the request.
The filename across a network may not be limited to the MS-DOS file naming conventions
described here. This depends on the type of file system on the node being accessed.
H
File Handles
When a file is opened in any mode or type, a number called a file handle is returned to you.
Make no assumptions about this number. The file handle is used in all subsequent file opera-
tions on that file. This number is how you refer to that file until the file is closed. A file handle
is a 32-bit unsigned number (dword).
File Open Modes
A file may be opened for reading and writing, or just reading alone. These two modes are called
Modify (read and write) and Read (read-only).
Only a single user may open a file in Modify mode. This user is granted exclusive access to the
file while it is open.
Multiple users may open and access a read-mode file. When a file is opened in Read mode, it
may not be opened in Modify mode by any user.
File Access Type
A file may be opened in Block or Stream mode.
Block Mode
Block mode operation is the fastest file access method because no internal buffering is required.
The data is moved in whole blocks by the fastest means determined by the device driver. No
internal file buffers are allocated by the file system in Block mode. The only restriction is that
whole blocks must be read or written. The standard block size is 512 bytes for disk devices.
Block mode operation allows use of the following file system functions:
ReadBlock()
WriteBlock()
CloseFile()
GetFileSize()
SetFileSize()
DeleteFile()

<!-- PDF page 226 -->
H
Part III ■ MMURTL-Specific Details
Stream Mode
In Stream mode, the file system allocates an internal buffer to use for all read and write opera-
tions. This is a one-page, 4096-byte buffer. Stream mode operation allows use of the following
file system functions:
CloseFile()
ReadBytes()
WriteBytesO
SetFileLFA()
GetFileLFA()
GetFileSize()
SetFileSize()
DeleteFile()
Errors are returned from any function not compatible with the file-access type you specified
when the file was opened.
Logical File Address
All files are logically stored as \-n bytes. The Logical File Address (LFA) is an unsigned dword
(dLFA). This is the byte offset in the file to read or write from. LFA 0 is the beginning of the
file. The file size minus 1 is the last logical byte in the file.
Block access file system functions require to you specify a Logical File Address (LFA). Block
access files have no internal buffers and do not maintain a current LFA or file pointer.
Stream access files have internal buffers and maintain your current LFA. This is called a file
pointer in some systems. For stream files, you do not specify an LFA to read or write from. The
current LFA for stream files is updated with each ReadBytesO and WriteBytesO function.
Additional functions are provided to allow you to find and set the current LFA. When a file is
initially opened for Stream access, the file pointer is set to 0, no matter what mode the file was
opened in
File System Requests
The file system is a message-based system service. This means it can be accessed directly with
the Request primitive. The procedural interface for Request has 1 2 parameters. The file system
is an ideal candidate for a message-based service. It meets all the requirements. The small amount
of time for message routing will not make any measurable difference in the speed of its opera-
tion because most of its time is spent accessing hardware. It also provides the shared access
required for a true multitasking system. The file system actually runs as a separate task.
Listing 14.1 is an example of a File System request in the C programming language.

<!-- PDF page 227 -->
Chapter 14 ■ File System Service
Listing 14.1. OpenFile request in C.
dError= Request (
"FILESYS ", /* ptr to name
1,
MyExch,
&MyRqhandle,
1 ,
/* wSvcCode
/* dRespExch
/* pRqHndlRet
/* nSendPtrs
&"AnyFile.doc" , /* pDatal
11, /* cbDatal
&FileHandle /* pData2
4, /* cbData2
1 , /* dData0
0, /* dDatal
0); /* dData2
of service */
- 1 for OpenFile */
- respond here */
- may be needed */
- 1 Send ptr */
- ptr to name */
- size of name */
- returned handle *,
Size of a handle */
ModeRead */
Block Type Access *i
not used */
Unused request parameters must be set to 0.
All message-based services use the same request-based interface. Using the request interface allows
for asynchronous program operation. You can make a request, then go do something else be-
fore you come back to wait or check the function to see if it's completed (true multitasking).
Each of the functions is identified by its Service Code number. Table 14.1 shows the service
codes the file system supports.
Table 14.1. File system service codes.
Function
Service Code
OpenFile
CloseFile
ReadBlock
WriteBlock
ReadBytes
WriteBytes
GetFileLFA
SetFileLFA
GetFileSize
SetFileSize
CreateFile
RenameFile
DeleteFile
CreateDirectory
DeleteDirectory
GetDirectorySector
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

<!-- PDF page 228 -->
Part III M MMURTL-Specific Details
Procedural Interfaces
If you don't have a need for asynchronous disk access, you can use a blocking procedural inter-
face which is included in the file system itself. The procedural interface actually makes the re-
quest for you by using your TSS Exchange. The request is transparent to the caller. It is easier
to use because it has fewer parameters. This means there is a simple procedural interface call
for each file system function. The blocking procedural interface is described with each of the
call descriptions that follow later in this chapter.
Device Access Through the File System
Teletype fashion stream access to the NUL, VID, and KBD devices may be accessed through
the file system, but only through the procedural interfaces. The Request interface will not al-
low device access. High-level language libraries that implement device access (such as putchar( )
in C) must use the procedural interfaces for file access.
All system device names are reserved and should not be used as filenames on the system. Table
14.2 lists the device names that are reserved and also the mode of stream access supported.
Table 14.2. Device stream access.
Device
Description
Access
NUL
NULL device
Write-only
KBD
Keyboard
Read-only
VID
Video
Write-only
LPT1
Printer 1
None
LPT2
Printer 2
None
COM1
RS-232 1
None
COM2
RS-232 2
None
COM3
RS-232 3
None
COM4
RS-232 4
None
FDO
Floppy disk
None
FD1
Floppy disk
None
HDO
Hard disk
None
HD1
Hard disk
None
Device access is currently implemented for NUL, KBD, and VID devices only.

<!-- PDF page 229 -->
Chapter 14 ■ File System Service
File System Functions in Detail
The following pages detail each of the file system functions that are available.
OpenFile
Procedural interface:
OpenFile (pName, dcbname, dOpenMode, dAccessType, pdHandleRet) : dError
OpenFile ( ) opens an existing file in the current path for block or stream operations. A full file
specification may be provided to override the current job path.
Opening in ModeModif y excludes all others from opening the file. Multiple users can open a file
in ModeRead. If a file is open in ModeRead, it cannot be opened in ModeModif y by any other users.
Procedural parameters:
pName — Pointer to the filename or full file specification
dcbName — Dword with length of the filename
dOpenMode— READ= 0, MODIFY= 1
dAccessType — Block= 0, Stream= 1.
pdHandleRet — Pointer to a dword where the handle to the file will be returned to you
Request Parameters for OpenFile:
wSvcCode
= 1
nSend
= 1
pDatal
= pName
cbDatal
= dcbName
pData2
= pdHandleRet
cbData2
= 4 (Size of a file handle)
dDataO
= dOpenMode
dDatal
= dAccessType
dData2
= Not used (0)
CloseFile
Procedural interface:
CloseFile (dHandle): dError
CloseFile () closes a file that was previously opened. If stream access type was specified, all
buffers are flushed and deallocated.

<!-- PDF page 230 -->
208 Part III M MMURTL-Specific Details
Procedural parameters:
dHandle — A dword that was returned from OpenFile( )
Request Parameters for CloseFile:
wSvcCode
= 2
nSend
= 0
pDatal
= 0
cbDatal
= 0
pData2
= 0
cbData2
= 0
dDataO
= dHandle
dDatal
= 0
dData2
-0
ReadBlock
Procedural interface:
ReadBlock(dHandle, pDataRet, nBytes, dLFA, pdnBytesRet) : dError
This reads one or more blocks from a file. The file must be opened for Block access or an error
occurs.
Procedural parameters:
dHandle — Dword with a valid file handle (as returned from 0penFile( )
pDataRet — Pointer to a buffer large enough to hold the count of blocks you specify to
read
nBytes — Dword with number of bytes to read. This mustbe a multiple of the block
size for the 5 1 2-byte disk.
dLFA — Logical File Address to read from. This mustbe a multiple of the block size.
pdnBlkRet — Pointer to a dword where the count of bytes successfully read will be
returned. This will always be a multiple of the block size («X 512).
Request parameters for ReadBlock:
wSvcCode
= 3
nSend
= 0
pDatal
= pDataRet
cbDatal
= nBytes (multiple of 512)
pData2
= pdnBytesRet
cbData2
= 4 (size of dnBytesRet)
dData0
= dHandle
dDatal
= 0
dData2
= 0

<!-- PDF page 231 -->
Chapter 14 ■ File System Service ^nq
WriteBlock
Procedural interface:
WriteBlock(dHandle, pData, nBytes, dLFA, pdnBytesRet) : dError
This writes one or more blocks to a file. The file must be opened for Block access in Modify
mode or an error occurs. Writing beyond the current file length is not allowed. SetFileSize( )
must be used to extend the file length if you intend to write beyond the current file size. See
the SetFileSize() section.
Procedural parameters:
dHandle — Dword with a valid file handle as returned from OpenFile( ).
pData — Pointer to the data to write
nBytes — Dword with number of bytes to write. This must always be a multiple of
512. One block -512 bytes.
dLFA — Logical File Address to write to. This mustbe a multiple of the block size.
pdnBytesRet — Pointer to a dword where the number of bytes successfully written will
be returned. This will always return a multiple of 512.
Request parameters for ReadBlock:
wSvcCode
= 4
nSend
= 1
pDatal
= pData
cbDatal
= nBytes (512= 1 Block)
pData2
= pdnBytesRet
cbData2
= 4 (size of dnBytesRet)
dDataO
= dHandle
dDatal
= 0
dData2
= 0
ReadBytes
Procedural interface:
ReadBytes (dHandle, pDataRet, nBytes, pdnBytesRet): dError
This reads one or more bytes from a file. The file must be opened for Stream access or an error
occurs. The bytes are read from the current LFA. The LFA is updated to the next byte address
following the data you read. Use SetFileLFA( ) to move the stream file pointer if you want to
read from an LFA other than the current LFA. Get File LFA may be used to find the current
LFA.

<!-- PDF page 232 -->
210 Part III ■ MMURTL-Specific Details
Parameters:
dHandle — Dword with a valid file handle, as returned from OpenFile( )
pDataRet — Pointer to a buffer large enough to hold the count of bytes you specify to
read
nBytes — Dword with number of bytes to read
pdnBytesRet — Pointer to a dword where the number of bytes successfully read will be
returned
Request Parameters for ReadBytes:
wSvcCode
= 5
nSend
= 0
pDatal
= pDataRet
cbDatal
= nBytes
pData2
= pdnBytesRet
cbData2
= 4 (size of dnBytesRet)
dDataO
= dHandle
dDatal
= 0
dData2
= 0
WriteBytes
Procedural interface:
WriteBytes (dHandle, pData, nBytes, pdnBytesRet): dError
This writes one or more bytes to a file. The file must be opened for stream access in Modify
mode or an error occurs. The bytes are written beginning at the current LFA. The LFA is up-
dated to the next byte address following the data you wrote. The file length is extended auto-
matically if you write past the current End Of File. Use SetFileLFA( ) to move the stream file
pointer if you want to read from an LFA other than the current LFA. GetFileLFA( ) may be
used to find the current LFA.
Procedural parameters:
dHandle — Dword with a valid file handle, as returned from OpenFile
pData — Pointer to the data to write
nBytes — Dword with number of bytes to write
pdnBytesRet — Pointer to a dword where the number of bytes successfully written will
be returned

<!-- PDF page 233 -->
Chapter 14 ■ File System Service H
Request parameters for ReadBytes:
wSvcCode
= 6
nSend
= 1
pDatal
= pData
cbDatal
= nBytes
pData2
= pdnBytesRet
cbData2
= 4 (size of dnBytesRet)
dData0
= dHandle
dDatal
= 0
dData2
= 0
GetFikLFA
Procedural Interface:
GetFilel_FA(dHandle, pdLFARet): dError
This gets the current LFA for files with stream mode access. An error occurs if the file is opened
for Block access.
Procedural parameters:
dHandle — Dword with a valid file handle, as returned from OpenFile
pdLFARet — Pointer to a dword where the current LFA will be returned
Request parameters for GetFileLFA:
wSvcCode
= 7
nSend
= 0
pDatal
= pdLFARet
cbDatal
= 4
pData2
= 0
cbData2
= 0
dData0
= dHandle
dDatal
= 0
dData2
= 0
SetFileLFA
Procedural interface:
SetFileLFA(dHandle, dLFA) : dError
This sets the current LFA (file pointer) for Stream Access files. An error occurs if the file is
opened for Block access. The file LFA can not be set past the End Of File (EOF). The file will
be set to EOF if you specify OFFFFFFFF hex (-1 for a signed long value in C).

<!-- PDF page 234 -->
I
212
Part III ■ MMURTL-Specific Details
Procedural parameters:
dHandle — Dword with a valid file handle, as returned from OpenFile( ).
dLFA — Dword with the LFA you want to set. OFFFFFFFF hex will set the current LFA
to End O fFile.
est parameters for SetFileLFA
wSvcCode
= 8
nSend
= 0
pDatal
= 0
cbDatal
= 0
pData2
= 0
cbData2
= 0
dData0
= dHandle
dDatal
= dLFA
dData2
= 0
GetFileSize
Procedural interface:
GetFileSize(dHandle, pdSizeRet): dError
This gets the current file size for files opened in any access mode.
Procedural parameters:
dhandle — Dword with a valid file handle, as returned from 0penFile( )
pdSizeRet — Pointer to a dword where the current size of the file will be returned
Request parameters for GetFileLFA:
wSvcCode
= 9
nSend
= 0
pDatal
= pdSizeRet
cbDatal
= 4
pData2
= 0
cbData2
= 0
dDataO
= dHandle
dDatal
= 0
dData2
= 0

<!-- PDF page 235 -->
Chapter 14 ■ File System Service
SetFileSize
Procedural interface:
SetFileSize (dHandle, dSize): dError
This sets the current file size for Stream and Block Access files. The file length, and allocated
space on the disk, will be extended or truncated as necessary to accommodate the size you specify.
The current LFA is not affected for Stream files unless it is now past the new EOF, in which
case it is set to EOF automatically.
Procedural parameters:
dHandle — Dword with a valid file handle as returned from OpenFile( )
dSize — Dword with the new size of the file
Request Parameters for SetFileSize:
wSvcCode
= 10
nSend
= 0
pDatal
= 0
cbDatal
= 0
pData2
= 0
cbData2
= 0
dDataO
= dHandle
dDatal
= dSize
dData2
= 0
CreateFile
Procedural interface:
CreateFile (pName, dcbName, dAttributes) : dError
This creates a new, empty file in the path specified. The file is closed after creation, and the file
is ready to be opened and accessed.
Procedural parameters:
pName — Pointer to the filename or full file specification to create
dcbName — Dword with length of the filename
dAttributes — Dword with one of the following values:
0 = Normal File
2 = Hidden File
4 = System File
6 = Hidden System file

<!-- PDF page 236 -->
214 |
Part III M MMURTI-Specific Details
Request parameters for CreateFile:
wSvcCode
= 11
nSend
= 1
pDatal
= pName
cbDatal
= dcbName
pData2
= 0
cbData2
= 0
dDatae
= dAttributes
dDatal
= 0
dData2
= 0
RenameFile
Procedural interface:
RenameFile (pName, dcbname, pNewName dcbNewName) : dError
This renames a file. The file must not be opened by anyone in any mode.
Procedural parameters:
pName — Pointer to the current filename or full file specification
dcbName — Dword with length of the current filename
pNewName — Pointer to the new filename or full file specification
dcbNewName — Dword with length of the new filename
Request parameters for RenameFile:
wSvcCode
nSend
pDatal
cbDatal
pData2
cbData2
dDataO
dDatal
dData2
= 12
= 1
= pName
= dcbName
= pNewname
= dcbNewname
= 0
= 0
= 0
DeleteFile
Procedural interface:
DeleteFile (dHandle): dError
This deletes a file from the system. No further access is possible, and the filename is available
for re-use.

<!-- PDF page 237 -->
Chapter 14 ■ File System Service
The file must be opened in Modify mode, which grants exclusive access.
Procedural parameters:
dHandle — Dword that was returned from OpenFile
Request parameters for DeleteFile:
wSvcCode
= 13
nSend
= 0
pDatal
= 0
cbDatal
= 0
pData2
= 0
cbData2
= 0
dDataO
= dHandle
dDatal
= 0
dData2
= 0
CreateDirectory
Procedural interface:
CreateDirectory (pPath, dcbPath): dError
This creates a new, empty directory for the path specified. The path must contain the new
directory name as the last element. This means all subdirectories above the last directory speci-
fied must already exist. If only the new directory name is specified, as opposed to a full path,
the directory is created in your current path, specified in the Job Control Block.
Procedural parameters:
pPath — Pointer to the path (directory name) to create
dcbPath — Dword with length of the path
Request parameters for CreateDirectory:
wSvcCode
= 14
nSend
= 1
pDatal
= pPath
cbDatal
= dcbPath
pData2
= 0
cbData2
= 0
dDataO
= 0
dDatal
= 0
dData2
= 0

<!-- PDF page 238 -->
I
216
Part III ■ MMURTL-Specific Details
DeleteDirectory
Procedural interface:
DeleteDirectory (pPath, dcbPath, fAHFiles): dError
This deletes a directory for the path specified. The path must contain the directory name as the
last element. If only the existing directory name is specified, as opposed to a full path, the name
is appended to the path from your job control block.
All files and subdirectories will be deleted if fAHFiles is non-zero.
Procedural parameters:
pPath — Pointer to the path, or directory name, to delete
dcbPath — Dword with length of the path
fAHFiles — If set to non-zero, all files in this directory, all subdirectories, and files in
subdirectories will be deleted.
Request parameter > for DeleteDirec
wSvcCode
15
nSend
1
pDatal
pPath
cbDatal
dcbPath
pData2
0
cbData2
0
dDataO
0
dDatal
0
dData2
0
GetDirSector
Procedural Interface:
GetDirSector (pPath, dcbPath, pSectRet, dSectNum): dError
Directories on the disk are one or more sectors in size. Each sector contains 16 directory en-
tries. This call returns one sector from a directory.
You specify which logical directory sector you want to read (from 0 to nTotalSectors - 1 ). The
directory must exist in the path specified. If no path is specified, the path in your JCB will be
used.
A single directory entry looks like this with 16 per sector:
Name 8 Bytes
Ext 3 Bytes

<!-- PDF page 239 -->
Chapter 14 ■ File System Service
Attribute 1 Byte
Reserved 10 Bytes
Time 2 Bytes
Date 2 Bytes
StartClstr 2 Bytes
FileSize 4 Bytes
The fields are read and used exactly as MS-DOS uses them.
Procedural parameters:
pPath — Pointer to the path, or directory name, to read the sector from
dcbPath — Dword with length of the path
pSectRet — Pointer to a 512-byte block where the directory sector will be returned
dSectNum — Dword with the logical sector number to return
Request parameters for GetDirSector:
wSvcCode = 16
nSend = 1
pDatal = pPath
cbDatal = dcbPath
pData2 = pSectRet
cbData2 =512
dDataO = dSectNum
dDatal = 0
dData2 = 0
ra
File System Theory
Understanding a bit more about how the file system is managed may help you make better use
of it.
Internal File System Structures
The file system has several internal structures it uses to manage the logical disks and open files.
Some structures are dynamically allocated, while others are part of the operating system data
segment.
File Control Blocks
All file system implementations have some structure that is used to manage an open file. Quite
a few of them use this same name, FCB.

<!-- PDF page 240 -->
21g Part III M MMURTL-Specific Details
The file system allocates file system structures dynamically when it is initialized. The maxi-
mum number of open files is currently limited to 128 files by this initial allocation.
The file system starts with an initial 4K (one memory page) of FCBs. Each FCB is 128 bytes.
Each open file is allocated an FCB. If the file is opened in Stream mode, you allocate a one-
page buffer (4K) for reading and writing.
The FCB actually contains a copy of the 32-byte directory entry from the disk. This is read
into the FCB when the file is opened.
File User Blocks
Because this is a multitasking operating system, the FCB is not direcdy associated with the job
that opened the file. Another structure, called the file user block (FUB), is what links the user
of a file to the FCB. There is one FUB for each instance of a user with an open file.
The FUBs contain the file pointer, the open mode, buffer pointers, and other things that are
required to manage the user's link to the file. The file handle is actually a pointer to the FUB.
Because it is a pointer, extra checking is done to ensure it is valid in every call. A bad pointer
could do some real damage here if you let it.
The FCB keeps track of the number of FUBs for the file. When it reaches 0, the FCB is
deallocated for re-use.
FAT Buffers
In the FAT file system, the location of every cluster of one or more sectors of a file is managed
in a linked list of sorts. This linked list is contained in a table called the File Allocation Table.
A more accurate name would be cluster allocation table because it's actually allocating clusters
of disk space to the files, it's not allocating files.
The disk is allocated in clusters, with each cluster having an associated entry in the FAT. On
most hard disks, they use what is called a FAT 16. This means each FAT entry is a 16-bit word.
The beginning of the FAT represents the beginning of the usable data area on the logical disk.
Let's look at an example of how you use the FAT to locate all parts of file on the disk. In this
example, you'll assume that each FAT entry is worth 4K on disk, which is cluster size, and
you'll assume the file size is 12K, or three entries.
Assume the directory entry you read says the first FAT entry is number 4.
X= not your file. L= Last entry in your file.
FAT Position 123456789 10
Entry in FAT X X X 5 6 L

<!-- PDF page 241 -->
Chapter 14 ■ File System Service
This means your file occupies the fourth, fifth, and sixth clusters in the data area on the disk.
The fourth entry points to the fifth, the fifth points to the sixth, and the sixth is marked as the
last. Have you ever gotten lost clusters when you ran a Chkdsk in MS-DOS? Who hasn't? It usually
means it found some clusters pointing to other clusters that don't end, or it found a beginning
cluster with no corresponding directory entry pointing to it.
From this example, you can see that every file you access will require a minimum of two disk
accesses to read the file, unless the piece of the FAT you need is already in memory.
This means it's important to attempt to keep as much FAT information in memory as you can
and manage it wisely. If your disk isn't too badly fragmented and if you are sequentially read-
ing all of a file into memory (such as when loading an executable file), you can usually do it
quite rapidly. Just IK of FAT space represents 2MB of data space on the disk if clusters are 4K.
A badly fragmented disk can force as many as four or five accesses to different parts of the FAT
just to read one file.
The disk controller (IDE/MFM) can read 256 contiguous sectors in one read operation. That's
128K. That's a lot of FAT (or file). However, because of the design of the FAT file system, the
actual contiguous read will usually be limited to the size of a cluster.
A great number of files are read and written sequentially. These include, run files being ex-
ecuted, source files for compilers, the initial filling of buffers for a word processor (and the
final save usually), vector image files, and raster image files. The list could go on and on. Large
database files are the major random users, and only a good level-4 mindreader knows where,
inside of a 400MB file, the user will strike next.
MMURTL's DOS FAT file system allocates a IK FAT buffer, based on file access patterns. A
simple LRU (least recently used) algorithm ensures the most accessed FAT sectors will remain
in memory.
File Operations
Reading a file is a multipart process. Look at what the file system goes through to open, read,
and close a file:
1. Look through the FCBs to see if someone already has it open in ModeRead. If so,
allocate an FUB and attach it. If it's open in ModeWrite, return an error ErcFilelnUse.
2. Locate the directory entry and read it into the FCB. Directories are stored as files.
This means that you have to go to the FAT to find where the directory is stored so
you can look through it. This is in itself time-consuming.
3. Load FAT buffer if required. The first cluster of the file is listed in the directory entry.
From this, you calculate which disk sector in the FAT has this entry, and you read it
into the FAT buffer if it is not already in one. The file is open.

<!-- PDF page 242 -->
220 |
Part III ■ MMURTL-Specific Details
Read
What actually happens on a read depends on whether the file is opened in Block or Stream
mode. If opened in Block mode, you simply issue the read to the device driver to read the sec-
tors into the caller's buffer (locating them using the FAT). The hardware is limited to 128K
reads in one shot, and can only do this if 1 28K of the file is located in contiguous sectors. Usually,
the commands to the device driver will be broken into several smaller reads as the file system
finds each contiguous section listed in the FAT.
Write
Writing to a file is a little more complicated, and depends on how the file was opened. If the
file is opened in Block mode, you issue the write command to the device driver to write out the
sectors as indicated by the user. It sounds simple, but much overhead is involved. If the sectors
have already been allocated in the FAT, you simply write over the existing data while you fol-
low the cluster chain. If the file is being extended, you must find unallocated clusters, attach
them to the chain, then write the data. On many disks there are two copies of the FAT for
integrity purposes. Both copies must be updated.
Close
This is the easy part. You flush buffers to disk if necessary for Write mode. Then you deallocate
the FUB and FCB.

+ 1492
- 0
library/Developing Your Own 32-Bit Operating System/Chapter-15-API-Specification/Chapter-15-source-text.md
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 3092
- 0
library/Developing Your Own 32-Bit Operating System/Chapter-16-MMURTL-Sample-Software/Chapter-16-source-text.md
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 275
- 0
library/Developing Your Own 32-Bit Operating System/Chapter-17-Introduction-to-the-Source-Code/Chapter-17-source-text.md Просмотреть файл

@@ -0,0 +1,275 @@
# Developing Your Own 32-Bit Operating System — Chapter-17: Introduction to the Source Code
- **Source**: /library/Developing Your Own 32-Bit Operating System/source-file.pdf
- **PDF pages**: 347–356
- **Pages without text**: 347, 348, 349, 356
- **Note**: Includes the Part IV divider plate and its blank verso before the chapter.

---

<!-- PDF page 347 -->


<!-- PDF page 348 -->


<!-- PDF page 349 -->


<!-- PDF page 350 -->
29q| Part IV ■ The Operating System Source Code
This chapter discusses each of the important source files, some of the things that tie all of the
source files together, and how to build the operating system using the tools included on the
CD-ROM.
One of my goals was to keep the source code organized in an easy-to-understand fashion. I
hope I've met this goal. You may find some of the source files rather large, but most of the
dependent functions will be contained in the file you are viewing or reading. You won't have
to go searching through hundreds of files to find a three-line function.
Comments in the Code
You'll find out that I make a lot of comments in my source code. By including many com-
ments, I can, a year or two later, go back and figure out what I did. It's not that I couldn't
actually figure out the code without the comments; I just expend the time up front instead of
later. In fact, sometimes I write the comments describing a small block of code before I actu-
ally code it. Doing so helps me think.
Calling Conventions
I bring up the topic of calling conventions here because, as you look through the code, you will
see that much of the assembler looks almost as if it were generated by a compiler. When a human
writes this much assembler (and the code is to be interfaced with assembler generated by a
compiler), the human becomes a compiler of sorts.
Many of the lower-level procedures use registers to pass data back and forth. Quite a few of
them, however, use the stack just as a compiler would. Two conventions are used throughout
the operating system. The most prominent and the preferred method is pushing parameters
left to right, with the called procedure cleaning the stack. The other is a mangled version of
what you'd find in most C compilers for functions with an ellipsis (...). I call it mangled 'be-
cause the arguments are still pushed left to right, but the number of variable arguments is passed
in the EDI register. The caller cleans the stack in this convention.
Organization
The MMURTL source code is organized in a modular fashion. Each logical section is con-
tained in one file (or more) that is either C or assembler source code. There are also header files
for the C source, and there are INCLUDE files for the assembler that tie publics, common
variables, and structures together.
All source files are located in one directory. There are 29 main source files (excluding header
and include files). A brief description of each of the main source files follows:

<!-- PDF page 351 -->
Chapter 17 ■ Introduction to the Source Code
MOSIDTASM This defines the basic Interrupt Descriptor Table (IDT). The IDT
is a required table for the Intel processors. This is really just a data position holder in
the data segment, because the entries in the IDT are done dynamically. The position
of this file in relationship to the others is critical because the offset to the IDT must be
identified to the processor. The IDT is the first file in the source, which means its data
will be at the lowest address. In fact, it ends up at physical and linear address 0.
MOSGDT.ASM This defines the Global Descriptor Table (GDT). This table is
also a required table for the Intel processors when they run in protected mode. The
GDT is 6K in size. Several selector entries are predefined so you can go directly into
protected mode. This ends up at address 800h physical and linear. The position of
this file is also critical, because the file is identified to and used by the processor. This
must be the second source file in the assembler template file.
MOSPDRJVSM This is also a data definition file that sets up the operating system's
page directory and first page table. This file should also remain in its current order
because I have hard-coded its address in some source modules.
MPublics.ASM This is also a table, but it is not used by the processor. This defines
the selector entries for all MMURTL public calls.
MAIN.ASM This is the first assembler file that contains code. The operating system
entry point is located here (the first OS instruction executed). Also some data is
defined here. This file should be left in the order I have it in. The ordering for the
remainder of the modules is not as important. On the other hand, I haven't tried
playing "musical source files" either. Leaving them in their current order would
probably be a good idea.
Keyboard.ASM The keyboard source goes against my own rule of not combining a
device driver with a system service. Keyboard.ASM was one of the first files written,
and it works rather well, so I am reluctant to begin rewriting it (although I will,
undoubtedly, someday). System services in assembly language are not fun to write or
maintain. Chapter 25, "Keyboard Code," explains all the mysteries behind this table-
driven piece of work.
Video ASM Video.ASM code and data was also one of the first pieces I wrote. You
have to see what you're doing, even in the earliest stages of a project. Chapter 26,
"Video Code," goes into detail on this file's contents.
Debugger.ASM This file contains the bulk of the debugger logic. Debugging is
never fun when you have to do it at the assembler level. Debugging a debugger is even
less fun.
UASM.C This file is the debugger disassembler. It is a table-driven disassembler
used to display instructions in the debugger. It's not very pretty, but it's effective and
accurate.
H

<!-- PDF page 352 -->
330
Part PV ■ The Operating System Source Code
DevDrvr-ASM The device-driver interface code and data are contained in this file.
This provides the re-entrancy protection and also the proper vectoring (indirect
calling) of device-driver functions.
Floppy.C Floppy disk device drivers are much harder to write than IDE hard disk
drivers. This is one complicated driver. It seems to me that hardware manufacturers
could be a little more compassionate toward software people. The reasoning behind
controlling the drive motors separately from the rest of the floppy electronics still
baffles me.
HardlDE.C The IDE hard disk device driver was fairly easy to write. I was sur-
prised. You will still see remnants of MFM drive commands in here because I started
with that. I think it still works with MFM drives, but I don't have any more to test it.
If you have MFM drives, you're on your own.
RS232.C This file contains the RS-232 UART driver. It drives two channels, but it
can be modified for four, if necessary. It supports the 16550 but only to enable the
input buffer (which helps).
Parallel. C The simple parallel port driver is contained here.
FSys.C This is an MS-DOS FAT-compatible file system. It has only the necessary
calls to get the job done. It provides stream access as well as block access to files.
JobC.C The program loader and most of the job-related functions are handled in
this file.
JobCodeASM Lower-level job functions and supporting code for functions in
JobC.C are contained in this file.
TmrCodcASM The interrupt service routine for the primary system timer — along
with all of the timer functions, such as Sleep ( ) — are in this file.
IntCode.ASM Interrupt Service Routine (ISR) handling code can be found here.
RQBCodcASM Request block management code is in this file.
DMACodcASM In many systems, when device driver writers need DMA, they
must handle it themselves. This file has routines that handle DMA for the program-
mer writing device drivers. They need only to know the channel, how much to move,
and the mode. Device driver writers will appreciate this.
NumCnvrt.ASM This is some code used internally to convert numbers to text and
vice-versa.
MemCodcASM The code memory allocation, address aliasing, and management of
physical memory can be found in this file.
SVCCodcASM System service management code such as RegisterSvc( ) is in this
file.
MiscCodcASM This file has all the little things you need, but you never know
where to put. High-speed string manipulation and comparison, as well as port I/O
support, is in this file.

<!-- PDF page 353 -->
Chapter 17 ■ Introduction to the Source Code
Kernel ASM All of the kernel calls and most of the kernel support code is in this
file. Chapter 18, "Kernel Code," discusses the kernel and presents the most important
pieces of its code along with discussions of the code.
ExceptASM Exception handlers such as those required for processor faults are in
this file. Most of them cause you to go directly into the Debugger.
InitCodcASM This file has all the helper routines that initialize most of the
dynamic operating-system functions.
Monitor.C The monitor program is contained in this file. It provides a lot of the
OS functionality, but it really isn't part of the operating system. You could replace
quite easily.
H
Building MMURTL
MMURTL requires the CM32 compiler (included on the CD-ROM) for the C source files
and the DASM assembler (also included on the CD-ROM). One MS-DOS batch file
(MakeALL.BAT) will build the entire operating system. This takes about 1.5 minutes on a
486/33.
Each of the C source files is turned into an assembler file (which is what the CM32 compiler
produces). All of the assembly language source files are then assembled with DASM.
DASM produces the operating system RUN file. The RUN file is in the standard MMURTL
Run file format, which is described completely in Chapter 28, "DASM: A 32-Bit Intel-Based
Assembler."
The only two MS-DOS commands you need are CM32 and DASM. An example of compiling one
of the source files is the following:
C:\MMURTL> CM32 Monitor.C
After all the C source files are compiled, you use the DASM command, as follows:
C:\MMURTL> DASM MMURTL. ATF
It's truly that simple. If you want an error file, specify /E on the command line after the name.
If you want a complete listing of all processor instructions and addresses, specify / L on the
command line after the name.
The following is the Assembler Template File (ATF). This file is required to assemble complex
programs with DASM. The assembler is discussed in detail in Chapter 28, but you don't need
to read all the DASM documentation to build MMURTL.
; Assembler Template File for MMURTL V1.0
.DATA
; These first 5 INCLUDES MUST be in this order!!!!
.INCLUDE MOSIDT.ASM ; Defines the Interrupt Descriptor Table
.INCLUDE MOSGDT.ASM ; 6K for GDT @ 00000800h Physical (1.5 Pages, 6K)
$

<!-- PDF page 354 -->
I
332
Part IV ■ The Operating System Source Code
.INCLUDE MOSPDR.ASM
.INCLUDE MPublics.ASM
.INCLUDE MAIN. ASM
; OS Page Directory & 1st Table (8k)
; Indirect calling address table
; Main OS code and some data & START address
; Ordering for the remainder of the modules is not as important.
; An asterisk (*) by the include file indicates it originates as
; a C source file and must be compiled first with
; CM32 (no switches needed).
INCLUDE Keyboard. ASM
Keyboard ISR and Service
INCLUDE Video. ASM
Video code and data
INCLUDE Debugger. ASM
Code & Data for Debugger
INCLUDE UASM.ASM
* Code & Data for Debugger Disassembler
INCLUDE DevDrvr.ASM
Code and Data for Device Driver Interface
INCLUDE Floppy. ASM
* Floppy Device Driver
INCLUDE HardlDE.ASM
* IDE Disk Device Driver
INCLUDE RS232.ASM
* Serial Comms Device Driver
INCLUDE Parallel. ASM
; Parallel Comms Device Driver
INCLUDE FSys.ASM
* File System Service
INCLUDE JobCode.ASM
Additional Job management
INCLUDE JOBC.ASM
* Loader/Job handling Code & Data
INCLUDE TmrCode.ASM
Timer Code
INCLUDE IntCode.ASM
ISR handling code
INCLUDE RQBCode.ASM
Request Block Code
INCLUDE DMACode.ASM
DMA Handling Code
INCLUDE NumCnvrt.ASM
Maybe Move to DLL later or can it
INCLUDE MemCode.ASM
Memory management code
INCLUDE SVCCode.ASM
System Service management code
INCLUDE MiscCode.ASM
Misc. code (string, array, I/O support)
INCLUDE Kernel. ASM
Kernel code
INCLUDE Except. ASM
Exception handlers
INCLUDE InitCode.ASM
Initialization support code
INCLUDE Monitor. ASM
* The monitor code & data
.END
If you've read all of the chapters preceding this one, you'll understand that all MMURTL pro-
grams are comprised of only two segments — code and data. The order of the assembler files in
the ATF file determines the order of the data and code. As each piece of code is assembled, it
is placed into the code segment in that order. The same is true for the data as it is encountered
in the files.
Using Pieces of MMURTL in Your OS
You'll find that most of the source code is modular. For instance, you can take the file system
code (FSYS.C) and turned it into a free-standing program with little effort. I built each section
of code in modular pieces and tried to keep it that way.
Even though I use CM32 to compile the C source files, the code can be compiled with any
ANSI C compiler. You will, no doubt, use another compiler for your project. You'll find that
many items in MMURTL depend on the memory alignment of some of the structures. This is
one thing you should watch. CM32 packs all structure fields completely. In fact,CM32 packs
all variables with no additional padding.

<!-- PDF page 355 -->
Chapter 17 ■ Introduction to the Source Code fTT7
Using Pieces of MMURTL in Other Projects
Some of you may find some of the code useful in projects that have absolutely nothing to do
with writing an operating system. The same basic warning about the alignment of structures
applies.
The assembler INCLUDE files that define all the structures actually define offsets from a
memory location. This is because DASM doesn't support structures, but you would have no
problem setting them up for TASM or MASM.
The calling conventions (described earlier) may get you into trouble if you want to use some of
the assembler routines from a high-level language. If your C compiler allows it, you can declare
functions as Pascal functions (some even use the old PLM type modifier), and they will work
fine. Otherwise, you can calculate the correct offsets and fix the EQU statements for access where
a C compiler would put them on the stack. You would also need to remove the numbers
following the RET and RETF statements so that the caller could clean the stack, as is the C con-
vention.

<!-- PDF page 356 -->


+ 2572
- 0
library/Developing Your Own 32-Bit Operating System/Chapter-18-Kernel-Code/Chapter-18-source-text.md
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 1714
- 0
library/Developing Your Own 32-Bit Operating System/Chapter-19-Memory-Management-Code/Chapter-19-source-text.md
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 714
- 0
library/Developing Your Own 32-Bit Operating System/Chapter-20-Timer-Management-Code/Chapter-20-source-text.md Просмотреть файл

@@ -0,0 +1,714 @@
# Developing Your Own 32-Bit Operating System — Chapter-20: Timer Management Code
- **Source**: /library/Developing Your Own 32-Bit Operating System/source-file.pdf
- **PDF pages**: 427–440
- **Pages without text**: 427

---

<!-- PDF page 427 -->
Timer Management Code

<!-- PDF page 428 -->
/Q/r Part PV ■ The Operating System Source Code
The timer code is the heart of all timing functions for the operating system. This file contains
all of the code that uses or deals with timing functions in MMURTL.
The timer interrupt service routine (ISR) is also documented here, along with a piece of the
kernel, actually the scheduler. It's at the end of the timer ISR.
Choosing a Standard Interval
Most operating systems have a timer interrupt function that is called at a fixed interval to up-
date a continuously running counter. MMURTL is no exception. I experimented with several
different intervals. I began with 50ms, but determined I couldn't get the resolution required
for timing certain events, and it also wasn't fast enough to provide smooth, preemptive
multitasking. I ended up with 10ms. Back when I started this project, 20-MHz machines were
"screamers," and I was even worried about consuming bandwidth on machines that were slower
than that. As you well know, with 100-MHz machines around, and anything less than
20-MHz all but disappearing, my worries were unfounded.
I did some time-consuming calculations to figure what percentage of CPU bandwidth I was
using with my timer ISR. I added up all of the clock cycles in the ISR for a maximum time-
consuming interrupt, as well as the fastest one possible. Somewhere between these two was the
average. The average really depends on program loading, because the timer interrupt sends
messages for the Sleep ( ) and Alarm ( ) functions. I was consuming less than 0.2 percent of available
bandwidth. Even with these calculations, however, testing is the only real way to be sure it will
function properly. Your bandwidth is the percentage of clocks executed for a given period,
divided by the total clocks for the same period. In a 10-ms period, we have 200,000 clocks on
a 20-MHz machine. My calculated worst case was approximately 1 50us, or 3,000 clocks, which
works out to about 1 .6 percent for the worst case. The average was far less. It was 0.2 percent.
Even more important than total bandwidth was the effect on interrupt latency. The most time-
critical interrupts are nonbuffered serial communications, especially synchronous. A single
communications channel running at 19,200 bps will interrupt every 520us. Two of them will
come in at 260us.
The following are the instructions in the timer interrupt service routine that could be "looped"
on for the maximum number of times in the worst case. The numbers before each instruction
are the count of clocks to execute the instruction on a 20-MHz-386, a slow machine. This
doesn't take into account nonaligned memory operands, certain problems with memory ac-
cess such as noncached un-decoded instructions, memory wait-states, and so on, so I simply
added 10 percent for the overhead, which is very conservative. See Listing 20.1.
Listing 20.1. Loop in timer ISR.
IntTmr01 :
6 CMP DWORD PTR [ EAX+f InUse ] , FALSE
3 JE IntTmr03

<!-- PDF page 429 -->
Chapter 20 ■ Timer Management Code H
6
CMP DWORD PTR [EAX+CountDown) ,0h
3
JNE IntTmr02
2
PUSH EAX
2
PUSH ECX
5
PUSH DWORD PTR [EAX+TmrRespExch]
2
MOV EAX, -1
2
PUSH EAX
2
PUSH EAX
40
CALL FWORD PTR ISendMsg
2
POP ECX
2
POP EAX
6
MOV DWORD PTR [EAX+flnUse] , FALSE
5
DEC nTmrBlksUsed
7
JMP IntTmr03
IntTmr02:
6
DEC DWORD PTR [EAX+CountDown]
IntTmr03:
2
ADD EAX.sTmrBlk
11
LOOP IntTmr01
This works out to 125 clocks for each loop, including the 10 percent overhead, plus 130 clocks
for the execution of ISendMsg ( ) in each loop. Multiply the total (255) times the maximum
number of timer blocks (64), and it totals 16,320 clocks. That's a bunch! On a 20-MHz ma-
chine, that would be 860us just for this worst-case interrupt. But hold on a minute! That only
happens if all of the tasks on the system want an alarm or must wake up from sleep exactly ax.
the same time. The odds of this happening are truly astronomical. The real worst case would
probably be more like 150us, and I tried a hundred calculations with a spreadsheet and finally
with test programs. This I could live with. The only additional time consumer is to tack on a
task switch at the end if you must preempt someone.
A task switch made by jumping to a TSS takes approximately 400 clocks on a 386, and 250 on
a 486 or a Pentium. Four hundred clocks on a 20-MHz CPU is 50 nanoseconds times 400, or
20us. This doesn't cause a problem at all.
Timer Data
The following section is the data and constants defined for the timer interrupt and associated
functions. SwitchTick and df Halted are flags for the task-scheduling portion of the timer in-
terrupt. See Listing 20.2.
Listing 20.2. Data and constants for timer code.
.DATA
.INCLUDE M0SEDF.INC
.INCLUDE TSS. INC
.ALIGN DWORD
; TIMER INTERRUPT COUNTER and TimerBlocks

<!-- PDF page 430 -->
408 I
The Operating System Source Code
Listing 20.2. continued
The Timer Block is a structure that contains
the exchange number of a task that is sleeping,
or one that has requested an alarm.
Each TimerBlock is 12 Bytes long
These are the offsets into the structure
sTmrBlk EQU 12
flnUse EQU 0 ;DD 00000000h
TmrRespExch EQU 4 ;DD 00000000h
CountDown EQU 8 ;DD 00000000h
EXTRN SwitChTick DD
EXTRN dfHalted DD
PUBLIC TimerTick DD 0 ; Incremented every 10ms (0 on bootup)
PUBLIC nTmrBlksUsed DD 0 ;Number of timer blocks in use
PUBLIC rgTmrBlks DB (sTmrBlk * nTmrBlks) DUP (0)
Timer Code
The timer interrupt and timing related function are all defined in the file TmrCode.ASM. The
three external near calls, the first things defined in this code segment, allow the timer interrupt
to access kernel helper functions from Kernel. ASM. The timer interrupt is part of the schedul-
ing mechanism. See Listing 20.3.
Listing 20.3. External functions for timer code.
.CODE
EXTRN ChkRdyQ NEAR
EXTRN enQueueRdy NEAR
EXTRN deQueueRdy NEAR
The Timer Interrupt
The timer interrupt checks the timer blocks for values to decrement. The timer interrupt fires
off every 1 0 milliseconds. The Sleep ( ) and Alarm ( ) functions set these blocks as callers require.
The timer interrupt code also performs the important function of keeping tabs on CPU hogs.
It's really the only part of task scheduling that isn't cooperative. It is a small, yet very important
part.
At all times on the system, only one task is actually executing. You have now interrupted that
task. Other tasks may be waiting to run at the ready queue. They may have been placed there

<!-- PDF page 431 -->
Chapter 20 ■ Timer Management Code
|
409 |
by other ISRs, and they may be of an equal, or higher, priority than the task that is now run-
ning, which is the one you interrupted.
You check to see if the same task has been running for 30ms or more. If so, you call ChkRdyQ
and check the priority of that task. If it is the same or higher, you switch to it. The task you
interrupted is placed back on the ready queue in exactly the state you interrupted it. You couldn't
reschedule tasks this way if you were using interrupt tasks because this would nest hardware task
switches. It would require manipulating items in the TSS to make it look as if they weren't
nested. Keep this in mind if you use interrupt tasks instead of interrupt procedures as I do. See
Listing 20.4.
Listing 20.4. The timer interrupt service routine.
PUBLIC IntTimer:
PUSHAD
INC TimerTick
CMP nTmrBlksUsed, 0
JE SHORT TaskCheck
IntTmr00:
LEA EAX.rgTmrBlks
MOV ECX.nTmrBlks
CLD
;INTS are disabled automatically
; Timer Tick, INT 20
;Anyone sleeping or have an alarm set'
;No...
;Yes!
;EAX has addr of Timer Blocks
;ECX has count of Timer Blocks
;Move forward thru the blocks
IntTmr01
CMP DWORD PTR [EAX+f InUse] .FALSE
JE IntTmr03 ;No
CMP DWORD PTR [EAX+CountDown] ,0h
;No
;Timer Block found?
goto next block
;Yes - is count at zero?
goto decrement it
;save ptr to rgTmpBlk
;save current count
[EAX+TmrRespExch] ;Yes - ISend Message
;FFFFFFFFh
; bogus msg
; bogus msg
;tell him his times up!
;get count back
;get ptr back
;Free up the timer block
;Correct count of used blocks
;skip decrement - empty blk
JNE IntTmr02
PUSH EAX
PUSH ECX
PUSH DWORD PTR
MOV EAX, -1
PUSH EAX
PUSH EAX
CALL FWORD PTR
POP ECX
POP EAX
MOV DWORD PTR [EAX+f InUse] .FALSE
DEC nTmrBlksUsed
JMP IntTmr03
ISendMsg
IntTmr02:
DEC DWORD PTR [EAX+CountDown] ;10ms more gone.
IntTmr03:
ADD EAX.sTmrBlk
LOOP IntTmr01
;next block please!
;unless were done
;We will now check to see if this guy has been running
;for more then 30ms (3 ticks). If so, we will
; switch him out if someone with an equal or higher pri
;is on the RdyQ

<!-- PDF page 432 -->
I
410
Part IV ■ The Operating System Source Code
Listing 20.4. continued
TaskCheck:
MOV EAX, df Halted
OR EAX, EAX
JNZ TaskCheckDone
;Is this from a HALTed state?
;YES.. no need to check tasks
MOV EAX, TimerTick
MOV EBX, SwitchTick
SUB EAX, EBX
CMP EAX, 3
JL SHORT TaskCheckDone
CALL ChkRdyQ
OR EAX, EAX
JZ TaskCheckDone
MOV ESI, pRunTSS
MOV DL, [ESI+Priority]
CMP DL, [EAX+Priority]
JC TaskCheckDone
CALL deQueueRdy
MOV EDI, EAX
MOV EAX, ESI
CALL enQueueRdy
MOV pRunTSS.EDI
MOV BX,[EDI+Tid]
MOV TSS_Sel,BX
INC _nSwitches
INC _nSlices
MOV EAX, TimerTick
MOV SwitchTick, EAX
PUSH 0
CALL FWORD PTR _EndOfIRQ
JMP FWORD PTR [TSS]
POPAD
IRETD
;Change this to change the "time slice"
;Hasn't been 30ms yet for this guy!
;Get next highest Pri in EAX (Leave Queued)
;Is there one at all??
;No. ..
;DL is pri of current
;Compare current pri to highest queued
;The CMP subtracts Pri of Queued from
;current pri. If the Queued Pri is LOWER
;or Equal, we want to Switch. That means
;if we got a CARRY on the subtract, his
; number was higher and we DON'T
;If current is higher(lower num), keep going!
Get high priority TSS off the RdyQ
and save in EDI
Put current one in EAX
and on the RdyQ
Make the TSS in EDI the Running TSS
Get the task ID
Put it in the JumpAddr for Task Swtich
Keep track of how many switches for stats
Keep track of how many preemptive switches
Save time of this switch for scheduler
;Must do this before we switch!
JMP TSS (This is the task swtich)
TaskCheckDone:
PUSH 0
CALL FWORD PTR
POPAD
IRETD
EndOflRQ
;Must do this before we switch!
SleepO
The sleep routine delays the calling task by setting up a timer block with a countdown value
and by setting up an exchange to send a message to when the countdown readies zero. The

<!-- PDF page 433 -->
Chapter 20 ■ Timer Management Code
timer interrupt sends the message and clears the block when it reaches zero. This requires an
exchange. The exchange used is the TSS_Exch in the TSS for the current task. See Listing 20.5.
Listing 20.5. Code for the Sleep function.
DelayCnt EQU [EBP+0Ch]
PUBLIC
DelayOl
Delay02:
Delay03:
_Sleep:
PUSH EBP
MOV EBP, ESP
MOV EAX, DelayCnt
CMP EAX, 0
JE Delay03
LEA EAX.rgTmrBlks
MOV ECX.nTmrBlks
CLD
CLI
CMP DWORD PTR [EAX+flnUse]
JNE Delay02
MOV EBX, DelayCnt
MOV [EAX+CountDownJ.EBX
MOV DWORD PTR [EAX+flnUse]
INC nTmrBlksUsed
MOV ECX.pRunTSS
MOV EBX, [ECX+TSS_Exch]
MOV [EAX+TmrRespExch],EBX
STI
PUSH EBX
ADD ECX,TSS_Msg
PUSH ECX
CALL FWORD PTR _WaitMsg
MOV EAX.ErcOk
JMP Delay03
STI
ADD EAX.sTmrBlk
LOOP Delay01
MOV EAX.ErcNoMoreTBs
MOV ESP, EBP
POP EBP
RETF 4
;See if there's no delay
;EAX points to timer blocks
;Count of timer blocks
; clear direction flag
;can't let others interfere
, FALSE ; Empty block?
;No - goto next block
;Get delay count
JRUE ;Use the Timer Block
;Up the blocksInUse count
;Get TSS_Exch for our use
;put it in timer block!
;Pass exchange (for WaitMsg)
;Offset of msg area
;and Wait for it to come back
;all is well
; unless were done
;Sorry, out of timer blocks
FAR return from publics
AkrmQ
Alarm ( ) sets up a timer block with a fixed message that will be sent when the countdown reaches
zero. The message is not repeatable. It must be set up each time. The message will always be
two dwords with OFFFFFFFFh (-1) in each. See Listing 20.6.

<!-- PDF page 434 -->
H
PartIV
The Operating System Source Code
Listing 20.6. Code for the Alarm function.
Procedural Interface:
Alarm ( nAlarmExch , AlarmCnt ) : dErc
AlarmExch
AlarmCnt
EQU [EBP+10h]
EQU [EBP+0Ch]
PUBLIC
Alarm01
_Alarm:
PUSH EBP
MOV EBP, ESP
MOV EAX, AlarmCnt
CMP EAX, 0
JE Alarm03
LEA EAX.rgTmrBlks
MOV ECX.nTmrBlks
CLD
CLI
CMP DWORD PTR [EAX+flnUse]
JNE Alarm02
MOV EBX, AlarmCnt
MOV [EAX+CountDown],EBX
MOV DWORD PTR [EAX+flnUse]
INC nTmrBlksUsed
MOV EBX, AlarmExch
MOV [ EAX+TmrRespExch ) , EBX
STI
MOV EAX.ErcOk
JMP Alarm03
;See if there's no delay
;EAX points to timer blocks
;Count of timer blocks
;clear direction flag
jcan't let others interfere
FALSE ; Empty block?
;No - goto next block
;Get delay count
TRUE ;Use the Timer Block
;Up the blocksInUse count
;put it in timer block!
;It's OK to interrupt now
;all is well
Alarm02:
STI
ADD EAX.sTmrBlk
LOOP Alarm01
MOV EAX.ErcNoMoreTBs
Alarm03:
MOV ESP, EBP
POP EBP
RETF 8
;It's OK to interrupt now
; unless were done
;Sorry, out of timer blocks
KillAkrmQ
KillAlarm searches the timer blocks looking for any block that is destined for the specified Alarm
exchange. All alarms that are set to fire off to that exchange are killed. If the alarm is already
queued through the kernel, which means the message has already been sent, nothing will stop
it. See Listing 20.7.

<!-- PDF page 435 -->
Chapter 20
Timer Management Code H
Listing 20.7. Code for the KillAlarm function.
; Procedural Interface:
; KillAlarm ( nAlarmExch ) :dErc
KAlarmExch EQU [EBP+0Ch]
PUBLIC _KillAlarm:
PUSH EBP
MOV EBP, ESP
CMP nTmrBlksUsed, 0
JE KAlarm03
MOV EBX, KAlarmExch
LEA EAX.rgTmrBlks
MOV ECX.nTmrBlks
CLD
No blocks in use
so we get out!
Get exchange for killing alarms to
EAX points to timer blocks
Count of timer blocks
clear direction flag
KAlarm01 CLI ;can't let others interfere
CMP DWORD PTR [EAX+f InUse] ,TRUE ;Block in use?
JNE KAlarm02 ;No - goto next block
CMP [EAX+TmrRespExch] ,EBX ;Does this match the Exchange?
JNE KAlarm02
MOV DWORD PTR [EAX+f InUse] , FALSE ;Make Empty
DEC nTmrBlksUsed ;Make blocksInUse correct
ST I
ADD EAX.sTmrBlk
LOOP KAlarm01
It's OK to interrupt now
unless were done
KAlarm03:
XOR EAX, EAX
MOV ESP, EBP
POP EBP
RETF 4
AL1 done
ErcOk
MicroDekyO
MicroDelay ( ) provides small-value timing delays for applications and device drivers. The count
is in 15us increments. The timing for this delay is based on the toggle of the refresh bit from
the system status port. The refresh bit is based on the system's quartz crystal oscillator, which
drives the processor clock.
The task is actually not suspended at all. This forms an instruction loop checking the toggled
value of an I/O port.
This call will not be very accurate for values less than 3 or 4 (45 to 60 microseconds). But it's
still very much needed. The call can also be inaccurate because of interrupts that are not dis-
abled. See Listing 20.8.

<!-- PDF page 436 -->
I
414
Part PV ■ The Operating System Source Code
Listing 20.8. Code for the MicroDelay function.
Procedural Interface
; MicroDelay (dDelay) :derror
PUBLIC _MicroDelay:
PUSH EBP
MOV EBP, ESP
MOV ECX, [EBP+0Ch]
CMP ECX, 0
JE MDL01
Get delay count
get out if they came in with 0!
MDL00:
IN AL, 61h
AND AL, 10h
CMP AH, AL
JE MDL00
MOV AH, AL
LOOP MDL00
;Get system status port
; check refrest bit
; Check toggle of bit
;No toggle yet
; Toggle! Move to AH for next compare
MDL01
XOR EAX
EAX
MOV ESP EBP ;
POP EBP
RETF 4
GetCMOSTimeQ
This reads the time from the CMOS clock on the PC-ISA machines. MMURTL doesn't keep
the time internally. The time is returned from the CMOS clock as a dword. The low order
byte is the seconds in Binary Coded Decimal (BCD); the next byte is the minutes in BCD; the
next byte is the Hours in BCD; and the high order byte is 0. See Listing 20.9.
Listing 20.9. Code to read the CMOS time.
; Procedural Interface:
; GetCMOSTime ( pdTimeRet ) : derror
pCMOSTimeRet EQU [EBP+12)
_GetCMOSTime:
PUSH EBP
MOV EBP, ESP
XOR EBX, EBX
Clear time return
MOV EAX, 04 h
OUT 70h,AL
IN AL,71h
MOV BL, AL
; Hours
SHL EBX, 8
MOV EAX,02h

<!-- PDF page 437 -->
Chapter 20 ■ Timer Management Code R
OUT 70h,AL
IN AL,71h
MOV BL.AL
SHL EBX, 8 ;Seconds
MOV EAX,00h
OUT 70h,AL
IN AL,71h
MOV BL.AL
MOV ESI, pCMOSTimeRet ;Give 'em the time
MOV [ESI], EBX
XOR EAX, EAX ; No Error
MOV ESP, EBP ;
POP EBP ;
RETF 4 ;
GetCMOSDateO
The date is returned from the CMOS clock as a DWord. The low-order byte is the day of the
week (BCD, 0-6 0=Sunday); the next byte is the day (BCD 1-31); the next byte is the month
(BCD 1-12); and the high-order byte is year (BCD 0-99). See Listing 20.10.
Listing 20.10. Code to read the CMOS date.
Procedural Interface:
GetCMOSDate(pdTimeRet) :derror
pCMOSDateRet EQU [EBP+12]
PUBLIC _GetCMOSDate:
PUSH EBP ;
MOV EBP, ESP ;
XOR EBX, EBX ;Clear date return
MOV EAX,09h ;Year
OUT 70h,AL
IN AL,71h
MOV BL, AL
SHL EBX, 8 ;
MOV EAX,08h ; Month
OUT 70h,AL
IN AL,71h
MOV BL, AL
SHL EBX, 8
MOV EAX,07h ;Day of month
OUT 70h,AL
IN AL,71h
MOV BL.AL
SHL EBX, 8 ;

<!-- PDF page 438 -->
Part IV ■ The Operating System Source Code
Listing 20.10. continued
MOV EAX,06h
OUT 70h,AL
IN AL,71h
MOV BL.AL
MOV ESI, pCMOSDateRet
MOV [ESI], EBX
XOR EAX, EAX
MOV ESP, EBP
POP EBP
RETF 4
;Day of week
;Give "em the time
; No Error
GetTimerTickQ
This returns the ever-increasing timer tick to the caller. MMURTL maintains a double word
counter that begins at zero and is incremented until the machine is reset or powered down.
Because it is a dword, there are over 4 billion ticks before rollover occurs. With a 10ms inter-
val, this amounts to 262,800,000 ticks per month. This means the system tick counter will
rollover approximately every 16 months. See Listing 20.1 1.
Listing 20.1 1 . Code to return the timer tick.
The procedural interface:
GetTimerTick(pdTickRet) :derror
The Current Timer Tick is returned (it's a DWord]
pTickRet EQU [EBP+12]
PUBLIC _GetTimerTick:
PUSH EBP
MOV EBP, ESP
MOV ESI, pTickRet
MOV EAX, Timer-Tick
MOV [ESI], EAX
XOR EAX, EAX
POP EBP
RETF 4
Beep_Work()
Beep_Work() is an internal function (a helper) used by the public calls Tone ( ) andBeep(). Hard-
ware timer number 2, which is connected to the system internal speaker, is used to generate

<!-- PDF page 439 -->
Chapter 20 M Timer Management Code
tones. The system clock drives the timer so the formula to find the proper frequency is a func-
tion of the clock's frequency. The length of time the tone is on is controlled by the Sleep ( )
function, which is driven by the system tick counter. See Listing 20.12.
Listing 20.12. Helper function for BeepQ and ToneQ.
;The clock freq to Timer 2 is 1.193182 Mhz
;To find the divisor of the clock, divide 1.193182Mhz by Desired Freq.
;This does all work for BEEP and TONE
;EBX needs the desired tone frequency in HERTZ
;ECX needs length of tone ON-TIME in 10ms increments
BEEP_Work:
MOV AL, 10110110b ; Timer 2, LSB, MSB, Binary
OUT 43h, AL
XOR EDX, EDX
MOV EAX, 1193182 ;1.193182Mhz
DIV EBX ;DIVIS0R is in EBX (Freq)
OUT 42h, AL ;Send quotient (left in AX)
MOV AL, AH
NOP
NOP
NOP
NOP
OUT 42h, AL
IN AL, 61h
OR AL, 00000011b
PUSH EAX
POP EAX
OUT 61 h, AL
PUSH ECX ;
CALL FWORD PTR _Sleep ;ECX is TIME ON in 50ms incs.
IN AL, 61 h
NOP
NOP
NOP
NOP
AND AL, 11111100b
OUT 61 h, AL
RETN
BeepQ
This function has nothing to with the Road Runner; if it had, I would have called it Beep_Beep ( ) .
(Sorry, I couldn't resist.) This provides a fixed tone from the system's internal speaker as a public
call for applications to make noise at the user. It uses the helper function Beep_Work described
earlier. See Listing 20.13.

<!-- PDF page 440 -->
418
Part W ■ The Operating System Source Code
Listing 20.1 3. Code to produce a beep.
Procedural Interface:
Beep( )
PUBLIC _Beep:
PUSH EBP
MOV EBP, ESP
MOV EBX, 800
MOV ECX, 35
CALL Beep_Work
POP EBP
RETF
Freq
350ms
ToneQ
Tone allows the caller to specify a frequency and duration of a tone to be generated by the system's
internal speaker. This call uses the helper function BeepWork described earlier. See Listing 20. 14.
Listing 20.14. Code to produce a tone.
Procedural Interface:
Tone(dFreq, dTickseRet) :derror
dFreq is a DWord with the FREQUENCY in HERTZ
dTicks is a DWord with the duration of the tone in
10ms increments
ToneFreq
EQU [EBP+10h]
ToneTime
EQU [EBP+0Ch]
PUBLIC
Tone:
PUSH EBF
MOV
EBP,
ESP
MOV
EBX,
ToneFreq
MOV
ECX,
ToneTime
CALL Beep Work
POP EBP
RETF 8

+ 1137
- 0
library/Developing Your Own 32-Bit Operating System/Chapter-21-Initialization-Code/Chapter-21-source-text.md
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 1531
- 0
library/Developing Your Own 32-Bit Operating System/Chapter-22-Job-Management-Code/Chapter-22-source-text.md
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 1656
- 0
library/Developing Your Own 32-Bit Operating System/Chapter-23-Debugger-Code/Chapter-23-source-text.md
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 2522
- 0
library/Developing Your Own 32-Bit Operating System/Chapter-24-Selected-Device-Driver-Code/Chapter-24-source-text.md
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 2334
- 0
library/Developing Your Own 32-Bit Operating System/Chapter-25-Keyboard-Code/Chapter-25-source-text.md
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 1185
- 0
library/Developing Your Own 32-Bit Operating System/Chapter-26-Video-Code/Chapter-26-source-text.md
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 3797
- 0
library/Developing Your Own 32-Bit Operating System/Chapter-27-File-System-Code/Chapter-27-source-text.md
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 1434
- 0
library/Developing Your Own 32-Bit Operating System/Chapter-28-DASM-A-32-Bit-Intel-Based-Assembler/Chapter-28-source-text.md
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 1012
- 0
library/Developing Your Own 32-Bit Operating System/Chapter-29-CM32-A-32-Bit-C-Compiler/Chapter-29-source-text.md
Разница между файлами не показана из-за своего большого размера
Просмотреть файл


+ 859
- 0
library/Developing Your Own 32-Bit Operating System/Front-Matter/Front-Matter-source-text.md Просмотреть файл

@@ -0,0 +1,859 @@
# Developing Your Own 32-Bit Operating System — Front-Matter: Front matter (covers, title, copyright, contents, preface, acknowledgments, about the author)
- **Source**: /library/Developing Your Own 32-Bit Operating System/source-file.pdf
- **PDF pages**: 1–22
- **Pages without text**: none

---

<!-- PDF page 1 -->
SAMS
PUBLISHING
CD-ROM contains source code for a 32-bit
operating system, a 32-bit assembler, a
W 32-bit C compiler, and more!
Master the details of 32-bit
programming and operating
systems
Explore interprocess
communications,
hardware interfaces,
systems programming,
file system services,
API specifications,
and more
Design and develop a feature-
\ rich, 32-bit operating system
Developing Your Own
Operating System

<!-- PDF page 2 -->
Have you ever wanted to write your own computer operating system?
Or have you ever wanted to find out what it would take?
Or have you just wanted to see the source code with real comments?
If so, then this book is for you! This book contains the ideas, sugges-
tions, and tools you will need — plus a sample operating system,
MMURTL.
You will see these features of MMURTL highlighted:
Micro-kernel
Message based
Real-time
Multitasking
Multithreaded
Client/server oriented
ISA 32-bit PC-compatible
The CD-ROM contains:
Complete MMURTL operating system source code
Sample applications, including source code
A 32-bit assembler, including source code
A 32-bit C compiler, including source code

<!-- PDF page 3 -->
irO
Developing Your Own
32-Bit Operating System
Richard A. Burgess
SAMS
PUBLISHING
201 West 103rd Street
Indianapolis, Indiana 46290

<!-- PDF page 4 -->
For Deborah, Valerie, and
Heather
Copyright © 1995 by Sams Publishing
FIRST EDITION
All rights reserved. No pan of this book shall be reproduced, stored in a retrieval
system, or transmitted by any means, electronic, mechanical, photocopying,
recording, or otherwise, without written permission from the publisher. No patent
liability is assumed with respect to the use of the information contained herein.
Although every precaution has been taken in the preparation of this book, the
publisher and author assume no responsibility for errors or omissions. Neither is
any liability assumed for damages resulting from the use of the information
contained herein. For information, address Sams Publishing, 201 W. 103rd St.,
Indianapolis, IN 46290.
International Standard Book Number: 0-672-30655-7
Library of Congress Catalog Card Number: 94-69271
98 97 96 95 4 3 2 1
Interpretation of the printing code: the rightmost double-digit number is the year
of the book's printing; the rightmost single-digit, the number of the book's
printing. For example, a printing code of 95-1 shows that the first printing of the
book occurred in 1995.
Composed in AGaramond and MCPdigital by Macmillan Computer Publishing
Printed in the United States of America
Trademarks
All terms mentioned in this book that are known to be trademarks or service marks
have been appropriately capitalized. Sams Publishing cannot attest to the accuracy
of this information. Use of a term in this book should not be regarded as affecting
the validity of any trademark or service mark.

<!-- PDF page 5 -->
Publisher
Richard K. Swadley
Acquisitions Manager
Greg Wiegand
Managing Editor
Cindy Morrow
Acquisitions Editors
Chris Denny
Stacy Hiquet
Development Editors
Todd Bumbalough
Bradley L. Jones
Software Development Specialist
Timothy Wilson
Production Editor
Deborah Frisby
Copy Editors
Fran Blauw
Johnna VanHoose
Joe Williams
Editorial Coordinator
Bill Whitmer
Editorial Assistants
Carol Ackerman
Sharon Cox
Lynette Quinn
Technical Reviewer
David McLarty
Marketing Manager
Team Supervisor
Gregg Bushyeager
Katy Bodenmiller
Assistant Marketing Manager
Page Layout
Michelle Milner
Charlotte Clapp
Cover Designer
Mary Ann Cosby
Judy Everly
Tim Amrhein
Shawn MacDonald
Book Designer
Tina Trettin
Alyssa Yesh
Proofreading
Director of Production and
Donna Martin
Manufacturing
Brian-Kent Proffitt
JeffValkr
Erich J. Richter
Production Manager
Indexer
Kelly Dobbs
GregEldred
Manufacturing Coordinator
Paul Gilchrist
Production Analysts
Angela D. Bannan
Bobbi Satterfield
Dennis Clay Hager
Support Services Supervisor
Mary Beth Wakefield
Graphics Image Specialists
Teresa Forrester
Clint Lahnen
Tim Montgomery
Dennis Sheehan
Greg Simsic
Susan VandeWalle
JeffYesh

<!-- PDF page 6 -->
Overview
Preface
I Introduction
1 Overview
2 General Discussion and Background
II Architecture and General Theory
3 The Tasking Model
Interprocess Communications
Memory Management
The Hardware Interface
OS Initialization
Programming Interfaces
III MMURTL-Specific Details
9 Application Programming
Systems Programming
The MMURTL Monitor Program
The Debugger
Keyboard Service
File System Service
API Specification
MMURTL Sample Software
IV The Operating System Source Code
17 Introduction to the Source Code
Kernel Code
Memory Management Code
Timer Management Code
Initialization Code
Job Management Code
10
11
12
13
14
15
16
18
19
20
21
22
3
13
25
41
55
81
99
115
129
141
159
181
189
201
221
265
327
335
377
405
419
439

<!-- PDF page 7 -->
23 Debugger Code
24 Selected Device Driver Code
25 Keyboard Code
26 Video Code
27 File System Code
V Additional Software
28 DASM: A 32-Bit Intel-Based Assembler
29 CM32: A 32-Bit C Compiler
Index
471
497
541
571
591
663
697
719

<!-- PDF page 8 -->
Contents
Part I Introduction
1 Overview 3
What This Book Is About '
5
Who Should Use This Book 6
My Basic Design Goals (Yours May Vary)
7
A Brief Description of MMURTL 9
Uses for MMURTL 9
Similarities to Other Operating Systems 10
Hardware Requirements 10
2 General Discussion and Background 13
Where Does One Begin? 14
You Are Where You Were When 14
The Development Platform and the Tools 1 5
The Chicken and the Egg 16
Early Hardware Investigation 17
The Real Task at Hand (Pun Intended) 20
A Starting Point 20
The Critical Path
21
Working Attitude 22
Part II Architecture and General Theory
3 The Tasking Model 25
Terms 26
Tasks
26
The Kernel 27
Preemptive Multitasking 27
Resource Management 27
Task Management 28
CPU Time 30
Single Tasking 30
Multitasking
31
Cooperative Multitasking 31
Preemptive Multitasking 32
Task Scheduling 32
Who, When, and How Long?
32
Scheduling Techniques 33
Other Scheduling Factors 35

<!-- PDF page 9 -->
Mixed Scheduling Techniques 36
The Tasking Model I Chose 38
Interrupt Handling 39
Interprocess Communications 41
Messaging and Tasks 42
Synchronization 42
Semaphores 43
Pipes 44
Messages 44
Reentrancy Issues 51
Interrupt Latency 53
Memory Management Issues 53
Memory Management 55
Basic Terms 56
Memory Model 57
Simple Flat Memory 57
Paged Flat Memory 58
Demand-Paged Flat Memory 59
Virtual Paged Memory 59
Demand Paged Virtual Memory 61
Segmented Memory 61
Memory Management 62
Tracking Linear Memory Allocation 63
Tracking Physical Memory 69
Initialization of Memory Management 69
Memory Protection 70
An Intel-Based Memory-Management Implementation 70
A Few More Words On Segmentation 71
How MMURTL Uses Paging 72
The Hardware Interface 81
Hardware Isolation 82
The CPU 82
The Bus Structure 83
Serial I/O
84
Parallel I/O 85
Block-Oriented Devices 85
The Keyboard 86
Video 87

<!-- PDF page 10 -->
Qjjj]
Developing Your Own 32-Bit Operating System
Direct Memory Access (DMA) 89
Timers 97
The Priority Interrupt Controller Unit (PICU) 98
7 The OS Initialization 99
Getting Booted „ 100
Boot ROM 100
The Boot Sector 101
Operating System Boot Image 110
Other Boot Options Ill
Basic Hardware Initialization 1 1 1
Static Tables and Structures 112
Initialization of Task Management 112
Initialization of Memory 113
Dynamic Tables and Structures 1 14
8 Programming Interfaces 115
Application Programming Interface 116
Documenting Your API 116
Procedural Interfaces 116
Systems Programming Interface 120
Internal Cooperation 120
Device Driver Interfaces 120
A Device-Driver Interface Example 121
Part III MMURTL-Specific Details
9 Application Programming 129
Terminology 130
Understanding 32-Bit Software 130
Operating-System Calling Conventions 131
Stack Usage 132
Memory Management 132
Operating-System Protection 134
Application Messaging 135
Starting a New Thread 136
Job Control Block 136
Basic Keyboard 138
Basic Video 138
Terminating Your Application 140
Replacing Your Application 140

<!-- PDF page 11 -->
Contents H
10 Systems Programming 141
Writing Message-Based System Services 142
Initializing Your System Service 142
A Simple System Service Example 143
The Request Block 144
Items in the Request Block 144
Asynchronous Services 146
System Service Error Handling 146
Writing Device Drivers 147
Device Driver Theory 148
Building Device Drivers 148
How Callers Reach Your Driver 149
Device Driver Setup and Installation 149
System Resources for Device Drivers 150
Detailed Device Interface Specification 152
Initializing Your Driver 157
Operating System Functions for Device Drivers 158
Standard Device Error Codes 158
1 1 The MMURTL Monitor Program 1 59
Active Job (Video and Keyboard) 160
Initial Jobs 160
Monitor Function Keys 161
Monitor Program Theory 161
Performance Monitoring 162
Monitor Source Listing 163
12 The Debugger 181
Using the Debugger 182
Entering the Debugger 182
Exiting the Debugger 182
Debugger Display 183
Debugger Function Keys 183
Debugging Your Application 185
Debugger Theory 185
13 Keyboard Service 189
Available Service 190
Read Keyboard 191
Notify On Global Keys 191
Cancel Notify On Global Keys 192
Assign Keyboard 192

<!-- PDF page 12 -->
m
Developing Your Own 32-Bit Operating System
Key Codes and Status 193
Alphanumeric Key Values 193
Shift-State Byte (Second Byte) 193
Lock-State Byte (Third Byte)
194
Numeric Pad Indicator 194
Key Codes :
194
Function Strip Key Codes 196
Numeric Pad Key Codes 197
Cursor, Edit, and Special Pad Key Codes 198
Your Keyboard Implementation 198
14 File System Service 201
File Specifications 202
Network File Request Routing 202
File Handles 203
File Open Modes 203
File Access Type 203
Block Mode 203
Stream Mode 204
Logical File Address 204
File System Requests 204
Procedural Interfaces 206
Device Access Through the File System 206
File System Functions in Detail 207
OpenFile
207
CloseFile 207
ReadBlock 208
WriteBlock 209
ReadBytes
209
WriteBytes
210
GetFileLFA 211
SetFileLFA 211
GetFileSize 212
SetFileSize 213
CreateFile 213
RenameFile 214
DeleteFile
214
Create Directory 215
DeleteDirectory 216
GetDirSector 216

<!-- PDF page 13 -->
Contents H
File System Theory 217
Internal File System Structures 217
File Operations 219
15 API Specification 221
Public Calls 222
Parameters to Calls (Args) 222
Stack Conventions 224
Calls Grouped by Functionality 224
Alphabetical Call Listing 227
AddCallGate 227
AddlDTGate 228
Alarm 229
AliasMem 229
AllocDMAPage 230
AllocExch 230
AllocOSPage 230
AllocPage 231
Beep 231
Chain 231
CheckMsg 232
ClrScr
232
Compare 233
CompareNCS 233
CopyData 233
CopyDataR 233
DeAliasMem 234
DeAllocExch 234
DeAllocPage 234
Devicelnit 235
DeviceOp 235
DeviceStat
236
DMASetUp 236
EditLine 237
EndOflRQ 237
Exitjob 238
FillData 238
GetCmdLine 238
GetCMOSTime 239
GetCMOSDate 239

<!-- PDF page 14 -->
H
Developing Your Own 32-Bit Operating System
GetDMACount 240
GetExitJob 240
GetlRQVector 240
GetJobNum 241
GetNormVid 241
GetPath .' 241
GetPhyAdd 242
GetpJCB 242
GetSysIn 242
GetSysOut
243
GetTimerTick 243
GetTSSExch 243
GetUserName 243
GetVidChar 244
GetVidOwner 244
GetXY 244
InByte
244
InDWord 245
InWord
245
InWords 245
InitDevDr 245
ISendMsg
246
KillAlarm 247
LoadNewJob 247
MasklRQ 248
MicroDelay 248
MoveRequest 248
NewTask 249
OutByte 249
OutDWord 249
OutWord 250
OutWords 250
PutVidAttrs
250
PutVidChars 250
QueryPages 251
ReadCMOS 251
ReadKbd 251
RegisterSvc 252
Request 252
Respond 254
ScrollVid
254

<!-- PDF page 15 -->
Contents H
SendMsg 254
SetCmdLine 255
SetExitJob 255
SetlRQVector 256
SetJobName 257
SetNormVid 257
SetPath 258
SetPriority
258
SetSysIn 258
SetSysOut
259
SetUserName 259
SetVidOwner 259
SetXY 260
Sleep
260
SpawnTask 261
Tone
261
TTYOut 262
UnMasklRQ 262
UnRegisterSvc 263
WaitMsg
263
16 MMURTL Sample Software 265
Command-Line Interpreter (CLI) 266
Internal Commands 266
External Commands 270
Global "Hot Keys" 271
CLI Source Listing 271
A Simple Editor 288
Editor Screen 288
Editor Commands 288
Editor Source Listing
290
DumbTerm 314
Print 317
Print Source Listing
317
System Service Example 322
Service.C Listing 322
TestSvc.C Listing 324
Part IV The Operating System Source Code
17 Introduction to the Source Code 327
Comments in the Code 328

<!-- PDF page 16 -->
H
Developing Your Own 32-Bit Operating System
Calling Conventions 328
Organization 328
Building MMURTL 331
Using Pieces of MMURTL in Your OS 332
Using Pieces of MMURTL in Other Projects 333
18 Kernel Code 335
Naming Conventions 336
Kernel Data 336
Local Kernel Helper Functions 337
enQueueMsg 337
deQueueMsg 338
deQueueTSS 339
enQueueRdy 340
deQueueRdy 340
ChkRdyQ
341
Internal Public Helper Functions 342
RemoveRdyJob 342
GetExchOwner 344
SetExchOwner 345
SendAbort 346
Public Kernel Functions 348
Request()
348
RespondQ 351
MoveRequest() 354
SendMsgQ 355
ISendMsgQ 357
WaitMsgf) 359
CheckMsgQ 363
NewTaskQ 366
SpawnTaskQ 368
AllocExchQ 370
DeAlbcExchO 371
GetTSSExchQ 373
SetPriorityO 374
GetPriorityO 374
19 Memory Management Code 377
Memory Management Data 378
Internal Code 378
InitMemMgmt 379
FindHiPage 381
FindLoPage
382

<!-- PDF page 17 -->
Contents H
MarkPage 383
UnMarkPage 384
LinToPhy 385
FindRun 385
AddRun 387
AddAliasRun 388
AddUserPT 390
AddOSPT 392
Public Memory Management Calls 394
AddGDTCallGate 394
AddlDTGate 395
AllocOSPage 396
AliasMem 397
DeAliasMem 400
DeAlbcPage 401
QueryMemPages 403
GetPhyAdd 404
20 Timer Management Code 405
Choosing a Standard Interval 406
Timer Data 407
Timer Code 408
The Timer Interrupt 408
SleepQ 410
AlarmQ 411
KillAUrmQ 412
MicroDelayO 413
GetCMOSTimeO 414
GetCMOSDateQ 415
GetTimerTickQ 416
Beep_Work() 416
BeepQ
417
Tone() 418
21 Initialization Code 419
OS Global Data 420
Operating System Entry Point 422
Initialization Helpers 429
22 Job Management Code 439
Reclamation of Resources
440
Job Management Helpers 440

<!-- PDF page 18 -->
H
Developing Your Own 32-Bit Operating System
Job Management Listing 448
23 Debugger Code 471
Debugger Interrupt Procedure 472
Debugger Source Listing 473
24 Selected Device Driver Code 497
IDE Disk Device Driver 498
The RS-232 Asynchronous Device Driver 519
RS232.h 519
The Device Driver, RS-232.C 520
25 Keyboard Code 541
Keyboard Data 542
Keyboard Code 550
Keyboard ISR
550
Keyboard Translation 551
Reading the Final Buffer 557
The Keyboard Service 557
Keyboard Procedural Interface 563
Debugger Keyboard 565
Keyboard Hardware Set Up
566
Keyboard Service Initialization 567
Hardware Helpers 568
26 Video Code 571
Virtual Video Concept 572
VGA Text Video 572
The Video Code 573
Relating MMURTL's Video Code to Your Operating System 589
27 File System Code 591
How MMURTL Handles FAT 592
Physical (Absolute) Sector Numbers 592
Hidden Sectors 592
Logical Block Address (LBA) 592
MS-DOS Boot Sector 593
File System Initialization 593
File System Listing
593
Part V Additional Software
28 DASM: A 32-Bit Intel-Based Assembler 663
DASM Concepts 664

<!-- PDF page 19 -->
Contents
xvii
Using DASM 665
Command Line Options 667
DASM Source Files 668
Local (Nonpublic) Declarations 669
Memory Model
669
DASM Program Structure 669
DOT Commands 670
DASM Versus DOS Assemblers 672
DASM Addressing Modes 673
Storage Declarations 675
DASM Peculiar Syntax 676
386 Instruction Set 682
Executable File Format 688
Error Codes From DASM 693
Your Own Assembler? 696
29 CM32: A 32-Bit C Compiler 697
Memory Usage and Addressing 699
Important Internal Differences 699
Variable-Length Argument Lists 699
CM32 Language Specifics 700
Far Calls
700
Interrupt Functions 700
Supported C Language Features 701
Library Header File Access 702
Limitations as Compared to ANSI 702
Far External Functions 703
Type Conversion and Pointers 704
Initialization of Static Variables 704
Other Implementation-Dependent Information 706
Using CM32 707
Command Line Options 707
Library Functions 708
Library Code Examples 709
Index 719

<!-- PDF page 20 -->
Preface
I'm taking this opportunity to give you some insight into the frame of mind I've been in for
the past few years while writing my own operating system. For years I have developed software
under the direction of others, using tools designed by others, eventually for others to use. The
tools I used were top-quality, the operating systems I used all worked as advertised for the most
part, and the jobs got done.
One of the more perplexing things I ran across was the tendency to over-design something.
When this tendency was added to the requirements for backward compatibility, the resulting
products could become endless projects, or even nightmares, if not carefully watched.
I wanted to break free of the bonds of backward compatibility. I wanted to produce a system
that was powerful, yet elegant and simple in its implementation. I also wanted to make use of
the more interesting designs and ideas I have run across over the years. When an avenue was
presented to me that could reduce the complexity of the code and sacrifice only a small amount
of power, it was my obvious path.
Quite often, I refer to Albert Einstein's words on simplicity: "Everything should be as simple
as possible, but not simpler." You'll run across this idea or a reference to it in several chapters
of this book. Simplicity was also the best way to provide you with a foundation of information
and ideas for your own operating system, especially if you've never undertaken such a gargan-
tuan project.
Here's one other important note: Version 1.0 (or the first-released version) of every operating
system I've ever used was not to be trusted with valuable information and was not a good basis
on which to build a time-critical project. I put MMURTL, the operating system included with
this book, in this same class. It's new, untested, and still under construction to a large extent.
Good luck, and have fun!
Richard A. Burgess

<!-- PDF page 21 -->
Acknowledgments
This has been a long five years. I have had a tremendous amount of help along the way. In
Chapter 2, "General Discussion and Background," I mention a friend who initially started with
me on this project. He is Reginald B. Carie, code-cutter extraordinaire. Very early in the game,
Reggie went on to make more important things than an operating system. He and his lovely
wife had a baby. Now that's work. Thanks Reg — I couldn't have gotten this far without you.
Not a whole lot of what we did together is left, but your enthusiasm will remain forever.
I was on active duty in the U.S. Coast Guard when I started MMURTL. Many people at the
U.S. Coast Guard Information Systems Center (now called the Telecommunications and In-
formation Systems Command) in Alexandria, Virginia, were backboards for my ideas and were
sources of information. Tom Clark (the one-man think tank) and Dan Haynes (the wunderkind
of programming) lost a few lunch hours to my babbling questions and ideas. Commander Scott
Bair laughed at me when I needed it. Thanks guys.
Dave McLarty of Convergent Consultants of Atlanta made some serious contributions while
listening to my ideas (and paying the phone bill most of the time). Dave knows real-time
multitasking and has taken it places I've never even been. Thanks Dave.
I had learned a lot at the 4 1/2-year mark, and I decided to share what I had learned in the
form of a book. I initially had delusions of grandeur for a commercial product, but that's too
much work. (Hey! I'm retired....) When I submitted the proposal to a publisher, I immedi-
ately got a positive response from Stacy Hiquet of Sams Publishing. Stacy was briefly my
acquisitions editor and was promoted off my book on to bigger things. Congratulations Stacy
and thanks. Chris Denny took over as acquisitions editor and has been so easy to work with
that it's almost scary. Thanks Chris. Brad Jones (famous author and all-around great guy) has
been one of my development editors. He has given me some serious insight into the publishing
industry. I have been doing my best not to be a thorn in his side, and he hasn't once destroyed
my will to live. Thanks Brad. Thanks also to Todd Bumbalough, development editor, who
helped guide the project in its later stages. Thanks to the production editor, Deborah Frisby
and to the copy editors, Fran Blauw, Johnna VanHoose, and Joe Williams.
I must also apologize to the copy editors and proofreaders. I do know the difference between
there, their, and they're, and also between its and it's. While typing at 60 WPM, listening to the
Crash Test Dummies and Boingo at about 100 decibels, and gulping down coffee and sodas
like nobody's business, anything can happen and usually does. Hundreds of pages in six
months. . .never again.
Special thanks to Nita, for some serious scanning of the mental manuscript.
Special thanks to Dave Dunfield of Dunfield Development Systems; Nepean, Ontario; Canada,
for his contributions to the CD-ROM and for the beginnings of the CM-32 compiler.

<!-- PDF page 22 -->
About the Author
Richard A. Burgess is a retired U.S. Coast Guard commissioned officer (1993). His first 10
years in the Coast Guard were in electronics technology and maintenance. His last 10 years of
service were all computer-science related; he was the primary software design engineer for the
Coast Guard's Communication Station Control System, and he headed the Coast Guard's
Software Development Standards Team. He taught computer electronics technology at the
Computer Learning Center ofWashington, D.C., for two years, and he consulted in and around
Washington under the guise of The D-Group. He is now the Chief Information Officer with
Optipat, Inc. of Arlington, Virginia, a rapidly growing, high-technology, patent-trademark-
and-copyright imaging firm. He would rather be on a beach somewhere in Florida and claims
he is working to that end.

+ 85
- 0
library/Developing Your Own 32-Bit Operating System/book-structure.md Просмотреть файл

@@ -0,0 +1,85 @@
# Developing Your Own 32-Bit Operating System — Structure

- **Author**: Richard A. Burgess (Sams Publishing, 1995, first edition; ISBN 0-672-30655-7)
- **Source**: `source-file.pdf` (772 PDF pages). This is an Internet Archive scan with an OCR text layer and no bookmarks.
- **Chapter count**: 29 chapters in five Parts, plus front and back matter.
- **Page numbers**: PDF pages, starting at 1. **Printed page N = PDF page N + 22** throughout the body (checked at every chapter start).
- **Subject**: The design and full source of MMURTL, a message-based, real-time, multitasking micro-kernel OS for 32-bit ISA PCs, along with its DASM assembler and CM32 C compiler.

| Reading order | Part | Section | Printed page | Starts on PDF page |
| --- | --- | --- | ---: | ---: |
| Chapter 1 | I Introduction | Overview | 3 | 25 |
| Chapter 2 | I | General Discussion and Background | 13 | 35 |
| Chapter 3 | II Architecture and General Theory | The Tasking Model | 25 | 47 |
| Chapter 4 | II | Interprocess Communications | 41 | 63 |
| Chapter 5 | II | Memory Management | 55 | 77 |
| Chapter 6 | II | The Hardware Interface | 81 | 103 |
| Chapter 7 | II | OS Initialization | 99 | 121 |
| Chapter 8 | II | Programming Interfaces | 115 | 137 |
| Chapter 9 | III MMURTL-Specific Details | Application Programming | 129 | 151 |
| Chapter 10 | III | Systems Programming | 141 | 163 |
| Chapter 11 | III | The MMURTL Monitor Program | 159 | 181 |
| Chapter 12 | III | The Debugger | 181 | 203 |
| Chapter 13 | III | Keyboard Service | 189 | 211 |
| Chapter 14 | III | File System Service | 201 | 223 |
| Chapter 15 | III | API Specification | 221 | 243 |
| Chapter 16 | III | MMURTL Sample Software | 265 | 287 |
| Chapter 17 | IV The Operating System Source Code | Introduction to the Source Code | 327 | 349 |
| Chapter 18 | IV | Kernel Code | 335 | 357 |
| Chapter 19 | IV | Memory Management Code | 377 | 399 |
| Chapter 20 | IV | Timer Management Code | 405 | 427 |
| Chapter 21 | IV | Initialization Code | 419 | 441 |
| Chapter 22 | IV | Job Management Code | 439 | 461 |
| Chapter 23 | IV | Debugger Code | 471 | 493 |
| Chapter 24 | IV | Selected Device Driver Code | 497 | 519 |
| Chapter 25 | IV | Keyboard Code | 541 | 563 |
| Chapter 26 | IV | Video Code | 571 | 593 |
| Chapter 27 | IV | File System Code | 591 | 613 |
| Chapter 28 | V Additional Software | DASM: A 32-Bit Intel-Based Assembler | 663 | 685 |
| Chapter 29 | V | CM32: A 32-Bit C Compiler | 697 | 719 |
| Back matter | — | Index | 719 | 741 |

Part divider plates (Part number and title, then a blank verso) sit on the two PDF pages before Chapters 1, 3, 9, 17 and 28. They are folded into those chapters' `source-text.md`, so their ranges start two pages before the chapter plate. Titles come from the chapter title plates. The printed Contents says "The OS Initialization" for Chapter 7, but the plate says "OS Initialization".

**Reading note**: Parts I–II (Ch 1–8) are general OS design theory. Part III (Ch 9–16) covers MMURTL-specific usage and API. Parts IV–V (Ch 17–29) are mostly annotated source listings, which are better skimmed or used as reference than read for coaching.

## Source Text Index
Extracted from `source-file.pdf` by `tools/split_book.py`. Read these instead of the PDF. All 772 pages are covered exactly once.

| Folder | Section | PDF pages | Pages without extractable text |
| --- | --- | --- | --- |
| Front-Matter | Front matter (covers, title, copyright, contents, preface, acknowledgments, about the author) | 1–22 | none |
| Chapter-01-Overview | Overview | 23–34 | 23, 24, 25, 34 |
| Chapter-02-General-Discussion-and-Background | General Discussion and Background | 35–44 | none |
| Chapter-03-The-Tasking-Model | The Tasking Model | 45–62 | 45, 46, 47 |
| Chapter-04-Interprocess-Communications | Interprocess Communications | 63–76 | 63 |
| Chapter-05-Memory-Management | Memory Management | 77–102 | 77 |
| Chapter-06-The-Hardware-Interface | The Hardware Interface | 103–120 | 103 |
| Chapter-07-OS-Initialization | OS Initialization | 121–136 | 121 |
| Chapter-08-Programming-Interfaces | Programming Interfaces | 137–148 | 137, 148 |
| Chapter-09-Application-Programming | Application Programming | 149–162 | 149, 150, 151 |
| Chapter-10-Systems-Programming | Systems Programming | 163–180 | 163 |
| Chapter-11-The-MMURTL-Monitor-Program | The MMURTL Monitor Program | 181–202 | 202 |
| Chapter-12-The-Debugger | The Debugger | 203–210 | 203 |
| Chapter-13-Keyboard-Service | Keyboard Service | 211–222 | 211, 222 |
| Chapter-14-File-System-Service | File System Service | 223–242 | 223 |
| Chapter-15-API-Specification | API Specification | 243–286 | 243, 286 |
| Chapter-16-MMURTL-Sample-Software | MMURTL Sample Software | 287–346 | 287 |
| Chapter-17-Introduction-to-the-Source-Code | Introduction to the Source Code | 347–356 | 347, 348, 349, 356 |
| Chapter-18-Kernel-Code | Kernel Code | 357–398 | 357, 398 |
| Chapter-19-Memory-Management-Code | Memory Management Code | 399–426 | 399 |
| Chapter-20-Timer-Management-Code | Timer Management Code | 427–440 | 427 |
| Chapter-21-Initialization-Code | Initialization Code | 441–460 | 441 |
| Chapter-22-Job-Management-Code | Job Management Code | 461–492 | 461, 492 |
| Chapter-23-Debugger-Code | Debugger Code | 493–518 | 493 |
| Chapter-24-Selected-Device-Driver-Code | Selected Device Driver Code | 519–562 | 519 |
| Chapter-25-Keyboard-Code | Keyboard Code | 563–592 | 563 |
| Chapter-26-Video-Code | Video Code | 593–612 | 593 |
| Chapter-27-File-System-Code | File System Code | 613–682 | 613 |
| Chapter-28-DASM-A-32-Bit-Intel-Based-Assembler | DASM: A 32-Bit Intel-Based Assembler | 683–718 | 683, 684 |
| Chapter-29-CM32-A-32-Bit-C-Compiler | CM32: A 32-Bit C Compiler | 719–740 | 719 |
| Back-Matter | Index, publisher ads, CD-ROM installation and contents, back cover | 741–772 | 741, 764, 766, 769 |

**Pages without text, all checked visually**: each is a Part or chapter title plate, a blank verso, or a blank ad page, so no content is lost and no OCR is needed.

**OCR caveats**: The text is the scan's OCR, so expect broken spacing, character errors (e.g. `l`/`I`/`1`, `()` read as `Q` or `O` in function names), and garbled source listings. Diagrams inside text pages are images that the text layer does not capture. When a figure or a listing's exact characters matter, check that PDF page.

Двоичные данные
library/Developing Your Own 32-Bit Operating System/source-file.pdf Просмотреть файл


+ 46
- 0
tools/split_book.py Просмотреть файл

@@ -141,7 +141,53 @@ def build_inference():
return secs return secs




def build_os32():
# Scanned book (no bookmarks): printed page N is PDF page N + 22. Chapters that open a Part
# take in the Part divider plate and its blank verso, two pages before the chapter plate.
chapters = [ # (n, title, printed start page, part that opens here)
(1, "Overview", 3, "I"),
(2, "General Discussion and Background", 13, ""),
(3, "The Tasking Model", 25, "II"),
(4, "Interprocess Communications", 41, ""),
(5, "Memory Management", 55, ""),
(6, "The Hardware Interface", 81, ""),
(7, "OS Initialization", 99, ""),
(8, "Programming Interfaces", 115, ""),
(9, "Application Programming", 129, "III"),
(10, "Systems Programming", 141, ""),
(11, "The MMURTL Monitor Program", 159, ""),
(12, "The Debugger", 181, ""),
(13, "Keyboard Service", 189, ""),
(14, "File System Service", 201, ""),
(15, "API Specification", 221, ""),
(16, "MMURTL Sample Software", 265, ""),
(17, "Introduction to the Source Code", 327, "IV"),
(18, "Kernel Code", 335, ""),
(19, "Memory Management Code", 377, ""),
(20, "Timer Management Code", 405, ""),
(21, "Initialization Code", 419, ""),
(22, "Job Management Code", 439, ""),
(23, "Debugger Code", 471, ""),
(24, "Selected Device Driver Code", 497, ""),
(25, "Keyboard Code", 541, ""),
(26, "Video Code", 571, ""),
(27, "File System Code", 591, ""),
(28, "DASM: A 32-Bit Intel-Based Assembler", 663, "V"),
(29, "CM32: A 32-Bit C Compiler", 697, ""),
]
index_start = 719 + 22
starts = [(n, t, s + 22 - (2 if part else 0), part) for n, t, s, part in chapters]
secs = [("Front-Matter", "Front matter (covers, title, copyright, contents, preface, acknowledgments, about the author)", 1, starts[0][2] - 1, "")]
for i, (n, t, a, part) in enumerate(starts):
end = starts[i + 1][2] - 1 if i + 1 < len(starts) else index_start - 1
note = f"Includes the Part {part} divider plate and its blank verso before the chapter." if part else ""
secs.append((ch(n), t, a, end, note))
secs.append(("Back-Matter", "Index, publisher ads, CD-ROM installation and contents, back cover", index_start, 772, ""))
return secs


BUILDERS = { BUILDERS = {
"Developing Your Own 32-Bit Operating System": build_os32,
"Context Engineering": build_cetx, "Context Engineering": build_cetx,
"The 12 Week Year": build_12wy, "The 12 Week Year": build_12wy,
"Your Best Year Ever": build_ybye, "Your Best Year Ever": build_ybye,


Загрузка…
Отмена
Сохранить

Powered by TurnKey Linux.