Dr Jefyll wrote:
Can you clarify, please, Andrew? I have a feeling I'm missing something. It seems to me that "just push all the key registers on the stack" will work whether you call the system with a BRK or with a JSR. (Maybe it's a little handier if the desired function is specified by contents of a register rather than by the signature byte of a BRK, but that remark applies whether the operating system is multi-tasking or not.) I'd say JSR and BRK are merely the mechanism that gets you from point A to point B. After you arrive, does it matter how you got there?
I'm guessing that Andrew meant that the software interrupt front end would do the register saving, eliminating the need for the caller to handle that chore. On the other hand, if the function is called as a subroutine then the code immediately before the call has to handle saving state, which compounds if multiple calls are made to the same routine.
In my 65C816 interrupts article, I highlight several of the advantages of using software interrupts to call system services. Here is the relevant prose:
Over the years, various methods that implement API calls have been devised, the two most common being that of treating a kernel function as a conventional subroutine or treating a kernel function as a specialized form of an interrupt service routine.
In the former case, a static jump table provides access to the internal kernel functions. Perhaps the best known example of a kernel jump table is the one present in the Commodore 64's "kernal" ROM, with which any Commodore eight bit assembly language programmer will be familiar. User programs access kernel functions by treating them as subroutines and pass parameters via the microprocessor registers. Each kernel function has a unique entry point in the jump table, which as the "static" adjective implies, appears at a fixed address, with entries in an immutable order. The result is that assembly language programs that use only the jump table to access the kernel are portable to any eight bit Commodore computer in which the required kernel functions are present.
In the interrupt service routine method, APIs are called via a kernel trap, which is a machine-dependent code sequence that transfers execution from the user program to the kernel. Each API call is assigned an immutable index number that tells the kernel what code must be executed to complete the desired function. Along with the API index number, any parameters to be passed to the kernel are loaded into the microprocessor's registers and/or pushed to the hardware stack before the call. Any parameters returned by the API are likewise loaded in the registers and/or placed on the stack. Implied is that the microprocessor has a large number of general purpose registers, has instructions to address the stack by means other than just pushes and pulls, or both capabilities.
Naturally, both API calling methods have their strong and weak points. Use of a jump table makes for simple user application programming and a generally less complicated kernel. Applications merely JSR to access the API and the kernel exits with RTS. The required kernel code can be very small and fast-executing, which was an important consideration in early home computers. However, once a system has been developed with a specific jump table layout, the design is essentially cast in concrete, even if future hardware and/or operating system revisions would be better served with a relocated kernel and/or rearranged jump table. The fact that applications must know where in memory the kernel is loaded and must be able to access that memory makes the kernel non-portable and if running in RAM instead of ROM, vulnerable to corrupting wild writes caused by program errors and/or malicious coding.
Calling APIs via a kernel trap offers the advantages of portability and isolation. User programs don't need to know specific addresses to access the kernel API—applications only need to know API index numbers. If a new kernel is released with a new API-accessible function, the lowest unused API index number is assigned to the new function, which will not affect any applications that were written prior to the kernel update. As a user-accessible jump table is not used for calling APIs, the kernel can be loaded anywhere in memory that is convenient.
Isolation offers the kernel some protection from misbehaving user applications, reducing the likelihood of random instructions or wild address pointers accidentally accessing and/or overwriting kernel space and causing system fatality. In most systems, a kernel trap causes a hardware context switch that may be used to modify the memory map, alter memory protection rules, and/or change instruction execution privileges, all of which can be used to tightly control what user programs can and cannot do.
The principal downsides to a kernel trap API calling method are greater code complexity, heavy stack usage and slower execution. As will be seen, a kernel trap API ultimately involves a software interrupt to switch execution from user mode to kernel mode. Therefore, code in both the API "front end" and "back end" is essentially a specialized form of an interrupt service routine, which adds complexity to the kernel. Also, since the API entry point is the same for all APIs, dispatch code is required to select the specific function that must be executed for a particular API, as well as determine how many parameters are expected by the API. That an API call culminates in an interrupt means that slower execution will occur due to hardware and software-induced latency.
The software interrupt method has a clear advantage over a jump table, but places more burden on the kernel and the microprocessor. With the 65C816, at least, the required code isn't difficult and one can use COP instead of BRK as a "trap" instruction. The 65C02 doesn't lend itself as well to this style of programming due to limited stack addressing capabilities. Also, the only software interrupt is BRK, which could give rise to some interesting debugging problems.
As always, everything has pros and cons.