C言語を勉強したことがあるか、新しい開発環境に出会ったことがあれば、「Hello world」を表示する最も単純なアプリケーションを少なくとも1回は作成したことでしょう。 したがって、C言語で可能なオプションの1つ:
#include <stdio.h>
int main(int argc, char * argv[], char * envp[])
{
puts("Hello world!");
return 0;
}
このコードを「hello.c」ファイルに保存し、gccコンパイラを使用して、次のコマンドを使用して実行可能ファイルをビルドします。
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セクションには、hello main関数のアセンブラコードが含まれています。 例からわかるように、プログラムコードサイズは46バイト(0x24)です。この例では、このセクションに格納される静的変数とグローバル変数を使用しないため、日付セクションは空です。 最後に、13バイト(0xd)の.rodataセクションには、文字列「Hello world!」が含まれています。
エクスポートおよびインポートされたオブジェクトの表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
この表では、最後の2行に注目しています。最も単純なプログラムで定義されているmain関数の説明と、他の場所で定義されている外部puts関数の説明です。
原則として、この例をLinuxでコンパイルし、結果のソースファイルをFreeBSDでリンクすると、ほとんどの場合問題なく動作します。 真およびその逆。 次に、プログラムhello.cのアセンブラコードを見てみましょう。
セクション.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構文スタイルへようこそ:)
それは簡単でしたが、今度はより複雑なことに進みましょう。 この記事の最初の例では、システムの実行可能ファイルを作成します。 私の場合、Slackware Linux。 オブジェクトファイルの内容はプロセッサアーキテクチャに依存しますが、オペレーティングシステムにわずかに依存することがわかっています。 結果のオブジェクトファイルからXameleonシステムの実行可能ファイルを作成する方法は? これを行うには、オブジェクトファイルをChameleonのライブラリ関数にリンク(クリック)する必要があります。
この例では、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のジャングルを「際限なく」掘り下げることができますが、この記事の目的は、これがChameleonシステムでどのように機能するかを示すことです。 ところで、puts関数の上記の例は最適であると主張しているわけではなく、単純な関数の例を示しているだけです。 カメレオンlibcがより最適に書かれているとしか信じられません。 ただし、ストーリーのトピックからは逸脱しているため、ビジネスに戻り、書き込み機能を詳しく見てみましょう。 この関数はPOSIX標準で定義されており、この例とオペレーティングシステムの相互作用を実装しているのはこの関数です。 次のコマンドでメモリを更新します:man 2 write
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
この例では、ファイル記述子番号1に書き込みます。これは、標準では標準出力ストリームの記述子にすぎません。 bufパラメータは、出力用のデータを含むメモリ領域へのポインタです(はい、文字列「Hello world」のアドレスがあります)。 3番目のパラメーターは、出力データのサイズです。
アプリケーションプログラマとシステムプログラマの違いは何ですか?アプリケーションは、「書き込み関数はデータの配列を開いているファイルに出力します」と言います。 システムは応答します:「書き込み関数は、ファイル記述子をシステムに渡し、データへのポインターと書き込むバイト数を渡します。」 どちらも正しいでしょうが、このブログが何と呼ばれているのか思い出させてください。 :)
そのため、ライブラリ関数writeはオペレーティングシステムのカーネルを指すことを理解しているため、これがどのように発生するかを理解する必要があります。 カメレオンシステムはL4ピスタチオマイクロカーネルの上に実装されているため、システムコールは2つのフェーズを持つIPCです。
- 転送フェーズ-書き込み機能、ファイル記述子、およびL4行を提供するファイルシステムサービスへの転送-メモリ領域を記述するデータ型。
- 受信フェーズ-ファイルシステムサービスから操作のステータスを受信します。
Chameleonのドキュメントでは、writeシステムコールは次のように説明されています。
このようにして、上図に示すように、POSIXシステム書き込み呼び出しは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ピスタチオマイクロカーネル構造です。
- L4_MsgTag_t-メッセージタグ。
- L4_Msg_t-メッセージデータを運ぶ構造。
- L4_StringItem_t-ラインL4
これらの構造は、message.hヘッダーファイルであるL4ピスタチオマイクロカーネルで定義されています。
次のコードは、ファイルシステムサービスに送信するメッセージを準備します。
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 );
fs_service_idは、ファイルシステムサービスの識別子を含むL4_ThreadId_t型の変数です。 取得方法は、CRT(C RunTimeコード)の魔法に移るときに、以下で説明します。 次に、ファイルシステムサービスからの応答を分析するコードを見てみましょう。
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変数を注意深く見る時が来ました。 Chameleonシステムは、最初はアプリケーションがファイルシステムサービスの識別子を知らないように設計されているため、何らかの方法で取得する必要があります。
プロセス、プログラムスレッド(実行スレッド)、およびメモリを含むすべてのリソースの分配は、スーパーバイザープロセスの責任です。 彼のシステムコールの1つで、彼の名前でサービス識別子を取得できます。 ファイルシステムと対話する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から呼び出されるため、ライブラリが初期化される前にスーパーバイザー識別子を取得する必要があります。
アプリケーションはどのようにしてスーパーバイザーIDを取得できますか? bytexに非常に近づいたので、Kernel Interface Page(KIP)と呼ばれるデータ構造を見てみましょう。 この構造は、L4ピスタチオマイクロドラフト仕様で説明されており、次のとおりです。
スーパーバイザはマイクロカーネルの観点から最初のユーザープロセスであるため、その識別子はKIPのThreadInfoフィールドに基づいて取得できます。 KIPの特徴は、マイクロカーネルがこのページを単一のコピーで保持しますが、各プロセスのアドレススペースにマップすることです。 KIPアドレスを取得するには、プロセスは次の一連のコマンドを実行する必要があります。
lock; nop
mov %eax, kip
アセンブラーロックとnopコマンドのシーケンスは、例外から戻る前に、マイクロカーネルをキャッチし、EAXレジスターのカーネルインターフェイスページアドレスを置き換える例外を発生させます。
最後に、カーネルインターフェイスページから受信したデータに基づいて、スーパーバイザーサービングストリームの識別子を見つけます。
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 System Developer Toolkitの最新バージョンに「触れる」
ことに興味があるでしょう。
ご清聴ありがとうございました。