Hi,
bluemoon wrote:You may still want to validate the value of DS/ES, but that's much quicker than changing its values.
There's an even faster way. Assuming the OS is designed for "flat memory model" (no segmentation), there are typically only 3 values that user-level code can load into DS or ES. One is the correct value (e.g. a DPL=3, flat read/write data descriptor), one is the code segment (e.g. a DPL=3, flat read/execute code descriptor), and the other is the CPU's "null descriptor". For all other values, code running at CPL=3 will get a general protection fault and the descriptor won't be loaded.
Now, if you assume that the kernel uses the same segments; you can guarantee that if user-space code has changed DS or ES to something wrong and the kernel uses the segment register to write to anything then the kernel will get a general protection fault. This means that the kernel's general protection fault handler can check if the exception was caused by "bad DS or ES set by user-space" and correct the segment register and retry the instruction. With this in place, there's no need to check DS or ES anywhere else (and no need to check, save or load DS or ES in interrupt handlers).
For FS and GS; these are typically used for special purposes. For example; GS might be used by the kernel to quickly find a "per-CPU" data structure. In this case, user-space code can still only load the same 3 descriptors into GS; but if user-space code loads the "flat data descriptor" into GS the kernel won't get a general protection fault. However; there's another trick. If you make sure that the per-CPU data structure is 4 KiB or less, and also make sure that the first 4 KiB in every virtual address space is "not present"; then if user-space code has loaded the "user data" or "user code" descriptor into GS and the kernel uses it you can guarantee that the kernel will get a page fault (all offsets into the per-CPU data structure will be below 0x00001000 which is a "not present" page) and the page fault handler can correct the segment register and retry the instruction. With this in place, there'd be no need to check GS anywhere else (and no need to check, save or load GS in interrupt handlers).
Basically what I'm saying is that with a few little tricks, it's possible for the kernel to ignore segment registers completely and still be immune to anything user-space code might do to the segment registers; and (assuming no processes are malicious and therefore no processes modify segment registers anyway) this method ends up being "zero cycles of overhead on the critical path" (e.g. only a little extra overhead in the exception handlers, which aren't that critical for performance anyway).
However; if you use virtual80x86 mode for anything this method will not work, because any value that could've been valid for real mode may have been left in DS, ES, FS or GS. Fortunately, there's no need to bother with virtual80x86 mode for anything.
choco wrote:- I didn't really look at register segment saving. What is the best practice? Pushing all of them one after the other? It is not to much costly to do this?
For interrupt handlers the best practices (in my opinion only) are:
- use tricks (see above) to avoid the need to touch segment registers in the first place
- avoid the "C function call" overhead for cases where it will be a significant increase in the total cost of an interrupt handler, by writing some interrupt handlers in pure assembly. Note: this mainly applies to very simple interrupt handlers. For example, consider "multi-CPU TLB shootdown" where one CPU sends an IPI and the interrupt handler used by other CPUs to handle the IPI only needs to do about 5 instructions (e.g. load an "address to invalidate", do an INVLPG instruction and return), where the extra mess caused by C function calling conventions can double the cost of the interrupt handler.
- avoid passing the interrupted code's registers to the C function for cases where there's no sane reason for the C interrupt handler to want them in the first place. Essentially; any/all C functions for IRQ handlers should be like "void myIRQhandler(void)". Note: for these cases C calling conventions guarantee that the called code will preserve various registers, and the assembly stub shouldn't bother saving or restoring any of these "callee preserved" registers.
- have a general purpose function to handle kernel panics, that any kernel code can call for any reason, that can be used in the same way you'd use "assert()" in well written C code. For example, you might have a kernel function that is only ever called by other kernel functions, which does "if( argument_passed_by_caller == NULL) { kernel_panic("Caller of foo passed NULL!\n");".
- have a general purpose function to handle crashes; which determines if the crash was caused by user-space code or kernel, and either terminates the process (if the crash was in user-space) or calls the general purpose kernel panic function (if the kernel crashed).
- realise that if you've got a general purpose function to handle crashes, then none of the code used in any "C exception handler function" will be used by any other "C exception handler function".
- realise that different exception handlers may need very different "assembly stubs" (and not just because some have error codes and some don't). For a very simple example, the very first thing a page fault handler should do is save CR2 somewhere safe (so that it can't be trashed by a second page fault), but this is pointless for any other exception handler. Double fault should probably use a hardware task switch and be extremely different. NMI and machine check are extremely tricky to get right (due to the fact that they can't be effectively masked/postponed and potential nesting) and also take very special care. The debug exception has special requirements (mostly involving messing with the "Resume Flag" before doing IRET so that single-step debugging works properly). For some of them (e.g. divide error) the assembly stub can immediately call the general purpose function to handle crashes with no intervening code.
- By combining the last 2 things it's easy to see that for exceptions it's best to have a special purpose assembly stubs for each exception, where each of the special purpose assembly stubs calls the corresponding special purpose C code for that exception (if any). Having "generic assembly stubs" for exception handlers and/or having a "common exception handler" in C (that is nothing more than a big "switch()" that calls the C function that should've been called in the first place) is purely idiotic.
Finally; please note that for a good tutorial you want simplified examples to make it easy for the reader to learn from (and not complicated code that confuses the reader and makes it hard to learn anything); and the code for a real OS is complicated (and has to handle a large number of things that can be ignored in a tutorial). Basically; it's impossible to have a good tutorial that contains example code that is usable in a real OS.
Cheers,
Brendan