Examining the machine code of a 100KB HTTP server — freelang.dev
binary archaeology · freedomlang
Examining the machine code of a 100 KB HTTP server
A 182-line FreedomLang program becomes a 96,250-byte Linux executable, every byte of which the compiler put there itself. Small enough to read, so let's read it.
cris · dosaygojuly 2026x86-64 · linux · elf~15 min
The lead demo in the freelang repo is a little HTTP/1.1 server, examples/httpd.flx, 182 lines of FreedomLang. My compiler is an ordinary JavaScript program, and when it compiles those lines for Linux it writes out a 96,250-byte executable that answers real curls with no libc, no C runtime, and no dynamic loader anywhere in the building.
A binary that small, with nothing smuggled in from shared libraries, is a binary a person can sit down and actaully read, which is a rare pleasure these days, so that is what this post does. We decode the file format by hand, watch the process get born, follow one GET /health through every system call it makes, and then watch the error machinery fire when a client connects and goes quiet. Every trace, listing, and number below came out of commands you can re-run yourself; I put them at the end.
96,250<br>bytes on disk
21<br>syscalls, boot → serve → exit
dynamic libraries
lines of /proc//maps
§1The specimen
Here is the part of the source that does the serving. If you have never seen FreedomLang before: op declares an operator, square brackets call one, => returns early, and with chaos { … } is how I model world failures — timeouts, closed peers, the weather of networking — as plain data your code matches on. Hold onto that; in §5 you get to see it as machine code.
examples/httpd.flxlines 33–53 · the connection handler<br>op handle_conn [sock] (<br>let req = http_read_request[sock, 1024, 3000] with chaos {<br>Timeout(_) => -2<br>Closed(_) => -3<br>_ => -5<br>};<br>if (req == -2) (<br>print " recv timeout";<br>tcp_close[sock] with chaos { _ => 0 };<br>=> 2;<br>if (req == -3) (<br>print " client closed early";<br>tcp_close[sock] with chaos { _ => 0 };<br>=> 3;<br>if (req == -5) (<br>print " recv error";<br>tcp_close[sock] with chaos { _ => 0 };<br>=> 5;
Routing reads the same way, match the request bytes, pick a response:
examples/httpd.flxlines 22–30<br>op pick_response [req] (<br>if (http_is_get[req, "/health"] == 1) (<br>=> http_ok_text_str["ok"];<br>if (http_is_get[req, "/"] == 1) (<br>=> http_ok_html_str[home_body[]];<br>=> http_not_found_text_str["not found"];
Compile it and poke it:
shellUbuntu 24.04 · x86-64<br>$ node freelang.js examples/httpd.flx httpd.elf --target=linux --emit=bin<br>$ ./httpd.elf 8080 &<br>httpd listening on 127.0.0.1:8080<br>$ curl -i http://127.0.0.1:8080/health<br>HTTP/1.1 200 OK<br>Content-Type: text/plain; charset=utf-8<br>Connection: close<br>Content-Length: 2
ok
Watch what that compile step never ran: no as, no ld, no cc. One Node process read the .flx and wrote a finished executable, and how it manages that is a whole story of its own, which I tell in the companion post. Today we read what it wrote.
§2120 bytes of paperwork, then code
Every Linux executable opens with an ELF header, and most binaries follow it with a small bureaucracy: program headers for a dozen segments, dynamic-linking tables, symbol tables, twenty-odd sections of varying dignity. I kept the paperwork to the legal minimum. Here are the first 208 bytes of the file:
xxd httpd.elffirst 208 bytes<br>00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000 .ELF............<br>00000010: 0200 3e00 0100 0000 7800 4000 0000 0000 ..>.....x.@.....<br>00000020: 4000 0000 0000 0000 af76 0100 0000 0000 @........v......<br>00000030: 0000 0000 4000 3800 0100 4000 0300 0200 ....@.8...@.....<br>00000040: 0100 0000 0700 0000 0000 0000 0000 0000 ................<br>00000050: 0000 4000 0000 0000 0000 4000 0000 0000 ..@.......@.....<br>00000060: fa77 0100 0000 0000 fa77 0100 0000 0000 .w.......w......<br>00000070: 0010 0000 0000 0000 488b 0424 4889 05c5 ........H..$H...<br>00000080: 7001 0048 89c2 488d 4424 0848 8905 be70 p..H..H.D$.H...p<br>00000090: 0100 4883 c201 48c1 e203 4801 d048 8905 ..H...H...H..H..<br>000000a0: b470 0100 4889 25d5 7001 00e8 1cc9 0000 .p..H.%.p.......<br>000000b0: e87c c800 00e8 0c00 0000 4831 ff48 c7c0 .|........H1.H..<br>000000c0: 3c00 0000 0f05 5548 89e5 4881 ecb0 2900
Offset 0x00: 7f 45 4c 46 — the ELF magic, then class 2 (64-bit) and endianness 1 (little). Offset 0x18: 78 00 40 00 — the entry point, 0x400078. Offset 0x38: 01 00 — one program header; 40 00 03 00 — three sections.
The entry point is 0x400078, the file loads at virtual address 0x400000, and 0x78 is 120, so the arithmetic tells you everything about the layout: 64 bytes of ELF header, 56 bytes of program header, and then machine code, immediately. Byte 121 of the file is already an instruction. There is no padding, no build-id note, no interpreter path pointing at ld-linux, because nothing here needs interpreting.
readelf confirms how little is going on:
readelf -l httpd.elfcaptured on Ubuntu 24.04<br>Elf file type is EXEC (Executable file)<br>Entry point 0x400078<br>There...