Web server in Assembler

Для изучения ассемблера предлагается создать простой веб-сервер, который будет эмулировать базу данных key-value.

  • Сервер должен поддерживать эндпоинты:
    • GET /{key} - получить значение по ключу key.
    • POST /{key} - сохранить значение из тела запроса по ключу key.
  • Сервер должен поддерживать параллельную обработку запросов.
  • Сервер должен хранить все данные на диске.
  • Весь код сервера должен быть на ассемблере.

Т.к. писать код на ассемблере сложно и недостаточно комфортно, то допустимы упрощения.

Ассемблерный код будет использовать системные вызовы socket, bind, listen, accept, fork, read, write, open, close для взаимодействия с файловой системой и сетевой подсистемой.

Для реализации сервера, который обрабатывает сетевые запросы, мы используем последовательность системных вызовов:

  • socket - создаёт сокет, который будет использоваться для связи по сети
  • bind - связывает сокет с определённым IP-адресом и портом, на которых сервер будет принимать соединения
  • listen - переводит сокет в состояние ожидания входящих соединений, позволяя ОС управлять запросами

После настройки сокета, сервер переходит в главный цикл:

  • Системный вызов accept блокирует выполнение программы, ожидая новое входящее соединение.
  • Когда клиент подключается, accept возвращает новый сокет, представляющий это соединение.
  • Вызывается fork для создания нового процесса для обработки входящего соединения.
  • Если pid == 0, то это дочерний процесс, который будет обрабатывать клиентский запрос.
  • Если pid > 0, то это родительский процесс, который будет ожидать новое входящее соединение.
    Не забываем закрыть неиспользуемые сокеты после fork (для родительского процесса - сокет клиента, для дочернего - сокет сервера).

Обработка клиентского сокета включает:

  • Чтение сырого http запроса из сокета.
  • Для GET запроса:
    • Получение key из запроса.
    • Открытие файла с именем key.
    • Чтение значения val из этого файла.
    • Закрытие файла.
    • Запись в сокет валидного http ответа со значением val.
    • Закрытие сокета.
  • Для POST запроса:
    • Получение key, val из запроса.
    • Создание файла с именем key.
    • Запись значения val в этот файл.
    • Закрытие файла.
    • Запись в сокет валидного http ответа.
    • Закрытие сокета.

Общая высокоуровневая схема на псевдоязыке:

socket processing

void main() {
    srv_socket = socket()
    bind(srv_socket)
    listen(srv_socket)

    while true {
        cli_socket = accept(srv_socket)

        pid = fork()
        if pid == 0 {
            close(srv_socket)
            process_request(cli_socket)
            exit(0)
        }

        close(cli_socket)
    }
}

request processing

void process_request(socket) {
    req = read(socket)
    method = parse_method(req)
    key = parse_url(req)

    if method == "GET" {
        fd = open(key)
        val = read(fd)
        close(fd)

        write(socket, http_status_ok)
        write(socket, val)
        close(socket)
    }

    if method == "POST" {
        val = parse_body(req)

        fd = create(key)
        write(fd, val)
        close(fd)

        write(socket, http_status_ok)
        close(socket)
    }
}

Рассмотрим основные моменты.

Системные вызовы socket, bind, listen.
Нужно помнить, что номер порта должен быть указан с учетом сетевого порядка байт.

socket

    # int socket(int domain, int type, int protocol);
    mov rdi, AF_INET       # domain
    mov rsi, SOCK_STREAM   # type
    mov rdx, IPPROTO_IP    # protocol
    mov rax, SYS_socket    # socket
    syscall

    test rax, rax
    js _exit_with_error


    mov [rbp-24], rax   # save srv_socket_fd


    # init sockaddr_in
    movw [rbp-16],   AF_INET
    movw ax, PORT
    xchg al, ah
    movw [rbp-16+2], ax
    movd [rbp-16+4], INADDR_ANY


    # int bind(int socket, const struct sockaddr *address, socklen_t address_len);
    mov rdi, [rbp-24]      # socket
    lea rsi, [rbp-16]      # address
    mov rdx, 16            # address_len
    mov rax, SYS_bind      # socket
    syscall

    test rax, rax
    js _exit_with_error


    # int listen(int socket, int backlog);
    mov rdi, [rbp-24]      # socket
    mov rsi, 0             # backlog
    mov rax, SYS_listen    # listen
    syscall

    test rax, rax
    js _exit_with_error

Основной цикл ожидания входящих соединений.
После срабатывания accept вызывается fork. Закрывается соответствующий неиспользуемый сокет в разных процессах.
Родительский процесс переходи к вызову accept. Дочерний обрабатывает входящее соединение и завершает работу через вызов exit.

accept

.accept:
    # int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);
    mov rdi, [rbp-24]        # socket
    mov rsi, 0               # address
    mov rdx, 0               # adress_len
    mov rax, SYS_accept      # accept
    syscall

    test rax, rax
    js _exit_with_error


    mov [rbp-32], rax   # save cli_socket_fd


    # pid_t fork(void);
    mov rax, SYS_fork   # fork
    syscall


    test rax, rax
    js _exit_with_error


    mov r14, rax   # pid

    # close cli_socket_fd in parent
    # close srv_socket_fd in child
    jz .accept_set_srv_socket_fd
    mov r15, [rbp-32]
    jmp .accept_close_fd
.accept_set_srv_socket_fd:
    mov r15, [rbp-24]
.accept_close_fd:
    # int close(int fd);
    mov rdi, r15         # fd
    mov rax, SYS_close   # close
    syscall


    # parent process wait new clients
    test r14, r14
    jg .accept


    # child process precesses client
    mov rdi, [rbp-32]
    call process_request


    # exit(EXIT_OK)
    mov rdi, EXIT_OK
    mov rax, SYS_exit
    syscall

Функция чтения данных из запроса пользователя и определение метода (GET или POST) по первой букве.
Все данные хранятся на стеке. Используется преамбула и эпилог для манипулирования размером стека.

process_request

process_request:
    # args:
    #   rdi = fd
    # vars:
    #   [rbp-8]    - 8b, fd
    #   [rbp-16]   - 8b, buffer_length
    #   [rbp-1040] - 1024b, buffer

    push rbp
    mov rbp, rsp
    sub rsp, 1040


    mov [rbp-8], rdi   # save client socket fd


    # ssize_t read(int fd, void buf[.count], size_t count);
    mov rdi, [rbp-8]      # fd
    lea rsi, [rbp-1040]   # buf
    mov rdx, 1024         # count
    mov rax, SYS_read     # read
    syscall


    cmpb [rbp-1040], 'P'
    je _process_post
    mov rdi, [rbp-8]
    lea rsi, [rbp-1040]
    call process_get
    jmp _endif
_process_post:
    mov rdi, [rbp-8]
    lea rsi, [rbp-1040]
    mov rdx, rax
    call process_post
_endif:


    mov rsp, rbp
    pop rbp
    ret

Обработка POST запроса.
Самое сложное - правильно извлечь из запроса ключ и значение. Все остальное делается просто

process_post

process_post:
    # args:
    #   rdi = socket_fd
    #   rsi = &buffer
    #   rdx = buffer_length
    # vars:
    #   [rbp-8]    - 8b, socket_fd
    #   [rbp-16]   - 8b, &req_buffer
    #   [rbp-24]   - 8b, buffer_length
    #   [rbp-32]   - 8b, file_fd

    push rbp
    mov rbp, rsp
    sub rsp, 32

    mov [rbp-8], rdi
    mov [rbp-16], rsi
    mov [rbp-24], rdx


    # set \x00 after "xxx" in "POST /xxx HTTP/1.1\r\n....."
    mov r8, rsi
    add r8, 5
.process_post_path:
    add r8, 1
    cmpb [r8], ' '
    jne .process_post_path
.process_post_path_end:
    movb byte [r8-1], 0


    # int open(const char *pathname, int flags, /* mode_t mode */);
    lea rdi, [rsi]+6    # pathname
    mov rsi, O_WRONLY   # flags
    xor rsi, O_CREAT    # flags
    mov rdx, 0777       # mode
    mov rax, SYS_open   # open
    syscall

    test rax, rax
    js _exit_with_error


    mov [rbp-32], rax   # save fd


    # parse data between '\n' and end of body
    mov r8, [rbp-16]
    add r8, [rbp-24]
.process_body:
    sub r8, 1
    cmpb [r8], '\n'
    jne .process_body
.process_body_end:
    add r8, 1   # data_offset

    mov r9, [rbp-16]
    add r9, [rbp-24]
    sub r9, r8   # data_length


    # ssize_t write(int fd, void buf[.count], size_t count);
    mov rdi, [rbp-32]    # fd
    mov rsi, r8          # buf
    mov rdx, r9          # count
    mov rax, SYS_write   # write
    syscall


    # int close(int fd);
    mov rdi, [rbp-32]   # fd
    mov rax, SYS_close  # close
    syscall

    test rax, rax
    js _exit_with_error


    # ssize_t write(int fd, const void buf[.count], size_t count);
    mov rdi, [rbp-8]                     # fd
    lea rsi, [response_header_ok]        # buf
    mov rdx, response_header_ok_length   # count
    mov rax, SYS_write                   # write
    syscall


    mov rsp, rbp
    pop rbp
    ret

Обработка GET запроса

process_get

process_get:
    # args:
    #   rdi = socket_fd
    #   rsi = &buffer
    # vars:
    #   [rbp-8]    - 8b, socket_fd
    #   [rbp-16]   - 8b, &req_buffer
    #   [rbp-24]   - 8b, file_fd
    #   [rbp-1048] - 1024b, file_buffer
    #   [rbp-1056] - 8b, file_buffer_length

    push rbp
    mov rbp, rsp
    sub rsp, 1056

    mov [rbp-8], rdi
    mov [rbp-16], rsi


    # set \x00 after "xxx" in "GET /xxx HTTP/1.1\r\n....."
    mov r8, rsi
    add r8, 4
.process_get_path:
    add r8, 1
    cmpb [r8], ' '
    jne .process_get_path
.process_get_path_end:
    movb byte [r8-1], 0


    # int open(const char *pathname, int flags, /* mode_t mode */);
    lea rdi, [rsi]+5    # pathname
    mov rsi, O_RDONLY   # flags
    mov rdx, 0          # mode
    mov rax, SYS_open   # open
    syscall

    test rax, rax
    js _exit_with_error

    mov [rbp-24], rax


    # ssize_t read(int fd, void buf[.count], size_t count);
    mov rdi, [rbp-24]     # fd
    lea rsi, [rbp-1048]   # buf
    mov rdx, 1024         # count
    mov rax, SYS_read     # read
    syscall

    mov [rbp-1056], rax


    # int close(int fd);
    mov rdi, [rbp-24]   # fd
    mov rax, SYS_close  # close
    syscall


    # ssize_t write(int fd, const void buf[.count], size_t count);
    mov rdi, [rbp-8]                     # fd
    lea rsi, [response_header_ok]        # buf
    mov rdx, response_header_ok_length   # count
    mov rax, SYS_write                   # write
    syscall


    # ssize_t write(int fd, const void buf[.count], size_t count);
    mov rdi, [rbp-8]      # fd
    lea rsi, [rbp-1048]   # buf
    mov rdx, [rbp-1056]   # count
    mov rax, SYS_write    # write
    syscall


    mov rsp, rbp
    pop rbp
    ret

Полный исходный код сервера:

server.s

.intel_syntax noprefix
.globl _start

.equ SYS_read, 0
.equ SYS_write, 1
.equ SYS_open, 2
.equ SYS_close, 3
.equ SYS_socket, 41
.equ SYS_accept, 43
.equ SYS_bind, 49
.equ SYS_listen, 50
.equ SYS_fork, 57
.equ SYS_exit, 60

.equ EXIT_OK, 0
.equ EXIT_ERROR, 1
.equ AF_INET, 2
.equ SOCK_STREAM, 1
.equ IPPROTO_IP, 0
.equ INADDR_ANY, 0
.equ O_RDONLY, 0
.equ O_WRONLY, 1
.equ O_CREAT, 64

.equ PORT, 8080


.section .data

response_header_ok:
    .ascii "HTTP/1.0 200 OK\r\n\r\n"
response_header_ok_length = . - response_header_ok


.section .text

_start:
    # vars:
    #   [rbp-16]   - 16b, sockaddr
    #   [rbp-24]   - 8b, srv_socket_fd
    #   [rbp-32]   - 8b, cli_socket_fd

    push rbp
    mov rbp, rsp
    sub rsp, 32


    # int socket(int domain, int type, int protocol);
    mov rdi, AF_INET       # domain
    mov rsi, SOCK_STREAM   # type
    mov rdx, IPPROTO_IP    # protocol
    mov rax, SYS_socket    # socket
    syscall

    test rax, rax
    js _exit_with_error


    mov [rbp-24], rax   # save srv_socket_fd


    # init sockaddr_in
    movw [rbp-16],   AF_INET
    movw ax, PORT
    xchg al, ah
    movw [rbp-16+2], ax
    movd [rbp-16+4], INADDR_ANY


    # int bind(int socket, const struct sockaddr *address, socklen_t address_len);
    mov rdi, [rbp-24]      # socket
    lea rsi, [rbp-16]      # address
    mov rdx, 16            # address_len
    mov rax, SYS_bind      # socket
    syscall

    test rax, rax
    js _exit_with_error


    # int listen(int socket, int backlog);
    mov rdi, [rbp-24]      # socket
    mov rsi, 0             # backlog
    mov rax, SYS_listen    # listen
    syscall

    test rax, rax
    js _exit_with_error


.accept:
    # int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);
    mov rdi, [rbp-24]        # socket
    mov rsi, 0               # address
    mov rdx, 0               # adress_len
    mov rax, SYS_accept      # accept
    syscall

    test rax, rax
    js _exit_with_error


    mov [rbp-32], rax   # save cli_socket_fd


    # pid_t fork(void);
    mov rax, SYS_fork   # fork
    syscall


    test rax, rax
    js _exit_with_error


    mov r14, rax   # pid

    # close cli_socket_fd in parent
    # close srv_socket_fd in child
    jz .accept_set_srv_socket_fd
    mov r15, [rbp-32]
    jmp .accept_close_fd
.accept_set_srv_socket_fd:
    mov r15, [rbp-24]
.accept_close_fd:
    # int close(int fd);
    mov rdi, r15         # fd
    mov rax, SYS_close   # close
    syscall


    # parent process wait new clients
    test r14, r14
    jg .accept


    # child process precesses client
    mov rdi, [rbp-32]
    call process_request


    # exit(EXIT_OK)
    mov rdi, EXIT_OK
    mov rax, SYS_exit
    syscall


    mov rsp, rbp
    pop rbp
    ret


_exit_with_error:
    # exit(EXIT_ERROR)
    mov rdi, EXIT_ERROR
    mov rax, SYS_exit
    syscall


process_request:
    # args:
    #   rdi = fd
    # vars:
    #   [rbp-8]    - 8b, fd
    #   [rbp-16]   - 8b, buffer_length
    #   [rbp-1040] - 1024b, buffer

    push rbp
    mov rbp, rsp
    sub rsp, 1040


    mov [rbp-8], rdi   # save client socket fd


    # ssize_t read(int fd, void buf[.count], size_t count);
    mov rdi, [rbp-8]      # fd
    lea rsi, [rbp-1040]   # buf
    mov rdx, 1024         # count
    mov rax, SYS_read     # read
    syscall


    cmpb [rbp-1040], 'P'
    je _process_post
    mov rdi, [rbp-8]
    lea rsi, [rbp-1040]
    call process_get
    jmp _endif
_process_post:
    mov rdi, [rbp-8]
    lea rsi, [rbp-1040]
    mov rdx, rax
    call process_post
_endif:


    mov rsp, rbp
    pop rbp
    ret


process_post:
    # args:
    #   rdi = socket_fd
    #   rsi = &buffer
    #   rdx = buffer_length
    # vars:
    #   [rbp-8]    - 8b, socket_fd
    #   [rbp-16]   - 8b, &req_buffer
    #   [rbp-24]   - 8b, buffer_length
    #   [rbp-32]   - 8b, file_fd

    push rbp
    mov rbp, rsp
    sub rsp, 32

    mov [rbp-8], rdi
    mov [rbp-16], rsi
    mov [rbp-24], rdx


    # set \x00 after "xxx" in "POST /xxx HTTP/1.1\r\n....."
    mov r8, rsi
    add r8, 5
.process_post_path:
    add r8, 1
    cmpb [r8], ' '
    jne .process_post_path
.process_post_path_end:
    movb byte [r8-1], 0


    # int open(const char *pathname, int flags, /* mode_t mode */);
    lea rdi, [rsi]+6    # pathname
    mov rsi, O_WRONLY   # flags
    xor rsi, O_CREAT    # flags
    mov rdx, 0777       # mode
    mov rax, SYS_open   # open
    syscall

    test rax, rax
    js _exit_with_error


    mov [rbp-32], rax   # save fd


    # parse data between '\n' and end of body
    mov r8, [rbp-16]
    add r8, [rbp-24]
.process_body:
    sub r8, 1
    cmpb [r8], '\n'
    jne .process_body
.process_body_end:
    add r8, 1   # data_offset

    mov r9, [rbp-16]
    add r9, [rbp-24]
    sub r9, r8   # data_length


    # ssize_t write(int fd, void buf[.count], size_t count);
    mov rdi, [rbp-32]    # fd
    mov rsi, r8          # buf
    mov rdx, r9          # count
    mov rax, SYS_write   # write
    syscall


    # int close(int fd);
    mov rdi, [rbp-32]   # fd
    mov rax, SYS_close  # close
    syscall

    test rax, rax
    js _exit_with_error


    # ssize_t write(int fd, const void buf[.count], size_t count);
    mov rdi, [rbp-8]                     # fd
    lea rsi, [response_header_ok]        # buf
    mov rdx, response_header_ok_length   # count
    mov rax, SYS_write                   # write
    syscall


    mov rsp, rbp
    pop rbp
    ret

process_get:
    # args:
    #   rdi = socket_fd
    #   rsi = &buffer
    # vars:
    #   [rbp-8]    - 8b, socket_fd
    #   [rbp-16]   - 8b, &req_buffer
    #   [rbp-24]   - 8b, file_fd
    #   [rbp-1048] - 1024b, file_buffer
    #   [rbp-1056] - 8b, file_buffer_length

    push rbp
    mov rbp, rsp
    sub rsp, 1056

    mov [rbp-8], rdi
    mov [rbp-16], rsi


    # set \x00 after "xxx" in "GET /xxx HTTP/1.1\r\n....."
    mov r8, rsi
    add r8, 4
.process_get_path:
    add r8, 1
    cmpb [r8], ' '
    jne .process_get_path
.process_get_path_end:
    movb byte [r8-1], 0


    # int open(const char *pathname, int flags, /* mode_t mode */);
    lea rdi, [rsi]+5    # pathname
    mov rsi, O_RDONLY   # flags
    mov rdx, 0          # mode
    mov rax, SYS_open   # open
    syscall

    test rax, rax
    js _exit_with_error

    mov [rbp-24], rax


    # ssize_t read(int fd, void buf[.count], size_t count);
    mov rdi, [rbp-24]     # fd
    lea rsi, [rbp-1048]   # buf
    mov rdx, 1024         # count
    mov rax, SYS_read     # read
    syscall

    mov [rbp-1056], rax


    # int close(int fd);
    mov rdi, [rbp-24]   # fd
    mov rax, SYS_close  # close
    syscall


    # ssize_t write(int fd, const void buf[.count], size_t count);
    mov rdi, [rbp-8]                     # fd
    lea rsi, [response_header_ok]        # buf
    mov rdx, response_header_ok_length   # count
    mov rax, SYS_write                   # write
    syscall


    # ssize_t write(int fd, const void buf[.count], size_t count);
    mov rdi, [rbp-8]      # fd
    lea rsi, [rbp-1048]   # buf
    mov rdx, [rbp-1056]   # count
    mov rax, SYS_write    # write
    syscall
     

    mov rsp, rbp
    pop rbp
    ret

Компилируем исходный файл server.s в исполняемый:

server_build

as -o server.o server.s
ld -o server server.o

Запускаем сервер (можно через strace для отслеживания состояния и ошибок).

server_run

./server

strace -f -s 256 ./server

С помощью curl выполняем тестовые запросы:

curl

curl -i http://0.0.0.0:8080/key_abc --data "value_abc"
HTTP/1.0 200 OK


curl -i http://0.0.0.0:8080/12345
curl: (52) Empty reply from server


curl -i http://0.0.0.0:8080/12345 --data "abcdef"
HTTP/1.0 200 OK


curl -i http://0.0.0.0:8080/12345
HTTP/1.0 200 OK
abcdef


curl -i http://0.0.0.0:8080/12345 --data "new_value"
HTTP/1.0 200 OK


curl -i http://0.0.0.0:8080/12345
HTTP/1.0 200 OK
new_value


curl -i http://0.0.0.0:8080/key_abc
HTTP/1.0 200 OK
value_abc

Видно, что сервер корректно обрабатывает запросы получение значния по ключу и на создание/изменение записи по ключу.
Только, если запрошенный ключ не существет, то сервер закрывает соединение без корректного ответа - не реализовано.

В выводе strace такое же поведение как и задумывалось. Порядок системных вызовов и их аргументы валидны.

strace

strace -f -s 256 ./server
execve("./server", ["./server"], 0x7fff3cce04e8 /* 16 vars */) = 0
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3
bind(3, {sa_family=AF_INET, sin_port=htons(8080), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
listen(3, 0)                            = 0
accept(3, NULL, NULL)                   = 4
fork(strace: Process 2067 attached
)                                  = 2067
[pid  2067] close(3 <unfinished ...>
[pid  2064] close(4 <unfinished ...>
[pid  2067] <... close resumed>)        = 0
[pid  2064] <... close resumed>)        = 0
[pid  2067] read(4,  <unfinished ...>
[pid  2064] accept(3, NULL, NULL <unfinished ...>
[pid  2067] <... read resumed>"POST /key_abc HTTP/1.1\r\nHost: 0.0.0.0:8080\r\nUser-Agent: curl/8.7.1\r\nAccept: */*\r\nContent-Length: 9\r\nContent-Type: application/x-www-form-urlencoded\r\n\r\nvalue_abc", 1024) = 160
[pid  2067] open("key_abc", O_WRONLY|O_CREAT, 0777) = 3
[pid  2067] write(3, "value_abc", 9)    = 9
[pid  2067] close(3)                    = 0
[pid  2067] write(4, "HTTP/1.0 200 OK\r\n\r\n", 19) = 19
[pid  2067] exit(0)                     = ?
[pid  2067] +++ exited with 0 +++
<... accept resumed>)                   = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=2067, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---
accept(3, NULL, NULL)                   = 4
fork(strace: Process 2069 attached
)                                  = 2069
[pid  2064] close(4 <unfinished ...>
[pid  2069] close(3 <unfinished ...>
[pid  2064] <... close resumed>)        = 0
[pid  2069] <... close resumed>)        = 0
[pid  2064] accept(3, NULL, NULL <unfinished ...>
[pid  2069] read(4, "GET /12345 HTTP/1.1\r\nHost: 0.0.0.0:8080\r\nUser-Agent: curl/8.7.1\r\nAccept: */*\r\n\r\n", 1024) = 80
[pid  2069] open("12345", O_RDONLY)     = -1 ENOENT (No such file or directory)
[pid  2069] exit(1)                     = ?
[pid  2069] +++ exited with 1 +++
<... accept resumed>)                   = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=2069, si_uid=1000, si_status=1, si_utime=0, si_stime=0} ---
accept(3, NULL, NULL)                   = 4
fork(strace: Process 2071 attached
)                                  = 2071
[pid  2071] close(3 <unfinished ...>
[pid  2064] close(4 <unfinished ...>
[pid  2071] <... close resumed>)        = 0
[pid  2064] <... close resumed>)        = 0
[pid  2064] accept(3, NULL, NULL <unfinished ...>
[pid  2071] read(4, "POST /12345 HTTP/1.1\r\nHost: 0.0.0.0:8080\r\nUser-Agent: curl/8.7.1\r\nAccept: */*\r\nContent-Length: 6\r\nContent-Type: application/x-www-form-urlencoded\r\n\r\nabcdef", 1024) = 155
[pid  2071] open("12345", O_WRONLY|O_CREAT, 0777) = 3
[pid  2071] write(3, "abcdef", 6)       = 6
[pid  2071] close(3)                    = 0
[pid  2071] write(4, "HTTP/1.0 200 OK\r\n\r\n", 19) = 19
[pid  2071] exit(0)                     = ?
[pid  2071] +++ exited with 0 +++
<... accept resumed>)                   = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=2071, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---
accept(3, NULL, NULL)                   = 4
fork(strace: Process 2073 attached
)                                  = 2073
[pid  2064] close(4)                    = 0
[pid  2073] close(3 <unfinished ...>
[pid  2064] accept(3, NULL, NULL <unfinished ...>
[pid  2073] <... close resumed>)        = 0
[pid  2073] read(4, "GET /12345 HTTP/1.1\r\nHost: 0.0.0.0:8080\r\nUser-Agent: curl/8.7.1\r\nAccept: */*\r\n\r\n", 1024) = 80
[pid  2073] open("12345", O_RDONLY)     = 3
[pid  2073] read(3, "abcdef", 1024)     = 6
[pid  2073] close(3)                    = 0
[pid  2073] write(4, "HTTP/1.0 200 OK\r\n\r\n", 19) = 19
[pid  2073] write(4, "abcdef", 6)       = 6
[pid  2073] exit(0)                     = ?
[pid  2073] +++ exited with 0 +++
<... accept resumed>)                   = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=2073, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---
accept(3, NULL, NULL)                   = 4
fork(strace: Process 2075 attached
)                                  = 2075
[pid  2075] close(3 <unfinished ...>
[pid  2064] close(4 <unfinished ...>
[pid  2075] <... close resumed>)        = 0
[pid  2064] <... close resumed>)        = 0
[pid  2075] read(4,  <unfinished ...>
[pid  2064] accept(3, NULL, NULL <unfinished ...>
[pid  2075] <... read resumed>"POST /12345 HTTP/1.1\r\nHost: 0.0.0.0:8080\r\nUser-Agent: curl/8.7.1\r\nAccept: */*\r\nContent-Length: 9\r\nContent-Type: application/x-www-form-urlencoded\r\n\r\nnew_value", 1024) = 158
[pid  2075] open("12345", O_WRONLY|O_CREAT, 0777) = 3
[pid  2075] write(3, "new_value", 9)    = 9
[pid  2075] close(3)                    = 0
[pid  2075] write(4, "HTTP/1.0 200 OK\r\n\r\n", 19) = 19
[pid  2075] exit(0)                     = ?
[pid  2075] +++ exited with 0 +++
<... accept resumed>)                   = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=2075, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---
accept(3, NULL, NULL)                   = 4
fork(strace: Process 2077 attached
)                                  = 2077
[pid  2064] close(4 <unfinished ...>
[pid  2077] close(3 <unfinished ...>
[pid  2064] <... close resumed>)        = 0
[pid  2077] <... close resumed>)        = 0
[pid  2064] accept(3, NULL, NULL <unfinished ...>
[pid  2077] read(4, "GET /12345 HTTP/1.1\r\nHost: 0.0.0.0:8080\r\nUser-Agent: curl/8.7.1\r\nAccept: */*\r\n\r\n", 1024) = 80
[pid  2077] open("12345", O_RDONLY)     = 3
[pid  2077] read(3, "new_value", 1024)  = 9
[pid  2077] close(3)                    = 0
[pid  2077] write(4, "HTTP/1.0 200 OK\r\n\r\n", 19) = 19
[pid  2077] write(4, "new_value", 9)    = 9
[pid  2077] exit(0)                     = ?
[pid  2077] +++ exited with 0 +++
<... accept resumed>)                   = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=2077, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---
accept(3, NULL, NULL)                   = 4
fork(strace: Process 2079 attached
)                                  = 2079
[pid  2064] close(4 <unfinished ...>
[pid  2079] close(3 <unfinished ...>
[pid  2064] <... close resumed>)        = 0
[pid  2079] <... close resumed>)        = 0
[pid  2064] accept(3, NULL, NULL <unfinished ...>
[pid  2079] read(4, "GET /key_abc HTTP/1.1\r\nHost: 0.0.0.0:8080\r\nUser-Agent: curl/8.7.1\r\nAccept: */*\r\n\r\n", 1024) = 82
[pid  2079] open("key_abc", O_RDONLY)   = 3
[pid  2079] read(3, "value_abc", 1024)  = 9
[pid  2079] close(3)                    = 0
[pid  2079] write(4, "HTTP/1.0 200 OK\r\n\r\n", 19) = 19
[pid  2079] write(4, "value_abc", 9)    = 9
[pid  2079] exit(0)                     = ?
[pid  2079] +++ exited with 0 +++
<... accept resumed>)                   = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=2079, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---

Как оказалось не так сложно написать на ассемблере не просто что-то рабочее, а работающий http сервер.
Размер исходного файла 300-400 строк. Размер исполняемого файла всего 8кб.
Конечно, не нужно так писать сервер.
Но так можно освоить ассемблер, и написать что-то компактное встаиваемое для других задач.

Похожее