From d113cac63165dd7997d8b6f08190efdd2c184c94 Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Mon, 10 Oct 2016 23:52:15 -0400 Subject: [PATCH 1/2] WIP --- _drafts/quirky-rpn.md | 385 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 385 insertions(+) create mode 100644 _drafts/quirky-rpn.md diff --git a/_drafts/quirky-rpn.md b/_drafts/quirky-rpn.md new file mode 100644 index 0000000..67400d7 --- /dev/null +++ b/_drafts/quirky-rpn.md @@ -0,0 +1,385 @@ +--- +title: A Quirky RPN Calculator JIT +--- + +This is the kind of thing +I enjoy tinkering with. +It's in my [dotfiles][jrp.c] repository, +and here it is +in literate programming style. + +[jrp.c]: https://github.com/programble/dotfiles/blob/b7ce8a5/.bin/jrp.c + + #if 0 + exec cc -Wall -Wextra -pedantic $@ $0 -ledit -o $(dirname $0)/jrp + #endif + +First is a little trick +to have a self-contained build script. +The `jrp.c` file is executable, +and the system shell will interpret it. +`#if 0` causes this +to be compiled out of the C code, +and is just a comment in shell. +`exec` causes the shell process +to be replaced by the C compiler, +so it never gets to any of +the following C code. + + #include + #include + #include + #include + #include + #include + #include + #include + +Of note are the header files +`err.h`, `sysexits.h`, and `histedit.h`. +The first provides the `err` function, +useful for exiting on errors. +The second provides conventional exit codes +for various conditions. +The last is the header file of [libedit][libedit], +a BSD-licensed Readline alternative. + +[libedit]: http://thrysoee.dk/editline/ + + typedef unsigned long qop; + typedef unsigned int dop; + +These are the two JIT operation types, +named for "quad word" and "double word." +The JIT generates +fixed-width operations of 8 bytes, +sometimes combining two operations of 4 bytes. + + typedef long qvalue; + typedef int dvalue; + typedef unsigned long uvalue; + +The values on the RPN stack +are signed 64-bit values. +For pushing small values, +the JIT will generate a 32-bit immediate. +The unsigned variation is for use +in binary formatting, +where arithmetic shifts must be avoided. + + typedef qvalue *(*jitFn)(qvalue *); + typedef void (*rtFn)(qvalue); + +The `jitFn` function pointer type +is the signature of code generated by the JIT. +It takes the RPN stack pointer +as a parameter, +and returns the new stack pointer. + +The `rtFn` type is the signature of C "runtime" functions, +which take a value from the stack +as a parameter +and cause side-effects. + + static char *formatBin(qvalue val) { + static char buf[sizeof(qvalue) * 8 + 1]; + uvalue uval = val; + size_t i = sizeof(buf); + do { + buf[--i] = '0' + (uval & 1); + } while (uval >>= 1); + return &buf[i]; + } + +C doesn't provide a binary format specifier, +so this is a function that does it. +It writes into a static buffer +large enough to hold each bit +and the null terminator. +This is fine because +there is no concurrency +and its return value is always used immediately. + + static void rtPrintAscii(qvalue val) { + printf("%c\n", (char)val); + } + static void rtPrintBin(qvalue val) { + printf("%s\n", formatBin(val)); + } + static void rtPrintOct(qvalue val) { + printf("%lo\n", val); + } + static void rtPrintDec(qvalue val) { + printf("%ld\n", val); + } + static void rtPrintHex(qvalue val) { + printf("%lx\n", val); + } + +This is the set of runtime functions, +i.e. C code called by the JIT compiled code. +They are just used for printing values. + + static const dop DOP_NOP = 0x90666666; // nop + +Now it gets interesting. +These are the half-size operations: +x86_64 machine code represented +as hexadecimal integers. +These are sort of backwards, +due to endianness, +so this would normally be written `66 66 66 90`, +a 4-byte no-op. + +I generated these opcodes with [NASM][nasm] +and inspected the output with [xx][xx], +my personal hexdump tool. + +[nasm]: http://nasm.us +[xx]: https://github.com/programble/dotfiles/blob/master/.bin/xx.c + + static const dop DOP_PUSH = 0xc7c74857; // push rdi; mov rdi, strict dword 0 + +A 4-byte push. +In the generated code, +this will be followed by a 4-byte immediate value. +The code actually keeps +the top value of the stack in `rdi`, +which is the register used for +passing the first function argument +in the [System V ABI for x86_64][sysvabi]. +I use the `strict dword` specifier here +to prevent NASM from optimizing +the immediate down to only a byte. + +[sysvabi]: https://software.intel.com/sites/default/files/article/402129/mpx-linux64-abi.pdf + + static const dop DOP_DROP = 0x9066665f; // pop rdi + static const dop DOP_DUP = 0x90666657; // push rdi + static const dop DOP_SWAP = 0x243c8748; // xchg rdi, [rsp] + +DROP pops the value on top of the real stack into `rdi`, +discarding the previous value of `rdi`. +DUP pushes the value in `rdi` onto the actual stack, +duplicating it. +SWAP swaps the value in `rdi` +with the value pointed to by `rsp`, +i.e. the top of the real stack. + + static const dop DOP_NEG = 0x90dff748; // neg rdi + static const dop DOP_ADD = 0xc7014858; // pop rax; add rdi, rax + static const dop DOP_QUO = 0x90c78948; // mov rdi, rax + static const dop DOP_REM = 0x90d78948; // mov rdi, rdx + static const dop DOP_NOT = 0x90d7f748; // not rdi + static const dop DOP_AND = 0xc7214858; // pop rax; and rdi, rax + static const dop DOP_OR = 0xc7094858; // pop rax; or rdi, rax + static const dop DOP_XOR = 0xc7314858; // pop rax; xor rdi, rax + + static const qop QOP_PROL = 0x5ffc8948e5894855; // push rbp; mov rbp, rsp; mov rsp, rdi; pop rdi + static const qop QOP_EPIL = 0x5dec8948e0894857; // push rdi; mov rax, rsp; mov rsp, rbp; pop rbp + static const qop QOP_RET = 0x90666666906666c3; // ret + static const qop QOP_CRT = 0xb848906690e58748; // xchg rsp, rbp; mov rax, strict qword 0 + static const qop QOP_CALL = 0x90665fe58748d0ff; // call rax; xchg rsp, rbp; pop rdi + static const qop QOP_PUSH = 0xbf48906690666657; // push rdi; mov rdi, strict qword 0 + static const qop QOP_SUB = 0x9066665f243c2948; // sub [rsp], rdi; pop rdi + static const qop QOP_MUL = 0x906666f8af0f4858; // pop rax; imul rdi, rax + static const qop QOP_DIV = 0x9066fff748994858; // pop rax; cqo; idiv rdi + static const qop QOP_SHL = 0x5f2424d348f98948; // mov rcx, rdi; shl qword [rsp], cl; pop rdi + static const qop QOP_SHR = 0x5f242cd348f98948; // mov rcx, rdi; shr qword [rsp], cl; pop rdi + + static int radix = 10; + + static struct { + qop *base; + qop *ptr; + dop dop; + } code; + + static struct { + qvalue *base; + qvalue *limit; + qvalue *ptr; + } stack; + + static void jitDop(dop op) { + if (code.dop) { + *code.ptr++ = (qop)op << 32 | code.dop; + code.dop = 0; + } else { + code.dop = op; + } + } + + static void jitQop(qop op) { + if (code.dop) jitDop(DOP_NOP); + *code.ptr++ = op; + } + + static void jitPush(qvalue imm) { + if ((dvalue)imm == imm) { + jitQop(DOP_PUSH | (qop)imm << 32); + } else { + jitQop(QOP_PUSH); + jitQop((qop)imm); + } + } + + static void jitCall(rtFn fn) { + jitQop(QOP_CRT); + jitQop((qop)fn); + jitQop(QOP_CALL); + } + + static void jitBegin(void) { + code.ptr = code.base; + jitQop(QOP_PROL); + } + + static void jitEnd(void) { + jitQop(QOP_EPIL); + jitQop(QOP_RET); + } + + static void jitInit(void) { + code.base = mmap(0, getpagesize(), PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0); + if (code.base == MAP_FAILED) err(EX_OSERR, "mmap"); + } + + static void stackInit(void) { + stack.base = mmap(0, 2 * getpagesize(), PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0); + if (stack.base == MAP_FAILED) err(EX_OSERR, "mmap"); + stack.limit = stack.base + getpagesize() / sizeof(qvalue); + stack.ptr = stack.limit; + } + + static void jitExec(void) { + int error; + error = mprotect(code.base, getpagesize(), PROT_READ | PROT_EXEC); + if (error) err(EX_OSERR, "mprotect"); + stack.ptr = ((jitFn)code.base)(stack.ptr); + if (stack.ptr > stack.limit) stack.ptr = stack.limit; + error = mprotect(code.base, getpagesize(), PROT_READ | PROT_WRITE); + if (error) err(EX_OSERR, "mprotect"); + } + + static void jitSrc(const char *src) { + bool quote = false; + while (*src) { + if (quote) { + jitPush(*src++); + quote = false; + continue; + } + + switch (*src) { + case ' ': break; + case 39: quote = true; break; + case 'B': radix = 2; break; + case 'O': radix = 8; break; + case 'D': radix = 10; break; + case 'X': radix = 16; break; + case ';': jitDop(DOP_DROP); break; + case ':': jitDop(DOP_DUP); break; + case 92: jitDop(DOP_SWAP); break; + case '_': jitDop(DOP_NEG); break; + case '+': jitDop(DOP_ADD); break; + case '-': jitQop(QOP_SUB); break; + case '*': jitQop(QOP_MUL); break; + case '/': jitQop(QOP_DIV); jitDop(DOP_QUO); break; + case '%': jitQop(QOP_DIV); jitDop(DOP_REM); break; + case '~': jitDop(DOP_NOT); break; + case '&': jitDop(DOP_AND); break; + case '|': jitDop(DOP_OR); break; + case '^': jitDop(DOP_XOR); break; + case '<': jitQop(QOP_SHL); break; + case '>': jitQop(QOP_SHR); break; + case ',': jitCall(rtPrintAscii); break; + case '.': switch (radix) { + case 2: jitCall(rtPrintBin); break; + case 8: jitCall(rtPrintOct); break; + case 10: jitCall(rtPrintDec); break; + case 16: jitCall(rtPrintHex); break; + } break; + + default: { + char *rest; + qvalue val = strtol(src, &rest, radix); + if (rest != src) { + src = rest; + jitPush(val); + continue; + } + } + } + + src++; + } + } + + static void jitDump(const char *path, FILE *file) { + size_t nitems = code.ptr - code.base; + size_t written = fwrite(code.base, sizeof(qop), nitems, file); + if (written < nitems) err(EX_IOERR, "%s", path); + } + + static char *prompt(EditLine *el __attribute((unused))) { + static char buf[4096]; + char *bufPtr = buf; + for (qvalue *stackPtr = stack.limit - 1; stackPtr >= stack.ptr; --stackPtr) { + size_t bufLen = sizeof(buf) - (bufPtr - buf) - 2; + switch (radix) { + case 2: bufPtr += snprintf(bufPtr, bufLen, " %s", formatBin(*stackPtr)); break; + case 8: bufPtr += snprintf(bufPtr, bufLen, " %lo", *stackPtr); break; + case 10: bufPtr += snprintf(bufPtr, bufLen, " %ld", *stackPtr); break; + case 16: bufPtr += snprintf(bufPtr, bufLen, " %lx", *stackPtr); break; + } + } + buf[0] = '['; + if (bufPtr == buf) bufPtr++; + *bufPtr++ = ']'; + *bufPtr++ = ' '; + *bufPtr = 0; + return buf; + } + + int main(int argc, char *argv[]) { + FILE *file = NULL; + char *path = getenv("JRP_DUMP"); + if (path) { + file = fopen(path, "w"); + if (!file) err(EX_CANTCREAT, "%s", path); + } + + jitInit(); + stackInit(); + + if (argc > 1) { + jitBegin(); + for (int i = 1; i < argc; ++i) + jitSrc(argv[i]); + jitEnd(); + if (file) jitDump(path, file); + jitExec(); + return EX_OK; + } + + EditLine *el = el_init(argv[0], stdin, stdout, stderr); + el_set(el, EL_PROMPT, prompt); + el_set(el, EL_SIGNAL, true); + + for (;;) { + int count; + const char *line = el_gets(el, &count); + if (count < 0) err(EX_IOERR, "el_gets"); + if (!line) break; + + jitBegin(); + jitSrc(line); + jitEnd(); + if (file) jitDump(path, file); + jitExec(); + } + + el_end(el); + return EX_OK; + } From e500a227c1f311b36680c0b5f4a50d95a7c171d5 Mon Sep 17 00:00:00 2001 From: Curtis McEnroe Date: Wed, 12 Oct 2016 01:03:48 -0400 Subject: [PATCH 2/2] WIP --- _drafts/quirky-rpn.md | 39 +++++++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/_drafts/quirky-rpn.md b/_drafts/quirky-rpn.md index 67400d7..2ad8a86 100644 --- a/_drafts/quirky-rpn.md +++ b/_drafts/quirky-rpn.md @@ -46,6 +46,8 @@ a BSD-licensed Readline alternative. [libedit]: http://thrysoee.dk/editline/ +--- + typedef unsigned long qop; typedef unsigned int dop; @@ -81,6 +83,8 @@ which take a value from the stack as a parameter and cause side-effects. +--- + static char *formatBin(qvalue val) { static char buf[sizeof(qvalue) * 8 + 1]; uvalue uval = val; @@ -120,6 +124,8 @@ This is the set of runtime functions, i.e. C code called by the JIT compiled code. They are just used for printing values. +--- + static const dop DOP_NOP = 0x90666666; // nop Now it gets interesting. @@ -158,25 +164,46 @@ the immediate down to only a byte. static const dop DOP_DUP = 0x90666657; // push rdi static const dop DOP_SWAP = 0x243c8748; // xchg rdi, [rsp] -DROP pops the value on top of the real stack into `rdi`, +DROP pops the value on top of the memory stack into `rdi`, discarding the previous value of `rdi`. -DUP pushes the value in `rdi` onto the actual stack, +DUP pushes the value in `rdi` onto the memory stack, duplicating it. SWAP swaps the value in `rdi` with the value pointed to by `rsp`, -i.e. the top of the real stack. +i.e. the top of the memory stack. static const dop DOP_NEG = 0x90dff748; // neg rdi static const dop DOP_ADD = 0xc7014858; // pop rax; add rdi, rax + +Negation and addition +are the only arithmetic operations +which can be done in 4 bytes. + static const dop DOP_QUO = 0x90c78948; // mov rdi, rax static const dop DOP_REM = 0x90d78948; // mov rdi, rdx + +These two don't work on their own. +They must be preceded by a `QOP_DIV` instruction +which, through `idiv`, +leaves the quotient in `rax` +and the remainder in `rdx`. + static const dop DOP_NOT = 0x90d7f748; // not rdi static const dop DOP_AND = 0xc7214858; // pop rax; and rdi, rax static const dop DOP_OR = 0xc7094858; // pop rax; or rdi, rax static const dop DOP_XOR = 0xc7314858; // pop rax; xor rdi, rax - - static const qop QOP_PROL = 0x5ffc8948e5894855; // push rbp; mov rbp, rsp; mov rsp, rdi; pop rdi - static const qop QOP_EPIL = 0x5dec8948e0894857; // push rdi; mov rax, rsp; mov rsp, rbp; pop rbp + +The bitops are straightforward. + +--- + + // push rbp; mov rbp, rsp; mov rsp, rdi; pop rdi + static const qop QOP_PROL = 0x5ffc8948e5894855; + // push rdi; mov rax, rsp; mov rsp, rbp; pop rbp + static const qop QOP_EPIL = 0x5dec8948e0894857; + +Beep. + static const qop QOP_RET = 0x90666666906666c3; // ret static const qop QOP_CRT = 0xb848906690e58748; // xchg rsp, rbp; mov rax, strict qword 0 static const qop QOP_CALL = 0x90665fe58748d0ff; // call rax; xchg rsp, rbp; pop rdi