...

воскресенье, 28 июля 2013 г.

[Из песочницы] Настоящий многопоточный веб-сервер на ассемблере под Linux

Добрый день, хабр!

Сегодня я вам расскажу как написать свой настоящий веб-сервер на асме.

Сразу скажу что мы не будем использовать дополнительные библиотеки типа libc. А будем пользоваться тем что предоставляет нам ядро.


Уже только ленивый не писал подобных статей, — сервер на perl, node.js, по-моему даже были попытки на php.


Вот только на ассемблере еще не было, — значит нужно заполнить пробелы.



Немного истории




Как-то раз мне нужно было хранить мелкие файлы (меньше 1Kb) их было ооочень много, я боялся за ext3, и решил я хранить все эти файлы в одном большом, а отдавать посредством веб-сервера, задавая в get параметре смещение и длину самого файла в hex виде.

Времени было прилично, решил я немного извратиться и написать это на асме.


Итак, приступим




Писать будем на FASM, т.к. нравится он мне, да и к Intel-синтаксису я привык.

Итак, стандартная процедура создания elf:



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 в hex



include 'str2hex.asm'


Принцип работы данной функции прост:


Забиваем в google.com.ua «Таблица ASCI», — распечатываем, и смотрим на нее…

Замечаем, что значения в ASCII от 0 — 9 соответствуют значениям от 30h до 39h


А значения от A до F в диапазоне от 41h до 46h


Входной параметр для макроса — адрес буфера в esi (по этому адресу — строка, которую надо перевести из str в hex)

Макрос просто проверяет код 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
}


P.S. Функция лишена обработчиков ошибок, поэтому надеюсь вы будете правильно задавать размер-смещение (обратите внимание, параметры регистрозависимы. Т.е. 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 (т.к. на 80м у меня работает lighttpd, и если поменять на 80й то в eax вернется 0 и произойдет ошибка -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


Тоже самое каcается и номеров порта:


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


Создаем структуру для accept и начинаем принимать соединения:




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 ;


Также тип контента. «application/octet-stream» — самый универсальный в данном случае



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


Так как наш сервер пока не поддерживает Keep-Alive то признаемся в этом:



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 (мы это сделали вместе с отправкой Connection: Close) и можно считать что с заголовками покончено


Ну а теперь собственно узнаем какой файл хочет скачать клиент. Для этого поместим в буфер запрос GET со сдвигом в 5 байтов влево, тем самым обрезая ненужную информацию(‘GET /’), оставляя только чистый ID размером в 16 байт.


Ах да, я все об 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.

Как пишут в мануалах:



Because this copying is done within the kernel, sendfile() is more efficient than the combination of read(2) and write(2), which would require transferring data to and from user space.




; 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 мы скопировали в ecx для функции sendfile, не сохраняя его в промежуточных регистрах\памяти.


success


Вот здесь в свое время я долго не спал по ночам, потому что не мог понять почему же после отправки всех байт файл не скачивается полностью, а за секунду до полного скачивания браузер пишет «Сетевая ошибка» и его не сохраняет. В sendfile ошибок не возникало, пришлось научится пользоваться chrome developer tools.


Оказывается что после отправки самого файла, браузер шлет заголовок, который сервер должен принять. Не важно какие там данные, — его все равно можно отослать в /dev/null но очень важно что бы сервер его прочел. Иначе браузер посчитает что с файлом что-то не то. Зачем именно так сделано — на 100% мне неизвестно. Мне кажется что это связано с возможным отсутствием Content-Length в заголовках, когда файл принять надо, а сколько данных браузер заведомо не знает. Буду признателен если кто-то откроет тайну )))


Итак, принимаем браузерный хедер:

Читаем из адреса в edi, в адрес buffer



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


Если заголовки не слишком большие то 1024 байта вполне хватит

(Если на этом домене не используете длинных кук и т.д.)


Закрытие файла и завершение:



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


Вообще файл можно держать открытым какое-то время в родителе, и использовать его остальными форками, для экономии времени. Но это не совсем правильный вариант.


И самое главное!

Никаких внешних библиотек!



root@server:/home/andrew# ldd server

not a dynamic executable



Ссылка для скачивания (можно проверить работает\нет, протестить бенчмарком ab например)))

http://ubuntuone.com/3yNexPG0yewlGnjNd6219W


P.S. В коде упущено множество проверок на ошибки, также в некоторых кусках кода не подчищается стек, наличие некоторых переменных подобрано вручную (за отсутствием нормальной документации), и в общем код не претендует на звание самого «чистого».


Сервер хорошо работает на многоядерных системах (проверено на Core I7 2600). Он обгоняет lighttpd у меня на сервере по статике почти в 4 раза, хотя я думаю что мой lighttpd просто не настроен на многоядерность.


Что быстро можно добавить:

Ну например cgi для любого языка (php, perl, python) и т.д. Также возможно убрать считывание из файла, и написать работу с файловой системой а также добавить виртуальные хосты. А вообще все ограничено только вашей фантазией.


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at fivefilters.org/content-only/faq.php#publishers. Five Filters recommends: 'You Say What You Like, Because They Like What You Say' - http://www.medialens.org/index.php/alerts/alert-archive/alerts-2013/731-you-say-what-you-like-because-they-like-what-you-say.html


Комментариев нет:

Отправить комментарий