25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

28KB

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

Programming Interfaces

]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

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

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.

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.

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.

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

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

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

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

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.

Powered by TurnKey Linux.