Skip to content

Instantly share code, notes, and snippets.

@mikesmullin
Last active December 21, 2015 16:18
Show Gist options
  • Save mikesmullin/6332482 to your computer and use it in GitHub Desktop.
Save mikesmullin/6332482 to your computer and use it in GitHub Desktop.
experimental asm alternative modern syntax
eax = ebx # mov
eax = &ebx # lea
push eax
pop eax
eax++ # inc
eax-- # dec
eax += 1 # add
eax -= ebx # sub
eax = &d * &e # imul
# and so on with idev, and, or xor, not, neg, shl, shr,
# labels will be the same
begin:
goto begin # jmp [0x+1]
if begin # cmp + jcondition (je, jne, jz, jg, jge, jl, jle)
# this is where the fun can be had--with coroutines! ala node callback pattern!!
a = (cb) ->
return # ret
a() # call
# jmp everybody! jmp everybody! jmp!
# http://en.wikibooks.org/wiki/X86_Assembly/Control_Flow
# http://stackoverflow.com/questions/1396909/ret-retn-retf-how-to-use-them
# http://en.wikipedia.org/wiki/Function_prologue
# http://stackoverflow.com/questions/8361942/assembly-registers-esp-and-ebp
# anything not a symbol is obfuscated in the symbol table
###
so there needs to be a better way of tracking typeof t_sizes in ASM
there should only be a list of registers
and how much you want of them should be defined explicitly as an bit/byte integer, not learned
same with all the instructions; dont memorize instruction names, instead explicitly type an integer
and same with all the relative positionings which normally wouldn't be allowed on the left side of an operator
also your declarations shouldn't need to be repeated (like C defs), without the magic
64bit_Registers: rax rbx rbc rdx rsi rdi rbp rsp r8 r9 r10 r11 r12 r13 r14 r15
32bit_Registers: eax, ebx, ecx, edx, esi, edi, ebp, esp, r8d, r9d, r10d, r11d, r12d, r13d, r14d, r15d
16bit_Registers: ax, bx, cx, dx, si, di, bp, sp, r8w, r9w, r10w, r11w, r12w, r13w, r14w, r15w
8bit_Registers: al, bl, cl, dl, sil, dil, bpl, spl, r8b, r9b, r10b, r11b, r12b, r13b, r14b, r15b
my_registers: a/accum/accumulator, b/base, c/count, d/data, dest/dest_idx/destination_index/di/e, src/src_idx/source_index/si/f, sp/stack_pointer, bp/base_pointer/stack.current, g/r8, h/r9, i/r10, j/r11, k/r12, l/r13, m/r14, n/r15
bp and sp should probably have operator symbols
and since referencing the args, return addr, local stack is so common it should be made easily readable with specialized simple macro syntax defaults
=& (by reference; the memory address) something old
= (initialization ONLY) something new
=* (by copy; load effective address (lea) into this memory location) something borrowed
i can probably do better than these cryptic operators; how about more word operators like "is" "and" "or"!
addrof
initto # only non-macro variable initialization
means # macro-only variable initialization
copybytesfrom
isaliasof
and let my operators become macro'ed as well to whatever operators people like to use lol! ya!
huge potential for abuse but our job is not to confine the author; only warn of inclarity at compile time
i also like to distinguish between lines that have consequence in asm level and lines that are just macros scaffolded on top of that
but it may need to be inverted because in most programs you wont spend so much time low-level
i'll do this by saying macros occur inside comments
but it means you have to use different comments // or /**/ if you dont want it to mean macro syntax
i should limit my reserved keyword globals to fully spelled out and limited set
and let the user alias these all they want in macros to something very unreadable if they so choose
i should probably have unique operators for macro vs non-macro mode so its ultra explicit! ULTRA EXPLICIT WITH MACROS FTW!
lets you program your own compiler warnings and strong typing! wohoo! fun.
hmm maybe those # aren't so helpful after all
better to use explicit operators and types
lets say that anything with a type or a isaliasof or isunitequivaltof or isaliasof those type of operators are limited to macros so they must be macros
these are the only valid ranges on register bit arrays: [0...8b], [0...16b], [0...32b], [0...64b]
but also: [0...1B]
we use 0...8b instead of 1..8b because bits of a byte are normally numbered zero-indexed from 0 to 7
it might be fun to account for LSB and MSB by allowing the user to transpose the range for human notation only
also we can do pointer relativity in either B or b (mod 8 only, of course)
= and ? and ! should be valid in variable naming
operators like > < should be valid to override as any other macro variable
because they all just map to asm.* instructions, anyway
i could have copybytesfromto and copybytestofrom! haha
shit i could have those plus copybytesfrom
may as well add copybytesto!
copybytesfromto (stack.base[0...64b] offset -32b) register.e[0...32b]) # mov %edi, -4(%rbp)
(copybytesfromto
(stack.base[0...64b] offset -32b)
(register.e[0...32b])) # mov %edi, -4(%rbp)
(stack.base[0...64b] offset -32b) copybytesfrom register.e[0...32b]
register.e[0...32b] copybytesto stack.base[0...64b] offset -32b # would be nice if parenthesis could stay unrequired here without being ambiguous
now i have define 3 types of operators, not just unary,binary,ternary
but i now have:
unary-left, unary-right,
binary-one-left-one-right, binary-both-right,
and ternary (no change here)
it could be fun to let users define the missing items from that matrix and see what kinds of crazy ass language evolves from the use of those operators! lol
register.e[0...32b] (stack.base[0...64b] offset -32b) youtwocopybytes # haha
# actually i did end up finding a practical use for this when you start using macro-down to define lossy operator overloading; you could use this to generate a very different language indeed
except unlike clojure, my parenthesis can still be optional motherfuckers! ya!
it may do well to camelcase the operators :/ not sure. not sure if they're case-sensitive either. variables definitely are tho.
until i'm sure, i think i should defer to giving the author most options
stack.push64 still is a little unclear to me
because traditionally an array push does not copy bytes if it can help it; it is by reference
and unless you think about what stack.base must be, its not immediately clear in the syntax (strong typing) that this is a primitive which can only be copied
it would be nice if the method/function/operator could define that
perhaps something more like:
stack.push64bits stack.base[0...64]
push is also misleading
in a normal js array, push also resizes the array
in c it doesn't but c is retarded that way
so in asm i'd like to avoid mixing metaphors, aka reconcile the hi/lo language differences
by using a different verb which communicates precisely the steps involved in a very explicit/verbose way
something more like: instead of push:
ah i can see now that for that matter, mov is also misleading. its not actually zeroing-out the original bytes as one would expect during a filesystem move
its merely copying them only. this is the type of language clarification i wanted to do primarily for ASM. so lets make new operators/opcodes/instructions:
copy64Bits copy8Bytes
copy32Bits copy4Bytes
copy16Bits copy2Bytes
copy8Bits copy1Byte
x
To From FromTo ToFrom
operators should be allowed to be case-insensitive? or force camelcase for readability and limited namespace/checking?
what does a push instruction do?
The push instruction places its operand onto the top of the hardware supported stack in memory. Specifically, push first decrements ESP by 4, then places its operand into the contents of the 32-bit location at address [ESP]. ESP (the stack pointer) is decremented by push since the x86 stack grows down - i.e. the stack grows from high addresses to lower addresses.
damn... i wonder if theres any reason to use push instead of issuing the two commands myself? i suppose by using push there is some optimization to be had through cpu cache
well so its not like i'm actually going to avoid using it
i just want to name it better
so it doesn't conflict with higher level notions of pushing
so i'll call it:
stack.decrementPointerBy64BitsAndCopy64BitsFrom
haha! if you want to alias it to push again then that's your problem.
TODO: asm.pushq should have some macro-level strong-type-check to ensure it always receives 1..64b
i like providing multiple operators that do the same thing:
def Byte stack.current
stack.current settypeof Byte
gives the author more flexibility to write like asm, clojure, or javascript
Unit, Object, Function are the only types you start with
we'll have type inference thatis optional
if you enjoy putting types next to every variable (essentially the double-entry accounting approach to programming)
then go ahead and do that
but if you prefer something more DRY, we'll have type inference for you
in addition we'll have operators that are specific to each of the 3 base types
Function prologue defineas () ->
prologue defineas () -> # Function is inferred because () -> is function operator syntax only
it looks like i'll want string interpolation happening inside ranges, as well
or some kind of typecasting operator from string to unit
defining a unit like
typedef Unit Bit b
should also reserve the plural keyword, and optionally reserve a single-character symbol
and units should have object-like properties: .plural, .singular, .symbol so you can programmatically figure out the variations via macros
those properties should always include the current unit number until i find a use case where i wouldn't want that
then i can add more methods to specifically just get the unit name by itself ( could happen )
now this way i don't need string interpolation within ranges yet
i can just say that any unit within a range translates to the numeric value with a strong-type check against the two units in the range (unless one unit is zero)
as well as strong-type check against the variable the range is operating on, with translation if units are defined as equal to each other in any quantity
i like this concept of unit types on numbers and their relativity to another. could be taken farther with units of time like s, sec, ms
but instead of defining them by default i'll let the user do it
i'll just provide the most verbose one within a very limited top-level keyword like org.java.Time.Units.milliseconds or some crap
i'll provide a root object which will hold everything in its namespace; it may be one of the only reserved words to start?
root.operators
root.units
then you can macro it up!
# asm registers object is magical; it only provides the largest registers, and you must access divisions of 2 within that
# and this object translates to actual asm methods
thats it! thats what this language should be
a macro language to end all macro languages
it transpiles to anything you want; C, Ruby, JavaScript, ASM, your own language, or DSL like Gherkin
so in this case i'll use the language to compile down to asm
register.e[0...32b] copy32BitsTo stack.base[0...64b] offsetAddressBy -32b # mov %edi, -4(%rbp)
also writable as:
register.e[0...32b] copy32BitsTo offsetAddress -32b stack.base[0...64b] # mov %edi, -4(%rbp)
FOUNDATIONAL PRINCIPLE:
i should start out with reserved words as verbose as possible and only one variation
but let the user macro shortcuts quickly from there
in a situation like -4(%rdp) if the unit isnt specified, it should be the same as the target
which is 64Bit so we'll either need a way to cast it to 8Byte or declare that way to begin with
it might be advantageous to declare it that way to begin with
then also when you use a bit range on a Byte unit, it could use that knowledge to force you to iterate only on a modulus of 8! yes!
###
# this first part would be relegated to some asm standard library headers:
typedef Unit Bit
b setAliasOf Bit
typedef Unit Byte
B setAliasOf Byte
1 Byte setUnitEquivalentOf 8 Bit
Object asm defineAs
Object register defineAs
Byte[8] rsp lexicalEquivalentTo "%rsp"
Bit[64] rbp lexicalEquivalentTo "%rbp"
Object register defineAs
Byte[8] stack_pointer setAliasOf asm.rsp
Bit[64] base_pointer setAliasOf asm.rbp
Bit[64] Function asm.pushq
Object stack defineAs
pointer setAliasOf register.stack_pointer
base setAliasOf register.base_pointer
decrementPointerBy64BitsAndCopy64BitsFrom setAliasOf asm.pushq
# our program:
stack.decrementPointerBy64BitsAndCopy64BitsFrom stack.base[0...64b] # pushq %rbp
stack.pointer[0...64b] copy64BitsTo stack.base[0...64b] # movq %rsp, %rbp
register.e[0...32b] copy32BitsTo offsetAddress -32b stack.base[0...64b] # mov %edi, -4(%rbp)
# now let's macro-up (non-lossy, DRY) to a high level of DRYness
prologue defineAs (unit) ->
root.stack["decrementPointerBy#{unit.plural.capitalized}AndCopy#{unit.plural.capitalized}From"] register.base_pointer[0...unit] # e.g. pushq %rbp
register.stack_pointer[0...unit] root.operators["copy#{unit.plural.capitalized}To"] register.base_pointer[0...unit] # e.g. movq %rsp, %rbp
prologue 64b
# now let's macro-down (lossy) to a "low level" of cryptic jargon
edi setAliasOf register.destination_index[0...32b]
rbp setAliasOf register.base_pointer[0...64b]
mov setAliasOf asm.mov
Operator root.my.operators.blah
type: BINARY_LEFT_RIGHT
forwardToExistingOperator: root.operators.offsetAddress # from BINARY_LEFT_RIGHT to BINARY_BOTH_RIGHT
symbol: '('
ending_symbol: ')' # optional
Operator root.my.operators.blah2
type: UNARY_RIGHT
forwardToExistingOperator: root.operators.noop
symbol: '%'
resetScopeExcept {edi, rbp, mov, my.operators.blah, my.operators.blah2, exitScope } = root
# this uses a combination of type inference
# and locally-scoped operator overloading
mov %edi, -4(%rbp)
# so yes, it can be done, but dear god i pray for those who actually prefer to use this
# as for now lets exit module scope and go back up to the high level (regaining our () operators)
exitScope
# i feel like i want a bulk copy. something that can take an array of bits of any length
# and plop them into memory using however many appropriately-sized mov instructions as necessary
# now this might be bad, because
# there are arch/System V/libC conventions about which registers your bits should go in when they are of certain type
# but for now, what the hell
register.e[0...32b] copy32BitsTo offsetAddress -32b stack.base[0...64b] # mov %edi, -4(%rbp)
# so remember you don't always need to specify the range; it can be type inferred that if you don't specify a range its the entire range
# which since we're in x86_64 arch mode then its 64b or 8B for the TO
# the FROM can be inferred as well by looking at the operator
copy32BitsFromTo register.source_index offsetAddress -64b of stack.base
copy32BitsFromTo register.d offsetAddress -96b of stack.base
copy32BitsFromTo register.c offsetAddress -128b of stack.base
# it would probably be convenient to list for every variable whether it contains a: Value, Address, or some Macro type
# aka: what if pointers and variables weren't the same
# remember: commas are NOT optional. they are simply not used here because copy32BitsFromTo is an BINARY_BOTH_LEFT _OPERATOR_, not a function!
# to make that clear, an alternative, possibly better, way to rewrite the above might be to treat it like a function:
copy32Bits from: register.di, to: -32b*1 offsetAddressOf register.base_pointer # movl %edi, -4(%rbp)
# but i kind of like the most terse option:
copy32BitsFromTo register.di -32b*1 offsetAddressOf register.base_pointer # movl %edi, -4(%rbp)
# however since this is a common pattern
-Xb*I offsetAddressOf register.base_pointer
# i'd prefer to make some short symbol for it
stack.getByIndex = (PM, U, I) -> U * I * PM offsetAddressOf register.base_pointer
stack.getLocalAddressByIndex = (U,I) -> stack.getByIndex -1, U+64b, I
stack.getArgumentAddressByIndex = (U,I) -> stack.getByIndex +1, U, I
return = ->
asm.ret
goto = (x) ->
asm.jmp x
epilogue = ->
leave # movq %rbp, %rsp; popq %rbp
return
.text:
.data:
yield data
example:
prologue 64b # push %dbp; movq %rsp, %rbp
copy32BitsFromTo register.di stack.getLocalAddressByIndex 32b, 1 # movl %edi, -4(%rbp)
copy32BitsFromTo register.si stack.getLocalAddressByIndex 32b, 2 # movl %esi, -8(%rbp)
copy32BitsFromTo register.d stack.getLocalAddressByIndex 32b, 3 # movl %edx, -12(%rbp)
copy32BitsFromTo register.c stack.getLocalAddressByIndex 32b, 4 # movl %ecx, -16(%rbp)
copy32BitsFromTo register.r8 stack.getLocalAddressByIndex 32b, 5 # movl %r8d, -20(%rbp)
copy32BitsFromTo register.r9 stack.getLocalAddressByIndex 32b, 6 # movl %r9d, -24(%rbp)
register.a[0...32b] copy32BitsFrom stack.getLocalAddressByIndex 32b, 2 # movl -8(%rbp), %eax
register.d[0...32b] copy32BitsFrom stack.getLocalAddressByIndex 32b, 1 # movl -4(%rbp), %edx
register.a[0...32b] plusEquals32BitsFrom register.d[0...32b] # addl %edx, %eax
register.a[0...32b] plusEquals32BitsFrom stack.getLocalAddressByIndex 32b, 3 # addl -12(%rbp), %eax
register.a[0...32b] plusEquals32BitsFrom stack.getLocalAddressByIndex 32b, 4 # addl -16(%rbp), %eax
register.a[0...32b] plusEquals32BitsFrom stack.getLocalAddressByIndex 32b, 5 # addl -20(%rbp), %eax
register.a[0...32b] plusEquals32BitsFrom stack.getLocalAddressByIndex 32b, 6 # addl -24(%rbp), %eax
register.a[0...32b] plusEquals32BitsFrom stack.getArgumentAddressByIndex 64b, 1 # addl 16(%rbp), %eax
register.a[0...32b] plusEquals32BitsFrom stack.getArgumentAddressByIndex 64b, 2 # addl 24(%rbp), %eax
register.a[0...32b] plusEquals32BitsFrom stack.getArgumentAddressByIndex 64b, 3 # addl 32(%rbp), %eax
register.a[0...32b] plusEquals32BitsFrom 3 # addl $3, %eax
epilogue 64b # popq %rbp; ret
global _start
_start:
# TODO: define a prologue that also can have an array of arguments
# TODO: that prologue could also track the number of arguments as the first arg if it takes a variable number :)
###
that's still more verbose than i'd like
we could define an operator like
plusEquals32BitsFrom +=32b
macroing up to linux syscalls
###
write = (String s) ->
var = content_for .data, ->
name = "s#{root.string_counter++}"
# TODO: define .ascii or .string within .data section
root.section.data[name] = s
return name
SYS_WRITE = 60
STDOUT = 1
movq SYS_WRITE, %rax # mov WRITE into eax
movq 10, %rdx # length of the string
movq var, %rsi # address of our string
movq STDOUT, %rdi # writing to stdout
syscall # call the kernel
###
macroing up to GNU C
TODO: find out how C returns various data types
type inference with function lines:
a = (Int b) -> c # type of a and lambda are both inferred to void; c is not attempted to be returned
Char a = (Int b) -> c # type of a, lamba, and c are inferred to Char
Char a = (b) -> c # compile error: must declare typeof b
Char a = Int (Int b) -> c # compile error: typeof Int lambda does not match declaration Char a
Char a = (Int b) -> Int c # compile error: typeof Char a does not match implicit return Int c; declare an explicit return or match types
###
Operator root.my.operators.function defineAs
symbol: '->'
symbol_prefix: ')'
symbol_prefix_prefix: '('
type: FUNCTION
symbol_suffix: 'return'
implicit_arguments: (args) ->
asm.subq sizeof args # subq $32, %rsp
for i, arg in args step -1 # loop backward like C does
movl arg, register[switch i
when 1 then ['di', (Bits) sizeof arg] # movl $1, %edi
when 2 then ['si', (Bits) sizeof arg] # movl $2, %esi
when 3 then ['d', (Bits) sizeof arg] # movl $3, %edx
when 4 then ['c', (Bits) sizeof arg] # movl $4, %ecx
when 5 then ['r8', (Bits) sizeof arg] # movl $5, %r8d
when 6 then ['r9', (Bits) sizeof arg] # movl $6, %r9d
when 7...99
unit = (Bits) sizeof arg
unit offsetAddressOf ['sp', unit]
# movl $9, 16(%rsp)
# movl $8, 8(%rsp)
# movl $7, (%rsp)
else
compileError 'invalid argument length'
]
implicit_call: ->
asm.call @name
implicit_before: -> prologue 64b
implicit_after: -> epilogue 64b
printf = (String s) ->
movl %eax, %edx
movl $.LC0, %eax
movl %edx, %esi
movq %rax, %rdi
movl $0, %eax
call printf
int example = (int a, b, c, d, e, f, g, h, i) ->
a + b + c + d + e + f + g + h + i + 3
int main = ->
printf "%i\n", example 1, 2, 3, 4, 5, 6, 7, 8, 9
0
###
i'm envisioning this language can do some amazing things:
* transpile a set of files in a single language into several files in different languages (both interpreted and machine)
* outputs could include machine code, jvm bytecode, asm, ansi c, javascript, custom scripting/configuration DSLs, and even human language translation
* its basically a high-level meta/recursive-macro language that can pretend to be anything you want; a quick way to emulate or make your own language
* of course, the macros you define with it are only available to you at compile time. after its compiled, your limited to the tools in your output and the machine executing those instructions
* but this could mean that for example you could write one psuedo-code and it could be compiled into three different languages
* or you could write a compiler with this language that can compile itself
* and then use that compiler to compile an runtime language interpreter of the same language
* so that now you could mix machine code with run-time scripting using the same language
i suppose we could just prefix the target language file suffix with an 'm' for macro
consider a directory structure like:
./
compiler.mc # a macro GNU C script
compiler.c # an GNU C script
compiler.ms # a macro GAS script
compiler.s # a GAS script
compile # a compiled Linux x86_64 ELF executable binary
interpreter.mc # a macro GNU C evented i/o runtime script interpreter
interpret # a compiled Linux x86_64 ELF executable binary
webapp.mjs # a macro Javascript
public/
favico.ico
./compile compiler.mc -oc compiler.c # compile macro-C to C
./gcc compiler.c -o compile # compile C to executable, proves valid for gcc
./compile compiler.mc -os compiler.s # compile macro-C to GAS
./gcc compiler.s -o compile # compile GAS to executable, proves valid for gcc
./compile compiler.ms -os compiler.s # compile macro-GAS to GAS
./gcc compiler.s -o compile # compile GAS to executable, proves valid for gcc
./compile compiler.mc -o compile # compile macro-C to executable
./compile compiler.ms -o compile # compile macro-GAS to executable
./compile interpreter.mc -p interpret # compile macro-C to executable interpreter (imagine something like node.exe)
./interpret webapp.mjs # http service executing high-level interpreted scripting language
except of course that we'd probably not use mjs when we have complete control of the processor this way
we could use the beloved macro language itself; just interpreting in the scope of the http service
but we would still be able to transpile from the macro language to some .js or .css when its time to serve client-side files that the browser might read
this way if you know how to program at a high level you can apply most of what you know to programming at a low level
theoretically, someone who could program server-side scripting language could also tweak their operating system,
if both were in the same macro language, you see :)
also, design patterns and huge libraries could be made extremely portable:
enumerators, data types, could be reused in the easiest case (thinking libs like Underscore.js)
drivers could be reimplemented across many languages in the hardest case (thinking OpenGL)
bottom line:
* writing C and ASM doesn't have to be an outdated, obfuscated, repetitive pain in the ass.
* strong typing can co-exist with DRYness
* having control over the hardware doesn't have to mean you can't go high-level sometimes in the same program
* starting high-level doesn't have to alienate you from the processor with so many bloated middleware dependencies inbetween
* having complete control over your computer doesn't have to require that you know more than two or three languages (this macro language, your processor's machine code, and interfacing with the outside world via prevailing language of the time like a browser via javascript)
* while there are many ways to implement parallelism/concurrency, each is fit for its particular purpose, and the language should empower to use any and all of them that you choose, without making unforgiving assumptions about the way you should be allowed to do it, should you choose to do it yourself
quite fantastic! possible? let's see...
i like to think this is not a wizardly solution
in fact its quite unwizardly
it takes the magic/unknown out of programming
suddenly you only have to know what is important right now
rather than what was important 20 years ago
you can do things the way you want to do them
rather than be forced to accept because 'this is the way it is'
despite knowing that the vendor is mercilessly abusing you by mainintaining backdoors in deliberately complex or secret software
###
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment