back
95 comments
I'll try and get this building for OSX.

For the uninitiated, might I recommend my: http://nickdesaulniers.github.io/blog/2014/04/18/lets-write-...

Though, this is written in yasm syntax, which is slightly different.

Also, keep an eye out for a blog post on Interpreters, Compilers, and JITs I'm working on (cleaning it up and getting it peer reviewed this or next week)!

update 1 Actually, would the syscall's be different between Linux and OSX? Let's find out, once this builds! hammers away

update 2 Got it building and linking. bus error when run, debugging with gdb.

update3 Can't generate dwarf2 debug symbols for OSX? $ yasm -g dwarf2

update 4 Careful, this tries to listen on port 80 [0] (0x5000 (LE) == 5*16^1 == 80), I would never run any assembly program off the web with elevated privileges. I recommend 0xB8B0 (LE, port 3000).

update 5

> Actually, would the syscall's be different between Linux and OSX?

Looks like yes: http://unix.stackexchange.com/a/3350 These might be close to shim out (OSX and Linux at least share a calling convention, unlink Windows). I'll upstream what I have.

[0] https://github.com/nemasu/asmttpd/blob/master/main.asm#L24

Freudian slip?

unlink(Windows) indeed...

It's been closer to 20 years since I last read a complete program in x86 assembly, so this is quite fun to look at.

I'm somehow disappointed (quite unreasonably, of course) that the code uses plain old zero-terminated C strings instead of something more exotic. One of the fun things about assembly is that you get to reinvent basic language features on the fly -- calling conventions, data layout, strings, everything.

It needs to do so to interoperate with the OS, so using those avoids having multiple conventions and converting between them.
AFAIK Linux doesn't use zero-terminated strings anywhere in its syscalls, or at least not in those like write, where you pass a size alongside the buffer.
However (almost?) all syscalls dealing with filesystem paths take null-terminated strings. See for example the implementation of the open() syscall:

https://github.com/torvalds/linux/blob/fb65d872d7a8dc629837a...

(Hence the need for the strncpy_from_user()-function: https://github.com/torvalds/linux/blob/fb65d872d7a8dc629837a...)

write() syscall writes a sequence of bytes (not string) therefore cannot use zero-terminated convention.
I'm surprised it doesn't do length-prefixed strings with a null terminator anyways. Makes a whole lot of things easier.
Out of curiosity What would you have done for strings?
Well, for a HTTP server, I don't have a specific idea... But in general, the fun part would be trying to come up with string representations that are optimized for the particular application.

The original 1984 Elite computer game is famous for its huge galaxy full of planets. Each of them had individual names and descriptions such as "Lave is most famous for its vast rain forests and the Laveian tree grub."

Yet those strings were never stored as plain strings. The game had to run in 32kB of memory, so almost all strings were stored in a tokenized form and expanded using a pseudo-random number generator:

http://wiki.alioth.net/index.php/Random_number_generator

That article shows how the planet description strings were stored and reconstructed on the fly. The base representation for the aforementioned description of planet Lave was only a handful of bytes: "\x8F is \x97"

So I think Elite is a pretty good example of an application written in assembly that didn't have anything like a generic string type.

As others have said, have a length field in with the string. This also has advantages other than making buffer overflows a lot harder, such as making string copying faster and easier. For example, let's have a skeletonized view of a normal string copy routine in assembly (disclaimer: my assembly is rusty, so this may not be completely right. void where prohibited):

        push rax ; save our registers
        push rdi
        push rsi
        mov rsi, location ; get the pointer to the right place
	mov rdi, destination
		
    beginning:
        mov rax, [rsi] ; copy contents to the register so we can compare
       test rax, rax ; compare our source to itself, if it's zero, it'll set a flag
       jz done ; we're done
       movsb ; copy the byte, increment the rsi and rdi registers
      jmp beginning
	
    done:
        pop rsi ; restore our registers
        pop rdi
        pop rax
		
In contrast, with a length parameter, we can do

        push rcx ; using a different register here
        push rdi
        push rsi
        mov rsi, location ; same as before
        mov rcx, length ; moving our length into the counter register
        mov rdi, destination

        cld ; okay our first change. Clearing the direction flag so the copy
            ; goes from the first byte to the end
        rep movsb ; it'll repeat cx times the movsb command, and then carry on
	    
        pop rsi ; restoring our registers
        pop rdi
        pop rcx
Having the length of strings means you can have much more concise code. It makes loops easier, makes your code cleaner, and in some environments, gives a speed boost.
Turbo/Borland Pascal used a NUL-optional string format of length (byte IIRC) followed by data. [0]

Java IIRC also uses a length-oriented format for string constants in .class files. [1] It's been a while since I wrote a .java to MIPS asm compiler in C++ from scratch (don't ask).

This is because real-world strings may contain 0 to N NULs and escaping them is too much of a PITA for serialized formats, so it's easier and common to do things like TYPE LENGTH DATA de/serialization. For modern, efficient binary de/ser, check out binc and msgpack [2,3].

0: http://math.uww.edu/~harrisb/courses/cs171/strings.html

1: https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.ht...

2: http://msgpack.org/

3: https://github.com/ugorji/binc/blob/master/SPEC.md

Could have a pointer, length pair. That's how it's done in some non-C languages.
I remember one program that had a string print subroutine that took zero args. It just used the return address from the stack to grab the nul-terminated string immediately following the JSR/CALL instruction. It then patched the return address on stack to return just after the nul.

Bad for storing data in .text, but still a neat hack that shaved whole tens of bytes off the program size.

It needs to be security-hole compatible.
I wrote httpdito, a web server for Linux in 386 assembly, a couple of years ago (mostly outdated discussion is at https://news.ycombinator.com/item?id=6908064; a README is at http://canonical.org/~kragen/sw/dev3/httpdito-readme) and I was happy to get the executable under 2000 bytes. I actually used it the other day to test a SPA, although for some things its built-in set of MIME-types leaves something to be desired.

But it doesn't have default documents, different kinds of error responses, TCP_CORK, sendfile() usage, content-range handling, or even request logging. So asmttpd is way more full-featured than httpdito, and it's still under 6K.

(...httpdito possibly doesn't have any bugs, either, though ☺)

Days ago I saw a lightweight httpd written in C here, yesterday a C++ header-only httpd library caught my mind, and now an httpd in assembly. I'm curious what would come next...
Oh, I bet someone writes httpd in vhdl or verilog... Unbeatable header parsing time I'm sure.

Or maybe in CSS. https://news.ycombinator.com/item?id=9567183

> Oh, I bet someone writes httpd in vhdl or verilog

I would love to see that.

Probably one written in JavaScript, I'm guessing.
That already exists, Node.js.
Shameless plug for a companion IRC bot in ARM assembly: https://github.com/wyc/armbot
The name is confusing, the first thing i thought of was an SMTP-server.
A little strange to see: "Sendfile can hang if GET is cancelled." in the readme and no corresponding issue. Not even one closed as "wontfix". Sounds like DOS?
Why would someone write a web server in assembly? just for fun?
Exactly. Because why not?
I have a few devices with 4 to 16mb of flash - an 8kb web server would be very useful*

*Granted, it's the wrong arch for those, MIPS would help me.

Probably to lower overhead associated with C language features, similar to the reason why many people write things in C instead of a higher level language.
To avoid overhead people implement specialized compilers suited for the task at hand. If anything, going down to C, and especially assembly will hurt performance as low-level code is much harder to optimize for obvious reasons. Above all of that real-world performance comes from proper system-level design, not micro-optimizations, and using a low-level language (be it C, C++ or assembly) will prevent one from quickly iterating over different ideas.
Maybe it's just me, but I honestly don't know what this is doing near the HN top. It's more or less literal translation from C.
Possibly because it illustrates that using assembly isn't necessarily the insane scary idea that it first seems to many (like me), even to those that should know better because they have used it in the past (like me).
No, a literal translation is what you get when you write a http server in C and inspect what assembly code it produces for x86.64. Since this assembly code is nowhere close to that output it is not a literal translation.
Only if you use a completley naive compiler, any level of optimisation moves from being a literal translation
No dependencies at all, runs on Docker 'FROM scratch', Nice! - https://registry.hub.docker.com/u/0xff/asmttpd/
I propose that a web framework be called Assembly on Ambulator.
Given that it's so small, 6k, I'd called the framework based on this Assembly on Alleys.

It could totally implement a DSL... call it "C" for convenience, that generates the required assembly code :-).

> call it "C" for convenience

As distinct from C, I take it.

benchmarks?

:-)

Just because it is in ASM, doesn't mean an exact equivalent in C won't smoke it performance wise. Just sayin... Aside of intellectual curiosity, one would be very hard pressed to write ASM code that is even barely more efficient than C code generated by a decent compiler, i.e. clang, or Intel C....
Just because it's asm doesn't mean it's fast, sure. Just because it's C doesn't mean it's faster than an implementation in your favourite scripting language.

Having said that, compilers do pretty badly on C-to-simd optimisation. The best you get is loop vectorization if it's really simple logic. You can usually get some pretty good wins there. The fact you lay out your memory for simd usually is a win all of its own due to cache prefetching even if you don't actually use any simd instructions. Compilers need heuristics to manage cache when you know what you're trying to do, (eg when should it use non-temporal writes, for example?) Fast C code is written while having a really clear mental model of the underlying architecture and the assembly that the C will produce with -O3 (or whatever flag is relevant to your compiler) and then checked with -S or objdump -D, profiled with callgrind/cachegrind, perf, rdtsc etc...

The compiler really can't "Do it for you" You /can/ use a compiler as one of your tools when /you/ do it. As Randy Hyde points out you can always beat the compiler because you can use its generated assembly language in every case you can't beat, so the absolute worst you get is a tie.

So yeah, you can totally smoke clang, Intel, microsoft and gnu C compiler and get paid something for doing it in certain industries too. :-)

Mike Acton being aggressively opinionated on the subject, but the lecture is really good (despite/because of) the bits you'll disagree with and the manner he'll rub you the wrong way. https://www.youtube.com/watch?v=rX0ItVEVjHc

That's often true. Really often true. But sometimes, a GOOD assembly programmer can beat a C compiler by a LOT. https://news.ycombinator.com/item?id=8508923 "Hand Coded Assembly Beats Intrinsics in Speed and Simplicity"
something like "wrk -d 10 -c 1000 -t 4 http://127.0.0.1/byte.txt (one byte file) did:

Requests/sec: 100.00 Transfer/sec: 11.91KB

for a JPEG image (200KB) the results are similar:

Requests/sec: 99.86 Transfer/sec: 19.27MB

Doesn't look like it supports HTTP pipelining:

    [trent@ubuntu/ttypts/4(~s/wrk)%] ./wrk -c 1 -t 1 --latency -d 5 http://localhost:8080/Makefile 
    Running 5s test @ http://localhost:8080/Makefile
      1 threads and 1 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency    35.00us    0.00us  35.00us  100.00%
        Req/Sec    10.00      0.00    10.00    100.00%
      Latency Distribution
         50%   35.00us
         75%   35.00us
         90%   35.00us
         99%   35.00us
      1 requests in 5.10s, 1.57KB read
    Requests/sec:      0.20
    Transfer/sec:     314.55B
Note the 1 request. For 1000 clients, it's only doing 1000 requests:

    [trent@ubuntu/ttypts/4(~s/wrk)%] ./wrk -c 1000 -t 1 --latency -d 5 http://localhost:8080/Makefile
    Running 5s test @ http://localhost:8080/Makefile
      1 threads and 1000 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency   307.93ms  552.88ms   1.63s    87.10%
        Req/Sec   414.29    439.07     1.34k    85.71%
      Latency Distribution
         50%    4.11ms
         75%  407.63ms
         90%    1.63s
             99%    1.63s
      1000 requests in 5.01s, 1.53MB read
      Socket errors: connect 0, read 41, write 0, timeout 0
    Requests/sec:    199.79
    Transfer/sec:    312.96KB
this is really really fast.
What's the performance like?
Amazing!