Web Server in Assembly

1 Goal
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.
2 Sample Implementation
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 communicationbind
- associates the socket with an IP address and portlisten
- 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 afterfork
(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.
- Parse the
- 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.
- Extract
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)
}
}
3 Assembly
3.1 What is needed to perform a system call in assembly?
- 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 inRAX
.
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
3.2 How to store local variables on the stack?
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
4 Implementation
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
5 Testing
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} ---
6 Useful links
- Linux system call information for various architechtures
- Code Browser for C, C++, Rust & Dart
- fork(2) — Linux manual page
- Building a Web Server - Computing 101 - pwn.college
7 Conclusions
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.