DEV Community

Leanid Herasimau
Leanid Herasimau

Posted on • Originally published at suddo.io on

I Wrote an HTTP Server in Assembly (and It Actually Works)

Today, we're doing just that. This is a wild, impractical, and incredibly fun experiment. Our mission: to build a functional HTTP server that serves a simple HTML page, using nothing but pure ARM64 assembly language on an Apple Silicon Mac. This isn't about replacing Nginx; it's about peeling back the layers of magic to understand what a web server really is.

But... Why Would Anyone Do This?

Let's be clear: you should not write your company's next microservice in assembly. This is an exercise in pure, unadulterated learning and curiosity. We're doing this to:

  • Demystify Networking: See firsthand that a web server is just a loop: accept a connection, write some text, close the connection.
  • Understand System Calls: Learn how a program asks the operating system to do things like open network ports and handle files.
  • Appreciate Abstractions: After this, you'll have a newfound respect for the frameworks that handle all this complexity for you.
  • Have Fun: Embrace the hacker spirit of getting as close to the metal as possible!

Step 1: The Blueprint - Setup and Constants

Every assembly program starts with some boilerplate. We need to declare our main function and tell the assembler which system functions we plan to use. On macOS, we don't use raw syscalls directly; instead, we call functions from the system library (libSystem), which is a more stable and portable approach.

// http_server_arm64_macos.s
.text
.globl _main

// We'll be calling these C functions from the macOS system library
.extern _socket
.extern _setsockopt
.extern _bind
.extern _listen
.extern _accept
.extern _write
.extern _close
Enter fullscreen mode Exit fullscreen mode

Next, we define some constants. These are just human-readable names for numbers that the socket API expects. You can find these values in the system headers on your Mac (e.g., in /usr/include/sys/socket.h).

// Constants (BSD/macOS values)
.equ AF_INET, 2 // Address Family: IPv4
.equ SOCK_STREAM, 1 // Socket Type: TCP
.equ SOL_SOCKET, 0xffff // Socket Level for setsockopt
.equ SO_REUSEADDR, 0x0004 // Allow reusing local addresses

.equ RESP_LEN, 172 // Total bytes in our HTTP response
.equ ADDR_LEN, 16 // sizeof(struct sockaddr_in)
.equ BACKLOG, 16 // Max pending connections for listen()
Enter fullscreen mode Exit fullscreen mode

Step 2: Opening a Line - Creating the Socket

The first real action is to ask the OS for a socket. A socket is like a file handle, but for network communication. According to the ARM64 calling convention on macOS, the first few arguments to a function are passed in registers x0, x1, x2, etc. The return value comes back in x0.

_main:
    // socket(AF_INET, SOCK_STREAM, 0)
    mov x0, #AF_INET // Argument 1: Domain (IPv4)
    mov x1, #SOCK_STREAM // Argument 2: Type (TCP)
    mov x2, #0 // Argument 3: Protocol (0 for default)
    bl _socket // Branch and Link (call the function)

    // The new socket's file descriptor is now in x0.
    // We save it in x19, a 'callee-saved' register, so it won't be overwritten.
    mov x19, x0
Enter fullscreen mode Exit fullscreen mode

Step 3: Claiming Our Turf - Binding to a Port

Now that we have a socket, we need to tell the OS, "Hey, I want this socket to listen on port 8585 for any incoming traffic." This is called binding. To do this, we need to construct a special C struct in memory called sockaddr_in.

Here’s what that struct looks like in our data section. It's a precise sequence of bytes representing our desired address (0.0.0.0) and port (8585).

// This goes in the .data section at the end of the file
.data
.align 4

// sockaddr_in for 0.0.0.0:8585
addr:
    .byte 16 // sin_len (16 bytes total)
    .byte AF_INET // sin_family (IPv4)
    .hword 0x8921 // sin_port (8585 in network byte order)
    .word 0 // sin_addr (0.0.0.0 means INADDR_ANY)
    .quad 0 // sin_zero[8] (padding)
Enter fullscreen mode Exit fullscreen mode

Notice the port: 8585 is 0x2189 in hex. We write it as 0x8921 because networks use 'big-endian' byte order, while our M1 Mac is 'little-endian'. We have to pre-swap the bytes!

With the struct defined, we can now call _bind.

    // bind(server_fd, &addr, sizeof(addr))
    mov x0, x19 // Arg 1: Our socket fd
    adrp x1, addr@PAGE // Get the high part of the address of 'addr'
    add x1, x1, addr@PAGEOFF // Add the low part to get the full address
    mov x2, #ADDR_LEN // Arg 3: The size of the struct
    bl _bind
Enter fullscreen mode Exit fullscreen mode

Step 4: The Server Loop - Listen, Accept, Write, Close

This is the heart of any server. It's an infinite loop that waits for a client, serves them, and then waits for the next one.

    // listen(server_fd, BACKLOG)
    mov x0, x19
    mov x1, #BACKLOG
    bl _listen

// This label marks the start of our infinite loop
accept_loop:
    // accept(server_fd, NULL, NULL) -> This BLOCKS until a client connects!
    mov x0, x19
    mov x1, #0
    mov x2, #0
    bl _accept
    mov x20, x0 // Save the new client_fd in x20

    // write(client_fd, response, RESP_LEN)
    mov x0, x20 // Arg 1: The client's socket
    adrp x1, response@PAGE // Get the address of our HTML response
    add x1, x1, response@PAGEOFF
    mov x2, #RESP_LEN // Arg 3: How many bytes to write
    bl _write

    // close(client_fd)
    mov x0, x20
    bl _close

    b accept_loop // Unconditional branch: Go back and wait for another client
Enter fullscreen mode Exit fullscreen mode

The final piece is the actual HTTP response we're sending. It's just a block of ASCII text in our data section, with all the required headers and our simple HTML.

// Prebuilt HTTP/1.1 response (RESP_LEN must match total bytes here)
response:
    .ascii HTTP/1.1 200 OK\r\n
    .ascii Content-Type: text/html; charset=utf-8\r\n
    .ascii Content-Length: 74\r\n
    .ascii Connection: close\r\n
    .ascii \r\n
    .ascii <!doctype html><html><body><h1>Hello from Assembler :)</h1></body></html>\n
Enter fullscreen mode Exit fullscreen mode

Putting It All Together: The Complete File

Here is the complete source code. Save it as http_server_arm64_macos.s.

// http_server_arm64_macos.s
// Minimal HTTP server on macOS ARM64 (Apple Silicon)
// Listens on port 8585 and replies with a tiny HTML page.

        .text
        .globl _main

        .extern _socket
        .extern _setsockopt
        .extern _bind
        .extern _listen
        .extern _accept
        .extern _write
        .extern _close

// ------------------------------------------------------------
// Constants (BSD/macOS values)
// ------------------------------------------------------------
        .equ AF_INET, 2
        .equ SOCK_STREAM, 1
        .equ SOL_SOCKET, 0xffff
        .equ SO_REUSEADDR, 0x0004

        .equ RESP_LEN, 172
        .equ ADDR_LEN, 16
        .equ BACKLOG, 16

// ------------------------------------------------------------
// main()
// ------------------------------------------------------------
_main:
        // socket(AF_INET, SOCK_STREAM, 0)
        mov x0, #AF_INET
        mov x1, #SOCK_STREAM
        mov x2, #0
        bl _socket
        mov x19, x0 // preserve server fd

        // setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &one, 4)
        mov x0, x19
        movz x1, #SOL_SOCKET
        mov x2, #SO_REUSEADDR
        adrp x3, one@PAGE
        add x3, x3, one@PAGEOFF
        mov x4, #4
        bl _setsockopt

        // bind(server_fd, &addr, sizeof(addr))
        mov x0, x19
        adrp x1, addr@PAGE
        add x1, x1, addr@PAGEOFF
        mov x2, #ADDR_LEN
        bl _bind

        // listen(server_fd, BACKLOG)
        mov x0, x19
        mov x1, #BACKLOG
        bl _listen

// Accept-Write-Close loop
accept_loop:
        // accept(server_fd, NULL, NULL)
        mov x0, x19
        mov x1, #0
        mov x2, #0
        bl _accept
        mov x20, x0 // client_fd

        // write(client_fd, response, RESP_LEN)
        mov x0, x20
        adrp x1, response@PAGE
        add x1, x1, response@PAGEOFF
        mov x2, #RESP_LEN
        bl _write

        // close(client_fd)
        mov x0, x20
        bl _close

        b accept_loop // handle next connection forever

// ------------------------------------------------------------
// Data
// ------------------------------------------------------------
        .data
        .align 4

// sockaddr_in for 0.0.0.0:8585
addr:
        .byte 16 // sin_len
        .byte AF_INET // sin_family
        .hword 0x8921 // sin_port (8585 in network byte order)
        .word 0 // sin_addr (0.0.0.0)
        .quad 0 // sin_zero[8]

// setsockopt value 1
one:
        .word 1

// Prebuilt HTTP/1.1 response
response:
        .ascii HTTP/1.1 200 OK\r\n
        .ascii Content-Type: text/html; charset=utf-8\r\n
        .ascii Content-Length: 74\r\n
        .ascii Connection: close\r\n
        .ascii \r\n
        .ascii <!doctype html><html><body><h1>Hello from Assembler :)</h1></body></html>\n
Enter fullscreen mode Exit fullscreen mode

Build and Run Your Creation

Open your terminal, navigate to where you saved the file, and run these commands.

1. Build the Executable

clang -o http_asm http_server_arm64_macos.s
Enter fullscreen mode Exit fullscreen mode

This command tells clang to assemble our .s file and link it against the necessary system libraries, creating an executable named http_asm.

2. Run the Server

./http_asm
Enter fullscreen mode Exit fullscreen mode

Your terminal will now hang—that's a good thing! It's blocked on the accept call, waiting for a connection. macOS might pop up a security prompt asking to allow incoming network connections; you'll need to approve it.

3. Test It!

Open a new terminal window and use curl to connect to your server.

curl http://localhost:8585
Enter fullscreen mode Exit fullscreen mode

You should see the glorious output: <!doctype html><html><body><h1>Hello from Assembler :)</h1></body></html>. You did it! You served a web page with pure assembly. To stop the server, go back to its terminal and press Ctrl+C.

Image

Welcome Back to Reality

We've journeyed to the lowest levels of software to build something we use every day. While you won't be deploying this to production, you now have a much deeper appreciation for what's happening when you type app.listen(8585) in your favorite framework. You've seen the sockets, the binding, the endless loop—the fundamental mechanics of the internet, written in the language of the machine itself. For more details on the instructions, check out the ARMv8-A Instruction Set Architecture documentation.

#arm #assembler #fun #news

Top comments (0)