Here's some tips for designing some API on both sides of the fence to help with dealing this stuff, and ease experimentation:
First, kernel side:
You are going to pass your arguments either directly or indirectly in registers. Even if you pass them in user-space stack, that's still pointed to by a register (%esp) so what I would do is inside kernel write some dispatch system that figure out which system call, and calls the correct handler with the user-sent arguments properly moved to kernel C stack (=normal C arguments, assuming C kernel) so that they are just normal arguments. Pass the operation code in some register (I'll assume %eax) to have this single dispatch point.
In handler prototype you can call pointers to user data something like
Code: Select all
typedef struct unsafe_data * unsafe_pointer;
If said struct doesn't really exists, it'll be impossible to dereference that unintentionally without a cast, and it makes it simple to find all places in source that you actually are dealing with unsafe pointers. You can then write a set of kernel helper functions that know how to check that those pointers are valid. Say:
Code: Select all
/* Copy from kernel to userpace memory */
int copy_unsafe_k2u(void *, unsafe_pointer *, int len);
/* Copy from userspace to kernel memory */
int copy_unsafe_u2k(unsafe_pointer *, void *, int len);
You can then use those to copy data from userspace to kernel space. If you want to reference it directly, you could add something like unsafe_peek/poke or whatever. Using separate functions means that if you want to change something like your memory management such that you need to change your validation code, you'll only need to modify one place.
You can extend the dispatcher function to deal with dynamic registration of new system calls or whatever you want, and you only have to modify the logic in the dispatcher function. But there's an added benefit: it doesn't matter how you call into the dispatch function, as long as you can give it the user registers in some sort of a struct (or whatever). So you can implement syscalls by far-calls, sysenter, interrupts, whatever... even all of them at once, and if you want to change that later, you only need to modify the code that calls the dispatcher (and returns back to userspace with the result).
As for userspace side of the fence, you can build one function (or inline assembler macro or whatever you happen to prefer) which knows how to do a system call with a given set of parameters. To rest of your userspace code, this should look like a normal function. You then include the macro and/or link the function to any code that uses system calls.
Now, if you decide to change the mechanism of system call, all you need to do on the userside is to modify this function to use the new method.
So... since you say you know how to do this all with an interrupt, I suggest you implement a setup similar to the above description (if you don't have it yet), check that it works, and either play with alternatives until you get them working, or just put it in the bin called "my kernel doesn't rely on a specific system call mechanism, and I have one mechanism that works, so I can go on and do something more productive first and come back and switch the mechanism if it seems later I would gain something".
Oh, and with some conditional checks on both sides of the fence, the same binary can use sysenter/exit (well, both Intel and AMD solutions for it, don't remember how much difference there was) on systems that can deal with that, and fall back to interrupts on systems that can not.
The real problem with goto is not with the control transfer, but with environments. Properly tail-recursive closures get both right.