Page 1 of 3

C++-like exception handling in kernel

Posted: Mon Apr 05, 2021 10:22 am
by Robert
Hi!

Is it possible to implement a C++-like exception handling mechanism in kernel code?
I mean throw means something like calling a sw exception with a specific value in eax/rax. And locally define handlers for identifying (by eax/rax value) and handling the exception. (So with a primitive alternative to RTTI).

Re: C++-like exception handling in kernel

Posted: Mon Apr 05, 2021 10:48 am
by kzinti
You can probably make something like setjmp() and longjump() work. But that won't call C++ destructors. If you want the latter, it's a lot of work, complicated and likely not worth it.

Re: C++-like exception handling in kernel

Posted: Mon Apr 05, 2021 12:15 pm
by Korona
You can make C++ exceptions work in the kernel (by linking against libgcc and taking the appropriate headers from libsupc++). There is also -fnon-call-exceptions to make GCC emit exception tables for each and every memory access.

But you probably should not do this. Even as a proponent of C++, I would suggest not to use exceptions or RTTI in the kernel.

Re: C++-like exception handling in kernel

Posted: Mon Apr 05, 2021 12:21 pm
by kzinti
There is actually a lot more work that that in making C++ exceptions work in the kernel.

Assuming x86_64, you need to configure your compiler for multi-lib support so that you can build a version of libgcc that has mcmodel=kernel and mno-red-zone.

You also need a pthread implementation (unless you decide to customize libgcc with your own threading model, which is even more work).

You also need malloc/free as libgcc will rely on that at initialisation and when throwing or handling exceptions (I can't remember the specifics).

Assuming x86_64, using the GCC provided C++ headers is also problematic: they use floating point numbers in a number of places and you can't have that without enabling SSE in your kernel (x86_64 ABI says float params / return values go in XMM registers), which opens a whole can of worms. You could define your own C++ headers and compile libgcc with them, I didn't try it.

I am sure there was more when I did it, I just can't recall everything right now.

And after all this work, you have something that only works with GCC (and not clang or other compilers). I concluded that it wasn't worth it to have C++ exceptions in the kernel.

Re: C++-like exception handling in kernel

Posted: Mon Apr 05, 2021 12:49 pm
by Korona
Oh I forgot about pthreads. Yes, that's annoying indeed.

(However, why won't that strategy work with Clang? Clang's RTTI and exception ABI is compatible with GCC's, and Clang can also use libgcc if you pass --gcc-toolchain=<path> to Clang.)

Re: C++-like exception handling in kernel

Posted: Mon Apr 05, 2021 12:50 pm
by kzinti
My bad, I assumed clang worked differently because it doesn't have libgcc (another assumption).

It is possible that the clang implementation has different requirements than libgcc, but this is unlikely to matter in practice if you are willing to add all the pieces required to support C++ exceptions.

Another alternative I looked into was to not use libgcc at all and write my own implementation to support C++ exceptions... But that's a lot of work as well.

Re: C++-like exception handling in kernel

Posted: Mon Apr 05, 2021 1:32 pm
by Korona
Yes, rolling your own exception library requires writing a DWARF parser; that is not really a nice exercise either.

Re: C++-like exception handling in kernel

Posted: Thu May 06, 2021 10:44 am
by TripleF
You can probably combine long/setjmp with the gcc destructor attribute. You can create try/catch using macros too.

Re: C++-like exception handling in kernel

Posted: Fri Mar 18, 2022 1:00 pm
by MakaAlbarn001
The problem there is that without exception support and rtti, classes aren't usable, without some very tricking programming.

Re: C++-like exception handling in kernel

Posted: Sat Mar 19, 2022 12:00 am
by kzinti
MakaAlbarn001 wrote:The problem there is that without exception support and rtti, classes aren't usable, without some very tricking programming.
That's not true. Classes are perfectly usable. The only problem you run into is that constructors can't throw and can't return values. This means that you have to use either static factory methods or an Initialize() method. RAII is still possible, but is more verbose as you have to check for return values (but that's no different than C).

C++ RTTI is garbage and you shouldn't use it with or without exceptions. But this is relatively easy to enable in kernel space if you really want it. Although you probably only want this to enable C++ exceptions.

But yes, I hear you... I really wanted to use exceptions in kernel space. I gave it a good shot and considerable time. I ended up concluding that it wasn't worth it.

Re: C++-like exception handling in kernel

Posted: Sun Mar 20, 2022 5:47 pm
by vvaltchev
I agree with @kzinti that it's not worth the effort to support C++ exceptions in a kernel.

I'll add that, from my personal point of view, it's probably better this way. Even if the code is more verbose (with error codes), in my experience it's easier to not handle an exception properly by mistake than to not check for the return value of function. In the kernel, you should be concerned about every detail and every corner case. In userspace you can just say that in certain cases we might fail in a generic way and even handle the crash of a child process. In the kernel, you should try to make crashes impossible to happen. Every failure (even hardware failures!) should have a safe handling. With exceptions, too many things can go wrong, for example when throwing code is called in destructors.

For many years the C++ code in the Joint Strike Fighter program didn't use exceptions at all, not because it was technically hard to do that (most of that software runs in userspace), but because it wasn't safe to do so. Recently, as far as I know, they started to use exceptions in a very limited way. So, I genuinely believe that kernel code should be written "as if" you'll die if your kernel crashes. That should be the quality level. And indeed, as you get more and more programs to run on your OS and they interact with each other, even small and apparently innocent bugs amplify and have terrible effects. So, that quality level becomes mandatory to keep a complex system in a stable condition.


Said that, even if I believe the "C mindset" is more appropriate for kernel development, you can certainly have some benefits by using C++. For example, RAII and templates. I decided that my kernel will use mostly C, but I implemented two files in C++ because it was more elegant to use templates than several copy-pasted macros with minor differences.

Re: C++-like exception handling in kernel

Posted: Wed Mar 23, 2022 5:50 am
by thewrongchristian
vvaltchev wrote:I agree with @kzinti that it's not worth the effort to support C++ exceptions in a kernel.

I'll add that, from my personal point of view, it's probably better this way. Even if the code is more verbose (with error codes), in my experience it's easier to not handle an exception properly by mistake than to not check for the return value of function. In the kernel, you should be concerned about every detail and every corner case. In userspace you can just say that in certain cases we might fail in a generic way and even handle the crash of a child process. In the kernel, you should try to make crashes impossible to happen. Every failure (even hardware failures!) should have a safe handling. With exceptions, too many things can go wrong, for example when throwing code is called in destructors.

For many years the C++ code in the Joint Strike Fighter program didn't use exceptions at all, not because it was technically hard to do that (most of that software runs in userspace), but because it wasn't safe to do so. Recently, as far as I know, they started to use exceptions in a very limited way. So, I genuinely believe that kernel code should be written "as if" you'll die if your kernel crashes. That should be the quality level. And indeed, as you get more and more programs to run on your OS and they interact with each other, even small and apparently innocent bugs amplify and have terrible effects. So, that quality level becomes mandatory to keep a complex system in a stable condition.


Said that, even if I believe the "C mindset" is more appropriate for kernel development, you can certainly have some benefits by using C++. For example, RAII and templates. I decided that my kernel will use mostly C, but I implemented two files in C++ because it was more elegant to use templates than several copy-pasted macros with minor differences.

I'm currently at a bit of an impasse.

I currently use C, but tried putting in place a vtable based object system, which mostly works but is cumbersome, so I thought C++ would be a better fit for what I want to do.

I also much prefer exception based error handling, and also include macro based try/throw/catch in C using setjmp/longjmp, which mostly works well. But the stack unwinding is the pain with this, as I have to install handlers to unwind any objects that need unwinding properly (like synchronisation objects), and this is cumbersome as well. I do have a facility to add "finally" blocks to try blocks, which can do this cleanup, but this is again added code that can be forgotten.

It's just nicer to have a C++ compiler generate the extra code for you in both cases, so I'd dearly love to move to C++ and C++ exceptions, but the stumbling block being having to get this unwinding working in the kernel, and I've not tried portng the unwinding yet.

There is no reason why properly supported C++ exceptions shouldn't be used if supported, if you're careful to make your code exception safe. Obviously, the caveat here is making the code exception safe, but that's no more burdonsome that making your code handle errors using return codes, and with stack unwinding calling destructors, it's arguably easier to make code exception safe than it is to ensure errors are properly and reliably propagated correctly.

I might, in the short term, have a half way house in which C++ objects that need to have destructors called, install the exception cleanup handler on construction, and stick with my current C based exception handling. That way, objects that need to be cleaned up can just derive from the class that installs a handler, without me otherwise having to manually code the cleanup registration.

Re: C++-like exception handling in kernel

Posted: Wed Mar 23, 2022 8:41 am
by nullplan
I recently saw a talk about error handling in C, and it basically advocated for returning error codes from functions in addition to anything else by way of structs (just return small structs by value), and then passing those error codes back into all the following functions so they can abort early. For example, when opening a file, the file opening function returns a structure that contains the file and a "valid" (or "error code") word. You then pass that structure into further functions, which return early if the file is not valid. That avoids the weird control flow you get from testing error codes, as well as the "goto fail" problem OpenSSL popularized, and you do not have to deal with the idiosyncrasies of setjmp()/longjmp().

Honestly, I have only ever actively used setjmp()/longjmp() once, and it was to avoid a larger code change. I had to notice a problem in a nested function and return to an outer one. In order to do it right, I would have had to edit quite a few functions to enable an error return, so I just hacked that together, but I would have done the former, if I had had the time to do it.

Oh and don't come to me with questions about the performance of returning (and passing) structs by value. Who cares at this point? You are still writing the code, so correctness is the number one goal, and speed a distant second (if not third, behind understandability, as I prioritize it).

Re: C++-like exception handling in kernel

Posted: Wed Mar 23, 2022 11:08 am
by kzinti
nullplan wrote:I recently saw a talk about error handling in C, and it basically advocated for returning error codes from functions in addition to anything else by way of structs (just return small structs by value), and then passing those error codes back into all the following functions so they can abort early. For example, when opening a file, the file opening function returns a structure that contains the file and a "valid" (or "error code") word. You then pass that structure into further functions, which return early if the file is not valid. That avoids the weird control flow you get from testing error codes, as well as the "goto fail" problem OpenSSL popularized, and you do not have to deal with the idiosyncrasies of setjmp()/longjmp().
This sounds similar to what std::expected<> is. Basically you return a union of a result and an error + a boolean flag that indicates whether or not the union is an actual result or an (unexpected) error. So for example:

Code: Select all

std::expected<MemoryMap*, efi::status_code> ExitBootServices();

efi::status_code efi_main(...)
{
    ...
    auto memoryMap = ExitBootServices();
    if (!memoryMap)
    {
        // Handle error, accessible at memoryMap.error();
        return memoryMap.error();
    }

    // Use memory map
    memoryMap->Print();
}
This type of construct is becoming a more and more popular as an alternative to exceptions. It is also reasonably efficient in term of space / runtime overhead (if any), especially when constexpr and rvo are taken into account. So popular that std::expected<> is now an accepted proposal for C++23.

Since I wanted to use std::expected<> in my bootloader (and possibly kernel) and since we don't have C++23 yet, I had to implement it myself. So if anyone is interested, it is available here: https://github.com/kiznit/expected.

Re: C++-like exception handling in kernel

Posted: Wed Mar 23, 2022 11:34 am
by kzinti
thewrongchristian wrote: I also much prefer exception based error handling, and also include macro based try/throw/catch in C using setjmp/longjmp, which mostly works well. But the stack unwinding is the pain with this, as I have to install handlers to unwind any objects that need unwinding properly (like synchronisation objects), and this is cumbersome as well. I do have a facility to add "finally" blocks to try blocks, which can do this cleanup, but this is again added code that can be forgotten.
I tried this, and ended up deciding that I wanted proper C++ exceptions in kernel space.
thewrongchristian wrote: It's just nicer to have a C++ compiler generate the extra code for you in both cases, so I'd dearly love to move to C++ and C++ exceptions, but the stumbling block being having to get this unwinding working in the kernel, and I've not tried portng the unwinding yet.

There is no reason why properly supported C++ exceptions shouldn't be used if supported, if you're careful to make your code exception safe.
Although it is absolutely possible to have C++ exceptions in your kernel and have them work properly, I concluded that this was more work than writing all the rest of the kernel.

In a nutshell (extremely simplified):
- You need to compile a stack unwinding library for your kernel that respects the kernel ABI (mcmodel, no floating point, etc.)
- That stack unwinding library is including C++ headers that have floating point numbers in them. This won't compile. You need to modify or provide new standard headers just to build the library.
- You will decide to just turn on floating point when building the library and not use the functionality in the kernel: this won't work, the compiler will start using XMM registers to move/set memory and you will end up with SSE instructions in your unwinding library.
- Add floating point (SSE) support to your kernel? Complexity will increase and performances suffer greatly. I spent two weeks on this, I concluded it was a dead end.
- You will need a pthread implementation/stubs in kernel space because the stack unwinding library is dependent on it. You can fake a lot of things here, but it is more work (I did implement enough to fake it).
- You will need a malloc/free implementation in kernel space, make sure it is thread/process-safe and doesn't use locks, because you wouldn't want to be locking each time you throw and catch exceptions. I looked into something tcmalloc, but it doesn't seem possible to easily integrate that beast into a kernel (unlike dlmalloc, which I used as a proof of concept).
- There is more I am sure, I haven't looked at this in some time.

Let's say you get all of this working. It only works with one compiler. If you want to switch to another one and/or another version, you will have to redo some of the work. There is also going to be extra work required to support other architectures.

You could instead decide to write your own unwinding library from scratch. This is even more work.

So in practice, I don't think it's worth the effort.