Contents

Web Server in Assembly

To learn Assembly, let’s build a simple web server that simulates a key-value database.

  • The server should support the following endpoints:
    • GET /{key} - retrieve the value for the key.
    • POST /{key} - store the request body as value for the key.
  • The server should support concurrent request handling.
  • The server should persist all data on disk.
  • All server code must be written in Assembly.

Since Assembly is difficult and inconvenient to work with, simplifications are allowed.

The Assembly code will use system calls like socket, bind, listen, accept, fork, read, write, open, close to interact with the filesystem and network stack.

To implement the server, which processes network requests, we use a sequence of system calls:

  • socket - creates a socket for network communication
  • bind - associates the socket with an IP address and port
  • listen - puts the socket into listening mode, allowing the OS to manage connections

Once the socket is set up, the server enters a main loop:

  • accept blocks until a new incoming connection arrives.
  • When a client connects, accept returns a new socket for the connection.
  • fork is used to create a new process to handle the connection.
  • If pid == 0, we’re in the child process handling the request.
  • If pid > 0, we’re in the parent, ready for the next connection.
    Don’t forget to close unused sockets after fork (client socket in parent, server socket in child).

Client socket handling includes:

  • Reading the raw HTTP request from the socket.
  • For a GET request:
    • Parse the key from the request.
    • Open a file named after the key.
    • Read the value val from the file.
    • Close the file.
    • Write a valid HTTP response with the val to the socket.
    • Close the socket.
  • For a POST request:
    • Extract key, val from the request.
    • Create a file named after the key.
    • Write val to the file.
    • Close the file.
    • Write a valid HTTP response.
    • Close the socket.

High-level pseudocode:

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)
    }
}
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)
    }
}
  • Determine the syscall arguments: how many and what types. You can find this info at: x64.syscall.sh, man7.org.
  • Place arguments in registers: RDI - first argument, RSI - second, RDX - third, RCX - fourth, R8 - fifth, R9 - sixth, others go on the stack.
  • Place the syscall number in RAX.
  • Execute the syscall instruction. The result will be in RAX.

Example for the function int socket(int domain, int type, int protocol):

# 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

Using process_get as an example:

process_get:
    # 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   # Calculate the total size of data to be saved on the stack
                    # socket_fd          - 8 bytes
                    # req_buffer         - 8 bytes
                    # file_fd            - 8 bytes
                    # file_buffer        - 1024 bytes
                    # file_buffer_length - 8 bytes
                    # Total 1056 bytes

                    # Next, these addresses are used to access variables:
                    # [rbp-8]    - 8 bytes, socket_fd
                    # [rbp-16]   - 8 bytes, &req_buffer (pointer to request buffer)
                    # [rbp-24]   - 8 bytes, file_fd
                    # [rbp-1048] - 1024 bytes, file_buffer
                    # [rbp-1056] - 8 bytes, file_buffer_length

    mov [rbp-8], rdi   # Save socket_fd
    mov [rbp-16], rsi  # Save req_buffer

    # ...

    mov rsp, rbp   # Revert stack
    pop rbp
    ret

Let’s look at the key parts.

Syscalls socket, bind, listen.
Remember to specify the port number in network byte order.

    # 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

Main loop waiting for incoming connections.
After accept triggers, fork is called. The corresponding unused socket is closed in different processes.
The parent process goes back to the accept call. The child handles the incoming connection and then exits by calling exit.

.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

Function to read data from the user request and determine the method (GET or POST) by the first letter.
All data is stored on the stack. Prologue and epilogue are used to manage stack size.

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 request processing.
The hardest part is correctly extracting the key and value from the request. Everything else is straightforward.

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 request processing

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

Full server source code:

.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

Compile the source file server.s into an executable:

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

Run the server (can be run with strace to monitor status and errors).

./server

strace -f -s 256 ./server

Use curl to perform test requests:

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

It is clear that the server correctly handles requests for getting a value by key and creating/updating a record by key.
Only if the requested key does not exist, the server closes the connection without a proper response — not implemented.

The strace output matches the expected behavior. The order of system calls and their arguments are valid.

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} ---

It turned out to be not that hard to write not just something working, but a working HTTP server in assembly.
The source file size is about 300-400 lines. The executable size is only 8kb.
Of course, you shouldn’t write servers like this in practice.
But this is a great way to learn assembly and write something compact and embeddable for other tasks.

Related Content