Linux用の真のマルチスレッドアセンブラーWebサーバー

こんにちは、ハブラ!
今日は、実際のWebサーバーをacmeで作成する方法を説明します。

libcのような追加のライブラリを使用しないことをすぐに言わなければなりません。 そして、カーネルが提供するものを使用します。

すでに怠zyな人だけがそのような記事を書いていません-私の意見では、perl上のサーバー、node.js、phpでさえ試みがありました。

アセンブラーがまだないというだけなので、空白を埋める必要があります。

ちょっとした歴史


小さなファイル(1Kb未満)を保存する必要があると、非常に多くのファイルがあり、ext3が怖かったので、これらすべてのファイルを1つの大きなファイルに保存し、getパラメーターでオフセットと長さを設定してWebサーバー経由で保存することにしました16進形式のファイル自体。

時間はまともだったので、私は少し変質してアクメに書くことにしました。

それでは始めましょう


FASMに書き込みます。 私はそれが好きで、Intelの構文に慣れています。

したがって、エルフを作成するための標準的な手順:

format elf executable 3 entry _start segment readable writeable executable 


以下は、いくつかの見出しデータです。

 HTTP200 db "HTTP/1.1 200 OK", 0xD,0xA ; CTYPE db "Content-Type: application/octet-stream", 0xD,0xA ; CNAME db 'Content-Disposition: attachment; filename="BIGTABLE"',0xD,0xA,0xD,0xA ; SERVER db 'Server: Kylie',0xD,0xA ; KeepClose db 'Connection: close',0xD,0xA,0xD,0xA ;    sendfile off_set dd 0x00 n_bytes dd 0x00 


また、すべての画像が保存されている最大のファイルへのパス:

 FILE1 db "/home/andrew/FILE.FBF",0 


便宜上、いくつかの定数を定義します。

 IPPROTO_TCP equ 0x06 SOCK_STREAM equ 0x01 PF_INET equ 0x02 AF_INET equ 0x02 


自作の翻訳関数をstrから16進数に接続しましょう

 include 'str2hex.asm' 


この関数の動作原理は簡単です:

google.com.uaの「ASCIテーブル」をハンマーで打ち込みます-印刷して表示します...
0〜9のASCII値は30h〜39hの値に対応することに注意してください。

41hから46hの範囲のAからFの値

マクロの入力パラメーターは、esiのバッファーのアドレスです(このアドレスは、strから16進数に変換する必要がある文字列です)
マクロは単に文字のASCIIコードをチェックし、それが39hより大きい場合は、A-Fを使用し、それ以下である場合、0-9を使用します

完全なコードは次のとおりです。
 ; esi,-    id  : ; eax -   Macro STR2HEX4 { local str2hex,bin2hex, out_buff, func, result, nohex ; //     (  9 (.. A..F)  ) cld ;//   (  ) mov edi,out_buff ; jmp func ;//    str2hex: cmp al,39h jle nohex sub al,07h nohex: sub al,30h ret out_buff dd 0x00 func: ; //   4  (32 ) mov ecx,4 bin2hex: lodsb ;//    call str2hex ;//   ASCII    shl al,4 ; //   4 (   4 ) mov bl,al ; //    bl lodsb ; //   call str2hex ; //  (   4 ) xor al,bl ; //      ; //  ,   AL        stosb ; //    edi    sub ecx,1 ; //    1 jecxz result ;   ecx != 0 jmp bin2hex ; result: ;//        eax xor eax,eax cld mov esi,out_buff lodsb shl eax,8 lodsb shl eax,8 lodsb shl eax,8 lodsb ;   -   eax } 


PS関数にはエラーハンドラがないため、サイズオフセットを正しく設定してください(パラメータでは大文字と小文字が区別されることに注意してください。つまり、A!= A、B =!Bなど)。

また、最大サイズと最大オフセット= 32ビット。

理解して、運転しました:
最後に、ソケットを作成します

 ; //     push IPPROTO_TCP ; IPPROTO_TCP (=6) push SOCK_STREAM ; SOCK_STREAM (=1) push PF_INET ; PF_INET (=2) ;socketcall mov eax, 102 ; //  102 (  ) mov ebx, 1 ; // 1      mov ecx, esp ; //       int 0x80 mov edi,eax ; //    edi, ..     cmp eax, -1 je near errn ; //    


ソケットが作成され、アドレス0.0.0.0(一般の人々-INADDR_ANY)およびポート8080にバインドされます(lighttpdは80mで機能するため、80番目に変更すると0がeaxに戻り、-EADDRINUSEエラーが発生します)ポートはすでに占有されています)

 ; binding push 16 ; socklen_t addrlen push ecx ; const struct sockaddr *my_addr push edi ; int sockfd mov eax, 102 ; socketcall() syscall mov ebx, 2 ; bind() = int call 2 mov ecx, esp ; //  int 0x80 cmp eax, 0 jne near errn ;//    (   ...) 


ところで、INADDR_ANYの使用について。 localhostまたはその他のアドレスを使用する場合は、「その逆」と記述する必要があります。 つまり
localhost = 127.0.0.1 = 0x0100007F
habrahabr.ru = 212.24.43.44 = 2C2B18D4

同じことがポート番号にも当てはまります。

8080 = 901Fh
25 = 1900h

もちろん、次のようなipを指定する必要はありません。

localhost db 127,0,0,1
habrahabr.ru db 212,24,43,44

など

最後に、ソケット自体のリッスンを開始して、新しい接続を受け入れます。

  push 1 ;// int backlog push edi ;// int sockfd pop esi push edi mov eax, 102 ; // syscall mov ebx, 4 ;//      (listen) mov ecx, esp ; //     int 0x80 


今重要なポイント。 なぜなら プロセスを処理し、親プロセスはforkの後に子からの戻りコードを期待し、子プロセスが終了すると、親はそれがまだ存在していると「考えます」。 したがって、ゾンビは子プロセスから出現します。 これらの信号を無視することを親に伝えると、誰も誰も待たず、ゾンビも現れません。

  mov eax,48 mov ebx,17 mov ecx,1 ; SIG_IGN int 0x80 


受け入れのための構造を作成し、接続の受け入れを開始します。
 push 0x00 push 0x00 ; struct sockaddr *addr push edi ; int sockfd sock_accept: mov eax, 102 ; socketcall() syscall mov ebx, 5 ; accept() = int call 5 mov ecx, esp int 0x80 ; //   : cmp eax, -1 je near errn mov edi, eax ;   edi   mov [c_accept],eax 


エラーがなく、コードのこの部分で終わった場合、新しいクライアントが接続されます

処理するプロセスを作成します。

 mov eax,2 ; //   sys_fork() int 0x80 cmp eax,0 jl exit ; if error 


ここで、フォークまたは親プロセスがどこにいるかを確認します。

 test eax,eax jnz fork ;       ( ) ; edi - accept descriptor ; //           mov eax, 6 ; close() syscall mov ebx, edi ; The socket descriptor int 0x80 ; Call the kernel jmp sock_accept fork: ;//  -    


それだけです! サーバーの「ヘッド」の準備ができました。

次は、子プロセス専用のコードです。

クライアントに200 OKのステータスを送信します
  mov eax, 4 ; write() syscall mov ebx, edi ; sockfd mov ecx, HTTP200 ; Send 200 Ok mov edx, 17 ; 17 characters in length int 0x80 ; 


また、コンテンツのタイプ。 「アプリケーション/オクテットストリーム」-この場合に最も普遍的

  mov eax, 4 ; write() syscall mov ebx, edi ; sockfd mov ecx, CTYPE ; Content-type - 'application/octet-stream' mov edx, 40 ; 40 characters in length int 0x80 ; Call the kernel 


サーバー名:
  mov eax, 4 ; write() syscall mov ebx, edi ; sockfd mov ecx, SERVER ; our string to send mov edx, 15 ; 15 characters in length int 0x80 ; Call the kernel 


サーバーはキープアライブをまだサポートしていないため、これを認めます。
  mov eax, 4 ; write() syscall mov ebx, edi ; sockfd mov ecx, KeepClose ; Connection: Close mov edx, 21 ; 21 characters in length int 0x80 ; Call the kernel 


最後に0xD 0xAを2回送信する必要があることに注意してください(Connection:Closeの送信と一緒にこれを行いました)。ヘッダーが終わったと想定できます。

さて、実際にクライアントがダウンロードしたいファイルを見つけます。 これを行うには、左に5バイトのシフトでGETリクエストをバッファーに入れ、不要な情報( 'GET /')をトリミングし、サイズが16バイトのクリーンなIDのみを残します。

ああ、私はすべてid、idについてです...そして彼はどんな人ですか? GETでファイル内のオフセットの32ビット値を指定し、その直後にファイルのサイズに等しい32ビット値を指定するだけで、すべてを実行することにしました。

つまり URLリクエストが次のような場合:

127.0.0.1/00003F480000FFFF

そのファイルオフセットは00003F48であり、要求されたデータのサイズは0000FFFFです

 mov esi,buffer ; //      id ( STR2HEX) push edi ;  edi ..    STR2HEX4 ;      esi pop edi ;  edi mov [off_set],eax ; //     eax,     


ここで、ファイルの先頭が指定されたオフセットになる大きなファイルを開く必要があります。

開いてください(ハンドルはeaxで保存されます):

 ; Open BIG file mov eax,5 mov ebx,FILE1 mov ecx, 2 int 0x80 


さて、完全に満足するために、sendfile関数を使用します。
彼らがマニュアルで言うように:

このコピーはカーネル内で行われるため、sendfile()は読み取り(2)と書き込み(2)の組み合わせよりも効率的であり、ユーザースペースとの間でデータを転送する必要があります。


 ; Send [n_bytes] from BIGTABLE starting at [off_set] send_file: mov ecx,eax ; file descriptor from previous function mov eax,187 mov ebx,edi ; socket mov edx,off_set ; pointer mov esi,[n_bytes] ; int 0x80 


ご覧のとおり、eaxからの記述子は、sendfile関数の中間レジスター/メモリーに保管せずにecxにコピーされました。

成功

ここで、一度に、私は長い間眠りませんでした。なぜなら、すべてのバイトを送信した後、ファイルが完全にダウンロードされず、完全ダウンロードの1秒前にブラウザーが「ネットワークエラー」と書き込み、保存しない理由を理解できなかったためです。 sendfileにエラーはありませんでした。Chrome開発者ツールの使用方法を学ぶ必要がありました。

ファイル自体を送信した後、ブラウザはサーバーが受け入れる必要のあるヘッダーを送信することがわかりました。 データが何であっても、/ dev / nullに送信できますが、サーバーがそれを読み取ることは非常に重要です。 そうでない場合、ブラウザはファイルに何か問題があると見なします。 なぜこれが正確に行われるのかは、私には100%不明です。 これは、ヘッダーにContent-Lengthが含まれていない可能性があること、ファイルを受け入れる必要がある場合、およびブラウザーが明らかに知らないデータ量が原因であると思われます。 誰かが秘密を明かしてくれたらありがたいです)))

したがって、ブラウザのヘッダーを受け入れます。
ediのアドレスからアドレスバッファーに読み取ります。

 ; Read the header mov eax,3 mov ebx,edi mov ecx,buffer mov edx,1024 int 0x80 


ヘッダーが大きすぎない場合は、1024バイトで十分です
(このドメインで長いCookieを使用しない場合など)

ファイルを閉じて終了する:
  mov eax, 6 ; close() syscall mov ebx, edi ; The socket descriptor int 0x80 ; Call the kernel ; end to pcntl_fork () mov eax,1 xor ebx,ebx int 0x80 


一般的に、ファイルは親でしばらく開いたままにしておくことができ、時間を節約するために他のフォークで使用できます。 しかし、これは正しい選択肢ではありません。

そして最も重要なこと!
外部ライブラリはありません!

ルート@サーバー:/ home / andrew#ldd server
動的実行可能ファイルではない


ダウンロードリンク(動作するかどうかを確認できます\いいえ、たとえば、ベンチマークabでテストします)))
http://ubuntuone.com/3yNexPG0yewlGnjNd6219W

PSコードは多くのエラーチェックを逃しました。また、一部のコードではスタックがクリアされず、一部の変数の存在は手動で選択されます(通常のドキュメントがないため)。一般に、コードは最も「クリーンな」ふりをしません。

サーバーは、マルチコアシステム(Core I7 2600でテスト済み)で正常に動作します。 私のサーバーのlighttpdはほぼ4倍静的に追い越されますが、lighttpdは単にマルチコア用に構成されていないと思います。

すぐに追加できるもの:
さて、たとえば、任意の言語(php、perl、python)などのcgiなど。 また、ファイルから読み取りを削除し、ファイルシステムで作業を書き込み、仮想ホストを追加することもできます。 一般的に、すべてはあなたの想像力によってのみ制限されます。

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


All Articles