Key Rust concepts for the kernel
Ojeda began by stressing that the talk was not meant to be a tutorial; to actually learn the language, one should get a good book and work from that. There is no way to cover everything needed in an hour of talk (a fact that became abundantly clear as time went on), but he hoped to be able to show some of the key ideas behind Rust. In the end, though, the only way to really understand the language is to sit down and write some code.
Reading material
There are a number of resources available for developers wanting to learn Rust, including several books. The definitive book appears to be The Rust Programming Language, which is available online for free. The book is a good introduction, he said, and is also an example of the documentation style that pervades the Rust project. Developers who are specifically interested in kernel development need not read the whole thing; much of the discussion on concurrency, for example, is not applicable to the kernel environment.
Programming Rust is also quite nice, he said. It is aimed at experienced programmers, and thus may be useful for kernel developers. This book is the only resource on his list that is not freely available, though. For readers who want to jump right in, Rust by Example presents a series of exercises, each of which introduces a new language feature.
Then, there is The Rustonomicon. This book, which is not complete, focuses on the challenges of writing unsafe Rust in particular. If the Rust-for-Linux project's goals are met, driver writers will not need to write unsafe Rust and, thus, may not need this book. There is a lot of gritty, low-level material in this book, but it also demonstrates a key point: most Rust developers should never have to deal with the language at this level.
Finally, The Rust Reference, which is also incomplete, is a good book for "people who like to read language standards". Most Rust developers will not need it. Ojeda summarized this section by suggesting either of the The Rust Programming Language or Programming Rust for most developers.
The Rust project offers a lot of other documentation too, of course. For the C language, Ojeda said, the standard is the documentation, but Rust is different. Documentation is everywhere, full of explanations and examples, and often generated directly from the code. A similar approach is being taken to the documentation generated by the kernel effort; it is being written using rustdoc rather than the kernel's Sphinx-based system. How to integrate those two bodies of documentation remains an open question.
Tools
Ojeda moved on to a couple of tools that are useful for looking at Rust code and the machine code that it is compiled to. One of those is the Rust playground, which can build code, run linters, examine assembly code, and more.
That said, he prefers Compiler Explorer; it offers a lot of the same features but is not limited to Rust. It can be used to compare assembly output produced by different languages, for example, and features a nice basic-block display for that purpose. Compiler Explorer can also run the code in question and display the results.
The language
While some people describe Rust as "a safer C", Ojeda doesn't like that term. It's more like a version of C with a safer type system, which is not the same as being safety-critical. Rust can also be described as a "cleaned-up C", he said. As an example, he presented a snippet of similar code in three languages:
map(v, [](const auto & x) { return x.get(); }); // C++ map(v, lambda x: x.get()) # Python map(v, |x| x.get()); // Rust
The Rust version is just cleaner and easier, he said. C++ may contain a lot of useful features, but developers often fear to use them; Rust makes it all easier.
He spent some time on the concept of "safety" in Rust, a discussion that repeated a fair amount from the first day. Rust is "safe" in that it has no undefined behavior; there are no situations where "the compiler has the freedom to do crazy things". Many undesirable behaviors, including kernel panics, memory leaks, and integer overflows, are well defined and, thus, Rust-safe (the compiler can be made to check for overflows though). And, of course, logic errors are "safe"; the language cannot guarantee that a program does what the developer intended.
But many other types of problems can be eliminated by coding in safe Rust. As an example of undefined behavior, Ojeda showed a simple C function:
int f(int a, int b) { return a/b; }
There are two ways that this function can wander into undefined behavior. The obvious one is if the caller passes zero for b; if that happens, the compiler is allowed to produce any result it likes. The other undefined situation comes about if a is INT_MIN and b is -1. There is no positive equivalent of the largest negative integer in the two's complement representation, so the result of the division cannot be represented. Once again, the compiler is allowed to improvise when that happens.
One can write a similar function in Rust:
pub fn f(a: i32, b: i32) -> i32 { a/b }
Using Compiler Explorer, Ojeda showed that this function, when compiled, contains tests for both of the above undefined-behavior cases. If either is encountered, the program will abort. That may not be what the programmer wanted, but neither is continuing to run in an undefined state.
There was some brief discussion of the performance cost for all of these checks. It certainly is not zero, but nobody seems to have measured what the impact really is in a performance-critical situation. Ojeda pointed out that, in the worst case, code can be put into an unsafe block, which will remove all of those checks. "Unsafe" was often described as an "escape hatch" during this conference — a way to remove many of the constraints placed by the language when they become too heavy.
There are also various tricks that can be applied to show the compiler that certain situations are impossible, at which point it will omit the checks automatically. If one defines a bounded-integer type, for example, the compiler knows that its value will be within its bounds and may not need to perform overflow checks. This technique often involves putting the necessary checks into a constructor, where they only need to be done once.
Ojeda provided one other example, being C code in a function that looks like this:
int x; /* not initialized */ while (f()) x = 42; return x;
Looking at the assembly output for this function, he pointed out that the compiled code just returns a constant 42. The loop body that assigns to x may never have executed, but to reference x in that case puts the program into undefined behavior. The compiler is allowed to assume that this will never happen; in that worldview, x must be assigned the value 42. Rust, instead, will throw a compile-time error in this case and force an explicit initialization of the variable.
Laurent Pinchart noted that developers must train their brains to avoid undefined behavior when working in either language. The difference is that the Rust compiler will catch mistakes, while a C compiler will often remain silent. After training your brain with a language like Rust, he said, the result will often be writing better C code as well.
The time for the session had long since run out by this point, but Ojeda
pressed through a series of additional slides describing other Rust language
features that are relevant to kernel developers. These include union types
that don't allow access to the wrong member, implicit freeing of
dynamically allocated objects, handling signed integer overflow, mechanisms
for avoiding data races, and
more. There is, in short, a lot in Rust for systems developers — much more
than can be covered in a brief conference session.
Index entries for this article | |
---|---|
Kernel | Development tools/Rust |
Conference | Kangrejos/2021 |
Posted Sep 18, 2021 0:23 UTC (Sat)
by roc (subscriber, #30627)
[Link] (11 responses)
Not sure if they have been correctly quoted, but to be clear, "unsafe { a/b }" does *not* remove the division-by-zero check or the overflow check. It is a common misconception that "unsafe removes all checks". In fact almost all checks still apply. "unsafe" enables five specific "superpowers", and that's all it does:
Posted Sep 18, 2021 7:14 UTC (Sat)
by matthias (subscriber, #94967)
[Link] (4 responses)
You can get rid of the overflow check by using wrapping division. This even works without unsafe, as INT_MIN/-1 in wrapping semantics is well defined, i.e., INT_MIN.
And I just realize that the article states that -INT_MAX/-1 is undefined. Of course -INT_MAX/-1 is simply INT_MAX.
Posted Sep 18, 2021 13:23 UTC (Sat)
by PengZheng (subscriber, #108006)
[Link]
Posted Sep 18, 2021 14:02 UTC (Sat)
by tialaramex (subscriber, #21167)
[Link]
I had to stare at this for a moment. Yes, it should say INT_MIN / -1 is undefined. Perhaps our Editor can consult his notes and, if that's what was actually said, correct the article, or else, point out in a parenthetical that it should have been INT_MIN.
Posted Sep 18, 2021 14:24 UTC (Sat)
by ojeda (subscriber, #143370)
[Link]
Note that we currently use quite a few of unstable features (of course, we want to minimize them as soon as possible).
> You can get rid of the overflow check by using wrapping division.
Indeed. I think having all the wrapping/saturating/... operations in `core` is a nice feature. In related news, C23 is getting checked integer operations (N2683).
> Of course -INT_MAX/-1 is simply INT_MAX.
I told Jonathan about the typo yesterday, but he is likely very busy with LPC coming up.
Posted Sep 18, 2021 14:27 UTC (Sat)
by corbet (editor, #1)
[Link]
Posted Sep 18, 2021 14:06 UTC (Sat)
by ojeda (subscriber, #143370)
[Link] (5 responses)
I assume "code" here is used in the general sense, not the literal `a / b` expression copy-pasted into an `unsafe` block.
In the actual talk, the assembly generated for different examples was shown, including e.g. a comparison with `unchecked_*()` etc.
> It is a common misconception that "unsafe removes all checks". In fact almost all checks still apply. "unsafe" enables five specific "superpowers", and that's all it does:
What is critical to understand is that `unsafe` gives the burden of the UB-less proof to the user. What rules are or not in place is second-order for maintainers evaluating Rust.
Moreover, saying "almost all checks still apply" is not particularly useful, because one still needs to know all the rules. In fact, I find the statement can be misleading for newcomers -- they may get overconfident thinking the compiler is going to catch "almost all" soundness violations.
Posted Sep 20, 2021 0:42 UTC (Mon)
by roc (subscriber, #30627)
[Link] (4 responses)
Posted Sep 20, 2021 0:44 UTC (Mon)
by roc (subscriber, #30627)
[Link] (2 responses)
Posted Sep 20, 2021 2:20 UTC (Mon)
by tialaramex (subscriber, #21167)
[Link] (1 responses)
That is, having declared your function unsafe (meaning a caller ought to read your documentation to ensure they use it safely), you are free to do anything unsafe inside it, even if you didn't intend to.
There is a (default off) lint to warn you that this might be a bad idea, and I especially encourage you to use this if you write unsafe functions that are, from Rust's point of view, technically safe (e.g. they actually control important hardware, and so callers must read your documentation to use them safely or bad things might happen, but they will not cause Undefined Behaviour as far as Rust is concerned and so do not technically need Rust's unsafe "super powers"). The reason the default isn't default on is that many common unsafe functions have line 1-2 lines inside them, and those are genuinely unsafe code, so with the lint you'd be adding redundant unsafe blocks inside these tiny unsafe functions.
Posted Sep 21, 2021 10:11 UTC (Tue)
by ojeda (subscriber, #143370)
[Link]
Indeed -- in Rust for Linux we are already using `unsafe_op_in_unsafe_fn` (as an error, in fact, rather than a warning) and I hope it becomes the default in a future Rust edition.
Posted Sep 20, 2021 9:49 UTC (Mon)
by ojeda (subscriber, #143370)
[Link]
This is correct, except for the "simply" ;)
Being able to prove those uses are sound can range from trivial to very complex.
For instance, it is trivial to know a dereference is valid if you just acquired a valid pointer within the same function. But in other cases the reasoning is non-local -- the "safety boundary" might be the type or the module.
Another example is doing FFI. It requires understanding the safety preconditions of a foreign function, which could be documented or not. If not documented, then unraveling that is likely to require further non-local reasoning. And, of course, all this in a different language.
Another complicating factor is that the `unsafe` rules are not perfectly specified, yet (yes, the "superpowers" are clearly defined, but those are just the "API", so to speak).
Posted Sep 18, 2021 13:29 UTC (Sat)
by PengZheng (subscriber, #108006)
[Link] (13 responses)
while (f())
IIRC, Coverity can catch this kind of error.
Posted Sep 18, 2021 14:42 UTC (Sat)
by tialaramex (subscriber, #21167)
[Link]
1. Because the language does not outright forbid this construction, these will necessarily be optional diagnostics. C has been around for decades at this point and we can say from experience that (to a first approximation) none of those who most need them will enable the optional diagnostics.
2. Because the language does not outright forbid this _type_ of construction, there may be cases where the programmer believes, correctly or otherwise, that forcing them to eliminate Undefined Behaviour costs performance, which invariably C programmers are loathe to accept. So those cases become reasons not to have the general warning, or, if it exists, not to enable it, particularly on large codebases like Linux.
3. Outright forbidding Undefined Behaviour is safer because it forces the language designers and compiler developers to address any corner cases. Without this you get a mismatch between the set of things the language actually defines, and the set of things diagnosed by your tools as Undefined Behaviour, with some things slipping through the gap between the two.
Posted Sep 18, 2021 17:12 UTC (Sat)
by flussence (guest, #85566)
[Link]
The downside is it needs higher settings than -O0 to understand code at that depth, but that's true of many optimising compilers.
Posted Sep 18, 2021 19:17 UTC (Sat)
by dave_malcolm (subscriber, #15013)
[Link] (2 responses)
Posted Sep 18, 2021 20:34 UTC (Sat)
by ojeda (subscriber, #143370)
[Link] (1 responses)
The analysis seems to bail out when the address is taken (e.g. https://godbolt.org/z/xE6deTTTG) for some reason, but not in all cases (see second example where it seems to be analyze the body of `g`).
Also, it seems the warning does not appear if optimizations are enabled, is that expected?
Posted Sep 19, 2021 21:28 UTC (Sun)
by dave_malcolm (subscriber, #15013)
[Link]
Re your 1st example: for better or worse, the analyzer works on the SSA representation. Taking the address of x means gcc doesn't use ssa names for x, and this changes what the analyzer "sees" (which can be seen with -fdump-ipa-analyzer=stderr)
Re your 2nd example: g has an empty body, so the analyzer "knows" that it doesn't write to x, and that x is still uninitialized. (this can be seen with -fanalyzer-verbosity=4)
Optimizations affecting the output is another side-effect of my choice to use the SSA representation (which gains us some things, but costs us others...) I may need to revisit that choice.
Posted Sep 24, 2021 14:27 UTC (Fri)
by error27 (subscriber, #8346)
[Link] (7 responses)
You are using a loop in your code. Traditionally, GCC used to assume that the code would enter loops at least once. Smatch would only assume that when it was certain about it. This caused a lot of false positives in Smatch when it's not intelligent enough about the callers. Actually "this function is never called with an empty list so we will always enter the loop". Or "this function will never be called with length zero."
In some cases I view handling size zero as a readability and hardening improvement. But other times it's maybe not clear what is the correct way to handle a condition like that and it's impossible anyway so we may as well leave the code as-is.
Posted Sep 24, 2021 19:54 UTC (Fri)
by nybble41 (subscriber, #55106)
[Link] (6 responses)
If programmers intend that a loop will always be entered at least once they should use a do/while loop, which is defined to have those semantics. The use of a plain while loop implies that the condition may be false on the first iteration. If Smatch was reporting false positives it was due to the code being incorrect, not any issue with Smatch's treatment of while loops.
Posted Sep 24, 2021 21:31 UTC (Fri)
by Wol (subscriber, #4433)
[Link]
FORTRAN-IV always did the test at the bottom of the loop so was guaranteed to execute at least once. Fortran-77 did the test at the top of the loop so would skip the loop if the test was false the first time round.
The compiler I used had a switch to apply the FORTRAN logic to Fortran.
Cheers,
Posted Sep 27, 2021 11:51 UTC (Mon)
by error27 (subscriber, #8346)
[Link] (4 responses)
And even if I thought one style was more readable than another, I'm not trying to be the style police. People sometimes suggest adding style rules to Smatch but I don't want any part of that.
The fix is to make the cross function analysis more robust. That ends up paying for itself in other ways as well.
Posted Sep 27, 2021 20:38 UTC (Mon)
by nybble41 (subscriber, #55106)
[Link] (3 responses)
Posted Sep 27, 2021 21:56 UTC (Mon)
by Wol (subscriber, #4433)
[Link] (2 responses)
DO ... WHILE ... UNTIL ... END DO
???
What's important (and this is where FORTRAN/Fortran failed), is that the test should be carried out where the test is written.
DO WHILE condition
or
DO
Cheers,
Posted Sep 27, 2021 23:13 UTC (Mon)
by nybble41 (subscriber, #55106)
[Link]
The context is C code compiled by GCC and Clang. C doesn't have any issue with the placement of the test. A while loop puts the test at the top of the loop, and evaluates it at the top of the loop (before the first iteration of the loop body). The do/while loop places the test at the bottom and evaluates it after executing the body of the loop.
However, if you use a while loop in a context where it would be *incorrect* (as in: results in reading from uninitialized variables) to exit the loop before the body had executed at least once, there is a bug in your code, or at least the very strong *potential* for one even if you think you know that the inputs will always cause the body to be executed. In the absence of some clear human- and machine-readable indication of the initial condition, such as assert() statements, the compiler or other static analysis tool should warn about the potentially uninitialized variable(s). Even with assert() statements I would consider it better to use do/while since it better reflects the actual flow control vs. "assert(expr); while (expr) { ... }" which has the false appearance of a test and branch prior to the first iteration which will (hopefully) be removed by the compiler as dead code.
Or you could structure the code so that it reliably does the right thing even if the initial condition results in the body of the loop not being executed. That would be ideal: easier to read and reason about, while handling a wider range of inputs without risking undefined behavior.
Posted Sep 27, 2021 23:22 UTC (Mon)
by anselm (subscriber, #2796)
[Link]
Standard FORTRAN didn't even have “DO WHILE … END DO” loops (although some implementations added them as a non-standard extension). You would emulate structured loops like these with “IF” and “GOTO” .
The other thing was that it was pretty easy to get loops wrong. The moral equivalent to a “FOR” loop in FORTRAN looks like
Posted Sep 19, 2021 1:22 UTC (Sun)
by dskoll (subscriber, #1630)
[Link] (2 responses)
In some cases, gcc on Linux gives a floating point exception and terminates the program in this case. I compiled the following code:
On the other hand, if you change the first assignment in main to int b = -1 then you get:
Posted Sep 19, 2021 17:17 UTC (Sun)
by ojeda (subscriber, #143370)
[Link]
If, instead, the standard defined that e.g. `abort()` is called, then the compiler would need to ensure that happens somehow (e.g. by guarding the division or by handling hardware exceptions).
Posted Sep 29, 2021 17:13 UTC (Wed)
by anton (subscriber, #25547)
[Link]
Posted Sep 29, 2021 17:24 UTC (Wed)
by anton (subscriber, #25547)
[Link]
Posted Oct 10, 2021 2:54 UTC (Sun)
by yodermk (subscriber, #3803)
[Link] (6 responses)
Posted Oct 10, 2021 7:50 UTC (Sun)
by jem (subscriber, #24231)
[Link] (5 responses)
Another fresh Rust book is Rust for Rustaceans by Jon Gjengset. This book is for "developers who have mastered the basics." I haven't read it myself yet, but I have been watching some of the author's Crust of Rust videos on YouTube. But be warned: some of the videos are over five hours long.
Posted Oct 10, 2021 15:44 UTC (Sun)
by jezuch (subscriber, #52988)
[Link] (2 responses)
For the curious, this podcast explains why: https://rustacean-station.org/episode/038-jon-gjengset/
Posted Oct 12, 2021 7:12 UTC (Tue)
by MrWim (subscriber, #47432)
[Link] (1 responses)
Posted Oct 12, 2021 13:31 UTC (Tue)
by jezuch (subscriber, #52988)
[Link]
Posted Oct 31, 2021 9:11 UTC (Sun)
by mcortese (guest, #52099)
[Link] (1 responses)
Posted Oct 31, 2021 11:00 UTC (Sun)
by peniblec (subscriber, #111147)
[Link]
Key Rust concepts for the kernel
https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html#u...
Key Rust concepts for the kernel
Key Rust concepts for the kernel
Thanks for your clarification.
Key Rust concepts for the kernel
Key Rust concepts for the kernel
It looks like somebody (other than me) fixed that error, apologies for the mistake.
INT_MAX
Key Rust concepts for the kernel
Key Rust concepts for the kernel
Key Rust concepts for the kernel
Key Rust concepts for the kernel
Key Rust concepts for the kernel
Key Rust concepts for the kernel
Key Rust concepts for the kernel
x = 42;
return x;
If a static analysis tool can, so can a compiler like GCC in the future?
Key Rust concepts for the kernel
Key Rust concepts for the kernel
FWIW, I've implemented that for GCC 12 (with -fanalyzer):
https://godbolt.org/z/Tjojofjzo
Key Rust concepts for the kernel
warning: use of uninitialized value 'x' [CWE-457] [-Wanalyzer-use-of-uninitialized-value]
9 | return x;
| ^
'test': events 1-3
|
| 7 | while (f())
| | ^
| | |
| | (1) following 'false' branch...
| 8 | x = 42;
| 9 | return x;
| | ~
| | |
| | (2) ...to here
| | (3) use of uninitialized value 'x' here
|
Key Rust concepts for the kernel
Key Rust concepts for the kernel
Key Rust concepts for the kernel
Key Rust concepts for the kernel
Key Rust concepts for the kernel
Wol
Key Rust concepts for the kernel
Key Rust concepts for the kernel
Key Rust concepts for the kernel
statements
END DO
statements
WHILE condition END DO
Wol
Key Rust concepts for the kernel
Key Rust concepts for the kernel
DO 10 I=1,10
…
10 CONTINUE
IIRC the line with the statement label “10” doesn't have to be “CONTINUE” (which is basically a no-op which is useful to hang statement labels off of), although it makes for more readable loops. But you would have to take care not to accidentally write something like
DO 10 I=1.10
…
10 CONTINUE
where, since spaces inside variable names are allowed in FORTRAN and variables don't need to be explicitly declared, the compiler would assume the first line to be an assignment statement rather than the beginning of a loop (having a “10 CONTINUE” statement without a corresponding “DO” isn't an error, either). FORTRAN 77 changed the syntax of “DO” to optionally allow
DO 10, I=1,10
which would prevent the error, and back in a previous life when I was the TA for a FORTRAN course at the university (I was young and needed the money) I would heavily penalise students who didn't avail themselves of this safety measure.
INT_MIN / -1
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
int im = INT_MIN;
int divide(int a, int b)
{
return a/b;
}
int main(int argc, char **argv)
{
int b = atoi(argv[1]);
printf("%d\n", divide(im, b));
}
And running it gives:
$ ./test -1
Floating point exception
$ ./test
-2147483648
INT_MIN / -1
My guess is: you compiled and ran on AMD64 (or IA-32), and the code as shown actually performs a division, and the processor produces an exception due to the integer overflow in this case, which the OS translates into a SIGFPE (with appropriate si_code if you pass SA_SIGINFO to sigaction). By contrast, in the case where gcc can see that b=-1, it tries to constant-fold the division and botches it (of course, they will claim that you bothed your program).
INT_MIN / -1
As it happens, I tried pretty hard to find a performance difference between checking before the division and checking afterwards (my theory was that checking afterwards should run in parallel with the division, while checking before should not). Even with specially crafted microbenchmarks, the differences were small on some CPUs and non-existent on others. Therefore I consider it unlikely that the check has any measurable effect in production code. However, I did not compare a variant without any check, so that's something a sufficiently interested individual can still do (maybe based on my microbenchmarks).
Key Rust concepts for the kernel
Learning resources
Learning resources
Learning resources
Learning resources
Learning resources
Do you know if it's available as epub, though? I could only find the Kindle version, which I'm not a big fan of.
Learning resources
"Programming Rust" (2nd edition) can be found DRM-free on ebooks.com. As for "Rust for Rustaceans", no starch press seems to provide PDF and ePub versions.
Learning resources