Ask HN: What are some examples of beautiful x86 assembly code?
This got me thinking: what are some examples of high-quality and/or beautiful x86 assembly? In fact, what about for other processor families as well?
This got me thinking: what are some examples of high-quality and/or beautiful x86 assembly? In fact, what about for other processor families as well?
.loop:
xadd rax,rdx
loop .loop
Fibonacci with two instructions.It's been a long time since I've done any assembly, and that was MIPS, but I don't see how there's any sort of exit condition or anything. I'm guessing there's more to xadd than `x + y = z`?
It’s sometimes difficult to think about the assembly code in situ when you start to think about the operating system doing a ton of context switching and paging etc. in the background, which can distract your thought process from what’s right in front of you (as well as the operating system’s software interrupts / system calls on top of the basic ISA, which is another abstraction!)
Older systems had the currently running program as the entire context of the system at that point in time - in a similar way to embedded programming, which is imho a much easier realm to learn assembly in once you’ve got a bit of basic electronics under your belt!
Even in those "old" systems of single address spaces and no protection, you're constantly getting timer interrupts, interrupts for I/O, etc., which your application might not have installed its own handler for.
I suspect we’re both making a similar point in a roundabout way - the operating system is both another layer of abstraction on top of the Instruction Set, while also making the programming process for that chipset somewhat easier (providing software interrupts etc. at the expense of bare metal understanding).
My argument is loosely that modern (x64) assembly is not so much targeting hardware as it is programming into a software abstraction (the operating system).
On your second question about different loops. First, you can loop through any register via (dec <reg>; jnz <label>), thus having nested loops. Second, all valuable registers are pushed onto the stack and popped before/after a call or an interrupt, so their contents don’t change. Some registers have to be saved by the caller and some by the callee, depending on a calling convention and an actual usage. The stack is always available (rsp points to its top), so you can offload temporary values and concentrate on current evaluation, loading values on demand later.
EDIT: Looks like it is. https://www.xorpd.net/pages/x86_adventures.html
The exercises are open source and can be found here: https://github.com/xorpd/asm_prog_ex I think that more experienced developers can take the instruction set and bash through the exercises.
strlen():
LEN: MOV #-1, R0
1⊙ INC R0
TSTB (R1)+
BNE 1⊙
or take strcpy(): COPY: MOVB (R1)+, (R0)+
BNE COPY
versus the C version: void strcpy(char *s, char *t)
{
while (*s++ = *t++)
;
}
It translates to the optimal machine code verbatim. It does not require any smarts of the compiler, at the expense of understanding on the side of the programmer. This is made possible by orthogonality of the instruction set, which in less well thought out designs was hacked around with special instructions (like LDIR on Z80). The price for that is you have to make compiler optimize these situations.Page 133 http://www.atarimania.com/documents/Asm_Lang_Prog_68K_Family...
* STRLEN - RETURNS LENGTH OP NULL TERMINATED STRING IN D0
* A0 -> STRING
STRLEN: MOVE.L A0,-(SP) SAVE REG
CLR.L D0 INITIALIZE
STRLENI:TST.B (A0)+ NULL?
BEQ STRLENR YES, RETURN
ADDQ.L #1, D0 BUMB COUNT
BRA STRLENI LOOP
STRLENR:MOVE.L (SP)+,A0 RESTORE REG
RTS
We might also want to copy a string:
* STRCPY - COPY A NULL TERMINATED STRING
* A0 -> SOURCE STRING
* A1 -> DESTINATION STRING
STRCPY: MOVEM.L A0-A1,-(SP) SAVE REGS
STRCPY1:MOVE.B (A0)+,(A1)+ MOVE A BYTE
BNE STRCPY1 GET ANOTHER IF NOT NULL
MOVEM.L (SP)+/A0-A1 RESTORE REGS
RTS
Next, we will want to compare two strings:
* STRCMP - COMPARE TWO NULL TERMINATED STRINGS
* A0 -> STRING 1
* A1 -> STRING 2
STRCMP: MOVEM.L A0-A1,-(SP) SAVE REGS
STRCMP1:CMPM.B (A0)+,(A1)+ COMPARE BYTES
BNE STRRET RETURN IF DIFFERENT
TST.B -1(A0) HAVE WE HIT A NULL?
BNE STRCMP1 NOW MORE BYTES LEFT
STRRET: MOVEM.L (SP)+,A0-A1 RESTORE REGS
RTS
Although I guess I'd implement strlen more like this (not sure if it works, been a long time since I last wrote anything for 68k): strlen: movem.l a0-a1,-(sp)
move.l a0, a1 ; copy a0 to a1
slenloop: tst.b (a0)+
bne slenloop
addq.l #1, a1
sub.l a0, a1
move.l a1, d0
movem.l (sp)+,a0-a1
rts
Just two instructions in the inner loop. But not sure whether address registers supported addq etc. MOV R0, -(SP)
....
MOV (SP)+, R0
for the PDP code. > cat > test.asm
strlen: movem.l %a0-%a1,-(%sp)
movl %a0, %a1 ;# copy a0 to a1
slenloop: tst.b (%a0)+
bne.b slenloop
addq.l #1, %a1
sub.l %a0, %a1
move.l %a1, %d0
movem.l (%sp)+,%a0-%a1
rts
^D
> m68k-linux-gnu-as test.asm && m68k-linux-gnu-objdump -d a.out
a.out: file format elf32-m68k
Disassembly of section .text:
00000000 <strlen>:
0: 48e7 00c0 moveml %a0-%a1,%sp@-
4: 2248 moveal %a0,%a1
00000006 <slenloop>:
6: 4a18 tstb %a0@+
8: 66fc bnes 6 <slenloop>
a: 5289 addql #1,%a1
c: 93c8 subal %a0,%a1
e: 2009 movel %a1,%d0
10: 4cdf 0300 moveml %sp@+,%a0-%a1
14: 4e75 rts
I wonder whether it actually works... I think same idea should be translatable to PDP-11 as well.[0]: It runs slower for short strings, though. Not sure where the break even point is.
But that gave me a new idea: copy pointer to d0 and complement the difference instead of addq, saving a1 register (+stack operations) and a move-instruction:
strlen: move.l %a0,-(%sp)
movl %a0, %d0 ;# d0 = a0;
slenloop: tst.b (%a0)+ ;# test *a0, post incr
bneb slenloop ;# loop if non-zero
sub.l %a0, %d0 ;# d0 = d0 - a0;
not.l %d0 ;# d0 = ~d0;
move.l (%sp)+,%a0
rts ;# d0 is the return value
Equivalent C-code (http://cpp.sh/2oecd): #include <stdio.h>
#include <stdint.h>
size_t strlen68k(char* a0) {
int zero_flag;
uintptr_t d0 = (uintptr_t) a0;
do {
// tstb (%a0)+
if (*a0 == 0)
zero_flag = 1;
else
zero_flag = 0;
a0++; // (%a0)+ (post increment)
} while(!zero_flag); // bneb slenloop
d0 = d0 - (uintptr_t) a0; // sub.l %a0, %d0
d0 = ~d0; // not.l %d0
return d0;
}
void strlen_test(char* str) {
printf("str \"%s\" length is %lu\n", str, strlen68k(str));
}
int main(void) {
strlen_test("");
strlen_test("a");
strlen_test("abcd");
return 0;
}
Again, untested, but I think it probably works. I guess this is also faster for all string lengths >0 as well.This idea might not work on PDP-11 anymore, depending on how binary arithmetic is implemented.
Lots of fun stuff there, like a scrolling Matrix screen saver in 8 bytes (!!) that jumps into the middle of an instruction: http://www.sizecoding.org/wiki/M8trix_8b
...
and yes, I consider it beautiful x86 code :)
There was some discussion of it here a while ago: https://news.ycombinator.com/item?id=942684 (though unfortunately the site is now dead)
The first half is some fun writing with stock good advice for software development, and the second half is a bunch of code examples the author finds interesting, many of which are in various flavors of assembly.
(I have no affiliation with the author, publisher, nor any other ulterior motive. Office consensus is that this is the best book we've done a book club around - highly recommended as just plain fun to read, if you're already writing software.)
http://www.jagregory.com/abrash-black-book/#thats-nicebut-it...
It also fully avoids all branches.
https://github.com/kaneton/appendix-bios
OT: This brings back memories of tinkering with the MS-DOS boot process. Back then, the BIOS would read the MBR and copy its contents to 0x7C00 and start execution from there. So you could assemble your own code (using no less than MS-DOS debug) and plonk it into the MBR. I remember doing things like fooling the boot loader into thinking there's less ram than there actually was (639kB instead of 640kB) and using the unaccounted 1kB for placing your own code that could be triggered by a captured interrupt... Fun times!
https://github.com/leni536/fast_hilbert_curve
I'm pretty proud of it. It calculates the coordinates of the nth point on the hilbert curve. No loops, no branches. It uses the pext, pdep and popcount intrinsics creatively.
Note the '.section .rodata' directive which actually places the quads pointers, seemingly interleaved with code, in a read-only data section.
Note also the dec/test/jle instructions implementing the while loop occur before the last of the eight copy operations, and interleaved with the next-to-last copy operation.
duff:
.LFB0:
.cfi_startproc
lea eax, [rdi+7]
mov r8d, 8
mov rcx, rdx
cdq
idiv r8d
mov r9d, eax
mov eax, edi
cdq
idiv r8d
cmp edx, 7
ja .L2
mov edx, edx
jmp [QWORD PTR .L4[0+rdx*8]]
.section .rodata
.align 8
.align 4
.L4:
.quad .L3
.quad .L5
.quad .L6
.quad .L7
.quad .L8
.quad .L9
.quad .L10
.quad .L11
.text
.L11:
mov al, BYTE PTR [rsi]
inc rsi
mov BYTE PTR [rcx], al
.L10:
mov al, BYTE PTR [rsi]
inc rsi
mov BYTE PTR [rcx], al
.L9:
mov al, BYTE PTR [rsi]
inc rsi
mov BYTE PTR [rcx], al
.L8:
mov al, BYTE PTR [rsi]
inc rsi
mov BYTE PTR [rcx], al
.L7:
mov al, BYTE PTR [rsi]
inc rsi
mov BYTE PTR [rcx], al
.L6:
mov al, BYTE PTR [rsi]
inc rsi
mov BYTE PTR [rcx], al
.L5:
mov al, BYTE PTR [rsi]
dec r9d
inc rsi
test r9d, r9d
mov BYTE PTR [rcx], al
jle .L2
.L3:
mov al, BYTE PTR [rsi]
inc rsi
mov BYTE PTR [rcx], al
jmp .L11
.L2:
ret
.cfi_endproc
___edit: formatting, sigh
[0] v 7.3.0 64bit; gcc -S -Os -masm=intel
It only specifies which section that part of "code" goes into, the linker pools it all up in a binary image and fills in the address for the tags when linking as directed by the linker script[0].
[0]: https://sourceware.org/binutils/docs/ld/Simple-Example.html
disassembly of Pokémon Red/Blue
https://github.com/pret/pokered
TONC GBA Programming Principles
[1]: https://en.wikipedia.org/wiki/Zilog_Z80#Datapoint_2200_and_I...
https://realboyemulator.wordpress.com/2013/01/02/the-nintend...
http://finalpatch.blogspot.com/2014/06/dissecting-128-byte-r...
It’s ineresting that when tools are new, like ASM in 1978, they give high leverage to the first to use them. Microsoft was able to leverage a small amount of code into a world changing platform. Now it would be nearly impossible to do the same with a team the same size.
But in 2018, the nascent state of ML tools looks similar to the nascent state of programming tools in 1978. And indeed we are seeing entire companies built around relatively basic AI in the scheme of things. As first movers these companies have the same kind of leverage with respect to AI that Microsoft did to Software in the 1980s.
Perhaps in 2058 someone will share a link to a Tensorflow script and we will all marvel at its terseness and apparent simplicity.
What code are you looking at? I see "Microsoft BASIC for 6502 Original Source Code" which runs for 6955 lines. By my reasoning that is more than a hundred pages.
https://github.com/linker3000/Historic-code-PC-Pascal-and-AS...
I tried to find the book a few years back, for nostalgic reasons, but couldn't.
Kind of opposite of what you're asking for :)
mov rdx, rax
cmp rax, rcx
cmovg rax, rcx
cmovg rcx, rdxmany intros are provided with source codes, and others could easily be viewed in disassemblers.
especially look at intros by such brilliant people like Digimind and Řrřola.
https://gist.githubusercontent.com/jwieder/7e7e643cc71c81f63...
http://win32assembly.programminghorizon.com/
It was quite enlightening to see common program constructs done only in assembly.
This guy was a genius (those were the times you didn't do it for profit, it was a game). Google for zmist...