We use OOStuBS/MPStuBS in our operating system course. In the first part of our two part lecture, the students implement basic IRQ handling, coroutine switching, and synchronisation primitives. We have no spatial or privilege isolation in place, since this is topic of the second lecture part.
Still, we want to differentiate between a user space and the kernel space. On a technical level, the kernel space or system level is defined by a big kernel lock; the so called guard. If a control flow enters the guard, it transitions to the kernel and leaves the kernel, when the guard is left. The guard concept is heavily coupled with our idea of IRQ handling in epilogues (similar to bottom-halves or deferred interrupt handlers).
Our proposed implementation of the system call interface uses facade pattern to expose some of the system functionality as "Guarded Services".
class Guarded_Scheduler {
static void resume() {
Secure section; // does guard.enter() in constructor
scheduler.resume();
// guard.leave() is called on Secure destructor
}
}
The used Secure
class uses a Resource Acquisition Is
Initialisation pattern to enter the guard on construction, and to
leave it upon destruction of the secure
object. But, as you see,
coding down this pattern is cumbersome and involves a lot of
boilerplate. Nobody, especially interested students, want to write
boilerplate. But, our OS is implemented in C++, so we have powerful
abstractions to implement a usable abstraction. In the following, I
will explain, how we can implement an easily extensible system-call
interface for a library operating system (everything is linked
together, and we have no spatial isolation).
A First Attempt
First, we start with a "simple" templated function that can wrap every member function of an object and call with the guard taken. The actual API usage looks like this:
syscall(&Scheduler::resume, scheduler);
syscall(&Scheduler::kill, scheduler, &other_thread);
The first argument to syscall()
might surprise some readers, since
it is a seldom used C++ feature. It is a "Pointer to Member" that
captures how we can access or call a member when having the
corresponding object at hand. The datatype of &Scheduler::resume
is
void (Scheduler::*)()
, which is similar to a function pointer
returning nothing and taking no arguments. &Scheduler::kill
has the
datatype void (Scheduler::*)(Thread *)
; it is a pointer to a member
function, which returns nothing but takes an Thread pointer as
argument. Both pointers only make sense with a Scheduler object at
hand. When we have a scheduler object at hand, we can use the rarely
used .*
operator:
((scheduler).*(&Scheduler.kill))(thread)
We now can combine this concept with C++11 templates to get the described syscall function:
template<typename Func, typename Class, typename ...Args>
inline auto syscall(Func func, Class &obj, Args&&... args) -> decltype((obj.*func)(args...)){
Secure secure;
return (obj.*func)(args...);
};
Huh, what happens here? Let's take this monster apart to understand
its working. So, it is a function template, it generates functions
depending on the types it is specialized for. You can think of this
specialization process like this: the compiler has a Schablone
(german word for template, but with the notion of scissors and paper)
at hand. When it sees a function call to syscall()
it fills the
missing parts in the Schablone with the argument types and compiles
the result a new function.
syscall(Func arg0, Class arg1, Args... args2_till_9001)
So, our syscall function takes at least two arguments, but can consume arbitrarily many arguments in its variadic part at the end (the Args...). The type of the first argument is bound to the type "Func", the second argument type is bound to the type "Class", all others are collected in the variadic type "Args". The func argument, which type Func, is pointer-to-member object, the obj argument the actual system object. So, now we can call the function with the other arguments.
(obj.*func)(args...)
But, our function, still has no return type. What to do? Here comes
C++ auto
and decltype
to the rescue. When using auto
as a return
type, the compiler excepts -> Type
after the closing parenthesis of
the function. The decltype()
built-in gives the type of the enclosed
expresion. So decltype((obj.*func)(args...))
is exactly the return
type of the given pointer-to-member-function argument.
Furthermore, we just have to allocate a Secure
object to make the
guard.enter()
and guard.leave()
calls. Voila, a system call
interface. But it still has some problems. We can call every method on
every object in the whole system. We have no notion of "allowed"
system calls and forbidden ones. Of course, in a library operating
system with no protection this is ok. Furthermore, we always have to
give the system object (e.g., scheduler) on each system call. I think,
we can do better here. So let's revisit our implementation.
A second Attempt
In our second attempt, we want to restrict the system-call interface
to certain classes. This gives coarse-grained control about the
methods that can be called via the syscall
interface. As a
side-effect, we can omit the actual system-object argument such that
we can write:
syscall(&Scheduler::resume)
syscall(&Scheduler::kill, that)
We implement a system_object
function that returns the system-object
singleton instance when called for a given type. We implement this
function only for those classes, we want to allow access via
syscall
. This gives us some control about the possible syscall targets.
template<typename Class>
Class& system_object();
// Get the scheduler singleton
Scheduler &scheduler = system_object<Scheduler>();
The template specialization can be done in the source file and does
not have to be put into the header. This allows us to hide the actual
system-object symbol from the rest of the system. For example, this
could be located in the thread/scheduler.cc
file:
static Scheduler instance;
template<>
Scheduler &system_object() {
return instance;
}
We still have to call this function from our system call
implementation. For this, we need to have the class type of the
underlying system object at hand. The only thing we have is the
pointer-to-member object that identifies the desired system-call
(&Scheduler::resume
). But, as you remember, the class type is part
of the type of such pointer-to-member types (Func
). We only have to
extract that information from the given type.
The concept of accessing information about types is called type
traits
. This is grandiloquent word for "a template that takes a type
and provides several types and constants". So let's look at our type
trait:
// Fail for all cases...
template<typename> struct syscall_traits;
// ..., except for deconstructing a pointer to member type
template<typename ReturnType, typename ClassType, typename ...Args>
struct syscall_traits<ReturnType(ClassType::*)(Args...)> {
typedef ReturnType result_type;
typedef ClassType class_type;
};
This syscall_traits
is only specialized for
pointer-to-member types and destructs the type of our
&Scheduler::resume
argument (void (Scheduler::*)()
) with the
pattern ReturnType (ClassType::*)(Args...)
. As you see, the
templates does only pattern matching on types and binds types to
template parameters. This can generally said for templates: The
<>
-line after the template keyword defines type variables, which can
be bound later on or have to be supplied by the user. With our type
trait we can simply access the instance class of our pointer-to-member
argument and can call system_object()
:
template<typename Func, typename ...Args>
inline auto syscall(Func func, Args&&... args) -> typename syscall_traits<Func>::result_type {
// We do everything with a taken guard
Secure secure;
// Get traits of systemcall
typedef typename syscall_traits<Func>::class_type system_object_type;
// Get a singleton instance for the given base type.
system_object_type &obj = system_object<system_object_type>();
return (obj.*func)(args...);
};
The first thing we see is that the deduced return type has changed. It
no has to use our type trait, since we have no system object at hand
we can use with decltype (-> decltype((obj.*func)(args...))
. Within
the body of the syscall function, we use the trait to extract the
system-object's class type from the Func
type and call system_object
to gain access to the singleton instance.
If we use syscall on a class that is not exposed via specializing
system_object<>
, we get an linker error and the developer is
informed that he wants to do bullshit.
So, what have we achieved in the second attempt? We have a cleaner system-call interface and do not have to supply the system object directly, but it is deduced from the supplied arguments. Furthermore, only annotated classes are suitable for being called via this interface. Nevertheless, we can still call all functions on these classes. In the third attempt we want to solve this as well.
The third and final Attempt
How can we annotate functions as being system calls? The only real thing we have at hand in static C++ land are types. So we have to annotate the function type of our system call somehow. The type of a method is defined by only a few pieces of information: The argument types, the class type, and the return type. The one thing that is always there, and that is not shared among several functions is the return type. We use the return type for our annotation by wrapping it into an marker struct:
template <typename T=void> struct syscall_return;
template<> struct syscall_return<void> { void get() {}; };
template <typename T>
class syscall_return {
T value;
public:
syscall_return(T&& x) : value(x) {}
operator T() { return value; }
T get() { return value; }
};
The syscall_return
wraps a type and contains a copy of
it. Furthermore, it implements a get()
method to access this inner
object and has the cast operator for T overloaded for easier
handling. The void
type is special here, and has to be handled
special, since it is a no-object type and cannot be instantiated.
We can no annotate functions in our Scheduler class:
struct Scheduler {
syscall_return<void> resume() {
printf("resume %d\n", (int)barfoo(23));
return syscall_return<>();
}
virtual syscall_return<int> increment(int i) {
return i+1;
}
}
As you see, we have to special case for void again ("Damn you void,
you and your voidness!"). But, the implicit cast via the constructor
makes it easy to return all other types. But we also have to adapt the
rest of our implementation. In the syscall_traits
template, the
matched pattern strips the syscall_return
wrapper from the
type. This will also cause all unwrapped return types to fail.
// ..., except for deconstructing a pointer to member type
template<typename ReturnType, typename ClassType, typename ...Args>
struct syscall_traits<syscall_return<ReturnType>(ClassType::*)(Args...)> {
typedef ReturnType result_type;
typedef ClassType class_type;
};
In the syscall
template, we only have to additionally call .get()
on the result:
template<typename Func, typename ...Args>
inline auto syscall(Func func, Args&&... args) -> typename syscall_traits<Func>::result_type {
// We do everything with a taken guard
Secure secure;
// Get traits of systemcall
typedef typename syscall_traits<Func>::class_type system_object_type;
// Get a singleton instance for the given base type.
system_object_type &obj = system_object<system_object_type>();
return (obj.*func)(args...).get();
};
And voila, we have a system call interface with annotations that
prevents the user to call unmarked functions via syscall
. All
abstractions from above come at zero run-time cost.
The only downside is that the user is still able to call the functions directly. But, this can never be solved in a library operating system.
I hope I could give you an impression what is possible with C++ templates in the context of a bare-metal operating system.