Эта статья будет посвящена программированию сетевых приложений в Linux на ассемблере. В сетевом программировании на ассемблере по сути нет ничего сложного. Если вы уже создавали сетевые приложения на любом другом языке программирования и даже для другой операционной системы, то вам будет гораздо проще. Как обычно замечания и пожелания приветствуются.
Приготовления
Для наших примеров потребуются заголовочные файлы из исходных текстов ядра, которые, если вы их установили, можно найти в файловой системе в каталоге /usr/src/linux (или /usr/src/linux-2.*.*, но лучше сделайте символическую ссылку /usr/src/linux на этот каталог). Для того, чтобы не работать с голыми цифровыми константами, можно сделать аналогичные ассемблерные файлы, с теми константами, которые Вам требуются, и по мере необходимости дополнять их. Для системного вызова ядра всегда используются две однотипные команды:
mov eax,<число>
int 0x80
Чтобы сделать наш код лаконичным можно описать макрос:
%macro syscall 1
mov eax,%1
int 0x80
%endmacro
Теперь при вызове функции ядра, можно просто вызвать макрос syscall:
syscall __NR_exit
Для отладки примеров могут пригодиться следующие приложения: edb (Evan's Debugger) – графический (на бибилотеке QT4) ассемблерный отладчик; WireShark — анализатор пакетов (снифер).
Принципы программирования сокетов
Для того, чтобы установить соединение с другим компьютером в сети нужно создать сокет – конечную точку (endpoint) соединения. Для создания соединения необходимы два сокета: на стороне клиента и на стороне сервера. Серверный сокет, обычно, создаёт программа-сервер при запуске, например, веб-сервер, а клиентский сокет создаёт приложение-клиент, например, браузер. Такая архитектура называется клиент-серверной. Активной стороной (инициирующей соединение и передачу данных) является приложение-клиент. Сервер ждёт подключения клиентов и отвечает на их запросы. Созданием сокета и получением его дескриптора занимается функция socket. Принимаемые параметры и описание, как и всех остальных функций, можно посмотреть в man-страницах (man 2 socket):
int socket(int domain, int type, int protocol);
Первым параметром передаётся семейство протоколов (определяется константами AF_* в заголовочном файле socket.h). Мы будем использовать константу AF_INET – семейство протоколов IPv4 (обычно, его просто называют IP). Вторым параметром передаётся тип соединения (константы SOCK_* из заголовочного файла net.h). В наших примерах используется SOCK_STREAM – соединение TCP. Последним параметром передаётся конкретный протокол для использования сокетом. Если передать 0, то будет выбран протокол по умолчанию для выбранного семейства протоколов. Результатом является дескриптор созданного сокета для дальнейшей идентификации сокета. Для установки соединения используется функция connect. Декларация функции на языке Си выглядит следующим образом:
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
Первый параметр – дескриптор сокета, который будет служит для приёма и передачи данных. Второй параметр структура, указывающая адрес удалённого хоста. Третий параметр – размер структуры, переданной вторым параметром. Для указания удалённого адреса используется структура sockaddr_in.
struct sockaddr_in {
sa_family_t sin_family; /* Address family */
__be16 sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
/* Pad to size of `struct sockaddr'. */
unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
sizeof(unsigned short int) - sizeof(struct in_addr)];
};
Поле sin_family – семейство протоколов, к которому относится адрес. Поле sin_port используется для указания номера порта к которому осуществляется подключение. Многие порты заранее известны (well-known ports) и закреплены за определёнными сервисами, например, для порт 80, обычно, используется для веб-сервера. Параметр sin_addr используется для указания адреса удалённого хоста. Для IPv4 протокола используется четырёх байтный адрес. Для локального тестирования можно использовать адрес 127.0.0.1 – адрес локального интерфейса (loop back). Номер порта и адрес передаются в сетевом формате: младший байт по младшему адресу. Для передачи данных используются стандартные функции read и write, в качестве первого параметра для них передаётся дескриптор сокета. Закрыть соединение можно с помощью стандартной функции close.
Простой HTTP клиент
Протокол HTTP описан в RFC 2068. Все запросы и ответы передаются в текстовом виде. Чтобы запросит у сервера файл используется несколько типов запросов. Один из них GET. Например,
GET / HTTP/1.1
Host: lug.yaroslavl.ru
Connection: close
Это запрос корневого файла (обычно index.html) у хоста lug.yaroslavl.ru, после передачи файла сервер должен закрыть соединение. Обратите внимание на одну пустую строку в конце запроса. Этой строкой мы указываем серверу, что текст запроса окончен.
Ответ от сервера может иметь следующий вид:
HTTP/1.1 200 OK
Connection: close
Transfer-Encoding: chunked
Content-Type: text/html; charset=utf-8
Все доступные сетевые функции на ассемблере можно вызвать с помощью системного вызова socketcall. Первым параметром передаётся номер вызываемой функции (константы SYS_* из заголовочного файла net.h), а вторым указатель на параметры для вызываемой функции.
В примере, также, показан способ изменения цвета вывода на консоль с помощью ESC-последовательностей (man console_codes).
; Включаем заголовочные файлы с константами
%include "../include/unistd.inc"
%include "../include/socket.inc"
%include "../include/net.inc"
%include "../include/in.inc"
; Макрос преобразующий слово из представления хоста
; в сетевое представоление (host to network short)
%define htons(a) (( a >> 8 ) & 0xFF) | (( a & 0xFF ) << 8 )
; Макрос для системного вызова
; Первым параметром нередаётся номер системного вызова
%macro syscall 1
mov eax,%1
int 0x80
%endmacro
; Декскриптор стандартного вывода
%define STD_OUT 1
section .bss
; Дескриптор прослушиваемого сокета
listenfd resd 1
; Дескриптор присоединённого сокета
connfd resd 1
section .text
; Строки для вывода на экран
global _start
_start:
; Изменяем цвет шрифта консоли
; Параметры для write
mov ebx,STD_OUT ; вывод на стандартное устройство вывода
mov ecx,esc_console_green ; ESC последовательность
mov edx,esc_console_green_size ; длина ESC последовательности
syscall __NR_write
; Создаем сокет
; sock_fd = socket(AF_INET, SOCK_STREAM, 0);
; Парамеры для socket
push 0 ; протокол по умолчанию для IPv4
push SOCK_STREAM ; TCP соединение
push AF_INET ; IPv4
; Параметры для socketcall
mov ebx,SYS_SOCKET ; номер вызова функции socket
mov ecx,esp ; параметры находяться в стеке
syscall __NR_socketcall
add esp,3*4 ; очищаем стек, занятый 3мя параметрами для socket
mov [sock_fd],eax ; сохраняем дескриптор сокета
; Подключение
; connect(sock_df, servaddr, sizeof(sockaddr_in))
; Парамеры для connect
push sockaddr_in_size ; размер структуры sockaddr_in
push servaddr ; смещение структуры с адресом сервера
push dword[sock_fd] ; дескриптор сокета для подключения
; Параметры для socketcall
mov ebx,SYS_CONNECT ; номер вызова функции connect
mov ecx,esp ; парамерты находятся в стеке
syscall __NR_socketcall
add esp,3*4 ; очищаем стек, занятый 3мя параметрами для connect
; Отсылка запроса
; write(sock_fd, get_query, get_query, strlen(get_query))
; Параметры для write
mov ebx,[sock_fd] ; записать в сокет (отослать данные)
mov ecx,get_query ; запрос
mov edx,get_query_len ; длина строки запроса
syscall __NR_write
; Приём данных
; read(sock_fd, buffer, sizeof(buffer))
.recv:
mov ebx,[sock_fd] ; чтение происходит из сокета
mov ecx,buffer ; в буфер
mov edx,buffer_size ; размер буфера
syscall __NR_read
mov [buffer_readed],eax ; сохранение количества считанных байтов
; Если больше нечего читать или ошибка
; то заверщаем приложение
cmp eax,0
jng .exit
; Вывод принятых данных на экран
; write(STD_OUT, buffer, buffer_readed)
mov ebx,STD_OUT ; Вывод на стандартное устройство вывода
mov ecx,buffer ; из буфера считанных данных из сокета
mov edx,[buffer_readed] ; количество считанных байтов
syscall __NR_write
; Продолжаем чтение
jmp .recv
.exit:
; Возвращаем значения по умолчанию для консоли
; Параметры для write
mov ebx,STD_OUT ; вывод на стандартное устройство вывода
mov ecx,esc_console_defaults ; ESC последовательность
mov edx,esc_console_defaults_size ; длина ESC последовательности
syscall __NR_write
; Закрытие сокета
; Параметры для close
mov ebx,[sock_fd] ; дескриптор сокета
syscall __NR_close
; Выход из программы
xor ebx,ebx ; код выхода 0
syscall __NR_exit
section .data
servaddr istruc sockaddr_in
at sockaddr_in.sin_family, dw AF_INET ; IPv4
at sockaddr_in.sin_port, dw htons(80) ; HTTP порт
at sockaddr_in.sin_addr, db 217,15,135,71 ; IP адрес lug.yaroslavl.ru (в сетевом формате)
at sockaddr_in.__pad, dd 0,0 ; неиспользуемая часть структры должна содержать нули
iend
get_query db "GET / HTTP/1.1",0xd,0xa ; Запрос корневого файла (обычно index.html)
db "Host: lug.yaroslavl.ru",0xd,0xa ; Имя хоста должно обязательно присутствовать (смотри RFC2616 секцию 14.23)
db "Connection: close",0xd,0xa ; Чтобы сервер сразу после отдачи файла закрывал соединение
db 0xd,0xa ; Окончание запроса идентифицируется пустой строкой
get_query_len equ $ - get_query ; Длина сообщения запроса
esc_console_green db 0x1b,'[32m' ; ESC последовательность: установить зеленый цвет символов
esc_console_green_size equ $ - esc_console_green
esc_console_defaults db 0x1b,'[0m' ; ESC последовательность: сбросить все атрибуты в их значения по умолчанию
esc_console_defaults_size equ $ - esc_console_defaults
section .bss
; Дескриптор сокета
sock_fd resd 1
; Буфер для чтения из буфера и вывода на консоль
buffer resb 4096
; Размер буфера
buffer_size equ $ - buffer
; Количество считанных байт
buffer_readed resd 1
Пример выводит на консоль зелёным цветом ответ от сервера, как изображено на рисунке. Консоль с запущенным webclient
Комментарии (0)
RSS свернуть / развернутьТолько зарегистрированные и авторизованные пользователи могут оставлять комментарии.