📜 ⬆️ ⬇️

Learning the L4 microkernel and writing the “Hello world” application for the Xameleon system

If you have ever studied C or come across a new development environment, you probably have written the simplest application at least once to print “Hello world”. So, one of the possible options in the C language:

#include <stdio.h>
int main(int argc, char * argv[], char * envp[])
{
puts("Hello world!");
return 0;
}

Let's save this code to the “hello.c” file and using the gcc compiler, create the executable file using the following command:
gcc hello.c -o hello

As a result, if the compiler, header files and libraries are installed on your system, we get the executable file hello. Perform it:
./hello

Elementary? Until you decide to build and run this application, for example, running your own written operating system. Next, I will talk in detail about this process and I bet that not everyone will find the strength to read the article to the end.

First, a little theory and simple things. Let's try to collect not an executable, but an object file. For this we need the following command:
gcc -c hello.c
the result is an object file hello.o. What is characteristic of object files? Their code is positionally independent, the object files contain tables of imported and exported functions and variables, and, more interestingly for us, the code does not depend much on the software platform, but is tied to the processor architecture. Why this is important, I will tell further, but for now let's take a closer look at the contents of the object file using the following command:
objdump -hxS hello.o

Object File Header
hello.o: file format elf32-i386
hello.o
architecture: i386, flags 0x00000011:
HAS_RELOC, HAS_SYMS
start address 0x00000000

Used sections and their size
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 0000002e 00000000 00000000 00000034 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000000 00000000 00000000 00000064 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 00000000 00000000 00000064 2**2
ALLOC
3 .rodata 0000000d 00000000 00000000 00000064 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .comment 00000012 00000000 00000000 00000071 2**0
CONTENTS, READONLY
5 .note.GNU-stack 00000000 00000000 00000000 00000083 2**0
CONTENTS, READONLY

The .text section contains the assembly code for the hello program's main function. As you can see from the example, the size of the program code is 46 bytes (0x24). The date section is empty, because our example does not use static and global variables that would be stored in this section. Finally, the 13-byte (0xd) .rodata section contains the string “Hello world!”.

The table of exported and imported objects
SYMBOL TABLE:
00000000 l df *ABS* 00000000 hello.c
00000000 ld .text 00000000 .text
00000000 ld .data 00000000 .data
00000000 ld .bss 00000000 .bss
00000000 ld .rodata 00000000 .rodata
00000000 ld .note.GNU-stack 00000000 .note.GNU-stack
00000000 ld .comment 00000000 .comment
00000000 g F .text 0000002e main
00000000 *UND* 00000000 puts

In this table, we are interested in the last two lines - the description of the main function, which is defined in our simplest program and the description of the external function of puts, which is defined elsewhere.
In principle, if you compile this example under Linux and link the resulting source file under FreeBSD, then most likely it will work without problems. True and vice versa. And now let's take a look at the assembly code of our program hello.c
')
Disassembly of section .text:
00000000 <main>:
0: 8d 4c 24 04 lea 0x4(%esp),%ecx
4: 83 e4 f0 and $0xfffffff0,%esp
7: ff 71 fc pushl -0x4(%ecx)
a: 55 push %ebp
b: 89 e5 mov %esp,%ebp
d: 51 push %ecx
e: 83 ec 04 sub $0x4,%esp
11: 83 ec 0c sub $0xc,%esp
14: 68 00 00 00 00 push $0x0
15: R_386_32 .rodata
19: e8 fc ff ff ff call 1a <main+0x1a>
1a: R_386_PC32 puts
1e: 83 c4 10 add $0x10,%esp
21: b8 00 00 00 00 mov $0x0,%eax
26: 8b 4d fc mov -0x4(%ebp),%ecx
29: c9 leave
2a: 8d 61 fc lea -0x4(%ecx),%esp
2d: c3 ret


* This source code was highlighted with Source Code Highlighter .
As a matter of fact, this is the assembler code of our application created by the compiler. By the way, welcome to AT & T syntax style :)

It was easy, and now let's move on to more complicated things. The very first example from the article will create an executable file for your system. In my case, Slackware Linux. We know that the contents of the object file depends on the processor architecture, but it depends little on the operating system. How to create an executable file for the Xameleon system from the resulting object file? To do this, you must associate (clicked) the object file with the library functions of the Chameleon.

Our example uses the puts function defined in the stdio.h header file. What is the puts function? This is a function that outputs a string and a line break to standard input / output.
For example, the puts function can be written like this:

int puts(char * str)
{
int status, len;

len = strlen(str); //
status = write( 1, str, len ); //
if( status == len ) status = write( 1, "\n", 1 ); // ,
if( status ==1 ) status += len;
return status;
}

It is possible to “infinitely” delve into the jungle of libc, but the purpose of the article is to show how this works in the Chameleon system. By the way, the above example of the puts function does not pretend to be optimal, it only demonstrates an example of the simplest function. You can only believe that the Chameleon libc is written more optimally. However, we are distracted from the topic of narration, so let's get back to business and look closely at the write function. This function is defined in the POSIX standard and it implements the interaction of our example with the operating system. Refresh your memory with the command: man 2 write

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);

Our example writes to file descriptor number 1, which, by standard, is nothing more than a standard output stream descriptor. The buf parameter is a pointer to the memory area containing the data to be output (yes, there will be the address of the string “Hello world”). The third parameter is the size of the output data.

What distinguishes application and system programmers?
The application will say: "The write function outputs an array of data to an open file." The system will respond: "The write function passes the file descriptor, a pointer to the data, and the number of bytes to write to the system." Both will be right, but I want to remind you of the name of this blog. :)

So, we understand that the library function write refers to the kernel of the operating system, so we need to understand how this happens. Since the Chameleon system is implemented on top of the L4 Pistachio microkernel, the system call is an IPC with two phases:
  1. Transmission Phase — transmits the file system service that serves the write function, a file descriptor, and an L4 string — a data type that describes a region of memory.
  2. Receive Phase — receives the status of the operation from the file system service.


In the Chameleon documentation, the write system call is described as follows.

Thus, the write system call POSIX is translated into IPC, shown in the figure above.

The source code of the library function write, which, on the one hand, provides POSIX write (), on the other hand, provides interaction with the operating system kernel:

ssize_t write(
int nFileDescriptor,
const void * pBuffer,
size_t nBytesWrite )
{
int nStatus;
int nChunk;
int nTotalSent;
char * pPointer;

L4_MsgTag_t tag;
L4_Msg_t msg;
L4_StringItem_t SendString;

nStatus = nTotalSent = 0;
pPointer = ( char *) pBuffer;

while ( nBytesWrite )
{
nChunk = (nBytesWrite > 4096) ? 4096 : nBytesWrite;
SendString = L4_StringItem ( nChunk, ( void *) pPointer );

L4_Clear( &msg );
L4_Set_Label( &msg, fsWriteFile );
L4_Append( &msg, nFileDescriptor );
L4_Append( &msg, &SendString );
L4_Load( &msg );
tag = L4_Call( fs_service_id );
if ( L4_IpcFailed(tag) )
{
nStatus = nTotalSent ? nTotalSent : XAM_EINTR;
break ;
}
else
{
L4_Store( tag, &msg );
nStatus = L4_Get( &msg, 0 );
if ( nStatus < 0 ) break;
nTotalSent += nStatus;
}

pPointer += nChunk;
nBytesWrite -= nChunk;
}

// POSIX workaround
if ( nStatus < 0 )
{
errno = nStatus;
nStatus = -1;
}
else
{
nStatus = nTotalSent;
}

return nStatus;
}


* This source code was highlighted with Source Code Highlighter .


It looks like something new from the greedy Chameleon developers. Let's take a closer look at the code and see how it corresponds to the WriteFile call from the documentation. The first thing that catches your eye is the boundary of the buffer size up to 4 kilobytes. This limitation is related to the specifics of the design of the file system module — it makes sense to transfer longer data with another system call, which provides a temporary mapping of the pages of the requesting process to the address space of the file system service. This feature goes far beyond the simple Hello world, so we will not consider it.
The following data types are the L4 Pistachio microkernel structures used for messaging.

These structures are defined in the header file message.h, the L4 Pistachio microkernel.

The following code prepares a message to send to the file system service:
SendString = L4_StringItem ( nChunk, (void*) pPointer ); //
L4_Clear( &msg ); //
L4_Set_Label( &msg, fsWriteFile ); //
L4_Append( &msg, nFileDescriptor ); //
L4_Append( &msg, &SendString ); //
L4_Load( &msg ); //


Next comes the system call of the microkernel, which, in fact, provides Inter Process Communication

tag = L4_Call( fs_service_id );

Where fs_service_id is a variable of type L4_ThreadId_t containing the file system service identifier. How to get it, I will tell below, when we turn to the magic of CRT (C RunTime code). Now consider the code that analyzes the response from the file system service:

if ( L4_IpcFailed(tag) )
{
nStatus = nTotalSent ? nTotalSent : XAM_EINTR;
break;
}
else
{
L4_Store( tag, &msg );
nStatus = L4_Get( &msg, 0 );
if( nStatus < 0 ) break;
nTotalSent += nStatus;
}


If the IPC is broken, then we check if the data was transmitted at the previous iteration and generate the corresponding return code. In the case of correct completion of IPC, we process the return code.
The description of the functions of the functions used with the L4_ prefix can be seen in the header files of the L4 Pistachio microkernel.

Complicated? I think yes. But we are close to magic and baysex. It is time to look carefully at the fs_service_id variable. The Chameleon system is designed in such a way that the application initially does not know the file system service identifier, so you need to get it in some way.

The allocation of all resources, including processes, program threads (execution threads) and memory, is supervisor process. One of his system calls allows you to get the service ID by its name. The initial initialization code for the libc functions that provide interaction with the file system is as follows:

static const char szServiceName[3] = "fs" ; //

L4_ThreadId_t fs_service_id = L4_nilthread;

extern "C" int xam_filesystem_init( void )
{
fs_service_id = GetDeviceHandle(szServiceName);
return L4_IsNilThread(fs_service_id) ? XAM_ENODEV : 0;
}


* This source code was highlighted with Source Code Highlighter .


Let's go even deeper through the code and see the implementation of the function GetDeviceHandle, which returns the ID of the requested service.

extern L4_ThreadId_t rootserver_id; // Supervisor'

L4_ThreadId_t GetDeviceHandle( const char * szDeviceName)
{
L4_MsgTag_t tag;
L4_Msg_t msg;
L4_ThreadId_t Handle;

Handle = L4_nilthread;

do {

L4_Clear(&msg);
L4_Set_Label(&msg, cmdGetDeviceHandle );
L4_Append(&msg, L4_StringItem( 1+strlen(szDeviceName), ( void *) szDeviceName) );
L4_Load(&msg);
tag = L4_Call( rootserver_id );
if ( L4_IpcFailed(tag) ) break ;
L4_Store( tag, &msg );
Handle.raw = L4_Get(&msg, 1);

} while ( false );

return Handle;
}

* This source code was highlighted with Source Code Highlighter .


You can draw an analogy with the previous example, but the difference is still there - this is the rootserver_id variable containing the Supervisor ID. Since the xam_filesystem_init and GetDeviceHandle functions are called from CRT, the Supervisor ID must be obtained before the library is initialized.

How can an application task get a Supervisor ID? We are already very close to bytesex, so let's consider a data structure called the Kernel Interface Page (KIP). This structure is described in the specification for the L4 Pistachio microcar and looks like this:


Since the supervisor is the first user process from the point of view of the microkernel, its identifier can be obtained based on the ThreadInfo field from the KIP. A special feature of KIP is that the microkernel keeps this page in a single copy, but displays it in the address space of each process. To get the KIP address, the process must run the following sequence of commands:

lock; nop
mov %eax, kip


The sequence of the assembler commands lock and nop will cause an exception that will intercept the microkernel and, before returning from the exception, will insert the address of the Kernel Interface Page in the EAX register.

Finally, the final touch is finding the identifier of the Supervisor's serving thread, based on the data obtained from the Kernel Interface Page.

mov kip, %eax
movw 198(%eax), %ax
shrw $4, %ax
movzwl %ax, %eax
addl $2, %eax
sall $14, %eax
orl $1, %eax
movl %eax, rootserver_id


Thus, the CRT0 module at the time of program start initializes the library functions exchanging with various services of the Chameleon system.

Dear readers, I am very surprised that you didn’t sleep, that you didn’t close the browser window and found the strength to read this far. You will probably be interested in “feeling” the latest version of the Xameleon Developer's Toolkit .

Thanks for attention.

Source: https://habr.com/ru/post/122216/


All Articles