Asking the Kernel

Learning Tock from the ground up · Chapter 7

‹ Contents

Asking the Kernel

Chapter 6 finished a wall. A process may reach its own code and its own memory, and the hardware refuses everything else — no pin, no timer, no neighbour. So a process cannot switch on a light. There is exactly one way to ask for anything, and it is one instruction wide.

What you'll be able to do at the end

  • Name the instruction a process uses to reach the kernel, and say where the number picking a syscall class is kept.
  • List the eight classes, and say which three never reach a driver and which two can reach a line of its code.
  • Read one request the way the kernel reads it: four registers, meaning four different things depending on the class.
  • Follow one event from inside a driver to a function running inside a process, naming the queue it waited in.

Plan on forty-five minutes. Chapters 5 and 6 are assumed: what a process is, and the fence the hardware puts round it. Chapter 4 comes back at the far end, because the thing that answers a request is the capsule chapter 4 was about.

In the book: doc/syscalls and TRD104, which specify the interface this chapter watches run.

Eleven words, before we start

Each is one sentence now and repeated in context below. Three of them are the names of particular requests, and the last is the only one describing traffic going the other way.

syscall
The one way a process may ask the kernel for anything at all. Chapter 5 named it; this chapter is the whole of it.
syscall class
One of eight kinds of request. Which kind is being made is an eight-bit number, and it travels inside the instruction rather than in a register.
svc
The Arm instruction that raises an exception deliberately. It carries a process into the kernel, and going the other way it carries the kernel into a process.
word
Four bytes, the width this processor moves in one go. The list you are reading calls its entries words in the ordinary sense; everywhere else in this chapter, a word is four bytes.
exception frame
The eight words the processor pushes onto a stack when it takes an exception. Every value a request and its answer carry travels in it. Which kind of request it is does not.
driver number
The part of a request that names a capsule. A board maps each one to a driver, or to nothing.
command
The class that tells a driver to do something now. It is the request that reaches a driver's own code in the ordinary case.
subscribe
The class that leaves a function pointer with the kernel, for it to call when something happens. The driver never sees the pointer, and the kernel keeps it until the process hands over another.
allowed buffer
A run of a process's own memory that the process has handed to the kernel to read or write, given as an address and a length.
yield
The class that gives the processor back. A process that never calls it never receives anything.
upcall
A call in the other direction: the kernel arranging for one of a process's own functions to run. Chapter 5 found the queue they wait in.

The wall with no door in it

Chapter 6 left a process that can reach nothing but itself. That is the whole point of it, and it is also a problem, because a process that can touch only its own memory can compute and do nothing else. It cannot light the LED on the board in front of you. The register that lights it sits at an address the hardware now refuses.

The way out cannot be a second gate in the fence. A gate would be an address the process is allowed to write, and any address a process is allowed to write is one a wrong pointer can reach by accident. What is wanted is an exit no pointer can find.

So the exit is not an address at all. It is an instruction, and its entire job is to stop the process and hand the processor to the kernel. On this chip that instruction is svc, and it is sixteen bits wide: eight of them say svc, and the low eight are a number the program chooses.

That number is the syscall class. Not the driver, not the operation, not the argument — those go in registers. What the instruction itself carries is which of eight kinds of question is being asked.

Figure 1 One instruction, and the number living inside it

Click each. Five requests, and two of them do not end the way the other three do.

the halfword in flash0xDF02
low eight bits2
what the kernel makes of itCommand

Class zero, yield, which gives the processor back. It is also the only class whose first register the decoder checks: three values are legal there, and anything else is treated as a broken request rather than a request for something missing.

Class two, command. This is the class the LED request further down uses. It is one of only two classes that reach a driver's own code, and the one the rest of this chapter keeps coming back to.

Class four, the read-only kind of handing memory over. Classes three, four and seven are three flavours of the same idea, which is why the numbers on their own tell you so little.

Nothing. The kernel converts eight-bit numbers into requests by matching zero through seven, and eight matches nothing, so the conversion returns nothing at all. A request that will not decode ends the process, which is the same ending chapter 6's forbidden store had.

The kernel's own, going the other way. Tock enters a process by executing svc 0xff, and the comment beside that line says it does not matter which number is used, because nothing reads it. What sorts the two directions apart is a single bit of the link register, which is the same bit chapter 6's fault handler branched on.

Notice where the class number is not. It is not in a register: it is the low byte of an instruction sitting in the process's own flash, which chapter 6 fenced as readable and executable and never writable. A process can get the contents of a request wrong. It cannot corrupt which kind of request it is making.

Eight kinds of question

The kernel's own list of syscall classes disagrees with itself, and it is worth seeing why before trusting any count. The file that defines them opens with the sentence "Tock supports six system calls", names six, describes a seventh three lines further down, and then declares an enumeration with eight in it. The enumeration is the one the hardware path uses. Eight is the number.

A useful way to hold them is by how far each one travels. Five of the eight begin with a driver number, so the kernel has to look up a capsule before it can go on. The other three never leave the core kernel: it can answer them out of what it already knows about the process.

Figure 2 Eight classes, and how far each one gets

Click each class. The left column is the number that goes in the instruction.

Give the processor back. Three variants: wait until something arrives, look and return either way, or wait for one particular thing. The core kernel handles it, and it is one of three classes a board's own request filter is never shown.

Leave a function pointer with the kernel. It names a driver, and the driver is never told. The kernel checks the pointer lands inside the process's own flash or its own RAM. Then it files the pointer, and hands back whichever one was there before.

Do something now. This is the class that reaches a driver's own code, and it does so in one line of the kernel: d.command(subdriver_number, arg0, arg1, process.processid()). Everything the driver may answer with is a value or an error.

Hand over a run of the process's memory for the kernel to read and write. It names a driver, and again the driver never receives it. The kernel checks the address and length against the process's own bounds and files the result.

The same, for memory the kernel will only read. One thing differs, and it matters: a read-only run may sit in the process's flash, where a read-write one may not. That is how a constant string gets printed.

Ask about, or move, the edges of the process's own memory, which the core kernel answers alone. Twelve operations numbered 0 to 11: two move the break chapter 5 named, five report where its own memory and flash begin and end, and the rest concern writeable flash and debugging.

Stop, and the last of the three the core kernel answers by itself. Two variants, terminate and restart, with a completion code beside them. Anything other than those two variants comes back as an ordinary unsupported-operation error, which the process is still running to read.

The third flavour of handing memory over, where the process keeps reading it while the kernel writes. This is the one other class that reaches a method on the driver, and exactly one capsule in the tree implements that method.

Notice that the numbers do not group anything. Classes 3, 4 and 7 are three versions of one request, and 7 sits three slots away from its siblings. Nothing about the numbering says so, and nothing in the kernel depends on which class sits beside which. It is a record of what was needed when, not a taxonomy.

Turning on the light

Chapter 4 left a request half-finished on purpose. A process asks for its first LED to be turned on; chapter 4 picked the request up on the kernel's side, inside the capsule, and left the crossing for here. Here is the crossing, and on this board it is five instructions and a return.

The LED driver's number is 0x00002. Command number 1 means turn one on, and the argument beside it says which one. So the request is four numbers in four registers, and then the instruction that sends them:

2002      movs r0, #2      // driver number: the LED driver
2101      movs r1, #1      // command number: on
2200      movs r2, #0      // which LED: the first
2300      movs r3, #0      // unused by this command
df02      svc 2            // class 2: command
4770      bx lr

Twelve bytes. The column on the left is what the assembler produced, and the last instruction is the wrapper returning, not part of the request. Notice df02: the 02 is the class, exactly as Figure 1 had it. The 02 at the end of 2002, four lines up, is the driver number, which on this board happens to be 2 as well. The two have nothing to do with each other.

One board note. That LED is on GPIO 25, and this board's driver is built with exactly one of them, which is why the next figure can show an index of 1 being refused. On a Pico 2 W, chapter 1 said, GPIO 25 is the radio's chip-select line rather than the LED, and this tree has no crate for that board at all.

Figure 3 One class, four requests, four registers

Click each request. Four different things to ask the kernel for, and one instruction. Look at what actually differs between them.

r0 — driver0x00002
r1 — command0
r2 — argument0
r3 — argument0

Command 0 is a convention rather than a rule: ask a driver for command 0 and a driver that is there answers. This one answers with how many LEDs it was built with, so on this board it comes back as 1.

The request from the listing above. The driver compares the argument against the number of LEDs it holds, finds 0 is in range, and turns that one on before the call returns. Nothing waits; the pin is already high.

Refused, and refused by the driver. It holds one LED, so index 1 is out of range and the answer is an invalid-argument error. The class was fine, the driver was found, and the argument was wrong.

Refused earlier, and by the board. Driver 3 is the buttons driver, and this board's lookup lists five numbers: alarm, console, LED, GPIO and inter-process communication. Three is not among them, so no driver is consulted and the answer is a no-such-device error.

Notice that the last two both fail and fail in different places. One is a driver saying no; the other is a board having nothing at that number, which the kernel discovers before any driver code runs. To the process both look the same — a number in r0 saying failure and a number in r1 saying which — and telling them apart is the whole reason command 0 exists.

What a driver is actually asked

Five classes begin with a driver number, so it would be reasonable to expect a driver to have five things to implement. The trait every driver implements has three methods, and only two of them are requests.

One is command. One is the seventh class, the shared-buffer flavour, which exactly one capsule in the tree uses. The third is not a request at all: it is the kernel asking a capsule to set aside its per-process storage, which is chapter 8's subject and appears here only as the method's name.

Figure 4 Three methods, and the six classes that reach none of them

Click each method. Three of them, and one is not a request from a process at all.

Four parameters in, one answer out: which operation, two words of data, and which process is asking. The answer is a wrapper around a small set of shapes: success, success carrying up to three values, or failure carrying an error. A driver that does not recognise the operation says so with an unsupported-operation error, and that is also what the trait does for a driver that does not write this method at all.

The seventh class, and the only allow that a driver sees. It exists to save a syscall. A process can revoke a buffer, read it and hand it back; the specification says this class is for the cases where paying a syscall to do that costs too much. The default is a refusal, and one capsule in the whole tree replaces it. That one publishes three of the kernel's own numbers into a buffer: how many times the process has been switched to, how much work is waiting for it, and the time. After one allow to hand the buffer over, a process reads all three without another syscall.

Not a request. The kernel calls it when a subscribe or an allow needs filing for a process that has never used this driver before. It has to ask, because only the capsule knows how large its own per-process storage is. It is the one method with no default, so a driver author cannot forget it silently.

Notice what is absent. There is no subscribe method and no read-write allow method, so a driver cannot see the function pointer a process registered and cannot be handed the buffer a process offered. The core kernel takes both, checks both, and files both. Chapter 4 said a capsule reaches only what it was handed; this is the same rule one level up, applied to what a process hands it.

The answer, in the same four words

Whether a request reached one of those methods or never left the core kernel, the answer comes back the same way. There is no returning from a syscall in the ordinary sense. The kernel is not on a stack the process can return through, and by the time the answer exists the process is not running. What the kernel does instead is write into the exception frame the svc pushed. The four words that carried the request out are the four words that carry the answer back.

The first of the four says what shape the other three are in. Shape is this chapter's word for it; the kernel calls them variants, and the numbering is the useful part. Of the ten shapes defined, every failure is numbered below 128 and every success is 128 or above, so a process can sort one from the other by looking at a single bit.

Figure 5 Five answers, and which registers each one touches

Click each answer. Watch which cells stay marked as untouched.

r0 — shape128
r1untouched
r2untouched
r3untouched

It worked and there is nothing to say about it. One register written, three left alone. This is what turning on an LED comes back as.

It worked and here is a number. Asking the LED driver how many LEDs it has comes back this way, with the 1 in r1.

The widest success shape there is: three values, filling every register the frame has for the purpose. One capsule in the tree returns it, handing back three counters about a network device in a single call rather than making the process ask three times.

It did not work, and r1 says why. There are thirteen errors in the whole kernel, numbered 1 to 13, with names like invalid-argument and no-such-device. A process compares against a number rather than parsing anything.

It did not work, and there is a number worth having anyway. Nothing in this tree has yet wanted one: the only caller of its constructor is the unit test that checks it encodes correctly. It is in the specification because failures and successes are numbered from opposite ends, which means a failure can carry data of its own without any chance of being read as a success.

Notice what "untouched" means. The kernel writes only the registers a shape needs, and the rest of the frame still holds what the process put there before the svc — its own arguments. So a process that reads r2 after a plain success reads back the argument it sent. That is an accident of the encoding rather than a promise, and the specification says nothing about it.

Things that do not finish

Turning on an LED finishes inside the call. The pin is high before the process runs again, and Figure 5 showed what that comes back as: a success with nothing in it. Most of what a process wants is not like that. A byte from the console arrives when somebody types it, and no amount of waiting inside a driver will make that sooner.

So a request that cannot finish is split in two. First subscribe, leaving a function pointer with the kernel. Then command, which starts the work and returns immediately, having promised nothing except that it started. Later the driver has something, and asks the kernel to run that function.

The kernel cannot. The process is not running, and even if it were, interrupting it to run one of its own functions would land a callback in the middle of whatever else it was doing. So the kernel writes down an upcall instead: which function, which three values, and which subscription it answers. Chapter 5 found where those upcalls go: room for ten of them, a constant belonging to the process implementation rather than to any board. This is what fills it.

A driver has something for a process — a byte arrived. The process is running right now, in the middle of its own code. When does the process's callback run?

That is what the word upcall suggests to anyone who has met an interrupt on a larger machine, and it is wrong here. There is no thread to interrupt with and nothing that stops a running process to hand it anything.

Right. The upcall goes on the end of a queue and nothing drains it. The process keeps running, none the wiser, until it calls yield — and delivery happens at that moment or not at all.

A timeslice ending puts the process back on the scheduler's queue. It does not deliver anything, and a process that never yields never receives an upcall however many timeslices it burns through.

The handler runs in the kernel and finishes long before any of this. What it leaves behind is an entry in a queue belonging to the process.

Figure 6 Six steps from a driver's event to a function running in a process

Give the driver events without ever yielding, and watch what the queue does. Ten is a real constant, and the eleventh event is the one worth producing for yourself.

waiting 0
delivered 0
dropped 0
process is running

Nothing waiting. Give the driver something to report. Waiting, and nothing is going to move it. A running process is never stopped to take one of these, so the driver's event has visibly happened to nobody. Full. Ten slots is a constant belonging to the process implementation rather than to any board. The next event is not queued: the driver is told so, and the kernel keeps nothing to retry from. That one is gone. What happens next is the driver's business, because nothing here remembers it — and a process that never yields reaches this state and stays in it. Delivered, exactly one. Yielding is what puts the process into a state where an upcall can be handed over, and it takes one; anything else waiting is waiting for the next yield.

A driver does not call the process's function and does not hold its address. It has a handle that lets it name one of its own upcalls and hand over three values, and the kernel finds the pointer it filed at subscribe time. A driver asking for a number it has no upcall for gets an error and nothing is scheduled.

The fourth value is the one worth knowing about. When the process subscribed, it passed a word of its own alongside the pointer, and the kernel kept it. It comes back here untouched. A callback can therefore tell which of its own requests it is answering, and the driver was never told anything about that.

Ten slots, shared by every driver a process uses. A full queue does not block and does not fault: the upcall is dropped and the driver is told the queue was full. What happens next is then the driver's business, because the kernel keeps nothing to retry from.

This is the step that surprises people. The process is running, and a running process is never stopped to take one of these. It runs until it asks for something, uses up its slice of time, or yields. Until one of those, the upcall sits in the queue and the driver's event has visibly happened to nobody.

Yielding puts the process into a state where upcalls can be delivered, and the kernel takes exactly one. If the queue holds three, the other two wait for the next yield. This is also why a process that never yields never receives anything: its queue fills to ten and the eleventh is dropped.

The last step is the one in the next section. The kernel does not call the function; it edits the eight words on the process's stack so that resuming the process lands inside the function instead of after the svc.

Notice that an upcall is nothing like an interrupt reaching into the process, which is what the word suggests to anyone who has met one on a larger machine. There is no thread, and nothing that stops the process to hand it one. There is a queue, and a moment the process chooses, and delivery happens at that moment or not at all.

One frame, three jobs

The exception frame is eight words. The processor pushes them when it takes the exception and pops the same eight back when the handler returns. That is the opening the kernel uses: change the words while they are sitting there, and the process resumes into something else.

The comment above the function that does it says what it amounts to in one line: "In effect, this converts svc into bl callback." The instruction the process executed was a request. By the time it finishes, it has become a call to a function, with the address after the request left behind as the place to come back to. The specification has names for the two endings: a request that is simply answered is a Direct Resume, and one that comes back inside a function is a Pushed Callback.

Two of the eight words are addresses of instructions, and both are written with their lowest bit set. That is not an odd address. On this processor an address used as a branch target carries one bit saying which instruction set to decode. This chip has only one of the two, the compact set called Thumb, so the bit is always set and the address underneath it is always even.

Figure 7 Eight words, and the three jobs each one does

Pick a job, and the same eight words say what they hold for it. Then click a word to read all three at once, and look for anything in the frame that says which job it was.

job

The kernel writes none of the eight. Every one of them is what the process put there before the svc, which is why a request cannot be corrupted on the way in by anything but the process itself. The kernel writes up to four, and only the ones the answer's shape needs. The rest still hold what the process sent, so reading r2 after a plain success gives back the argument you passed — an accident of the encoding, and not a promise. The kernel writes seven of the eight, leaving out exactly the one the syscall interface ignores. Two words are the whole trick: pc becomes the function, and lr becomes the address after the svc, so the callback returns into the request that started it.

Going out: the driver number, or the yield variant, or the exit variant, depending on the class. Coming back: which shape the answer is. Carrying a callback: the driver's first value. Three different meanings, one word, and which one applies is never written down anywhere in the frame.

Going out: the command or subscribe number within the driver. Coming back: an error code, or the first value of a success. Carrying a callback: the driver's second value.

Going out: an argument, or the address of a buffer, or a function pointer. Coming back: a second value. Carrying a callback: the driver's third and last value.

Going out: an argument, or a buffer's length, or the word a process wants handed back to its own callback. Coming back: a third value. Carrying a callback: that same word, returned untouched.

Nothing, ever. The source is blunt about it: "Stack offset 4 is R12, which the syscall interface ignores". The processor saves it because the calling convention says a called function may destroy it, and the syscall path has no use for it in either direction.

Where to resume when the thing at pc returns. For a plain answer nothing is written here. To deliver a callback the kernel writes the address just after the svc, so that when the callback returns, the process carries on from the request as though it had simply come back.

Where to resume now. For a plain answer this is already the instruction after the svc and is left alone. To deliver a callback the kernel writes the function's address here instead. This one word is the whole of the trick.

The condition flags and a little processor state. Saved on the way in and put back on the way out. Where a callback is being delivered the kernel writes back the value it saved from the original request, rather than inventing one.

Notice that answering a request and delivering a call are the same operation on the same eight words. Two of the three jobs write only the first four; the third writes seven, leaving out exactly the one the interface ignores. Nothing in the frame records which job was done, because nothing needs to: the process resumes at whatever pc now says, and finds out from the code it lands in.

A buffer through the wall

A command hands a driver two words of data and nothing more. A line of text to print is not two numbers, and neither is a reading to be filled in. For anything larger the process hands over a piece of its own memory: an address and a length, in a class made for it. What the kernel ends up holding is an allowed buffer, and the classes that make one are three of the eight.

Which sounds like exactly what chapter 6's fence exists to prevent, and is not. The fence stops a process reaching out. This is a process letting the kernel reach in, and the kernel runs on the privileged side, where chapter 6's regions do not apply at all. It could read that memory unasked. What the class buys is not access. It is a record of consent, with the bounds checked.

The check is software this time, in the process implementation rather than in the processor, and it lands on the same two addresses. The buffer must start at or after the start of the process's memory, and end at or before its break. Those are the two numbers chapter 6 wrote into a hardware register to build the region; here they are compared in Rust to answer a different question about the same span.

A process hands the kernel a buffer near the top of its heap, then finishes with it and wants its heap space back. Can it move its break down past where that buffer was?

The kernel has no way to know a driver has finished. There is no call that says so, and nothing tracks it, so it assumes none of them ever has.

A zero-length allow does take the buffer back from the driver, and the kernel accepts it without checking any address at all. It does not lower the mark.

Right. Offering a buffer raises a mark to its far end, and nothing a running process can do lowers that line. The cost is permanent for the life of the process, and the only refund is not being that process any more: the restart variant of exit rebuilds its memory from the start.

Nothing faults. The request comes back having moved the break somewhere legal, or not moved it; a memop that cannot be satisfied is an error value, not a fault.

Figure 8 Offer the kernel a buffer, and watch the comparisons

Drag the buffer across the map and swap what the kernel may do with it. Two things worth producing for yourself: a read-only offer sitting in flash, which is accepted, and a length of zero, which is accepted without any comparison being made at all.

0x20008080
256 bytes

the process's flash

the process's RAM

the comparisonagainst its memoryagainst its flash
end >= startyesyes
start >= the flooryesno
end <= the ceilingyesno

The first row is asked every time and cannot be made to fail here, because no length this bench offers is large enough to wrap past the end of the address space. It is shown because the kernel asks it before either of the others.

the run offered0x20008080 to 0x20008180
what comes backaccepted
the mark now0x20008000

Both ends land inside the process's own memory, so the kernel builds a checked handle, files it, and hands back whichever allowed buffer that driver held before. Make the offer and the mark moves up to the far end of it.

Accepted, and only because it is read-only. A read-write offer is checked against the process's memory alone; a read-only one is checked against its memory or its flash, and a constant string lives in flash. This is how printing a literal works without copying it into RAM first. The mark does not move, because nothing in flash is memory the break can be dragged over.

Accepted with no comparison made at all, deliberately, whatever address it names. A length of zero is how a process takes a buffer back: it replaces whatever the driver held with something that cannot be read or written, so no address could make it unsafe.

Refused. Flash is not memory the kernel will write, so a read-write offer is only ever checked against the process's RAM, and a flash address fails that floor before anything else is asked. Offer the same run as read-only and it is accepted.

Refused, with an invalid-argument error, and nothing about the fence in chapter 6 was involved. The comparison is arithmetic on two pointers, made before any handle exists, and the same comparison refuses a length so large that adding it wrapped around the end of the address space.

Refused, because the run starts below the floor. Both checks ask this first, and the address here is beneath the start of anything this process owns.

Notice that the mark only ever goes up. Make a high offer, then a low one, and the mark stays where the high one put it. Once a process has offered a stretch of memory, it may not move its break back below the far end of it, and nothing a running process can do lowers that line. The kernel has no way to know a driver has finished with an allowed buffer, so it assumes none of them ever has. One thing does clear it, and it is the button labelled restart the process: the restart variant of exit, back in Figure 2, which rebuilds the process's memory from the start. So the cost is permanent for the life of a process, and the only refund is not being that process any more.

Three requests the door refuses

A door that cannot refuse is a hole. This one refuses in two quite different ways, and the difference is worth more than either case on its own. One kind ends the process, without any driver being asked and without anything being returned to read. The other is an ordinary answer, which the process collects and carries on from. Which of the two a bad request gets turns on whether the kernel could make sense of it at all.

Figure 9 Three refusals, and only one of them survivable

Click each. The third is the odd one out, and the reason is the point of the figure.

There is no class 8. The step that turns four registers and a number into a request matches 0 through 7 and returns nothing for anything else, and the caller turns that nothing into a fault. The process is stopped, and the board's policy decides what stopping means, exactly as in chapter 6.

The same ending, one step further in. Yield is the only class whose first register the decoder validates: 0, 1 and 2 are the three variants, and a 3 there makes the whole request undecodable rather than unsupported. The class was real, the argument was not, and the result is a stopped process either way.

An answer. The request decoded, the class exists, the board looked 3 up and had nothing there, so a no-such-device error goes back in r1 and the process keeps running. Nothing is faulted, because nothing was malformed — the process asked a well-formed question about something absent.

Notice the line the kernel is drawing. A request it cannot parse is treated as a broken process, because a process that emits nonsense at the boundary has already lost the plot. A request it can parse but cannot satisfy is treated as a conversation. Exit is the same shape: an unknown exit variant comes back as an unsupported-operation error rather than a fault, and the process that asked to stop is still running to read it.

Check yourself

Where does the kernel find out which class of request a process made?

The third. The handler has no register to read it from. It takes the program counter off the frame, steps back one halfword to the instruction that caused the exception, reads it, and masks off the low eight bits. r0 is the driver number for five of the eight classes, which is what makes the first answer tempting.

A process subscribes, sends a command, and then never yields. When does its callback run?

The second. Upcalls are only taken off the queue for a process that has yielded, so a process that never yields accumulates ten of them and then starts losing them. The third answer is the near miss worth naming. Losing the processor at the end of a timeslice is not yielding: it puts the process back in line to run again, not into the state where upcalls are delivered.

A syscall comes back with 1 in r0. What did the process get?

The first. Failures are numbered from 0 and successes from 128, so 1 is the second failure shape: an error code and one extra value. The success carrying one value is 129, which is the same idea shifted by the one bit that separates the two families.

Why can a driver not hold on to a buffer a process allowed it?

The third. There is no method on the trait through which a read-write buffer could arrive, so the question of holding one does not come up. The first answer is the one to be careful with: the unit really does not refuse it, because a driver runs privileged and chapter 6's regions were never pointed at the kernel.

What you can say now

A process reaches the kernel through one instruction, and the kind of request it is making is a number inside that instruction rather than in any register it can corrupt. There are eight kinds. Three are answered by the core kernel out of what it knows about the process. Five name a capsule, and of those, exactly one reaches a line of that capsule's own code in the ordinary case.

The four argument registers land in the frame that instruction pushes. They mean different things on the way out and on the way back, and the kernel answers by writing into them where they sit. When something arrives later, the same eight words are edited again, so that resuming the process runs one of its own functions and then carries on from the request that started it.

Two things crossed the boundary in this chapter that the chapter could not finish. One is the function pointer a process leaves behind for an upcall, which the kernel files somewhere. The other is the allowed buffer it lends, which the kernel files in the same place. That place is inside the process's own memory, it has no allocator behind it, and it is the last mechanism this series has left to explain.

Everything above, checked against source

Every claim on this page comes from the Tock tree at commit 83bad9388, the same commit chapters 4, 5 and 6 were pinned to. The instruction encodings in Figure 1 and in the listing were assembled rather than looked up, from syscall-demo.s beside this page.

  • The eight classes and their numbers — SyscallClass, kernel/src/syscall.rs:87–96, converted from a byte at :101–116
  • The file's own opening line saying six, three lines before it describes a seventh — the same file, :9–22, "Tock supports six system calls"
  • Eight kinds of request, each with its own set of arguments — Syscall, the same file, :150–236, "Structure representing an invocation of the [`SyscallClass::Yield`] system call class"
  • Which classes carry a driver number — driver_number, the same file, :239–268, returning something for five of the eight, "Get the `driver_number` for the syscall classes that use driver numbers"
  • Four registers and a number becoming one of those shapes — syscall_from_register_arguments_trd104, kernel/src/utilities/arch_helpers.rs:42–107
  • Yield being the only class whose first register is checked there, and 0, 1 and 2 being the whole of it — the same function, :50–66
  • Anything that will not decode becoming a fault — arch/cortex-m/src/syscall.rs:509–512
  • The class number coming out of the instruction rather than a register — the same file, :478–497, stepping the program counter back one halfword and masking the low eight bits
  • The arguments travelling in r0 to r3 and the class identifier travelling in the instruction — doc/reference/trd104-syscalls.md:103–108, where the row for the class identifier says svc rather than a register
  • The kernel entering a process with the same instruction, and the number there meaning nothing — arch/cortex-v7m/src/lib.rs:318–322, "It doesn't matter which SVC number we use here as it is not used in the exception handler"
  • One bit of the link register sorting the two directions apart — the same file, :73–82, the same bit chapter 6's fault handler branched on
  • That handler being this board's, so the bit above is the one this board branches on — arch/cortex-m33/src/lib.rs:47, where the Cortex-M33 takes the v7-M one, "const SVC_HANDLER: unsafe extern "C" fn() = cortexv7m::svc_handler_arm_v7m"
  • The frame being eight words — arch/cortex-m/src/syscall.rs:195–196, "Space for 8 u32s: r0-r3, r12, lr, pc, and xPSR"
  • Three classes a board's request filter is never shown — kernel/src/kernel.rs:761–771
  • Five classes routed by driver number — the same file, :850–857
  • The one line that reaches a driver's own code — d.command(subdriver_number, arg0, arg1, process.processid()), the same file, :1035, "let _ =process.remove_pending_upcalls(upcall_id)"
  • A driver number a board does not list — the same file, :1022, and the board's own lookup at boards/raspberry_pi_pico_2/src/main.rs:46–49, which names one and passes the rest to boards/raspberry_pi_pico_2/src/lib.rs:97–103, which names four
  • Subscribe checking the pointer against the process's flash or its RAM — kernel/src/kernel.rs:903–911, and is_valid_upcall_function_pointer, kernel/src/process_standard.rs:1416–1422
  • Subscribe handing back whichever pointer was there before — kernel/src/kernel.rs:944, with the queued calls to the old one dropped at :1005–1011
  • An unknown exit variant coming back as an error rather than a fault — the same file, :1411–1417
  • The trait a driver implements, and its three methods — kernel/src/syscall_driver.rs:262–390: command at :268, allow_userspace_readable at :288, allocate_grant at :390
  • allocate_grant being the one with no default, "to help prevent accidentally forgetting to implement this function" — the same file, :312–313
  • Failures numbered from 0 and successes from 128 — TRD104SyscallReturnVariant, kernel/src/utilities/arch_helpers.rs:187–198
  • Only the registers a shape needs being written — encode_syscall_return_trd104, the same file, :285–296, where a plain failure writes two of the four
  • The upcall's fourth argument being the word the process passed at subscribe time — encode_upcall_trd104_ptr, the same file, :649–663, and argument3: self.appdata at kernel/src/upcall.rs:188
  • A driver scheduling a call by number, with three values — schedule_upcall, kernel/src/grant.rs:610–614
  • The upcall being built and queued — kernel/src/upcall.rs:183–190, into enqueue_task at kernel/src/process_standard.rs:598, which returns out-of-memory on a full ring at :625–628
  • Ten slots — CALLBACK_LEN, the same file, :1738, the constant chapter 5 named, "const CALLBACK_LEN: usize = 10"
  • Upcalls being delivered only to a process that has yielded — kernel/src/kernel.rs:602–628
  • Exactly one upcall per yield, because delivering it puts the process back into the running state — set_process_function, kernel/src/process_standard.rs:1483–1515, setting State::Running at :1523
  • The frame being edited so the process resumes at the function — set_process_function, arch/cortex-m/src/syscall.rs:364–418, with "In effect, this converts svc into bl callback" at :375 and the writes at :404–418
  • R12 being pushed and ignored — the same file, :393, "Stack offset 4 is R12, which the syscall interface ignores"
  • The low bit on an address meaning Thumb rather than an odd address — the line above it, :392, "Instruction addresses require |1 to indicate thumb code", and the two places it is written: callback.pc.addr() | 1 and state.yield_pc | 1 at :404–405
  • The two endings, and the names the page borrows for them — doc/reference/trd104-syscalls.md:310–337, "Direct Resume" and "Pushed Callback"
  • A read-write buffer being checked against the process's memory alone — build_readwrite_process_buffer, kernel/src/process_standard.rs:1054–1113, refusing with an invalid-argument error at :1111
  • A read-only buffer being allowed to sit in flash — build_readonly_process_buffer, the same file, :1145–1147
  • The two addresses that decide it — in_app_owned_memory, the same file, :2545–2556, comparing against mem_start() and app_break, "If this method returns `true`, the buffer is guaranteed to be accessible to the process and to not overlap with the grant region"
  • Those being the same two the region was built from — memory_start set to what the unit handed back at :2215 and :1937, and the region's far end moved to the break at :979–983
  • A length of zero accepting any address — the same file, :1067–1082 and :1129–1144
  • The mark that goes up and the break that cannot pass it — the same file, :1089–1090 and :975
  • The only thing that lowers it being a restart, which rebuilds the whole layout — reset, the same file, :2463, reached from try_restart at :795 and from exit variant 1 at kernel/src/kernel.rs:1406–1408
  • Why nothing a running process does lowers it, in the kernel's own words — kernel/src/processbuffer.rs:440–456, "a given Process implementation must assume that the memory described by a once-allowed ProcessBuffer is still in use"
  • The three-value success having one caller in the tree, returning counters about a network device — capsules/extra/src/ethernet_tap.rs:488–492
  • The failure-carrying-a-value shape having none — CommandReturn::failure_u32, kernel/src/syscall_driver.rs:36–40, whose only caller in the tree is its own unit test at :423–424
  • What the one userspace-readable capsule publishes — capsules/extra/src/read_only_state.rs:23–32, a switch count, a pending-task count and a time in ticks
  • Thirteen errors in the whole kernel, numbered 1 to 13 — ErrorCode, kernel/src/errorcode.rs:13–44
  • One capsule implementing allow_userspace_readablecapsules/extra/src/read_only_state.rs:98, the only implementation in the tree besides the trait's own default, "fn allow_userspace_readable("
  • Twelve memop operations, and what each one is for — kernel/src/memop.rs:14–44, dispatched at :48–160
  • The LED driver's number, and its command numbers — capsules/core/src/led.rs:67 and :102–153, with the driver numbers themselves at capsules/core/src/driver.rs:15–19
  • The plain Pico 2 having one LED, on GPIO 25boards/raspberry_pi_pico_2/src/main.rs:38 and :93–95
  • Command 0 being the convention for asking whether a driver is there — kernel/src/syscall.rs:34–41, and the LED driver answering with its count at capsules/core/src/led.rs:108
  • svc assembling to 0xDF00 plus its argument, in one halfword — observed, not looked up: syscall-demo.s beside this page, through arm-none-eabi-as -mcpu=cortex-m33 -mthumb and arm-none-eabi-objdump -d

Text, diagrams and interactive figures © Jon Hillesheim 2026, licensed CC BY-SA 4.0 — share and adapt freely with credit, under the same license. Tock source excerpts quoted above remain under their own Apache-2.0 OR MIT license and are not relicensed here, as does syscall-demo.s beside this page, which is source rather than prose.

Every line reference below links to that commit on the fork it was read from, at the lines it names.