
Writing an OS in Rust: Catching CPU Exceptions - phil-opp
http://os.phil-opp.com/catching-exceptions.html
======
expression
Haven't been following this blog, but one thing caught my eye while skimming
the post:

>The implementations of these methods need to modify the correct bits of the
u16 without touching the other bits. For example, we would need the following
bit-fiddling to set the stack index:

    
    
        self.0 = (self.0 & 0xfff8) | stack_index;
    

>Or alternatively:

    
    
        self.0 = (self.0 & (!0b111)) | stack_index;
    

>Or:

    
    
        self.0 = ((self.0 >> 3) << 3) | stack_index;
    

>Well, none of these variants is really readable and it’s very easy to make
mistakes somewhere. Therefore I created a BitField type with the following
API:

    
    
        self.0.set_range(0..3, stack_index);
    

>I think it is much more readable, since we abstracted away all bit-masking
details.

Personally, I disagree. Whilst it is easy to make mistakes when writing bit-
fiddling code (there seems to be one in the second example, unless Rust has on
of the more weird !-operators in programming languages), or any code for that
matter, the meaning of all the bitwise operations is easy to recognise.

Without knowing the API, the transformed line rings no bells. How many APIs do
you want to learn?

~~~
sgmansfield
I was just about to post this exact same thing. Now there's an introduction of
ranges and another API that is necessary to understand in order to grok this
code. Bit twiddling, ANDing, and ORing are specific and absolute.

For example, what happens when I specify a range of 0..100 for a 32 bit int?
what if the value is too big for the range? APIs around small operations like
this introduce ambiguity that wasn't present before.

~~~
sjolsen
>Bit twiddling, ANDing, and ORing are specific and absolute

So what? You could implement addition using bitwise operators, but it makes
for much more readable and less error-prone code to abstract it into an
addition operator. Code should clearly express intent, and if your intent is
to set, say, bits 5 though 9 of some register to some value, the clearest
expression of that is something like (given a right-open range convention,
which is well-established in this particular language):

    
    
        register.set_bits(5..10, value);
    

If I look at the above statement, I know what the author of the code
_intended_ it to do and can rely on (or manually verify once) the correctness
of the implementation of "set_bits." If instead I'm reading code like

    
    
        register = (register & 0xFC1F) | (value << 5);
    

I have to work out in my head what the bit fiddling is doing, and from that
_guess_ what the author meant the code to do. I can't know whether he or she
made a mistake in translating the intent to bitwise operations (say I know
from context that value is less than 8; did they _really_ mean to clear bits 8
and 9?), because the intent isn't expressed anywhere.

>For example, what happens when I specify a range of 0..100 for a 32 bit int?

What happens is that you've violated a precondition of the API. Because this
is Rust, I would expect the operation to be implemented in such a way as to
panic (in debug mode), or just fail to compile, since selecting a bit range
like this at run time is extremely uncommon.

>what if the value is too big for the range?

Same thing as if you weren't using the abstraction around the bitwise
operators: either you'd clobber higher bits, or the implementation would
truncate the value.

Again, though, I would expect a debug-mode panic.

>APIs around small operations like this introduce ambiguity that wasn't
present before.

I'm not sure what "ambiguity" you see here. There's the issue of whether the
range is right-open or -closed, but again right-open ranges are well-
established in Rust.

------
plgs
I've been following this series of articles. As a total beginner in the field,
I found it to be a quite approachable introduction and learned a lot reading
it. Thank you !

~~~
phil-opp
Thanks :)

------
ipsi
Is it actually 100% necessary to hand-craft assembly for bootstrapping the OS?

I suppose it could be that it's simpler to write it out in assembly, since the
C (/etc) code would look basically the same - just a lot of assigning cryptic
variables to cryptic pointers?

Or, based on a quick Google search
([http://stackoverflow.com/questions/3022046/is-it-possible-
to...](http://stackoverflow.com/questions/3022046/is-it-possible-to-
access-32-bit-registers-in-c)), is it because you _cannot_ write to these
registers from C, thus needing to drop down to assembly?

~~~
kaushiks
It generally doesn't matter how the machine code came to be. It is
theoretically possible to write an operating system for (say) x86 in (say)
Javascript. The reason people generally pick a "systems" language and drop
down to assembly when necessary is due to two broad reasons:

1\. As you'd pointed out, the language in question might not let you generate
certain instructions. For instance how do you express LGDT in C? You can't.
You'd instead write it in assembly (or inline assembly) and expose it to the C
world as a function (or in a compiler that supports it, an intrinsic) that it
can call.

2\. A CPU's understanding of state is defined around the notion of registers
and memory. A programming language however exposes a higher level abstraction.
For instance, a function call in C is an abstraction that defines both
transfer of control (which the CPU knows about) and persistence of local state
(saving and restoring registers, stack pointer etc., which the CPU knows
nothing about). This is called the ABI and is merely a previously agreed upon
convention. Higher level languages (say Java) are able to provide richer
abstractions than merely an ABI as their "agreed upon conventions" are defined
at a much higher level than in terms of a CPU (registers and memory). Java,
for instance even defines a virtual architecture in terms of which facilities
offered in the language are defined. User code written in one of these higher
level languages depends on these facilities being available to them at any
time. This is what makes it unsuitable for writing certain sections of OS
code. For instance, the CPU expects the OS to save and restore register state
around an interrupt handler. If said handler was written in C (as a function)
though, it'd expect its first six arguments in certain registers etc. -
conventions, the CPU knows nothing about. You can think of the CPU as exposing
its own, simple ABI which can only be implemented by writing certain sections
of code in assembly (This is not entirely true though, as one might be able to
mimic something like this in certain compilers, especially C compilers, that
let the programmer write "naked" function bodies and custom prolog / epilog
code). This is also why most parts of the OS, that don't directly interact
with the CPU, can and are written in a higher level language. For e.g. The
Singularity OS by MSR was written in C# and assembly.

~~~
bogomipz
Isn't the LGDT just an address though? You provide a starting address and a
length offset that's it. Why can't that be expressed in C? Does it live in a
register not visible to the compiler?

You mentioned: "For instance, a function call in C is an abstraction that
defines both transfer of control (which the CPU knows about) and persistence
of local state (saving and restoring registers, stack pointer etc., which the
CPU knows nothing about). "

Doesn't the CPU know about this "persistence of local state"? I mean it
pushing and popping that state form the stack right? I'm curious what you mean
there.

What's a "naked function" in this context? I think of naked function as one
that doesn't have a return statement.

Cheers.

~~~
kaushiks
LGDT is an instruction that Loads the Global Descriptor Table [1].

The CPU doesn't know about the persistence of local state in that it doesn't
know (in this case), the significance of anything that is being pushed onto /
restored from a frame around a call. It knows that a "PUSH R9" writes the
value of R9 onto the region in the stack being currently pointed to by the
stack pointer (RSP). It however doesn't know that this is being done because,
the current frame has a live value in R9, which, per the ABI, the function
being called is allowed to trash, as it is considered a volatile, callee saved
register. Like I'd said earlier, these are just agreed upon conventions that
might even change across compilers.

I'd used the term "naked" the way the VC++ compiler uses it. A "naked"
function is one without a prolog or an epilog [2].

[1]
[https://pdos.csail.mit.edu/6.828/2008/readings/i386/LGDT.htm](https://pdos.csail.mit.edu/6.828/2008/readings/i386/LGDT.htm)

[2] [https://msdn.microsoft.com/en-
us/library/21d5kd3a.aspx](https://msdn.microsoft.com/en-
us/library/21d5kd3a.aspx)

~~~
bogomipz
I see, that makes sense. Thanks for the clarification and the links!

------
Animats
This is interesting, but it's not the hard part of exception handling. The
next step is doing something to the state of the process that got the
exception. If the exception comes from a userspace process, that's OK, because
those processes aren't part of the Rust OS. But if the exception comes from
kernel code, there's a problem. Rust code has to mess with the internal state
of Rust code.

Signals have the same issue in ordinary Rust programs. See [1]. What's the
state of the Rust world when you're in a signal handler?

[1] [https://github.com/rust-lang/rust/issues/11203](https://github.com/rust-
lang/rust/issues/11203)

~~~
Manishearth
That's a two year old, closed, issue, from when rust had a runtime, probably
doesn't apply anymore.

I don't think signal handlers are a big deal because global mutable state in
rust is rare; and when it exists it probably is immune to the issues -- global
mutable state in rust is required to be thread safe (since rust doesn't know
if you are using threads), which _should_ make it signal -safe too.

~~~
bogomipz
Not sure what you mean, doesn't Rust have a runtime now?

~~~
nemaar
It has a runtime comparable to C. Back in those days, it had a much larger
one.

~~~
bogomipz
Ok, that's what I thought : )

It interesting that they stripped down the runtime from previous(larger)
iterations. Are those discussion discussed anywhere that you know about? It
seems like that would be an interesting read.

~~~
Manishearth
[https://github.com/rust-
lang/rfcs/blob/master/text/0230-remo...](https://github.com/rust-
lang/rfcs/blob/master/text/0230-remove-runtime.md) and the PR/issue linked
there.

There are also old mailing list discussions on rust-dev somewhere which
discuss this IIRC.

------
kinsley
Woww thus informative

