├── .gitignore ├── moondust.lua ├── run ├── .vscode ├── tasks.json ├── settings.json ├── extensions.json └── on_editor_save.lua ├── examples ├── hello_world.lua ├── call.lua ├── windows_sleep.lua ├── hello_world_alt.lua ├── dump_gas.lua ├── labels.lua ├── bench.lua └── vec3_length.lua ├── tests ├── init.lua ├── comparisons.lua └── execute.lua ├── readme.md └── moondust ├── util.lua ├── gas.lua ├── x86_64.lua └── assembler.lua /.gitignore: -------------------------------------------------------------------------------- 1 | test_focus.lua -------------------------------------------------------------------------------- /moondust.lua: -------------------------------------------------------------------------------- 1 | return require("moondust/assembler") -------------------------------------------------------------------------------- /run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ $1 = "test" ]; then 4 | luajit tests/init.lua 5 | fi 6 | 7 | if [ $1 = "build" ]; then 8 | luajit build/build.lua 9 | fi -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // -jp=Fpa - 2 | { 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "label": "onsave", 7 | "type": "shell", 8 | "command": "cd ${workspaceFolder} && luajit -e \"assert(loadfile('.vscode/on_editor_save.lua'))('${file}','${workspaceFolder}')\"" 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "triggerTaskOnSave.tasks": { 3 | "onsave": [ 4 | "examples/**/*.lua", 5 | "moondust/**/*.lua", 6 | "tests/**/*.lua", 7 | "test_focus.lua" 8 | ] 9 | }, 10 | "search.exclude": { 11 | "moondust/x86_64_data.lua": true, 12 | "build/x86data.js": true, 13 | "build/json.lua": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/hello_world.lua: -------------------------------------------------------------------------------- 1 | local ffi = require("ffi") 2 | local asm = require("moondust.assembler") 3 | 4 | local mcode = asm.compile(function(a) 5 | local msg = "hello world!\n" 6 | 7 | local STDOUT_FILENO = 1 8 | local WRITE = jit.os == "Linux" and 1 or 0x2000004 9 | 10 | mov(rax, WRITE) 11 | mov(rdi, STDOUT_FILENO) 12 | mov(rsi, asm.object_to_address(msg)) 13 | mov(rdx, #msg) 14 | syscall() 15 | 16 | ret() 17 | end) 18 | 19 | local func = ffi.cast("void (*)()", mcode) 20 | 21 | func() -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | 5 | // List of extensions which should be recommended for users of this workspace. 6 | "recommendations": ["gruntfuggly.triggertaskonsave"], 7 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 8 | "unwantedRecommendations": [] 9 | } 10 | -------------------------------------------------------------------------------- /tests/init.lua: -------------------------------------------------------------------------------- 1 | 2 | print("running examples..") 3 | 4 | --loadfile("examples/dump_gas.lua")() 5 | loadfile("examples/hello_world.lua")() 6 | loadfile("examples/hello_world_alt.lua")() 7 | loadfile("examples/labels.lua")() 8 | loadfile("examples/vec3_length.lua")() 9 | 10 | print("examples ran!") 11 | 12 | print("test complete!") 13 | 14 | print("running tests..") 15 | 16 | if jit.os ~= "Windows" then 17 | loadfile("tests/comparisons.lua")() 18 | end 19 | loadfile("tests/execute.lua")() 20 | 21 | print("tests ran!") 22 | -------------------------------------------------------------------------------- /examples/call.lua: -------------------------------------------------------------------------------- 1 | local ffi = require("ffi") 2 | local asm = require("moondust.assembler") 3 | 4 | local r = asm.reg 5 | 6 | local a = asm.assembler() 7 | 8 | do 9 | local msg = "hello world!" 10 | 11 | a:push(r.rbp) 12 | a:mov(r.rbp, r.rsp) 13 | 14 | a:lea(r.rdi, r(asm.object_to_address(msg))) 15 | a:mov(r.rdx, asm.address_of("puts")) 16 | a:call(r.rdx) 17 | a:pop(r.rbp) 18 | 19 | a:ret() 20 | end 21 | 22 | local mcode = a:compile() 23 | local func = ffi.cast("void (*)()", mcode) 24 | 25 | func() -------------------------------------------------------------------------------- /examples/windows_sleep.lua: -------------------------------------------------------------------------------- 1 | local ffi = require("ffi") 2 | local asm = require("moondust.assembler") 3 | 4 | local r = asm.reg 5 | 6 | local a = asm.assembler() 7 | local ms = 2000 8 | do 9 | a:mov(r.rcx, ms) 10 | a:mov(r.rdx, asm.address_of("Sleep", "Kernel32.dll")) 11 | a:call(r.rdx) 12 | a:ret() 13 | end 14 | 15 | local mcode = a:compile() 16 | local func = ffi.cast("uint64_t (*)()", mcode) 17 | 18 | print("sleeping for "..(ms/1000).." seconds..") 19 | local t = os.clock() 20 | func() 21 | print("slept for " .. (os.clock() - t) .. " seconds") -------------------------------------------------------------------------------- /examples/hello_world_alt.lua: -------------------------------------------------------------------------------- 1 | local ffi = require("ffi") 2 | local asm = require("moondust.assembler") 3 | 4 | local r = asm.reg 5 | 6 | local a = asm.assembler() 7 | 8 | do 9 | local msg = "hello world!\n" 10 | 11 | local STDOUT_FILENO = 1 12 | local WRITE = jit.os == "Linux" and 1 or 0x2000004 13 | 14 | a:mov(r.rax, WRITE) 15 | a:mov(r.rdi, STDOUT_FILENO) 16 | a:mov(r.rsi, asm.object_to_address(msg)) 17 | a:mov(r.rdx, #msg) 18 | a:syscall() 19 | 20 | a:ret() 21 | end 22 | 23 | local mcode = a:compile() 24 | local func = ffi.cast("void (*)()", mcode) 25 | 26 | func() -------------------------------------------------------------------------------- /examples/dump_gas.lua: -------------------------------------------------------------------------------- 1 | -- this requires that lua can execute gcc, as and objdump (so typically in a unix envionment with dev tools) 2 | 3 | local gas = require("moondust.gas") 4 | 5 | print(gas.dump_asm("mov [rax + 4], eax")) 6 | 7 | print("single line:") 8 | print(gas.dump_asm("mov rcx, 10")) 9 | print("multiple lines:") 10 | print(gas.dump_asm([[ 11 | mov eax,4 12 | mov ebx,1 13 | mov ecx,edi 14 | mov edx,eax 15 | int 0x80 16 | mov eax,1 17 | mov ebx,0 18 | int 0x80 19 | ]])) 20 | 21 | 22 | print(gas.dump_asm("push 0")) 23 | 24 | print(gas.dump_asm("call 0xdeadb")) 25 | print(gas.dump_c([[ 26 | #include 27 | 28 | int main(void) { 29 | //int (*lol)(const char*) = 1; 30 | int test = puts("hello world"); 31 | } 32 | ]])) 33 | -------------------------------------------------------------------------------- /examples/labels.lua: -------------------------------------------------------------------------------- 1 | local ffi = require("ffi") 2 | local asm = require("moondust.assembler") 3 | 4 | local mcode = asm.compile(function(a) 5 | local msg = "hello world!\n" _G.msg_ref = msg 6 | 7 | local STDOUT_FILENO = 1 8 | local WRITE = jit.os == "Linux" and 1 or 0x2000004 9 | local i = r10 10 | 11 | mov(i, rdi) 12 | 13 | label("loop") do 14 | mov(rax, WRITE) 15 | mov(rdi, STDOUT_FILENO) 16 | mov(rsi, asm.object_to_address(msg)) 17 | mov(rdx, #msg) 18 | syscall() 19 | inc(i) 20 | 21 | cmp(i, 10) 22 | jne("loop") 23 | end 24 | 25 | jmp("no!") 26 | mov(rax, 777) 27 | label("no!") 28 | 29 | -- yes! 30 | mov(rax, 1337) 31 | 32 | ret() 33 | end) 34 | 35 | local func = ffi.cast("uint64_t (*)(uint64_t)", mcode) 36 | 37 | print(tonumber(func(0))) -------------------------------------------------------------------------------- /examples/bench.lua: -------------------------------------------------------------------------------- 1 | local ffi = require("ffi") 2 | local asm = require("moondust.assembler") 3 | 4 | local r = asm.reg 5 | 6 | local a = asm.assembler() 7 | 8 | local x,y,z = 10.5, 23, 123 9 | print(math.sqrt(x*x + y*y + z*z)) 10 | 11 | 12 | local input = ffi.new("double[3]", x,y,z) 13 | local output = ffi.new("double[1]", 0) 14 | 15 | do 16 | a:movsd(r.xmm0, r(asm.object_to_address(input + 0))) 17 | a:movsd(r.xmm1, r(asm.object_to_address(input + 1))) 18 | a:movsd(r.xmm2, r(asm.object_to_address(input + 2))) 19 | 20 | a:mulsd(r.xmm0, r.xmm0) 21 | a:mulsd(r.xmm1, r.xmm1) 22 | a:mulsd(r.xmm2, r.xmm2) 23 | 24 | a:addsd(r.xmm2, r.xmm1) 25 | a:addsd(r.xmm1, r.xmm0) 26 | 27 | a:sqrtsd(r.xmm0, r.xmm0) 28 | 29 | a:movsd(r(asm.object_to_address(output)), r.xmm0) 30 | 31 | a:ret() 32 | end 33 | 34 | local mcode = a:compile() 35 | 36 | local func = ffi.cast("void (*)()", mcode) 37 | 38 | func() 39 | 40 | print(output[0]) -------------------------------------------------------------------------------- /examples/vec3_length.lua: -------------------------------------------------------------------------------- 1 | local ffi = require("ffi") 2 | local asm = require("moondust.assembler") 3 | 4 | local r = asm.reg 5 | 6 | local a = asm.assembler() 7 | 8 | local x,y,z = 10.5, 23, 123 9 | print(math.sqrt(x*x + y*y + z*z)) 10 | 11 | 12 | local input = ffi.new("double[3]", x,y,z) 13 | local output = ffi.new("double[1]", 0) 14 | 15 | do 16 | a:movsd(r.xmm0, r(asm.object_to_address(input + 0))) 17 | a:movsd(r.xmm1, r(asm.object_to_address(input + 1))) 18 | a:movsd(r.xmm2, r(asm.object_to_address(input + 2))) 19 | 20 | a:mulsd(r.xmm0, r.xmm0) 21 | a:mulsd(r.xmm1, r.xmm1) 22 | a:mulsd(r.xmm2, r.xmm2) 23 | 24 | a:addsd(r.xmm2, r.xmm1) 25 | a:addsd(r.xmm1, r.xmm0) 26 | 27 | a:sqrtsd(r.xmm0, r.xmm0) 28 | 29 | a:movsd(r(asm.object_to_address(output)), r.xmm0) 30 | 31 | a:ret() 32 | end 33 | 34 | local mcode = a:compile() 35 | 36 | local func = ffi.cast("void (*)()", mcode) 37 | 38 | func() 39 | 40 | print(output[0]) -------------------------------------------------------------------------------- /.vscode/on_editor_save.lua: -------------------------------------------------------------------------------- 1 | local did_something = false 2 | 3 | local function run_lua(path, ...) 4 | did_something = true 5 | print("running ", path) 6 | assert(loadfile(path))(...) 7 | end 8 | 9 | local function has_test_focus() 10 | local f = io.open("test_focus.lua") 11 | 12 | if not f or (f and #f:read("*all") == 0) then 13 | if f then f:close() end 14 | 15 | return false 16 | end 17 | 18 | return true 19 | end 20 | 21 | local path = ... 22 | local normalized = path:lower():gsub("\\", "/") 23 | 24 | if normalized:find("on_editor_save.lua", nil, true) then return end 25 | 26 | if normalized:find("/moondust/", nil, true) then 27 | if not path then error("no path") end 28 | 29 | local is_lua = path:sub(-4) == ".lua" 30 | 31 | if not is_lua then return end 32 | 33 | if has_test_focus() then 34 | print("running test focus") 35 | run_lua("./test_focus.lua") 36 | elseif path:find("/moondust/moondust/", nil, true) then 37 | run_lua("tests/init.lua") 38 | elseif path:find("/tests/", nil, true) then 39 | run_lua(path) 40 | end 41 | end 42 | 43 | if not did_something then 44 | print("not sure how to run " .. path) 45 | print("running as normal lua") 46 | run_lua(path) 47 | end 48 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # moondust (WORK IN PROGRESS) 2 | 3 | This is an x86-64 instruction encoder with a simple JIT assembler written in LuaJIT with FFI. 4 | 5 | I'm mainly doing this for educational purposes as I've always wanted to learn how binaries are executed. If all goes well I hope it can be used to do fun performance optimizations alongside LuaJIT and maybe even Lua 5.3 6 | 7 | At the moment it roughly supports 32 and 64 bit general purpose registers, indirect addressing, displacement and scaling. 8 | 9 | It looks like this at the moment using the high level wrapper. Syntax resembles intel style. 10 | 11 | ```lua 12 | local ffi = require("ffi") 13 | local asm = require("moondust.assembler") 14 | 15 | local mcode = asm.compile(function() 16 | local msg = "hello world!\n" 17 | 18 | -- syntax resembles intel style 19 | mov(rax, 1) -- WRITE 20 | mov(rdi, 1) -- STDOUT 21 | mov(rsi, asm.object_to_address(msg)) 22 | mov(rdx, #msg) 23 | 24 | syscall() 25 | 26 | jmp("no!") 27 | mov(rax, 777) 28 | label("no!") 29 | 30 | mov(rax, 1337) 31 | 32 | ret() 33 | end) 34 | 35 | local func = ffi.cast("uint64_t (*)(uint64_t)", mcode) 36 | 37 | print(func(0)) 38 | ``` 39 | 40 | Each instruction in the above example are roughly translated like this: 41 | 42 | ```lua 43 | mov(rbx + rcx * 4 - 0x20, rax) 44 | -- >>> 45 | local x86_64 = require("moondust.x86_64") 46 | bytes = x86_64.encode("mov", { 47 | reg = "rbx", 48 | index = "rcx", 49 | scale = 4, 50 | disp = -0x20, 51 | }, 52 | { 53 | reg = "rax", 54 | }) 55 | -- where the bytes are simply placed into a buffer 56 | ``` 57 | 58 | x86_64 instructions are sourced from https://github.com/asmjit/asmdb 59 | 60 | To run tests: `./run test` 61 | 62 | To build x86_64_data.lua from x86_64.json: `./run build` 63 | 64 | To require module `require('moondust')` 65 | 66 | TODO 67 | * more refactoring and better code separation 68 | * shorter way of encoding modrm+sib and rex prefix, the code feels stupid at the moment 69 | * suppport instructions like simd, vex, xop, etc 70 | * windows support 71 | -------------------------------------------------------------------------------- /tests/comparisons.lua: -------------------------------------------------------------------------------- 1 | package.path = package.path .. ";./src/?.lua" 2 | 3 | local ffi = require("ffi") 4 | local gas = require("moondust.gas") 5 | local util = require("moondust.util") 6 | local x86_64 = require("moondust.x86_64") 7 | local asm = require("moondust.assembler") 8 | local r = asm.reg 9 | 10 | 11 | local function compare(str, ...) 12 | local name = util.string_split(str, " ")[1] 13 | local data = x86_64.encode(name, ...) 14 | 15 | local strtbl = {} for i,v in ipairs({...}) do strtbl[i] = tostring(v) end 16 | --print(name .. " " .. table.concat(strtbl, ", ") .. " | " .. table.concat(data.metadata.opcode, " ")) 17 | local str, ok = gas.dump_asm(str, function(bytes) return util.string_binformat(bytes, 15, " ", true) end, data.bytes) 18 | if not ok then 19 | print("======================") 20 | local info = debug.getinfo(2) 21 | print(info.source:sub(2) .. ":" .. info.currentline) 22 | print(str) 23 | for k,v in pairs(data.metadata) do print(k,v) end 24 | print("======================") 25 | end 26 | end 27 | 28 | compare("mov r12, rdi", r.r12, r.rdi) 29 | compare("mov r12, [0x1]", r.r12, r(0x1)) 30 | compare("inc r12", r.r12) 31 | compare("mov rcx, rbx", r.rcx, r.rbx) 32 | compare("mov rcx, [0x1]", r.rcx, r(0x1)) 33 | compare("mov ecx, [0x1]", r.ecx, r(0x1)) 34 | compare("mov ax, [0x1]", r.ax, r(0x1)) 35 | compare("mov al, [0x1]", r.al, r(0x1)) 36 | compare("mov rcx, [0xdead]", r.rcx, r(0xdead)) 37 | compare("mov rcx, [rbx]", r.rcx, r(r.rbx)) 38 | compare("mov [rcx], rbx", r(r.rcx), r.rbx) 39 | compare("mov rcx, [rbx*1]", r.rcx, r.rbx * 1) 40 | compare("mov rcx, [2*rbx]", r.rcx, r.rbx * 2) 41 | compare("mov rcx, [4*rbx]", r.rcx, r.rbx * 4) 42 | compare("mov rcx, [8*rbx]", r.rcx, r.rbx * 8) 43 | compare("mov rcx, [1*rbx+0xdead]", r.rcx, r.rbx * 1 + 0xdead) 44 | compare("mov rcx, [2*rbx+0xdead]", r.rcx, r.rbx * 2 + 0xdead) 45 | compare("mov rcx, [4*rbx+0xdead]", r.rcx, r.rbx * 4 + 0xdead) 46 | compare("mov rcx, [8*rbx+0xdead]", r.rcx, r.rbx * 8 + 0xdead) 47 | compare("mov rcx, [1*rdx+rbx+0xdead]", r.rcx, 1 * r.rbx + r.rdx + 0xdead) 48 | compare("mov rcx, [2*rdx+rbx+0xdead]", r.rcx, 2 * r.rbx + r.rdx + 0xdead) 49 | compare("mov rcx, [4*rdx+rbx+0xdead]", r.rcx, 4 * r.rbx + r.rdx + 0xdead) 50 | compare("mov rcx, [8*rdx+rbx+0xdead]", r.rcx, 8 * r.rbx + r.rdx + 0xdead) 51 | compare("mov [rbx*1], rcx", r.rbx * 1, r.rcx) 52 | compare("mov [rbx*2], rcx", r.rbx * 2, r.rcx) 53 | compare("mov [rbx*2+0xdead], rcx", r.rbx * 2 + 0xdead, r.rcx) 54 | compare("mov [rbx*1+1024], rcx", r.rbx + 1024, r.rcx) 55 | compare("movsd xmm1, xmm0", r.xmm0, r.xmm1) 56 | compare("push rbp", r.rbp) 57 | compare("mov rbp, rsp", r.rbp, r.rsp) 58 | compare("lea rax, [1337222223]", r.rax, r(1337222223)) 59 | compare("call rax", r.rax) 60 | compare("lea rdi, [rip + 0xf * 2]", r.rdi, r(r.rip + 0xf * 2)) 61 | compare("lea rdi, [rip + 0xf]", r.rdi, r(r.rip + 0xf)) 62 | compare("lea rdi, [rip]", r.rdi, r(r.rip)) 63 | 64 | compare("mov [rbp], ebx", r(r.rbp+0), r.ebx) 65 | compare("mov [rbp+1], ebx", r(r.rbp+1), r.ebx) 66 | compare("mov [rbp+123123], ebx", r(r.rbp+123123), r.ebx) 67 | compare("mov [rbp], ecx", r(r.rbp), r.ecx) 68 | compare("mov [rbp], ebx", r(r.rbp+0), r.ebx) 69 | 70 | 71 | -- TODO 72 | --compare("add QWORD PTR [rbp-1], 2", "add", r(r.rbp - 1), 2) 73 | --compare("mov ebx, [eax+1]", "mov", r.ebx, r(r.eax+1)) 74 | --compare("mov [eax+1], ebx", "mov", r(r.eax+1), r.ebx) 75 | --compare("mov [rax+123123], ebx", "mov", r(r.rax+123123), r.ebx) -------------------------------------------------------------------------------- /tests/execute.lua: -------------------------------------------------------------------------------- 1 | package.path = package.path .. ";./src/?.lua" 2 | 3 | local ffi = require("ffi") 4 | local gas = require("moondust.gas") 5 | local x86_64 = require("moondust.x86_64") 6 | local asm = require("moondust.assembler") 7 | local r = asm.reg 8 | 9 | local return_value = r.rax 10 | local arg1, arg2, arg3, arg4, arg5, arg6 = r.rdi, r.rsi, r.rdx, r.rcx, r.r8, r.r9 11 | local temp1, temp2 = r.r10, r.r11 12 | local stack_pointer = r.rsp 13 | local frame_pointer = r.rbp 14 | local global_pointer = r.r15 15 | 16 | local function check(res, op, target, msg) 17 | if op == "==" then 18 | if res == target then 19 | 20 | else 21 | msg = msg:gsub("TARGET", tostring(target)) 22 | print(msg .. " got " .. tostring(res) .. " instead") 23 | end 24 | else 25 | error("unhanled op " .. op, 2) 26 | end 27 | end 28 | 29 | local function generic_output(output, assemble) 30 | local a = asm.assembler() 31 | assemble(a, output) 32 | 33 | local mcode = a:compile() 34 | local func = ffi.cast("void (*)()", mcode) 35 | func() 36 | return output[0] 37 | end 38 | 39 | local function generic_return(assemble) 40 | local a = asm.assembler() 41 | 42 | assemble(a) 43 | 44 | local mcode = a:compile() 45 | 46 | local func = ffi.cast("uint64_t (*)()", mcode) 47 | 48 | return func() 49 | end 50 | 51 | local x,y,z = 10.5, 23, 123 52 | check(math.sqrt(x*x + y*y + z*z), "==", generic_output(ffi.new("double[1]"), function(a, output) 53 | local input = ffi.new("double[3]", x,y,z) 54 | a:movsd(r.xmm0, r(asm.object_to_address(input + 0))) 55 | a:movsd(r.xmm1, r(asm.object_to_address(input + 1))) 56 | a:movsd(r.xmm2, r(asm.object_to_address(input + 2))) 57 | 58 | a:mulsd(r.xmm0, r.xmm0) 59 | a:mulsd(r.xmm1, r.xmm1) 60 | a:mulsd(r.xmm2, r.xmm2) 61 | 62 | a:addsd(r.xmm2, r.xmm1) 63 | a:addsd(r.xmm1, r.xmm0) 64 | 65 | a:sqrtsd(r.xmm0, r.xmm0) 66 | 67 | a:movsd(r(asm.object_to_address(output)), r.xmm0) 68 | 69 | a:ret() 70 | end, "sqrt(x*x + y*y + z*z) should be TARGET")) 71 | 72 | check(1337ull, "==", generic_return(function(a) 73 | a:push(ffi.new("uint32_t", 1337)) 74 | a:pop(r.rax) 75 | a:ret() 76 | end), "should be TARGET") 77 | 78 | check(1337ull, "==", generic_return(function(a) 79 | a:mov(r.rax, ffi.new("uint64_t", 1337)) 80 | 81 | a:jmp("no!") 82 | a:mov(r.rax, 777) 83 | a:label("no!") 84 | a:ret() 85 | end), "should be TARGET") 86 | 87 | check(0x28, "==", generic_return(function(a) 88 | a:mov(r.rax, 0xa) 89 | a:shl(r.rax, 2) 90 | a:ret() 91 | end), "rax should be TARGET") 92 | 93 | check(6, "==", generic_output(ffi.new("double[1]"), function(a, output) 94 | a:movsd(r.xmm0, r(asm.object_to_address(ffi.new("double[1]", 3)))) 95 | a:mulsd(r.xmm0, r(asm.object_to_address(ffi.new("double[1]", 2)))) 96 | a:movsd(r(asm.object_to_address(output)), r.xmm0) 97 | 98 | a:ret() 99 | end), "expected TARGET") 100 | 101 | check(5, "==", generic_output(ffi.new("double[1]"), function(a, output) 102 | a:movsd(r.xmm0, r(asm.object_to_address(ffi.new("double[1]", 3)))) 103 | a:addsd(r.xmm0, r(asm.object_to_address(ffi.new("double[1]", 2)))) 104 | a:movsd(r(asm.object_to_address(output)), r.xmm0) 105 | 106 | a:ret() 107 | end), "expected TARGET") 108 | 109 | check(5, "==", generic_output(ffi.new("int[1]", 2), function(a, output) 110 | a:mov(r.rdx, asm.object_to_address(output) - 1024) 111 | 112 | a:mov(r.rax, 3) 113 | a:add(r.rdx + 1024, r.rax) 114 | 115 | a:ret() 116 | end), "expected TARGET") 117 | 118 | check(5, "==", generic_return(function(a, output) 119 | a:mov(return_value, 3) 120 | a:add(return_value, 2) 121 | 122 | a:ret() 123 | end), "expected TARGET") 124 | 125 | check(3, "==", generic_return(function(a, output) 126 | a:mov(return_value, 3) 127 | a:push(return_value) 128 | a:pop(arg1) 129 | a:mov(return_value, arg1) 130 | a:ret() 131 | end), "expected TARGET") 132 | 133 | 134 | do -- 2+5 memory 135 | local a = asm.assembler() 136 | local res = ffi.new("int[1]", 2) 137 | 138 | do 139 | a:mov(return_value, 3) 140 | a:add(r(asm.object_to_address(res)), return_value) 141 | 142 | a:ret() 143 | end 144 | 145 | local mcode = a:compile() 146 | local func = ffi.cast("int (*)()", mcode) 147 | check(func(), "==", 3, "rax should be TARGET") 148 | check(res[0], "==", 5, "memory destination should be TARGET") 149 | end -------------------------------------------------------------------------------- /moondust/util.lua: -------------------------------------------------------------------------------- 1 | local util = {} 2 | 3 | function util.string_totable(self) 4 | local tbl = {} 5 | for i = 1, #self do 6 | tbl[i] = self:sub(i, i) 7 | end 8 | return tbl 9 | end 10 | 11 | function util.string_split(self, separator, plain_search) 12 | if separator == nil or separator == "" then 13 | return util.string_totable(self) 14 | end 15 | 16 | if plain_search == nil then 17 | plain_search = true 18 | end 19 | 20 | local tbl = {} 21 | local current_pos = 1 22 | 23 | for i = 1, #self do 24 | local start_pos, end_pos = self:find(separator, current_pos, plain_search) 25 | if not start_pos then break end 26 | tbl[i] = self:sub(current_pos, start_pos - 1) 27 | current_pos = end_pos + 1 28 | end 29 | 30 | if current_pos > 1 then 31 | tbl[#tbl + 1] = self:sub(current_pos) 32 | else 33 | tbl[1] = self 34 | end 35 | 36 | return tbl 37 | end 38 | 39 | function util.string_trim(self, char) 40 | if char then 41 | char = char:patternsafe() .. "*" 42 | else 43 | char = "%s*" 44 | end 45 | 46 | local _, start = self:find(char, 0) 47 | local end_start, end_stop = self:reverse():find(char, 0) 48 | 49 | if start and end_start then 50 | return self:sub(start + 1, (end_start - end_stop) - 2) 51 | elseif start then 52 | return self:sub(start + 1) 53 | elseif end_start then 54 | return self:sub(0, (end_start - end_stop) - 2) 55 | end 56 | 57 | return self 58 | end 59 | 60 | function util.string_levenshtein(a, b) 61 | local distance = {} 62 | 63 | for i = 0, #a do 64 | distance[i] = {} 65 | distance[i][0] = i 66 | end 67 | 68 | for i = 0, #b do 69 | distance[0][i] = i 70 | end 71 | 72 | local str1 = util.string_totable(a) 73 | local str2 = util.string_totable(b) 74 | 75 | for i = 1, #a do 76 | for j = 1, #b do 77 | distance[i][j] = math.min( 78 | distance[i-1][j] + 1, 79 | distance[i][j-1] + 1, 80 | distance[i-1][j-1] + (str1[i-1] == str2[j-1] and 0 or 1) 81 | ) 82 | end 83 | end 84 | 85 | return distance[#a][#b] 86 | end 87 | 88 | function util.string_readablehex(str) 89 | return (str:gsub("(.)", function(str) str = ("%X"):format(str:byte()) if #str == 1 then str = "0" .. str end return str .. " " end)) 90 | end 91 | 92 | function util.string_hexformat(str, chunk_width, row_width, space_separator) 93 | row_width = row_width or 4 94 | chunk_width = chunk_width or 4 95 | space_separator = space_separator or " " 96 | 97 | local str = util.string_split(util.string_readablehex(str):lower(), " ") 98 | local out = {} 99 | 100 | local chunk_i = 1 101 | local row_i = 1 102 | 103 | for _, char in pairs(str) do 104 | table.insert(out, char) 105 | table.insert(out, " ") 106 | 107 | if row_i >= (row_width * chunk_width) then 108 | table.insert(out, "\n") 109 | chunk_i = 0 110 | row_i = 0 111 | end 112 | 113 | if chunk_i >= chunk_width then 114 | table.insert(out, space_separator) 115 | chunk_i = 0 116 | end 117 | 118 | row_i = row_i + 1 119 | chunk_i = chunk_i + 1 120 | end 121 | 122 | return util.string_trim(table.concat(out)) 123 | end 124 | 125 | 126 | function util.number2binary(num, bits) 127 | bits = bits or 32 128 | local bin = {} 129 | 130 | for i = 1, bits do 131 | if num > 0 then 132 | rest = math.fmod(num,2) 133 | table.insert(bin, rest) 134 | num = (num - rest) / 2 135 | else 136 | table.insert(bin, 0) 137 | end 138 | end 139 | 140 | return table.concat(bin):reverse() 141 | end 142 | 143 | function util.binary2number(bin) 144 | return tonumber(bin, 2) 145 | end 146 | 147 | function util.string_binformat(str, row_width, space_separator, with_hex, format) 148 | row_width = row_width or 8 149 | space_separator = space_separator or " " 150 | 151 | local str = util.string_totable(str) 152 | local out = {} 153 | 154 | local chunk_i = 1 155 | local row_i = 1 156 | 157 | for _, char in pairs(str) do 158 | 159 | local bin = util.number2binary(char:byte(), 8) 160 | if with_hex then 161 | table.insert(out, ("%02X/"):format(char:byte())) 162 | end 163 | 164 | if format then 165 | local str = "" 166 | local bin = util.string_totable(bin) 167 | 168 | 169 | local offset = 1 170 | for _, num in ipairs(util.string_totable(format)) do 171 | num = tonumber(num) 172 | table.insert(bin, num + offset, "-") 173 | offset = offset + 1 174 | end 175 | 176 | table.insert(out, table.concat(bin)) 177 | else 178 | table.insert(out, bin) 179 | end 180 | table.insert(out, space_separator) 181 | 182 | if row_i >= row_width then 183 | table.insert(out, "\n") 184 | row_i = 0 185 | end 186 | 187 | row_i = row_i + 1 188 | end 189 | 190 | return util.string_trim(table.concat(out)) 191 | end 192 | 193 | 194 | return util -------------------------------------------------------------------------------- /moondust/gas.lua: -------------------------------------------------------------------------------- 1 | local util = require("moondust.util") 2 | 3 | local gas = {} 4 | 5 | function gas.dump_asm(code, format_func, compare, left_align) 6 | local tbl = gas.asm_to_table(code) 7 | if tbl then 8 | return gas.format_table(tbl, nil, format_func, compare, left_align) 9 | end 10 | end 11 | 12 | function gas.dump_bin(bin, format_func, compare, left_align) 13 | local tbl = gas.bin_to_table(bin) 14 | if tbl then 15 | return gas.format_table(tbl, nil, format_func, compare, left_align) 16 | end 17 | end 18 | 19 | function gas.dump_c(code, format_func) 20 | if not code:find("int main") then 21 | local template = [[ 22 | #include 23 | 24 | int main( int argc, char *argv[] ) 25 | { 26 | ]]..code..[[ 27 | return 0; 28 | } 29 | ]] 30 | 31 | code = template 32 | end 33 | 34 | local tbl = assert(gas.c_to_table(code)) 35 | if tbl then 36 | local str = gas.format_table(tbl, nil, format_func) 37 | return str 38 | end 39 | end 40 | 41 | function gas.format_table(tbl, skip_print_matched, format_func, compare, left_align) 42 | format_func = format_func or util.string_hexformat 43 | if compare then 44 | tbl[1].compare_bytes = compare 45 | end 46 | local ok = true 47 | 48 | local out = {} 49 | 50 | do 51 | local longest_left = 0 52 | local longest_right = 0 53 | 54 | for _, data in ipairs(tbl) do 55 | data.hex = format_func(data.bytes) 56 | 57 | longest_left = math.min(math.max(longest_left, #data.guess), 99) 58 | longest_right = math.min(math.max(longest_right, #data.hex), 99) 59 | end 60 | 61 | for i, data in ipairs(tbl) do 62 | local str = string.format("%s: %-"..longest_left.."s: %-"..longest_right.."s", data.address, data.guess, data.hex) 63 | if not skip_print_matched then 64 | table.insert(out, str) 65 | end 66 | 67 | local compare_bytes = data.compare_bytes or (compare and compare[i] and compare[i].bytes) 68 | 69 | if compare_bytes and compare_bytes ~= data.bytes then 70 | if skip_print_matched then 71 | table.insert(out, str) 72 | end 73 | 74 | local hex = format_func(compare_bytes) 75 | 76 | hex = ("%-"..longest_right.."s"):format(hex) 77 | hex = (" "):rep(longest_left + #data.address + #": " * 2) .. hex 78 | 79 | hex = hex:gsub("(%s+)$", "") 80 | 81 | local diff = "" 82 | for i = 1, #str do 83 | if str:sub(i, i) == hex:sub(i, i) then 84 | diff = diff .. " " 85 | else 86 | diff = diff .. hex:sub(i, i) 87 | end 88 | end 89 | 90 | if #util.string_trim(diff) == 0 then 91 | diff = hex 92 | end 93 | 94 | table.insert(out, diff .. " << DIFF") 95 | table.insert(out, "") 96 | 97 | ok = false 98 | end 99 | end 100 | end 101 | 102 | return table.concat(out, "\n"), ok 103 | end 104 | 105 | local function to_table(str, c_source, execute, raw) 106 | if not c_source and not raw then 107 | if not str:find("_start", nil, true) then 108 | str = ".global _start\n.text\n_start:\n" .. str 109 | str = str:gsub("; ", "\n") 110 | end 111 | str = str .. "\n" 112 | end 113 | 114 | local function go() 115 | local bin 116 | if not raw then 117 | if c_source then 118 | local f, err = io.open("temp.c", "wb") 119 | 120 | if not f then 121 | return nil, "failed to read temp.c: " .. err 122 | end 123 | 124 | f:write(str) 125 | f:close() 126 | if not os.execute("gcc -S temp.c") then return nil, "failed to compile C code" end 127 | else 128 | local f, err = io.open("temp.s", "wb") 129 | 130 | if not f then 131 | return nil, "failed to read temp.s: " .. err 132 | end 133 | 134 | f:write(".intel_syntax noprefix\n" .. str) 135 | f:close() 136 | end 137 | 138 | if not os.execute("as -march=generic64 -o temp.o temp.s") then return nil, "failed to assemble temp.S" end 139 | if not os.execute("ld -dynamic-linker /lib64/ld-linux-x86-64.so.2 -lc -s -o temp temp.o") then return nil, "failed to generate executable from temp.o" end 140 | 141 | if execute then 142 | os.execute("./temp") 143 | end 144 | 145 | local f, err = io.popen("objdump -M intel -M suffix --special-syms --disassemble-zeroes -S -M amd64 --insn-width=16 --disassemble temp") 146 | if not f then 147 | return nil, "failed to read temp.dump: " .. err 148 | end 149 | bin = f:read("*all") 150 | f:close() 151 | else 152 | local f, err = io.open("temp", "wb") 153 | if not f then 154 | return nil, err 155 | end 156 | f:write(str) 157 | f:close() 158 | 159 | local f, err = io.popen("objdump -M suffix -b binary -m i386:x86-64 --special-syms --disassemble-zeroes -S -M amd64 --insn-width=16 --disassemble -D temp") 160 | if not f then 161 | return nil, "failed to read temp.dump: " .. err 162 | end 163 | bin = f:read("*all") 164 | f:close() 165 | end 166 | 167 | local content = bin:match("<.text>:(.+)") or bin:match("<.data>:(.+)") 168 | if not content then 169 | return nil, "failed to find .text" 170 | end 171 | 172 | local chunk = util.string_trim(content):gsub("\n%s+", "\n") 173 | 174 | local tbl 175 | for line in (chunk.."\n"):gmatch("(.-)\n") do 176 | tbl = tbl or {} 177 | local address, bytes, guess = line:match("^(.-):%s+(%S.-) %s+(%S.+)") 178 | guess = guess:gsub(",", ", ") 179 | guess = guess:gsub("%s+", " ") 180 | guess = util.string_trim(guess) 181 | 182 | 183 | local bin = "" 184 | 185 | local hex_numbers = util.string_split(bytes, " ") 186 | 187 | 188 | for _, hex in ipairs(hex_numbers) do 189 | bin = bin .. string.char(tonumber(hex, 16)) 190 | end 191 | 192 | table.insert(tbl, {address = address, bytes = bin, guess = guess}) 193 | end 194 | 195 | return tbl 196 | end 197 | 198 | local res, err = go() 199 | 200 | os.remove("temp.o") 201 | os.remove("temp.s") 202 | os.remove("temp") 203 | 204 | return res, err 205 | end 206 | 207 | function gas.asm_to_table(str, execute) 208 | return to_table(str, false, execute) 209 | end 210 | 211 | function gas.c_to_table(str, execute) 212 | return to_table(str, true, execute) 213 | end 214 | 215 | function gas.bin_to_table(str) 216 | return to_table(str, true, execute, true) 217 | end 218 | 219 | return gas -------------------------------------------------------------------------------- /moondust/x86_64.lua: -------------------------------------------------------------------------------- 1 | local ffi = require("ffi") 2 | local util = require("moondust.util") 3 | 4 | 5 | local x86_64 = {} 6 | 7 | _G.x86_64 = x86_64 8 | x86_64.map = require("moondust.x86_64_data") 9 | _G.x86_64 = nil 10 | 11 | do 12 | x86_64.reginfo = {} 13 | 14 | do -- integers 15 | local base = { 16 | "ax", "cx", "dx", "bx", 17 | "sp", "bp", "si", "di", 18 | } 19 | 20 | for _, bit in ipairs({64, 32, 16, 8}) do 21 | local tbl = {} 22 | 23 | if bit == 64 then 24 | for i, v in ipairs(base) do tbl[i] = "r" .. v; tbl[i + 7 + 1] = "r" .. (i+7) end 25 | elseif bit == 32 then 26 | for i, v in ipairs(base) do tbl[i] = "e" .. v; tbl[i + 7 + 1] = "r" .. (i+7) .. "d" end 27 | elseif bit == 16 then 28 | for i, v in ipairs(base) do tbl[i] = v; tbl[i + 7 + 1] = "r" .. (i+7) .. "w" end 29 | else 30 | tbl = { 31 | "al", "cl", "dl","bl", 32 | "ah", "ch", "dh", "bh", 33 | "spl", "bpl", "sil", "dil", 34 | "r8b", "r9b", "r10b", "r11b", 35 | "r12b", "r13b", "r14b", "r15b", 36 | } 37 | end 38 | 39 | for i, reg in ipairs(tbl) do 40 | x86_64.reginfo[reg] = { 41 | bits = bit, 42 | extra = i > 8, 43 | index = (i - 1)%8, 44 | } 45 | end 46 | end 47 | 48 | x86_64.reginfo["rip"] = { 49 | bits = 64, 50 | rip = true, 51 | } 52 | end 53 | 54 | do -- xmm 55 | for i = 0, 15 do 56 | x86_64.reginfo["xmm" .. i] = { 57 | bits = "xmm", 58 | extra = i > 8, 59 | index = i%8, 60 | } 61 | end 62 | end 63 | end 64 | 65 | 66 | local REX_FIXED_BIT = 0b01000000 67 | local REX = { 68 | W = 0b00001000, -- 64bit mode 69 | R = 0b00000100, -- r8-r15 70 | X = 0b00000010, -- r8-r15 71 | B = 0b00000001, -- r8-r15 72 | } 73 | 74 | local VEX_2_BYTES_PREFIX = 0xC5 75 | local VEX_3_BYTES_PREFIX = 0xC4 76 | local XOP_PREFIX = 0x8F 77 | 78 | function x86_64.encode_rex(W, flip, B, R, X) 79 | if flip then 80 | B, R, X = X, B, R 81 | end 82 | 83 | local rex = REX_FIXED_BIT -- Fixed base bit pattern 84 | 85 | if W then 86 | rex = bit.bor(rex, REX.W) 87 | end 88 | 89 | if R then 90 | rex = bit.bor(rex, REX.R) 91 | end 92 | 93 | if X then 94 | rex = bit.bor(rex, REX.X) 95 | end 96 | 97 | if B then 98 | rex = bit.bor(rex, REX.B) 99 | end 100 | 101 | return string.char(rex) 102 | end 103 | 104 | do 105 | local function encode_modrm_reg2reg(a, b) 106 | local out = 0b11000000 --11 000 000 107 | out = bit.bor(out, a) -- 11 000 a 108 | out = bit.bor(out, bit.lshift(b, 3)) -- 11 b a 109 | return out 110 | end 111 | 112 | function x86_64.encode_modrm_sib(op1, op2) 113 | local reg1 = op1.reg and x86_64.reginfo[op1.reg].index 114 | local reg2 115 | local index 116 | local base 117 | local scale 118 | local disp 119 | local disp_type = "uint32_t" 120 | 121 | local modrm 122 | local sib 123 | 124 | local rip 125 | 126 | if type(op2) == "number" then 127 | reg2 = op2 128 | else 129 | index = x86_64.reginfo[op2.index] and x86_64.reginfo[op2.index].index 130 | reg2 = x86_64.reginfo[op2.reg] and x86_64.reginfo[op2.reg].index 131 | 132 | if op2.indirect then 133 | base = reg2 134 | reg2 = nil 135 | end 136 | 137 | if x86_64.reginfo[op2.reg] and x86_64.reginfo[op2.reg].rip then 138 | rip = true 139 | end 140 | 141 | disp = op2.disp 142 | scale = op2.scale 143 | end 144 | 145 | if reg1 and reg2 then 146 | reg1, reg2 = reg2, reg1 147 | end 148 | 149 | local mod = 0b0000000 150 | local r = reg1 151 | local m = reg2 152 | 153 | if base == 5 then 154 | mod = 0b01000000 155 | m = base 156 | disp = disp or 0 157 | if disp < 128 then 158 | disp_type = "uint8_t" 159 | else 160 | mod = 0b10000000 161 | end 162 | elseif reg2 then 163 | mod = 0b11000000 164 | m = reg2 165 | elseif index then 166 | mod = 0b10000000 167 | m = 0b00000100 168 | elseif rip then 169 | m = 0b00000101 170 | disp = disp or 0 171 | elseif scale or disp then 172 | m = 0b00000100 173 | elseif base then 174 | m = base 175 | end 176 | 177 | modrm = bit.bor(mod, bit.lshift(r, 3), m) 178 | 179 | if (index or scale or disp) and not rip and base ~= 5 then 180 | sib = 0 181 | 182 | if scale then 183 | local pattern = 0b00 184 | 185 | if scale == 1 then 186 | pattern = 0b00 187 | elseif scale == 2 then 188 | pattern = 0b01 189 | elseif scale == 4 then 190 | pattern = 0b10 191 | elseif scale == 8 then 192 | pattern = 0b11 193 | else 194 | error("invalid sib scale: " .. tostring(scale)) 195 | end 196 | 197 | sib = bit.bor(sib, bit.lshift(pattern, 6)) 198 | end 199 | 200 | if index then 201 | sib = bit.bor(sib, bit.lshift(index, 3), base) 202 | else 203 | sib = bit.bor(sib, bit.lshift(base or 0b100, 3), 0b101) 204 | end 205 | 206 | disp = disp or 0 207 | end 208 | 209 | local str = "" 210 | 211 | if modrm then 212 | str = str .. string.char(modrm) 213 | end 214 | 215 | if sib then 216 | str = str .. string.char(sib) 217 | end 218 | 219 | if disp then 220 | str = str ..x86_64.encode_int(disp_type, disp) 221 | end 222 | 223 | return str 224 | end 225 | end 226 | 227 | function x86_64.encode_int(t, int) 228 | if type(int) == "cdata" then 229 | int = ffi.cast(t, int) 230 | elseif type(int) == "number" then 231 | int = ffi.new(t, int) 232 | end 233 | 234 | return ffi.string(ffi.new(t.."[1]", int), ffi.sizeof(t)) 235 | end 236 | 237 | local function helper_error(tbl, str) 238 | local candidates = {} 239 | 240 | for key in pairs(tbl) do 241 | table.insert(candidates, {key = key, score = util.string_levenshtein(key, str)}) 242 | end 243 | 244 | table.sort(candidates, function(a, b) return a.score < b.score end) 245 | 246 | local found = "" 247 | for i = 1, 5 do 248 | if candidates[i] then 249 | found = found .. "\t" .. candidates[i].key .. "\n" 250 | end 251 | end 252 | 253 | return found 254 | end 255 | 256 | local type_translate = { 257 | i8 = "int8_t", 258 | i16 = "int16_t", 259 | i32 = "int32_t", 260 | i64 = "int64_t", 261 | 262 | u8 = "uint8_t", 263 | u16 = "uint16_t", 264 | u32 = "uint32_t", 265 | u64 = "uint64_t", 266 | } 267 | 268 | function x86_64.get_typestring(mnemonic, ...) 269 | if not x86_64.map[mnemonic] then 270 | return nil, "no such function " .. mnemonic .. "\ndid you mean one of these?\n" .. helper_error(x86_64.map, mnemonic) 271 | end 272 | 273 | local str = {} 274 | local max = select("#", ...) 275 | local lua_number = false 276 | local lua_address = false 277 | 278 | for i = 1, max do 279 | local arg = select(i, ...) 280 | 281 | local found = false 282 | 283 | if type(arg) == "table" and (arg.reg or arg.disp or arg.base) then 284 | if not arg.reg and not arg.base then 285 | if type(arg.disp) == "cdata" then 286 | local size = ffi.sizeof(arg.disp) * 8 287 | str[i] = "m" .. size 288 | else 289 | str[i] = "m?" 290 | lua_number = true 291 | end 292 | elseif arg.indirect then 293 | str[i] = "m" .. x86_64.reginfo[arg.reg].bits 294 | else 295 | if x86_64.reginfo[arg.reg].bits == "xmm" then 296 | str[i] = "xmm[7:0]" 297 | else 298 | str[i] = "r" .. x86_64.reginfo[arg.reg].bits 299 | end 300 | end 301 | elseif type(arg) == "number" then 302 | str[i] = "i?" 303 | lua_number = true 304 | else 305 | local found = false 306 | if type(arg) == "cdata" then 307 | for k,v in pairs(type_translate) do 308 | if ffi.istype(v, arg) then 309 | str[i] = k 310 | found = true 311 | break 312 | end 313 | end 314 | end 315 | if not found then 316 | str[i] = type(arg) 317 | end 318 | end 319 | end 320 | 321 | if lua_number then 322 | for i, arg in ipairs(str) do 323 | if arg:sub(-1) == "?" then 324 | local num = select(i, ...) 325 | if type(num) == "table" and num.disp then 326 | num = num.disp 327 | end 328 | 329 | for _, bits in ipairs({"8", "16", "32", "64"}) do 330 | str[i] = arg:sub(0, 1) .. bits 331 | local test = table.concat(str, ",") 332 | 333 | if bits == "8" and num > -128 and num < 128 and x86_64.map[mnemonic][test] then 334 | break 335 | elseif bits == "16" and num > -13824 and num < 13824 and x86_64.map[mnemonic][test] then 336 | break 337 | elseif bits == "32" and num > -2147483648 and num < 2147483648 and x86_64.map[mnemonic][test] then 338 | break 339 | elseif x86_64.map[mnemonic][test] then 340 | break 341 | end 342 | end 343 | end 344 | end 345 | end 346 | 347 | str = table.concat(str, ",") 348 | 349 | if not x86_64.map[mnemonic][str] then 350 | return nil, mnemonic .. " does not take arguments " .. str .. "\ndid you mean one of these?\n" .. helper_error(x86_64.map[mnemonic], str) 351 | end 352 | 353 | return str 354 | end 355 | 356 | function x86_64.encode(mnemonic, ...) 357 | local typestr, err = x86_64.get_typestring(mnemonic, ...) 358 | 359 | if not typestr then 360 | error(err, 3) 361 | end 362 | 363 | if x86_64.pre_encode then 364 | local res = x86_64.pre_encode(mnemonic, typestr, ...) 365 | if res ~= nil then 366 | return res 367 | end 368 | end 369 | 370 | local data = x86_64.map[mnemonic][typestr] 371 | local ok, bytes = pcall(data.func, ...) 372 | 373 | if not ok then 374 | print(data.lua) 375 | print(...) 376 | 377 | local a,b = ... 378 | print("op1:") 379 | for k,v in pairs(a or {}) do 380 | print(k,v) 381 | end 382 | 383 | print("op2:") 384 | for k,v in pairs(b or {}) do 385 | print(k,v) 386 | end 387 | error(bytes, 2) 388 | end 389 | 390 | return { 391 | name = mnemonic, 392 | bytes = bytes, 393 | arg_types = typestr, 394 | args = {...}, 395 | metadata = data, 396 | } 397 | end 398 | 399 | return x86_64 -------------------------------------------------------------------------------- /moondust/assembler.lua: -------------------------------------------------------------------------------- 1 | local ffi = require("ffi") 2 | local x86_64 = require("moondust.x86_64") 3 | 4 | local asm = {} 5 | 6 | do 7 | local meta = {} 8 | meta.__index = meta 9 | 10 | function meta:__tostring() 11 | local str = "" 12 | 13 | if self.indirect then 14 | str = str .. "[" 15 | end 16 | 17 | if self.reg then 18 | str = str .. self.reg 19 | end 20 | 21 | if self.index then 22 | str = str .. "+" .. self.index 23 | end 24 | 25 | if self.scale then 26 | str = str .. "*" .. self.scale 27 | end 28 | 29 | if self.disp then 30 | if self.reg or self.index then 31 | str = str .. (self.disp > 0 and "+" or "-") .. tostring(self.disp) 32 | else 33 | str = str .. tostring(self.disp) 34 | end 35 | end 36 | 37 | if self.indirect then 38 | str = str .. "]" 39 | end 40 | 41 | return str 42 | end 43 | 44 | function meta:copy() 45 | return setmetatable({ 46 | reg = self.reg, 47 | base = self.base, 48 | index = self.index, 49 | scale = self.scale, 50 | disp = self.disp, 51 | }, meta) 52 | end 53 | 54 | 55 | function meta:__call() 56 | local new = asm.reg(self.reg, self.index, self.scale, self.disp, true) 57 | return new 58 | end 59 | 60 | function meta.__add(l, r) 61 | if type(l) == "number" then 62 | r,l = l,r 63 | end 64 | 65 | if getmetatable(r) == meta then 66 | l = l:copy() 67 | l.index = r.reg or l.index 68 | l.disp = r.disp or l.disp 69 | l.scale = r.scale or l.scale 70 | l.indirect = true 71 | end 72 | 73 | if type(r) == "number" then 74 | l = l:copy() 75 | l.disp = r 76 | l.scale = l.scale or 1 77 | l.indirect = true 78 | end 79 | 80 | return l 81 | end 82 | 83 | function meta.__sub(l, r) 84 | if type(l) == "number" then 85 | r,l = l,r 86 | end 87 | if type(r) == "number" then 88 | l = l:copy() 89 | l.disp = -r 90 | l.indirect = true 91 | end 92 | 93 | return l 94 | end 95 | 96 | function meta.__mul(l, r) 97 | if type(l) == "number" then 98 | r,l = l,r 99 | end 100 | 101 | if type(r) == "number" then 102 | l = l:copy() 103 | l.scale = r 104 | l.indirect = true 105 | end 106 | 107 | return l 108 | end 109 | 110 | local function create_reg(reg, index, scale, disp, indirect) 111 | return setmetatable({ 112 | reg = reg, 113 | index = index, 114 | disp = disp, 115 | scale = scale, 116 | indirect = indirect, 117 | }, meta) 118 | end 119 | 120 | do 121 | local lib_meta = {} 122 | lib_meta.__index = lib_meta 123 | function lib_meta:__call(reg, index, scale, disp, indirect) 124 | if getmetatable(reg) == meta then 125 | local new = reg:copy() 126 | new.indirect = true 127 | return new 128 | elseif type(reg) ~= "string" then 129 | disp = reg 130 | reg = nil 131 | indirect = true 132 | end 133 | return create_reg(reg, index, scale, disp, indirect) 134 | end 135 | 136 | asm.reg = {} 137 | 138 | for reg in pairs(x86_64.reginfo) do 139 | asm.reg[reg] = create_reg(reg) 140 | end 141 | 142 | asm.reg = setmetatable(asm.reg, lib_meta) 143 | end 144 | end 145 | 146 | function asm.addr(num) 147 | return ffi.cast("void *", num) 148 | end 149 | 150 | do 151 | ffi.cdef("char *strerror(int errnum);") 152 | local function last_error(num) 153 | num = num or ffi.errno() 154 | local err = ffi.string(ffi.C.strerror(num)) 155 | return err == "" and tostring(num) or err 156 | end 157 | 158 | if jit.os ~= "Windows" then 159 | ffi.cdef[[ 160 | char *mmap(void *addr, size_t length, int prot, int flags, int fd, long int offset); 161 | int munmap(void *addr, size_t length); 162 | ]] 163 | 164 | local PROT_READ = 0x1 165 | local PROT_WRITE = 0x2 166 | local PROT_EXEC = 0x4 167 | 168 | local MAP_PRIVATE 169 | local MAP_ANONYMOUS 170 | 171 | if jit.os == "OSX" then 172 | MAP_PRIVATE = 0x0002 173 | MAP_ANONYMOUS = 0x1000 174 | else 175 | MAP_PRIVATE = 0x02 176 | MAP_ANONYMOUS = 0x20 177 | end 178 | 179 | local MAP_FAILED = ffi.cast("char *", -1) 180 | 181 | function asm.executable_memory(str) 182 | local mem = ffi.C.mmap(nil, #str, bit.bor(PROT_READ, PROT_WRITE, PROT_EXEC), bit.bor(MAP_PRIVATE, MAP_ANONYMOUS), -1, 0) 183 | if mem == MAP_FAILED then 184 | return nil, last_error() 185 | end 186 | ffi.copy(mem, str) 187 | return mem 188 | end 189 | else 190 | ffi.cdef([[ 191 | void *VirtualAlloc(void *lpAddress, size_t dwSize, uint16_t flAllocationType, uint16_t flProtect); 192 | int VirtualProtect(void *lpAddress, size_t dwSize, uint16_t flNewProtect, uint16_t *lpflOldProtect); 193 | ]]) 194 | 195 | local PAGE_EXECUTE_READWRITE = 0x40 196 | local PAGE_READWRITE = 0x04 197 | local MEM_COMMIT = 0x00001000 198 | 199 | 200 | function asm.executable_memory(str) 201 | local mem = ffi.C.VirtualAlloc(nil, #str, MEM_COMMIT, PAGE_READWRITE) 202 | if mem == nil then 203 | return nil, "failed to allocate memory" 204 | end 205 | 206 | local temp = ffi.new("uint16_t[1]") 207 | if ffi.C.VirtualProtect(mem, #str, PAGE_EXECUTE_READWRITE, temp) == 0 then 208 | return nil, "failed to mark memory as executable" 209 | end 210 | 211 | ffi.copy(mem, str) 212 | 213 | return mem 214 | end 215 | end 216 | 217 | 218 | function asm.object_to_address(var) 219 | if type(var) == "cdata" or type (var) == "string" then 220 | return ffi.cast("uint64_t", var) 221 | end 222 | 223 | return loadstring("return " .. string.format("%p", var) .. "ULL")() 224 | end 225 | 226 | do 227 | if ffi.os == "Windows" then 228 | ffi.cdef([[ 229 | void *LoadLibraryA(const char *lpLibFileName); 230 | void *GetProcAddress(void *hModule, const char* lpProcName); 231 | ]]) 232 | function asm.address_of(name, lib) 233 | local handle = ffi.C.LoadLibraryA(lib) 234 | local ptr = ffi.C.GetProcAddress(handle, name) 235 | return asm.object_to_address(ptr) 236 | end 237 | else 238 | ffi.cdef([[ 239 | void *dlopen(const char *filename, int flag); 240 | char *dlerror(void); 241 | void *dlsym(void *handle, const char *symbol); 242 | int dlclose(void *handle); 243 | ]]) 244 | 245 | function asm.address_of(name, lib) 246 | local handle = ffi.C.dlopen(lib, 1) 247 | local ptr = ffi.C.dlsym(handle, name) 248 | return asm.object_to_address(ptr) 249 | end 250 | end 251 | end 252 | end 253 | 254 | local function check_gas(data) 255 | local util = require("util") 256 | local gas = require("gas") 257 | 258 | local str = data.name .. " " 259 | local types = util.string_split(data.arg_types, ",") 260 | for i = 1, #data.args do 261 | local arg, type = data.args[i], types[i] 262 | 263 | if type:sub(1,1) == "i" or type:sub(1,1) == "u" then 264 | if _G.type(arg) == "string" then 265 | arg = tostring(asm.ObjectToAddress(arg)):sub(0,-3) 266 | elseif _G.type(arg) == "cdata" then 267 | arg = tonumber(arg) 268 | end 269 | 270 | str = str .. tostring(arg) 271 | end 272 | if type:sub(1,1) == "r" or type:sub(1,1) == "m" then 273 | str = str .. tostring(arg) 274 | end 275 | if i ~= #data.args then 276 | str = str .. "," 277 | end 278 | end 279 | 280 | print(gas.dump_asm(str, function(bytes) return util.string_binformat(bytes, 15, " ", true) end, data.bytes, false)) 281 | end 282 | 283 | do 284 | local meta = {} 285 | 286 | function meta:__index(key) 287 | if meta[key] then 288 | return meta[key] 289 | end 290 | 291 | if x86_64.map[key] then 292 | return function(_, ...) 293 | return self:encode(key, ...) 294 | end 295 | end 296 | end 297 | 298 | function meta:get_size() 299 | return self.size 300 | end 301 | 302 | function meta:write(str) 303 | table.insert(self.buffer, str) 304 | self.size = self.size + #str 305 | end 306 | 307 | function meta:encode(name, ...) 308 | local data = x86_64.encode(name, ...) 309 | if type(data) == "table" and type(data.bytes) == "string" then 310 | self:write(data.bytes) 311 | end 312 | return data 313 | end 314 | 315 | function meta:label(name) 316 | local label = {name = name, start_pos = self:get_size()} 317 | table.insert(self.labelsi, label) 318 | self.labels[name] = label 319 | end 320 | 321 | function meta:get_label(name) 322 | return self.labels[name] 323 | end 324 | 325 | function meta:compile() 326 | if #self.buffer == 0 then 327 | return nil, "nothing to assemble" 328 | end 329 | 330 | local str = table.concat(self.buffer) 331 | 332 | local found = {} 333 | for _, stop in ipairs(self.labelsi) do 334 | if stop.stop_pos then 335 | for _, start in ipairs(self.labelsi) do 336 | if start.name == stop.name and start.start_pos then 337 | table.insert(found, { 338 | name = start.name, 339 | start = start.start_pos, 340 | stop = stop.stop_pos, 341 | mnemonic = stop.mnemonic 342 | }) 343 | end 344 | end 345 | end 346 | end 347 | 348 | table.sort(found, function(a, b) return a.stop < b.stop end) 349 | local offset = 0 350 | 351 | for i, label in ipairs(found) do 352 | local start = label.start 353 | local stop = label.stop 354 | 355 | local rel = start - stop 356 | 357 | if rel < 0 then 358 | rel = rel - #x86_64.encode(label.mnemonic, rel).bytes -- FIX ME 359 | end 360 | 361 | local bytes = x86_64.encode(label.mnemonic, rel).bytes 362 | 363 | str = str:sub(1, stop + offset) .. bytes .. str:sub(stop + 1 + offset, #str) 364 | offset = offset + #bytes 365 | end 366 | 367 | local mem = assert(asm.executable_memory(str)) 368 | 369 | if mem == nil then 370 | return nil, "failed to map memory" 371 | end 372 | 373 | return mem, #str 374 | end 375 | 376 | function asm.assembler() 377 | local self = setmetatable({}, meta) 378 | 379 | self.buffer = {} 380 | self.size = 0 381 | self.labels = {} 382 | self.labelsi = {} 383 | 384 | function x86_64.pre_encode(name, argstr, a,b,c,d,e) 385 | local info = x86_64.map[name][argstr] 386 | if info.has_relative and type(a) == "string" then 387 | table.insert(self.labelsi, {name = a, stop_pos = self:get_size(), mnemonic = name}) 388 | return false 389 | end 390 | end 391 | 392 | return self 393 | end 394 | end 395 | 396 | local type_translate = { 397 | i8 = "int8_t", 398 | i16 = "int16_t", 399 | i32 = "int32_t", 400 | i64 = "int64_t", 401 | 402 | u8 = "uint8_t", 403 | u16 = "uint16_t", 404 | u32 = "uint32_t", 405 | u64 = "uint64_t", 406 | } 407 | 408 | for k,v in pairs(type_translate) do 409 | asm[k] = function(num) return ffi.new(k, num) end 410 | end 411 | 412 | function asm.compile(func, validate) 413 | local a = asm.assembler() 414 | 415 | local env = {} 416 | 417 | for k,v in pairs(type_translate) do 418 | env[k] = function(num) return ffi.new(k, num) end 419 | end 420 | 421 | env.pos = function() return a:get_size() end 422 | env.label = function(name) return a:label(name) end 423 | 424 | setfenv(func, setmetatable({}, {__index = function(s, key) 425 | if env[key] then 426 | return env[key] 427 | end 428 | 429 | if x86_64.map[key] then 430 | return function(...) 431 | local data, err = a:encode(key, ...) 432 | if validate and data then 433 | check_gas(data) 434 | end 435 | end 436 | end 437 | 438 | if x86_64.reginfo[key] then 439 | return asm.reg(key) 440 | end 441 | 442 | return _G[key] 443 | end}))(a) 444 | 445 | return a:compile() 446 | end 447 | 448 | return asm --------------------------------------------------------------------------------