Pull to refresh

Изучаем микроядро L4 и пишем приложение «Hello world» для системы Xameleon

Reading time12 min
Views3.8K
Если вы когда-либо изучали язык Си или сталкивались с новой средой разработки, то наверняка хотя бы раз писали простейшее приложение, выводящее «Hello world». Итак, один из возможных вариантов на языке Си:

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

Сохраним этот код в файл «hello.c» и с помощью компилятора gcc cоберём исполняемый файл используя следующую команду:
gcc hello.c -o hello

В результате, если на вашей системе установлен компилятор, файлы заголовков и библиотеки, получим исполняемый файл hello. Выполним его:
./hello

Элементарно? До тех пор, пока вы не решите собрать и запустить это приложение, например, под управлением своей собственноручно написанной операционной системы. Далее я подробно расскажу об этом процессе и держу пари, что не каждый найдёт в себе силы дочитать статью до конца.

Сначала немного теории и простых вещей. Попробуем собрать не исполняемый, а объектный файл. Для этого нам понадоится следующая команда:
gcc -c hello.c
в результате получаем объектный файл hello.o. Что характерно для объектных файлов? Их код позиционно независим, объектные файлы содержат таблицы импортируемых и экспортируемых функций и переменных и, что более интересно для нас, код мало зависит от программной платформы, но завязан на архитектуру процессора. Почему это важно, я расскажу далее, а сейчас пристальнее заглянем в содержимое объектного файла, используя следующую команду:
objdump -hxS hello.o

Заголовок объектного файла
hello.o: file format elf32-i386
hello.o
architecture: i386, flags 0x00000011:
HAS_RELOC, HAS_SYMS
start address 0x00000000

Используемые секции и их размер
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

Секция .text содержит ассемблерный код функции main программы hello. Как видно из примера, размер кода программы — 46 байт (0x24) Секция дата пустая, потому что наш пример не использует статические и глобальные переменные, которые бы хранились в этой секции. Наконец, секция .rodata размером 13 байт (0xd) содержит строку «Hello world!».

Таблица экспортируемых и импортируемых объектов
SYMBOL TABLE:
00000000 l df *ABS* 00000000 hello.c
00000000 l d .text 00000000 .text
00000000 l d .data 00000000 .data
00000000 l d .bss 00000000 .bss
00000000 l d .rodata 00000000 .rodata
00000000 l d .note.GNU-stack 00000000 .note.GNU-stack
00000000 l d .comment 00000000 .comment
00000000 g F .text 0000002e main
00000000 *UND* 00000000 puts

В этой таблице нам интересны две последних строки — описание функции main, которая определена в нашей простейшей программе и описание внешней функции puts, которая определена где-то ещё.
В принципе, если вы скомпилируете этот пример под Linux, а полученный исходный файл слинкуете под FreeBSD, то скорее всего он без проблем заработает. Верно и наоборот. А теперь заглянем в ассемблерный код нашей программы 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.
Собственно говоря, это и есть ассемблерный код нашего приложения, созданный компилятором. Кстати, добро пожаловать в AT&T syntax style :)

Это было просто, а теперь перейдём к более сложным вещам. Самый первый пример из статьи создаст исполняемый файл для вашей системы. В моём случае — Slackware Linux. Мы знаем, что содержимое объектного файла зависит от архитектуры процессора, но слабо зависит от операционной системы. Как же из полученного объектного файла создать исполняемый файл для системы Xameleon? Для этого необходимо связать (сликновать) объектный файл с библиотечными функциями Хамелеона.

В нашем примере использована функция puts, определённая в заголовочном файле stdio.h Что же такое функция puts? Это функция, которая выводит строку и перевод строки в стандартный поток ввода/вывода.
Например, функцию puts можно записать таким образом:

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;
}

Можно «бесконечно» углубляться в дебри libc, но цель статьи — показать, как это работает в системе Хамелеон. Кстати, вышеописанный пример функции puts не претендует на оптимальность, он лишь демонстрирует пример простейшей функции. Вам остаётся только верить, что Хамелеоновская libc написана более оптимально. Впрочем, мы отвлеклись от темы повествования, поэтому вернёмся к делу и пристально посмотрим на функцию write. Эта функция определена в стандарте POSIX и именно она реализует взаимодействие нашего примера с операционной системой. Освежим свою память командой: man 2 write

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

Наш пример пишет в файловый дескриптор номер 1, который по стандарту есть не что иное, как дескриптор стандартного потока вывода. Параметр buf — указатель на область памяти, содержащую данные для вывода (да, там будет адрес строки «Hello world»). Третий параметр — размер выводимых данных.

Что отличает прикладного и системного программистов?
Прикладной скажет: «Функция write выводит массив данных в открытый файл». Системный ответит: «Функция write передаёт системе файловый дескриптор, указатель на данные и количество байт для записи». Оба будут правы, но я хочу напомнить как называется этот блог. :)

Итак, мы понимаем, что библиотечная функция write обращается к ядру операционной системы, поэтому нам надо понять, как это происходит. Поскольку система Хамелеон реализована поверх микроядра L4 Pistachio, то системный вызов — это IPC с двумя фазами:
  1. Фаза передачи — передаёт сервису файловой системы, который обслуживает функцию write, файловый дескриптор и L4 строку — тип данных, описывающий регион памяти.
  2. Фаза приёма -принимает от сервиса файловой системы статус выполнения операции.


В документации Хамелеона системный вызов write описан следующим образом.

Таким образом системный POSIX вызов write транслируется в IPC, показанный на рисунке выше.

Исходный код библиотечной функции write, которая, с одной стороны обеспечивает POSIX write(), с другой — обеспечивает взаимодействие с ядром операционной системы:

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.


Выглядит как что-то новое от жадных разработчиков Хамелеона. Давайте пройдёмся по коду внимательнее и посмотрим, насколько он соответствует вызову WriteFile из документации. Первое, что бросается в глаза, это граничение размера буфера до 4-х килобайт. Это ограничение связано со спецификой проектирования модуля файловой системы — более длинные данные имеет смысл передавать другим системным вызовом, обеспечивающим временное отображение страниц запрашивающего процесса в адресное пространство сервиса файловой системы. Эта возможность уходит далеко за пределы простого Hello world, поэтому рассматривать её не будем.
Следующие типы данных — структуры микроядра L4 Pistachio, используемые для обмена сообщениями
  • L4_MsgTag_t — тэг сообщения.
  • L4_Msg_t — структура, несущая данные сообщения.
  • L4_StringItem_t — строка L4

Эти структуры определены в заголовочном файле message.h, микроядра L4 Pistachio.

Следующий код готовит сообщение для передачи сервису файловой системы:
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 ); // Готовит сообщение к передаче


Далее происходит системный вызов микроядра, который, собственно, и обеспечивает Inter Process Communication

tag = L4_Call( fs_service_id );

Где fs_service_id это переменная типа L4_ThreadId_t, содержащая идентификатор сервиса файловой системы. Как его получить, я расскажу ниже, когда перейдём к магии CRT (C RunTime code). А сейчас рассмотрим код, анализирующий ответ от сервиса файловой системы:

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;
}


Если IPC оборвано, то проверяем, были ли переданы данные на предыдущей итерации и генерируем соответствующий код возврата. В случае корректного завершения IPC, обрабатываем код возврата.
Описание функций используемых функций с префиксом L4_ можно увидеть в заголовочных файлах микроядра L4 Pistachio.

Сложно? Думаю, да. Но мы уже близко к магии и байтсексу. Наступило время посмотреть внимательно на переменную fs_service_id. Система Хамелеон спроектирована таким образом, что изначально приложение не знает идентификатор сервиса файловой системы, поэтому его необходимо каким-то образом получить.

За распределением всех ресурсов, включая процессы, программные потоки (нити исполнения) и память, отвечает процесс Supervisor. Один его из его системных вызовов позволяет получить идентификатор сервиса по его имени. Код начальной инициализации функций libc, обеспечивающих взаимодействие с файловой системой, выглядит следующим образом:

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.


Пройдёмся ещё глубже по коду и посмотрим реализацию функции GetDeviceHandle, возвращающую идентификатор запрашиваемого сервиса.

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.


Можно провести аналогию с предыдущим примером, но отличие все же есть — это переменная rootserver_id, содержащая идентификатор Супервизора. Поскольку функции xam_filesystem_init и GetDeviceHandle вызываются из CRT, то идентификатор Supervisor'а необходимо получить до инициализации библиотеки.

Каким образом прикладная задача может получить идентификатор Супервизора? Мы уже очень близко подошли к байтсексу, поэтому рассмотрим структуру данных, называемую Kernel Interface Page (KIP). Эта структура описана в спецификации микродяра L4 Pistachio и выглядит следующим образом:


Поскольку супервизор — это первый пользовательский процесс с точки зрения микроядра, то его идентификатор можно получить на основании поля ThreadInfo из KIP. Особенность KIP в том, что микроядро держит эту страницу в единственном экземпляре, но отображает её в адресное пространство каждого процесса. Чтобы получить адрес KIP, процессу необходимо выполнить следующую последовательность команд:

    lock;    nop
    mov    %eax, kip


Последовательность ассемблерных команд lock и nop вызовет исключение, которое перехватит микроядро и перед возвратом из исключения подставит в регистр EAX адрес Kernel Interface Page.

Наконец, последний штрих — нахождение идентификатора обслуживающего потока Супервизора, на основе данных, полученних из 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


Таким образом модуль CRT0 в момент старта программы инициализирует библиотечные функции, совершающие обмен с различными сервисами системы Хамелеон.

Уважаемые читатели, мне очень удивительно, что вас не сморил сон, что вы не закрыли окно браузера и нашли в себе силы дочитать до этого места. Вероятно, вам будет интересно «пощупать» свежую версию Инстрментария разработчика системы Xameleon.

Спасибо за внимание.
Tags:
Hubs:
+36
Comments19

Articles