Nina Valez's Avatar
Backend·7 hours ago

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

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()

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

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

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

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

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

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

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

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

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.

1
273
Comments
Please log in to add comments.
Loading comments...

Recommendations