This commit is contained in:
2025-07-05 16:25:01 +02:00
commit 6d9d52e31c
2 changed files with 332 additions and 0 deletions

18
README.md Normal file
View File

@@ -0,0 +1,18 @@
# webserver
Written as a tiny project to learn more about 64-bit x86 assembly and the Linux kernel ABI. The syntax here is Intel for GNU AS. The code is public domain.
### Build process
```
as -g server.s -o server.o && ld server.o -o exe
sudo ./exe
```
### Testing
```
nc localhost 80
GET /anything HTTP/1.0
POST /anything HTTP/1.0
```

314
server.s Normal file
View File

@@ -0,0 +1,314 @@
# HTTP GET/POST concurrent webserver targeting Linux ABI for x86_64 processors
# This code is public domain
# Author: xamidev
.intel_syntax noprefix
.global _start
.equ AF_INET, 2
.equ SOCK_STREAM, 1
.equ NULL, 0
.equ O_RDONLY, 0
.equ O_WRONLY, 1
.equ O_CREAT, 64
.section .text
_start:
# write(unsigned int fd, char* buf, size_t count)
mov rax, 1
mov rdi, 1
lea rsi, [rip + str_welcome]
mov rdx, 24
syscall
# socket(AF_INET, SOCK_STREAM, NULL)
mov rax, 41
mov rdi, AF_INET
mov rsi, SOCK_STREAM
mov rdx, NULL
syscall
# Save sockfd somewhere
mov r8, rax
# bind(int sockfd, struct sockaddr_in*, int addrlen)
mov rax, 49
mov rdi, r8
lea rsi, [rip + sockaddr_in]
mov rdx, 16
syscall
# listen(int sockfd, int backlog)
mov rax, 50
mov rdi, r8
mov rsi, 0
syscall
.accept_incoming_connections:
# accept(int sockfd, struct sockaddr_in*, int* addrlen)
mov rax, 43
mov rdi, r8
mov rsi, NULL
mov rdx, NULL
syscall
# Save sockfd to close it later
mov r14, rax
# fork() -> new child process for incoming connection; PID in rax
mov rax, 57
syscall
# If return value is zero, we're in the child process: we can process the connection
# If return value is non-zero, we're in the parent process: we have to get back to accepting incoming connections
test rax, rax
jz .continue
jmp .close_actual_sockfd
.close_actual_sockfd:
# close(unsigned int fd)
mov rax, 3
mov rdi, r14
syscall
jmp .accept_incoming_connections
.continue:
# close(unsigned int fd)
mov rax, 3
mov rdi, r8
syscall
# read(unsigned int fd, char* buf, size_t count)
mov rax, 0
mov rdi, r14
lea rsi, [rip + request_buf]
mov rdx, 1024
syscall
# Save read bytes (full request size)
mov r13, rax
.parse_http_path:
# Parse the HTTP request
# When one space is found, extract bytes until next space
lea rsi, [rip + request_buf]
xor rcx, rcx
.find_start:
mov al, byte [rsi + rcx]
cmp al, ' '
je .path_start
inc rcx
jmp .find_start
.path_start:
inc rcx
lea rdi, [rsi+rcx]
xor rdx, rdx
.path_loop:
mov al, byte [rdi + rdx]
cmp al, ' '
je .path_done
mov byte [path_buf + rdx], al
inc rdx
jmp .path_loop
.path_done:
# Detect if request is GET or POST (first char is G: GET, else POST)
.parse_get_post:
lea rsi, [rip + request_buf]
mov al, byte [rsi]
cmp al, 69 # 'E' (2nd letter, rsi+1)
je .get_request
jmp .post_request
.post_request:
# open(const char* filename, int flags, int mode)
mov rax, 2
lea rdi, [path_buf+1]
mov rsi, O_WRONLY | O_CREAT # O_RDONLY for GET requests
mov rdx, 0x1FF # All permissions
syscall
# Save fd
mov r10, rax
.parse_request_content:
# When we find \r\n\r\n we know that the content follows.
# request_buf already contains full request.
lea rsi, [rip + request_buf]
xor rcx, rcx
.find_body:
mov al, byte [rsi+rcx]
cmp al, 13 # \r
jne .next
mov al, byte [rsi+rcx+1]
cmp al, 10 # \n
jne .next
mov al, byte [rsi+rcx+2]
cmp al, 13
jne .next
mov al, byte[rsi+rcx+3]
cmp al, 10
jne .next
add rcx, 4
jmp .body_found
.next:
# Threshold to avoid infinite looping if request is malformed
inc rcx
cmp rcx, 512
jl .find_body
jmp .error_exit
.body_found:
lea rdi, [rsi+rcx]
lea rsi, [rip+request_content_buf]
mov rbx, r13
sub rbx, rcx
mov r13, rbx
# Byte amount of request content is now in r13
# Now we copy this to a buffer
xor rcx, rcx
.content_copy_loop:
cmp rcx, r13
jge .done_copy
mov al, byte [rdi+rcx]
mov byte [rsi+rcx], al
inc rcx
jmp .content_copy_loop
.done_copy:
dec r13
# Byte length in r13 (decrement cause of null-byte)
# Content in request_content_buf
# Here, as we have a POST request, we will have to write to filename instead of reading from it.
# write(unsigned int fd, const char* buf, size_t count)
mov rax, 1
mov rdi, r10
lea rsi, [rip + request_content_buf+1] # ADDRESS OF REQUEST CONTENT BUFFER
mov rdx, r13 # AMOUNT OF BYTES IN REQUEST CONTENT
syscall
# Save read bytes
mov r15, rax
# close(unsigned int fd)
mov rax, 3
mov rdi, r10
syscall
# We simply return a 200 OK here
# write(unsigned int fd, const char* buf, size_t count)
mov rax, 1
mov rdi, r14
lea rsi, [rip + response]
mov rdx, 19
syscall
# close(unsigned int fd)
mov rax, 3
mov rdi, r14
syscall
jmp .normal_exit
.get_request:
# open(const char* filename, int flags, int mode)
mov rax, 2
lea rdi, [path_buf+1]
mov rsi, O_RDONLY
mov rdx, NULL # who cares?
syscall
# Save fd
mov r10, rax
# read(unsigned int fd, char* buf, size_t count)
mov rax, 0
mov rdi, r10
lea rsi, [rip + file_buf]
mov rdx, 1024
syscall
# Save read bytes
mov r15, rax
# close(unsigned int fd)
mov rax, 3
mov rdi, r10
syscall
# write(unsigned int fd, const char* buf, size_t count)
mov rax, 1
mov rdi, r14
lea rsi, [rip + response]
mov rdx, 19
syscall
# write(unsigned int fd, const char* buf, size_t count)
mov rax, 1
mov rdi, r14
lea rsi, [rip + file_buf]
mov rdx, r15
syscall
# close(unsigned int fd)
mov rax, 3
mov rdi, r14
syscall
.normal_exit:
# exit(0)
mov rax, 60
mov rdi, 0
syscall
.error_exit:
mov rax, 60
mov rdi, -1
syscall
.section .data
response:
.asciz "HTTP/1.0 200 OK\r\n\r\n"
str_welcome:
.asciz "Starting HTTP server...\n"
# Total size: 16 bytes
sockaddr_in:
.word AF_INET
# Port 80 = 0x0050 (unsigned short, 2 bytes)
# Big endian: bytes are in reverse order
.word 0x5000
# 32-bits of zeroes: 0.0.0.0
.int 0x00000000
.zero 8
.section .bss
path_buf:
.zero 64
request_buf:
.zero 1024
file_buf:
.zero 1024
stat_buf:
.zero 1024
request_content_buf:
.zero 1024