mirror of
https://github.com/genodelabs/genode.git
synced 2024-12-23 15:32:25 +00:00
146 lines
6.4 KiB
Plaintext
146 lines
6.4 KiB
Plaintext
|
|
|
|
=======================================
|
|
Genode on seL4 - IPC and virtual memory
|
|
=======================================
|
|
|
|
|
|
Norman Feske
|
|
|
|
|
|
This is the second part of a series of hands-on articles about bringing Genode
|
|
to the seL4 kernel.
|
|
[http://genode.org/documentation/articles/sel4_part_1 - Read the previous part here...]
|
|
|
|
After having created a minimalistic root task consisting of two threads, we
|
|
can move forward with exercising the functionality provided by the kernel,
|
|
namely inter-process communication and the handling of virtual memory.
|
|
Once we have tested those functionalities in our minimalistic root task
|
|
environment, we will be able to apply the gained knowledge to the actual
|
|
porting effort of Genode's core process.
|
|
|
|
|
|
Inter-process communication
|
|
###########################
|
|
|
|
In the L4 universe, the term IPC (inter-process communication) usually stands
|
|
for synchronous communication between two threads. In seL4, IPC has two uses.
|
|
First, it enables threads of different protection domains (or the same
|
|
protection domain) to exchange messages. So information can be transferred
|
|
across protection-domain boundaries. Second, IPC is the mechanism used to
|
|
delegate access rights throughout the system. This is accomplished by sending
|
|
capabilities as message payload. When a capability is part of a message, the
|
|
kernel translates the local name of the capability in the sender's protection
|
|
domain to a local name in the receiver's protection domain.
|
|
|
|
In Genode, IPC is realized as a two thin abstractions that build upon each
|
|
other:
|
|
|
|
# At the low level, the IPC library _src/base/ipc/ipc.cc_ is responsible
|
|
for sending and receiving messages using the kernel mechanism. It has a
|
|
generic interface _base/include/base/ipc.h_, which supports the marshalling
|
|
and un-marshalling of message arguments and capabilities using C++ streaming
|
|
operators. Genode users never directly interact with the IPC library.
|
|
|
|
# Built on top the IPC library, the so-called RPC framework adds the notion
|
|
of RPC functions and RPC objects. RPC interfaces are declared using
|
|
abstract C++ base classes with a few annotations. Under the hood, the
|
|
RPC framework uses C++ meta-programming techniques to turn RPC definitions
|
|
into code that transfers messages via the IPC library. In contrast to
|
|
the IPC library, the RPC library is platform-agnostic.
|
|
|
|
To enable Genode's RPC mechanism on seL4, we merely have to provide a
|
|
seL4-specific IPC library implementation. To warm up with seL4's IPC
|
|
mechanism, however, we first modify our test program to let the main thread
|
|
perform an IPC call to the second thread.
|
|
|
|
To let the second thread receive IPC messages, we first need to create a
|
|
synchronous IPC endpoint using the 'seL4_Untyped_RetypeAtOffset' function
|
|
with 'seL4_EndpointObject' as type, an offset that skips the already allocated
|
|
TCB (the TCB object has a known size of 1024 bytes) and the designated
|
|
capability number, let's call it EP_CAP. Of course, we have to create the
|
|
entrypoint before starting the second thread.
|
|
|
|
As a first test, we want the second thread to receive an incoming message.
|
|
So we change the entry function as follows:
|
|
|
|
! PDBG("call seL4_Wait");
|
|
! seL4_MessageInfo_t msg_info = seL4_Wait(EP_CAP, nullptr);
|
|
! PDBG("returned from seL4_Wait, call seL4_Reply");
|
|
! seL4_Reply(msg_info);
|
|
! PDBG("returned from seL4_Reply");
|
|
|
|
At the end of the main function, we try call the second thread via 'seL4_Call':
|
|
|
|
! PDBG("call seL4_Call");
|
|
! seL4_MessageInfo_t msg_info = seL4_MessageInfo_new(0, 0, 0, 0);
|
|
! seL4_Call(EP_CAP, msg_info);
|
|
! PDBG("returned from seL4_Call");
|
|
|
|
When executing the code, we get an error as follows:
|
|
|
|
! int main(): call seL4_Call
|
|
! void second_thread_entry(): call seL4_Wait
|
|
! Caught cap fault in send phase at address 0x0
|
|
! while trying to handle:
|
|
! vm fault on data at address 0x4 with status 0x6
|
|
! in thread 0xe0100080 at address 0x10002e1
|
|
|
|
By looking at the output of 'objdump -lSd', we see that fault happens at the
|
|
instruction
|
|
! mov %edi,%gs:0x4(,%ebx,4)
|
|
The issue is the same as the one we experienced for the main thread - we
|
|
haven't initialized the GS register with a proper segment, yet. This can be
|
|
easily fixed by adding a call to our 'init_ipc_buffer' function right at the
|
|
start of the second thread's entry function. Still, the program does not work
|
|
yet:
|
|
|
|
! vm fault on data at address 0x4 with status 0x6
|
|
! in thread 0xe0100080 at address 0x10002e8
|
|
|
|
Looking at the objdump output, we see that the fault still happens at the same
|
|
instruction. So what is missing? The answer is that we haven't equipped the
|
|
second thread with a proper IPC buffer. The attempt to call 'seL4_Wait',
|
|
however, tries to access the IPC buffer of the calling thread. The IPC buffer
|
|
can be configured for a thread using the 'seL4_TCB_SetIPCBuffer' function. But
|
|
wait - what arguments do we need to pass? In addition to the TCB capability,
|
|
there are two arguments a pointer to the IPC buffer and a page capability,
|
|
which contains the IPC buffer. Well, I had hoped to get away without dealing
|
|
with the memory management at this point. I figure that setting up the IPC
|
|
buffer for the second thread would require me to create a seL4_IA32_4K page
|
|
object via 'seL4_Untyped_RetypeAtOffset' and insert a mapping of the page
|
|
within the roottask's address space, and possibly also create and install a
|
|
page table object.
|
|
|
|
To avoid becoming side-tracked by those memory-management issues, I decide
|
|
to assign the IPC buffer of the second thread right at the same page as
|
|
the one for the initial thread. Both the local address and the page
|
|
capability for the initial thread's IPC buffer are conveniently provided by
|
|
seL4's boot info structure. So let's give this a try:
|
|
|
|
! /* assign IPC buffer to second thread */
|
|
! {
|
|
! static_assert(sizeof(seL4_IPCBuffer) % 512 == 0,
|
|
! "unexpected seL4_IPCBuffer size");
|
|
!
|
|
! int const ret = seL4_TCB_SetIPCBuffer(SECOND_THREAD_CAP,
|
|
! (seL4_Word)(bi->ipcBuffer + 1),
|
|
! seL4_CapInitThreadIPCBuffer);
|
|
!
|
|
! PDBG("seL4_TCB_SetIPCBuffer returned %d", ret);
|
|
! }
|
|
|
|
With the initialization of the IPC buffer in place, we finally get our
|
|
desired output:
|
|
|
|
! int main(): call seL4_Call
|
|
! void second_thread_entry(): call seL4_Wait
|
|
! void second_thread_entry(): returned from seL4_Wait, call seL4_Reply
|
|
! int main(): returned from seL4_Call
|
|
! void second_thread_entry(): returned from seL4_Reply
|
|
|
|
|
|
|
|
|
|
|