├── .github └── workflows │ └── build.yml ├── .gitignore ├── .nojekyll ├── .npmignore ├── .vscode └── launch.json ├── Dockerfile ├── LICENSE.md ├── Makefile ├── README.md ├── cli ├── .npmignore ├── README.md ├── cli.js ├── debug.js ├── debug.s ├── debug32.s ├── elf.js ├── files.js ├── main.js └── package.json ├── codemirror ├── README.md ├── assembly.grammar ├── assembly.js ├── compilerPlugin.js ├── debugPlugin.js ├── errorPlugin.js ├── package.json ├── parser.js ├── parser.terms.js ├── shellcodePlugin.js └── tokenizer.js ├── core ├── README.md ├── bitfield.js ├── compiler.js ├── directives.js ├── instructions.js ├── main.js ├── mnemonicList.js ├── mnemonics.js ├── operands.js ├── operations.js ├── package.json ├── parser.js ├── relocations.js ├── sections.js ├── shuntingYard.js ├── statement.js ├── symbols.js └── utils.js ├── gh-pages ├── code.js ├── compartment.js ├── editor.js ├── favicon.ico ├── index.html ├── make_editor.js └── style.css ├── package-lock.json ├── package.json └── t ├── basic.js ├── exit.js ├── opcodes.js ├── operands.js ├── recursive.js ├── resize.js ├── sections.js └── symedit.js /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Test, Build and Deploy 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | test: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | 12 | - uses: actions/setup-node@v3 13 | with: 14 | node-version: 22 15 | 16 | - name: Get the latest version of binutils and install GAS 17 | run: | 18 | LATEST_RELEASE_URL=$(wget -q -O - ftp://ftp.gnu.org/gnu/binutils/ | grep -o "ftp[^\"]*.tar.gz" | sort -V | tail -n 1) 19 | wget -m ${LATEST_RELEASE_URL} -nd -O binutils-latest.tar.gz 20 | mkdir binutils 21 | tar xf binutils-latest.tar.gz -C binutils --strip-components 1 22 | cd binutils 23 | ./configure 24 | if [ $(which sudo) ] 25 | then sudo make all-gas install-gas 26 | else 27 | su -c "make all-gas install-gas" 28 | fi 29 | as --version 30 | 31 | - name: Install and Build 32 | run: | 33 | npm install 34 | npm run build --prefix cli 35 | npm run build --prefix codemirror 36 | 37 | - name: Test 38 | run: | 39 | npm test 40 | 41 | build-and-deploy: 42 | runs-on: ubuntu-latest 43 | needs: test 44 | steps: 45 | - uses: actions/checkout@v3 46 | 47 | - uses: actions/setup-node@v3 48 | with: 49 | node-version: 22 50 | 51 | - name: Install and Build 52 | run: | 53 | npm install 54 | npm run build 55 | 56 | - name: Deploy 🚀 57 | uses: JamesIves/github-pages-deploy-action@4.1.4 58 | with: 59 | branch: gh-pages 60 | folder: gh-pages 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test.js 3 | cli/debug 4 | cli/debug32 5 | gh-pages/index.js* -------------------------------------------------------------------------------- /.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NewDefectus/defasm/77eeba9742351daf249521b4495285d94cdd1912/.nojekyll -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NewDefectus/defasm/77eeba9742351daf249521b4495285d94cdd1912/.npmignore -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Test", 8 | "runtimeExecutable": "npm", 9 | "runtimeArgs": [ 10 | "run-script", 11 | "test" 12 | ], 13 | "port": 9229, 14 | "skipFiles": [ 15 | "/**" 16 | ] 17 | }, 18 | { 19 | "name": "Launch Chrome", 20 | "request": "launch", 21 | "type": "pwa-chrome", 22 | "url": "http://localhost:8000", 23 | "webRoot": "${workspaceFolder}/gh-pages" 24 | }, 25 | { 26 | "type": "node", 27 | "request": "launch", 28 | "name": "Run server", 29 | "runtimeExecutable": "npm", 30 | "runtimeArgs": [ 31 | "run-script", 32 | "dev" 33 | ], 34 | "port": 9229, 35 | "skipFiles": [ 36 | "/**" 37 | ] 38 | } 39 | ] 40 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16.8.0-alpine3.14 as builder 2 | 3 | RUN mkdir /empty 4 | RUN apk add --no-cache binutils gcc g++ python3 make linux-headers 5 | 6 | RUN npm install -g caxa 7 | COPY ./cli /usr/local/defasm/cli 8 | 9 | RUN caxa -i /usr/local/defasm/cli -o /usr/bin/defasm -- \ 10 | "{{caxa}}/node_modules/.bin/node" "{{caxa}}/main.js" 11 | 12 | FROM scratch 13 | 14 | COPY --from=0 /lib/ld-musl-x86_64.so.1 /lib/libz.so.1 /lib/ 15 | COPY --from=0 /empty /proc 16 | COPY --from=0 /empty /tmp 17 | COPY --from=0 /usr/bin/defasm /usr/bin/ld /usr/bin/ 18 | COPY --from=0 \ 19 | /usr/lib/libstdc++.so \ 20 | /usr/lib/libstdc++.so.6.0.28 \ 21 | /usr/lib/libstdc++.so.6 \ 22 | /usr/lib/libgcc_s.so.1 \ 23 | /usr/lib/libbfd-2.35.2.so \ 24 | /usr/lib/libctf.so.0 \ 25 | /usr/lib/ 26 | 27 | ENTRYPOINT [ "/usr/bin/defasm" ] -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021, Alon Ran 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | docker build -t defasm/cli . 3 | docker inspect -f "{{ .Size }}" defasm/cli 4 | run: 5 | docker run --rm -i defasm/cli -r -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DefAssembler 2 | DefAssembler is an incremental x86-64 assembler written in JavaScript. It aims to be relatively lightweight, easy to use, and efficient. For a quick demonstration, I recommend checking out the [GitHub Pages site](https://newdefectus.github.io/defasm/). DefAssembler is also the assembler used by [Code Golf](https://code.golf/fizz-buzz#assembly). 3 | 4 | DefAssembler is currently available in the following npm packages, all of which are developed under this repository: 5 | * `@defasm/core` - this is the main package of the assembler; it exports the functionality required to assemble code. 6 | * `@defasm/cli` - a command-line utility allowing you to assemble code and output ELF object files. It also provides an option to link and execute the code immediately, which displays register contents, flags, and signal information upon the program's crash. 7 | * `@defasm/codemirror` - a collection of [CodeMirror 6](https://codemirror.net/6/) plugins which demonstrate the assembler's real-time assembly capabilities. 8 | 9 | See each package's respective README file for more information. 10 | 11 | For development, clone this repository and run `npm install`. Run `npm run dev` to open the demo on port 8000 (uses esbuild's watch feature, so you should refresh after making changes in the code). `npm test` runs the test suite (currently quite small; contributions are very much welcome!). -------------------------------------------------------------------------------- /cli/.npmignore: -------------------------------------------------------------------------------- 1 | debug 2 | debug32 -------------------------------------------------------------------------------- /cli/README.md: -------------------------------------------------------------------------------- 1 | # DefAssembler - Command-line Utility 2 | This package exports a command-line program called `defasm`, with which you can assemble and (optionally) execute Assembly source code. To use the package, install it globally with `npm install -g @defasm/core` and then run `defasm --help`. 3 | 4 | Note that the package does *not* come with an emulator. When executing with `-r`, the assembled code is saved into an ELF file and then executed directly. As such, the execution utility is currently functional only on systems that support ELF files (e.g. Linux). 5 | 6 | ## Options 7 | * `-i`, `--intel` - use Intel syntax when assembling (defaults to AT&T) 8 | * `-m32`, `-m64` - compile for 32- or 64-bit machines, respectively (defaults to whatever the current machine supports, or 64-bit if irrelevant) 9 | * `-o FILE`, `--output FILE` - set the path to the output file (defaults to 'a.out' in the current directory, or /tmp/asm.out if `--run` is provided) 10 | * `-x`, `--executable` - generate an executable file from the input file (note that it will not be linked against other files) 11 | * `-w`, `--writable` - make the .text section writable 12 | * `-r`, `--run` - if given, the assembler will execute the program and print crash information. All parameters following this flag are sent to the program as runtime arguments 13 | * `--size-out=FD` - set the file descriptor to write the number (in ASCII) of bytes generated by the assembler to -------------------------------------------------------------------------------- /cli/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import fs from "fs"; 4 | import { arch } from "os"; 5 | 6 | import { AssemblyState } from "@defasm/core"; 7 | import { createExecutable, createObject, debug } from "./main.js"; 8 | 9 | 10 | let args = process.argv.slice(2); 11 | let code = undefined; 12 | let sizeOutFD = null; 13 | let makeExecutable = false, execute = false, writable = false; 14 | let outputFile = null, inputFile = null; 15 | let runtimeArgs = []; 16 | let assemblyConfig = {}; 17 | 18 | if(args[0] === '-h' || args[0] === '--help') 19 | { 20 | console.log( 21 | `Usage: defasm [options] [file] 22 | Options: 23 | -i, --intel Use Intel syntax when assembling (defaults to AT&T) 24 | -m32, -m64 Compile for 32- or 64-bit machines, respectively (defaults to whatever the current machine supports, or 64-bit if irrelevant) 25 | -o, --output FILE Set the path to the output file (defaults to 'a.out' in current directory, or /tmp/asm if --run is provided) 26 | -x, --executable Generate an executable file from the input file (note that it will not be linked against other files) 27 | -w, --writable Make the .text section writable 28 | -r, --run [arguments...] If given, the assembler will execute the program and print crash information. All parameters following this flag are sent to the program as runtime arguments 29 | --size-out=FD Set the file descriptor to write the number (in ASCII) of bytes generated by the assembler to` 30 | ); 31 | process.exit(); 32 | } 33 | 34 | if(arch() === 'ia32') 35 | assemblyConfig.bitness = 32; 36 | 37 | try 38 | { 39 | while(args.length > 0) 40 | { 41 | let arg = args.shift(); 42 | if(arg[0] !== '-') 43 | { 44 | inputFile = arg; 45 | continue; 46 | } 47 | 48 | if(arg.startsWith('--size-out=')) 49 | { 50 | sizeOutFD = parseInt(arg.slice('--size-out='.length)); 51 | if(isNaN(sizeOutFD)) 52 | throw "--size-out expects a file descriptor"; 53 | } 54 | else switch(arg) 55 | { 56 | case '-r': 57 | case '--run': 58 | makeExecutable = execute = true; 59 | runtimeArgs = args; 60 | args = []; 61 | break; 62 | 63 | case '-o': 64 | case '--output': 65 | outputFile = args.shift(); 66 | if(outputFile === undefined) 67 | throw "No output file given"; 68 | break; 69 | 70 | case '-m32': 71 | assemblyConfig.bitness = 32; 72 | break; 73 | 74 | case '-m64': 75 | assemblyConfig.bitness = 64; 76 | break; 77 | 78 | case '-i': 79 | case '--intel': 80 | assemblyConfig.syntax = { intel: true, prefix: false }; 81 | break; 82 | 83 | case '-x': 84 | case '--executable': 85 | makeExecutable = true; 86 | break; 87 | 88 | case '-w': 89 | case '--writable': 90 | assemblyConfig.writableText = true; 91 | break; 92 | 93 | default: 94 | throw "Unknown flag " + arg; 95 | } 96 | } 97 | 98 | if(outputFile === null) 99 | outputFile = execute ? '/tmp/asm' : 'a.out'; 100 | 101 | if(inputFile === null) 102 | { 103 | code = ""; 104 | process.stdin.on("data", x => code += x.toString()); 105 | process.stdin.on("end", assemble); 106 | } 107 | else 108 | { 109 | try { code = fs.readFileSync(inputFile).toString(); } 110 | catch(e) { throw "Couldn't read file " + inputFile; } 111 | assemble(); 112 | } 113 | } 114 | catch(e) 115 | { 116 | console.error(e); 117 | process.exit(1); 118 | } 119 | 120 | function assemble() 121 | { 122 | // Ensure the output path is correct 123 | if(outputFile[0] != '/' && outputFile[0] != '.') 124 | outputFile = './' + outputFile; 125 | 126 | let state = new AssemblyState(assemblyConfig), size = 0; 127 | 128 | try 129 | { 130 | state.compile(code, { haltOnError: true }); 131 | } 132 | catch(e) 133 | { 134 | console.error(e); 135 | return; 136 | } 137 | finally 138 | { 139 | size = state.head.length(); 140 | if(sizeOutFD !== null) 141 | { 142 | fs.write(sizeOutFD, size + '\n', err => err && console.warn("Failed writing to size-out")); 143 | fs.close(sizeOutFD, err => err && console.warn("Failed closing size-out")); 144 | } 145 | } 146 | 147 | if(makeExecutable) 148 | { 149 | createExecutable(outputFile, state); 150 | if(execute) 151 | process.exit(debug(outputFile, runtimeArgs, state)); 152 | } 153 | else 154 | createObject(outputFile, state); 155 | 156 | process.exit(0); 157 | } -------------------------------------------------------------------------------- /cli/debug.js: -------------------------------------------------------------------------------- 1 | import { AssemblyState } from "@defasm/core"; 2 | import { execFileSync } from "child_process"; 3 | import { readFileSync } from "fs"; 4 | 5 | import { resolve, dirname } from 'path'; 6 | import { fileURLToPath } from 'url'; 7 | 8 | const signalNames = { 9 | 4: "illegal instruction", 10 | 5: "trace/breakpoint trap", 11 | 7: "bus error", 12 | 8: "floating point error", 13 | 11: "segmentation fault", 14 | 17: "EXIT", 15 | 29: "i/o error", 16 | 31: "bad system call" 17 | }; 18 | 19 | /** 20 | * @param {AssemblyState} state 21 | * @param {Number} address 22 | */ 23 | function findInstruction(state, address) 24 | { 25 | for(const section of state.sections) 26 | { 27 | const segment = section.programHeader; 28 | if(address >= segment.p_vaddr && address < Math.ceil((segment.p_vaddr + segment.p_memsz) / 0x1000) * 0x1000) 29 | { 30 | const localAddress = address - segment.p_vaddr; 31 | let node = section.head.next; 32 | while(node) 33 | { 34 | if(node.statement.length > 0 && node.statement.address >= localAddress) 35 | return { section, instr: node.statement }; 36 | node = node.next; 37 | } 38 | return { section, instr: null }; 39 | } 40 | } 41 | 42 | return { section: null, instr: null }; 43 | } 44 | 45 | function execute(path, args, bitness) 46 | { 47 | const debuggerName = bitness === 32 ? 'debug32' : 'debug'; 48 | execFileSync(resolve(dirname(fileURLToPath(import.meta.url)), `./${debuggerName}`), [path, ...args], { stdio: 'inherit' }); 49 | const data = readFileSync('/tmp/asm_trace'); 50 | const signo = data.readUInt32LE(0); 51 | const status = data.readUInt32LE(4); 52 | const errorAddr = bitness === 64 ? data.readBigUInt64LE(8) : data.readUint32LE(8); 53 | return { 54 | errorAddr, 55 | status, 56 | signal: signalNames[signo] ?? `unknown signal (${signo})`, 57 | dump: data.toString('ascii', bitness === 64 ? 16 : 12) 58 | }; 59 | } 60 | 61 | /** 62 | * @param {string} path 63 | * @param {string[]} args 64 | * @param {AssemblyState} state 65 | * @returns 66 | */ 67 | export function debug(path, args, state) 68 | { 69 | const { signal, status, errorAddr, dump } = execute(path, args, state.bitness); 70 | if(signal === "EXIT") 71 | return status; 72 | 73 | let errLine = null; 74 | let pos = "on"; 75 | if(signal === "trace/breakpoint trap") // Weird behavior with int3 76 | errorAddr--; 77 | 78 | let { instr: crashedInstr, section: crashedSection } = findInstruction(state, Number(errorAddr)); 79 | if(crashedInstr === null) 80 | { 81 | if(crashedSection !== null) 82 | { 83 | pos = 'after'; 84 | state.iterate((instr, line) => { 85 | if(instr.section === crashedSection) 86 | errLine = line; 87 | }); 88 | } 89 | } 90 | else 91 | { 92 | state.iterate((instr, line) => { 93 | if(errLine) 94 | return; 95 | if(instr == crashedInstr) 96 | errLine = line; 97 | }); 98 | } 99 | 100 | console.warn(`Signal: ${signal}${ 101 | errLine !== null ? ` ${pos} line ${errLine}` : '' 102 | } (%${ 103 | state.bitness === 64 ? 'rip' : 'eip' 104 | } was ${errorAddr.toString(16).toUpperCase().padStart(state.bitness >> 2, '0')})`); 105 | console.warn(dump); 106 | return 0; 107 | } -------------------------------------------------------------------------------- /cli/debug.s: -------------------------------------------------------------------------------- 1 | sys_write = 1 2 | sys_open = 2 3 | sys_close = 3 4 | sys_fork = 57 5 | sys_execve = 59 6 | sys_exit = 60 7 | sys_ptrace = 101 8 | sys_prctl = 157 9 | sys_waitid = 247 10 | 11 | PTRACE_TRACEME = 0 12 | PTRACE_CONT = 7 13 | PTRACE_GETREGS = 12 14 | PTRACE_GETSIGINFO = 16898 15 | 16 | STDOUT_FILENO = 1 17 | PR_SET_PDEATHSIG = 1 18 | SIGTRAP = 5 19 | SIGTERM = 15 20 | P_PID = 1 21 | WSTOPPED = 2 22 | WEXITED = 4 23 | O_WRONLY = 1 24 | O_CREAT = 0o100 25 | 26 | .text 27 | .globl _start 28 | _start: 29 | 30 | mov $sys_fork, %eax 31 | syscall 32 | 33 | test %eax, %eax 34 | jz execFile 35 | 36 | # Tracing the file 37 | .bss 38 | siginfo: 39 | si_signo: .quad 0 40 | .octa 0 41 | si_status: .quad 0 42 | .octa 0, 0, 0, 0, 0, 0 43 | 44 | regs: 45 | r_R15: .quad 0 46 | r_R14: .quad 0 47 | r_R13: .quad 0 48 | r_R12: .quad 0 49 | r_RBP: .quad 0 50 | r_RBX: .quad 0 51 | r_R11: .quad 0 52 | r_R10: .quad 0 53 | r_R9: .quad 0 54 | r_R8: .quad 0 55 | r_RAX: .quad 0 56 | r_RCX: .quad 0 57 | r_RDX: .quad 0 58 | r_RSI: .quad 0 59 | r_RDI: .quad 0 60 | r_ORIG_RAX: .quad 0 61 | r_RIP: .quad 0 62 | r_CS: .quad 0 63 | r_FLAGS: .quad 0 64 | r_RSP: .quad 0 65 | r_SS: .quad 0 66 | r_FS_BASE: .quad 0 67 | r_GS_BASE: .quad 0 68 | r_DS: .quad 0 69 | r_ES: .quad 0 70 | r_FS: .quad 0 71 | r_GS: .quad 0 72 | 73 | outputData: 74 | signo: .long 0 75 | status: .long 0 76 | errAddr: .quad 0 77 | outputDataLength = . - outputData 78 | 79 | .section .rodata 80 | outputPath: .asciz "/tmp/asm_trace" 81 | 82 | .text 83 | mov %eax, %esi # %esi now stores the child's PID 84 | call waitpid 85 | 86 | continue: 87 | 88 | mov $sys_ptrace, %eax 89 | mov $PTRACE_CONT, %edi 90 | mov $0, %r10d 91 | syscall 92 | 93 | call waitpid 94 | 95 | # Get registers 96 | mov $sys_ptrace, %eax 97 | mov $PTRACE_GETREGS, %edi 98 | mov $regs, %r10d 99 | syscall 100 | 101 | # Get signal information 102 | mov $sys_ptrace, %eax 103 | mov $PTRACE_GETSIGINFO, %edi 104 | mov $siginfo, %r10d 105 | syscall 106 | 107 | cmpl $SIGTRAP, (%r10) 108 | jne endDebug 109 | call dumpState 110 | jmp continue 111 | 112 | endDebug: 113 | 114 | mov si_signo, %eax; mov %eax, signo 115 | mov si_status, %eax; mov %eax, status 116 | mov r_RIP, %eax; mov %eax, errAddr 117 | 118 | # Write to file 119 | mov $sys_open, %eax 120 | mov $outputPath, %edi 121 | mov $O_WRONLY | O_CREAT, %esi 122 | mov $0o666, %edx 123 | syscall 124 | 125 | mov %eax, %edi # %edi now stores the file descriptor 126 | mov $sys_write, %eax 127 | mov $outputData, %esi 128 | mov $outputDataLength, %edx 129 | syscall 130 | 131 | mov %edi, dumpFile 132 | call dumpState 133 | 134 | mov $sys_close, %eax 135 | syscall 136 | 137 | # Exit 138 | mov $sys_exit, %eax 139 | mov $0, %edi 140 | syscall 141 | 142 | 143 | 144 | 145 | # Execute the file 146 | execFile: 147 | # prctl, to ensure the child process 148 | # doesn't continue after its parent dies 149 | mov $sys_prctl, %eax 150 | mov $PR_SET_PDEATHSIG, %edi 151 | mov $SIGTERM, %esi 152 | syscall 153 | 154 | # Enable ptrace 155 | mov $sys_ptrace, %eax 156 | mov $PTRACE_TRACEME, %edi 157 | syscall 158 | 159 | # execve 160 | mov $sys_execve, %eax 161 | pop %rcx 162 | pop %rcx 163 | mov (%rsp), %rdi 164 | mov %rsp, %rsi 165 | mov $0, %edx 166 | syscall 167 | 168 | 169 | 170 | 171 | waitpid: 172 | mov $sys_waitid, %eax 173 | mov $P_PID, %edi 174 | mov $siginfo, %edx 175 | mov $WSTOPPED | WEXITED, %r10d 176 | mov $0, %r8d 177 | syscall 178 | ret 179 | 180 | .section .rodata 181 | hexaChars: .string "0123456789ABCDEF" 182 | 183 | .data 184 | outputBuffer: .byte 0 185 | dumpFile: .long STDOUT_FILENO 186 | 187 | .text 188 | 189 | # Print the value of %rax in 16 hexadecimal characters 190 | printQuad: 191 | push %rbx 192 | push %rdx 193 | push %rsi 194 | push %rdi 195 | push %rcx 196 | 197 | mov $16, %ebx 198 | mov $outputBuffer, %esi 199 | 200 | nibbleLoop: 201 | rol $4, %rax 202 | mov %eax, %edx 203 | and $0xF, %edx 204 | mov hexaChars(%rdx), %dl 205 | mov %dl, (%rsi) 206 | 207 | # Print the nibble 208 | push %rax 209 | mov $1, %dl 210 | mov $sys_write, %eax 211 | mov dumpFile, %edi 212 | syscall 213 | pop %rax 214 | 215 | dec %ebx 216 | jnz nibbleLoop 217 | 218 | pop %rcx 219 | pop %rdi 220 | pop %rsi 221 | pop %rdx 222 | pop %rbx 223 | ret 224 | 225 | .section .rodata 226 | 227 | registerOrder: 228 | .long r_RAX, r_R8, r_RBX, r_R9, r_RCX, r_R10, r_RDX, r_R11 229 | .long r_RSI, r_R12, r_RDI, r_R13, r_RSP, r_R14, r_RBP, r_R15 230 | .long r_FLAGS 231 | 232 | flagOrder: 233 | .byte 0, 6, 11, 7, 10, 2 234 | 235 | dumpAlertString: .string "SIGTRAP DETECTED - DUMPING STATE\n" 236 | dumpAlertSize = . - dumpAlertString 237 | 238 | dumpString: .string "\ 239 | Registers: 240 | %rax = \0 %r8 = \0 241 | %rbx = \0 %r9 = \0 242 | %rcx = \0 %r10 = \0 243 | %rdx = \0 %r11 = \0 244 | %rsi = \0 %r12 = \0 245 | %rdi = \0 %r13 = \0 246 | %rsp = \0 %r14 = \0 247 | %rbp = \0 %r15 = \0 248 | Flags (\0): 249 | Carry = \0 Zero = \0 250 | Overflow = \0 Sign = \0 251 | Direction = \0 Parity = \0 252 | 253 | \0" 254 | 255 | flagMessages: 256 | .string "0 (no carry) ", "1 (carry) " 257 | .string "0 (isn't zero) ", "1 (is zero) " 258 | .string "0 (no overflow) ", "1 (overflow) " 259 | .string "0 (positive) ", "1 (negative) " 260 | .string "0 (up) ", "1 (down) " 261 | .string "0 (odd) ", "1 (even) " 262 | flagMsgLen = 16 263 | 264 | .text 265 | dumpState: 266 | push %rsi 267 | push %rdi 268 | push %rdx 269 | push %rcx 270 | push %rbx 271 | 272 | cmpl $STDOUT_FILENO, dumpFile 273 | jne postPrintAlert 274 | mov $dumpAlertString, %esi 275 | mov $dumpAlertSize, %edx 276 | mov $STDOUT_FILENO, %edi 277 | mov $sys_write, %eax 278 | syscall 279 | postPrintAlert: 280 | 281 | xor %ebx, %ebx 282 | mov $dumpString, %esi 283 | 284 | registerLoop: 285 | mov %esi, %edi 286 | mov $-1, %ecx 287 | xor %al, %al 288 | repnz scasb 289 | 290 | push %rdi 291 | not %ecx 292 | dec %ecx 293 | mov %ecx, %edx 294 | mov $sys_write, %eax 295 | mov dumpFile, %edi 296 | syscall 297 | pop %rsi 298 | 299 | cmp $23, %bl 300 | jge endRegisterLoop 301 | 302 | cmp $17, %bl 303 | jge printFlag 304 | # Select register and print it 305 | mov registerOrder(, %rbx, 4), %eax 306 | mov (%rax), %rax 307 | call printQuad 308 | jmp nextRegister 309 | printFlag: 310 | # Select appropriate flag message 311 | push %rsi 312 | 313 | mov %bl, %dl 314 | sub $17, %dl 315 | mov flagOrder(%rdx), %al 316 | shl $1, %dl 317 | bt %eax, r_FLAGS 318 | adc $0, %dl 319 | 320 | imul $flagMsgLen, %edx, %edx 321 | lea flagMessages(%rdx), %esi 322 | 323 | mov $flagMsgLen, %edx 324 | mov $sys_write, %eax 325 | mov dumpFile, %edi 326 | syscall 327 | 328 | pop %rsi 329 | nextRegister: 330 | inc %ebx 331 | jmp registerLoop 332 | endRegisterLoop: 333 | 334 | pop %rbx 335 | pop %rcx 336 | pop %rdx 337 | pop %rdi 338 | pop %rsi 339 | ret -------------------------------------------------------------------------------- /cli/debug32.s: -------------------------------------------------------------------------------- 1 | sys_exit = 1 2 | sys_fork = 2 3 | sys_write = 4 4 | sys_open = 5 5 | sys_close = 6 6 | sys_execve = 11 7 | sys_ptrace = 26 8 | sys_prctl = 172 9 | sys_waitid = 284 10 | 11 | PTRACE_TRACEME = 0 12 | PTRACE_CONT = 7 13 | PTRACE_GETREGS = 12 14 | PTRACE_GETSIGINFO = 16898 15 | 16 | STDOUT_FILENO = 1 17 | PR_SET_PDEATHSIG = 1 18 | SIGTRAP = 5 19 | SIGTERM = 15 20 | P_PID = 1 21 | WSTOPPED = 2 22 | WEXITED = 4 23 | O_WRONLY = 1 24 | O_CREAT = 0o100 25 | O_TRUNC = 0o1000 26 | 27 | .text 28 | .globl _start 29 | _start: 30 | 31 | mov $sys_fork, %eax 32 | int $0x80 33 | 34 | test %eax, %eax 35 | jz execFile 36 | 37 | # Tracing the file 38 | .bss 39 | siginfo: 40 | si_signo: .quad 0 41 | .octa 0 42 | si_status: .quad 0 43 | .octa 0, 0, 0, 0, 0, 0 44 | 45 | regs: 46 | r_EBX: .long 0 47 | r_ECX: .long 0 48 | r_EDX: .long 0 49 | r_ESI: .long 0 50 | r_EDI: .long 0 51 | r_EBP: .long 0 52 | r_EAX: .long 0 53 | r_XDS: .long 0 54 | r_XES: .long 0 55 | r_XFS: .long 0 56 | r_XGS: .long 0 57 | r_ORIG_EAX: .long 0 58 | r_EIP: .long 0 59 | r_XCS: .long 0 60 | r_EFLAGS: .long 0 61 | r_ESP: .long 0 62 | r_XSS: .long 0 63 | 64 | outputData: 65 | signo: .long 0 66 | status: .long 0 67 | errAddr: .long 0 68 | outputDataLength = . - outputData 69 | 70 | .section .rodata 71 | outputPath: .asciz "/tmp/asm_trace" 72 | 73 | .text 74 | mov %eax, %ecx # %ecx now stores the child's PID 75 | call waitpid 76 | 77 | continue: 78 | 79 | mov $sys_ptrace, %eax 80 | mov $PTRACE_CONT, %ebx 81 | mov $0, %esi 82 | int $0x80 83 | 84 | call waitpid 85 | 86 | # Get registers 87 | mov $sys_ptrace, %eax 88 | mov $PTRACE_GETREGS, %ebx 89 | mov $regs, %esi 90 | int $0x80 91 | 92 | # Get signal information 93 | mov $sys_ptrace, %eax 94 | mov $PTRACE_GETSIGINFO, %ebx 95 | mov $siginfo, %esi 96 | int $0x80 97 | 98 | cmpl $SIGTRAP, (%esi) 99 | jne endDebug 100 | call dumpState 101 | jmp continue 102 | 103 | endDebug: 104 | 105 | mov si_signo, %eax; mov %eax, signo 106 | mov si_status, %eax; mov %eax, status 107 | mov r_EIP, %eax; mov %eax, errAddr 108 | 109 | # Write to file 110 | mov $sys_open, %eax 111 | mov $outputPath, %ebx 112 | mov $O_WRONLY | O_CREAT | O_TRUNC, %ecx 113 | mov $0o666, %edx 114 | int $0x80 115 | 116 | mov %eax, %ebx # %ebx now stores the file descriptor 117 | mov $sys_write, %eax 118 | mov $outputData, %ecx 119 | mov $outputDataLength, %edx 120 | int $0x80 121 | 122 | mov %ebx, dumpFile 123 | call dumpState 124 | 125 | mov $sys_close, %eax 126 | int $0x80 127 | 128 | # Exit 129 | mov $sys_exit, %eax 130 | mov $0, %ebx 131 | int $0x80 132 | 133 | 134 | 135 | 136 | # Execute the file 137 | execFile: 138 | # prctl, to ensure the child process 139 | # doesn't continue after its parent dies 140 | mov $sys_prctl, %eax 141 | mov $PR_SET_PDEATHSIG, %ebx 142 | mov $SIGTERM, %ecx 143 | int $0x80 144 | 145 | # Enable ptrace 146 | mov $sys_ptrace, %eax 147 | mov $PTRACE_TRACEME, %ebx 148 | int $0x80 149 | 150 | # execve 151 | mov $sys_execve, %eax 152 | pop %ecx 153 | pop %ecx 154 | mov (%esp), %ebx 155 | mov %esp, %ecx 156 | mov $0, %edx 157 | int $0x80 158 | 159 | 160 | 161 | 162 | waitpid: 163 | mov $sys_waitid, %eax 164 | mov $P_PID, %ebx 165 | mov $siginfo, %edx 166 | mov $WSTOPPED | WEXITED, %esi 167 | mov $0, %edi 168 | int $0x80 169 | ret 170 | 171 | .section .rodata 172 | hexaChars: .string "0123456789ABCDEF" 173 | 174 | .data 175 | outputBuffer: .byte 0 176 | dumpFile: .long STDOUT_FILENO 177 | 178 | .text 179 | 180 | # Print the value of %eax in 8 hexadecimal characters 181 | printDword: 182 | push %ebx 183 | push %ecx 184 | push %edx 185 | 186 | mov $8, %esi 187 | mov $outputBuffer, %ecx 188 | 189 | nibbleLoop: 190 | rol $4, %eax 191 | mov %eax, %edx 192 | and $0xF, %edx 193 | mov hexaChars(%edx), %dl 194 | mov %dl, (%ecx) 195 | 196 | # Print the nibble 197 | push %eax 198 | mov $sys_write, %eax 199 | mov dumpFile, %ebx 200 | mov $1, %edx 201 | int $0x80 202 | pop %eax 203 | 204 | dec %esi 205 | jnz nibbleLoop 206 | 207 | pop %edx 208 | pop %ecx 209 | pop %ebx 210 | ret 211 | 212 | .section .rodata 213 | 214 | registerOrder: 215 | .long r_EAX, r_ESI, r_EBX, r_EDI, r_ECX, r_ESP, r_EDX, r_EBP 216 | .long r_EFLAGS 217 | 218 | flagOrder: 219 | .byte 0, 6, 11, 7, 10, 2 220 | 221 | dumpAlertString: .string "SIGTRAP DETECTED - DUMPING STATE\n" 222 | dumpAlertSize = . - dumpAlertString 223 | 224 | dumpString: .string "\ 225 | Registers: 226 | %eax = \0 %esi = \0 227 | %ebx = \0 %edi = \0 228 | %ecx = \0 %esp = \0 229 | %edx = \0 %ebp = \0 230 | Flags (\0): 231 | Carry = \0 Zero = \0 232 | Overflow = \0 Sign = \0 233 | Direction = \0 Parity = \0 234 | 235 | \0" 236 | 237 | flagMessages: 238 | .string "0 (no carry) ", "1 (carry) " 239 | .string "0 (isn't zero) ", "1 (is zero) " 240 | .string "0 (no overflow) ", "1 (overflow) " 241 | .string "0 (positive) ", "1 (negative) " 242 | .string "0 (up) ", "1 (down) " 243 | .string "0 (odd) ", "1 (even) " 244 | flagMsgLen = 16 245 | 246 | .text 247 | dumpState: 248 | push %esi 249 | push %edi 250 | push %edx 251 | push %ecx 252 | push %ebx 253 | 254 | cmpl $STDOUT_FILENO, dumpFile 255 | jne postPrintAlert 256 | mov $sys_write, %eax 257 | mov $STDOUT_FILENO, %ebx 258 | mov $dumpAlertString, %ecx 259 | mov $dumpAlertSize, %edx 260 | int $0x80 261 | postPrintAlert: 262 | 263 | xor %ebx, %ebx 264 | mov $dumpString, %ecx 265 | 266 | registerLoop: 267 | push %ebx 268 | mov %ecx, %esi 269 | mov %ecx, %edi 270 | mov $-1, %ecx 271 | xor %al, %al 272 | repnz scasb 273 | 274 | push %edi 275 | not %ecx 276 | dec %ecx 277 | mov $sys_write, %eax 278 | mov dumpFile, %ebx 279 | mov %ecx, %edx 280 | mov %esi, %ecx 281 | int $0x80 282 | pop %ecx 283 | 284 | pop %ebx 285 | cmp $15, %bl 286 | jge endRegisterLoop 287 | 288 | cmp $9, %bl 289 | jge printFlag 290 | # Select register and print it 291 | mov registerOrder(, %ebx, 4), %eax 292 | mov (%eax), %eax 293 | call printDword 294 | jmp nextRegister 295 | printFlag: 296 | # Select appropriate flag message 297 | push %ebx 298 | push %ecx 299 | 300 | mov %bl, %dl 301 | sub $9, %dl 302 | mov flagOrder(%edx), %al 303 | shl $1, %dl 304 | bt %eax, r_EFLAGS 305 | adc $0, %dl 306 | 307 | imul $flagMsgLen, %edx, %edx 308 | 309 | mov $sys_write, %eax 310 | mov dumpFile, %ebx 311 | lea flagMessages(%edx), %ecx 312 | mov $flagMsgLen, %edx 313 | int $0x80 314 | 315 | pop %ecx 316 | pop %ebx 317 | nextRegister: 318 | inc %ebx 319 | jmp registerLoop 320 | endRegisterLoop: 321 | 322 | pop %ebx 323 | pop %ecx 324 | pop %edx 325 | pop %edi 326 | pop %esi 327 | ret -------------------------------------------------------------------------------- /cli/elf.js: -------------------------------------------------------------------------------- 1 | import { pseudoSections } from '@defasm/core/sections.js'; 2 | 3 | const relocTypesAMD64 = { 4 | NONE : 0, 5 | 64 : 1, 6 | PC32 : 2, 7 | GOT32 : 3, 8 | PLT32 : 4, 9 | COPY : 5, 10 | GLOB_DAT : 6, 11 | JUMP_SLOT : 7, 12 | RELATIVE : 8, 13 | GOTPCREL : 9, 14 | 32 : 10, 15 | '32S' : 11, 16 | 16 : 12, 17 | PC16 : 13, 18 | 8 : 14, 19 | PC8 : 15, 20 | PC64 : 24, 21 | GOTOFF64 : 25, 22 | GOTPC32 : 26, 23 | SIZE32 : 32, 24 | SIZE64 : 33 25 | }; 26 | 27 | const relocTypes386 = { 28 | NONE : 0, 29 | 32 : 1, 30 | PC32 : 2, 31 | GOT32 : 3, 32 | PLT32 : 4, 33 | COPY : 5, 34 | GLOB_DAT : 6, 35 | JUMP_SLOT : 7, 36 | RELATIVE : 8, 37 | GOTOFF : 9, 38 | GOTPC : 10, 39 | "32PLT" : 11 40 | }; 41 | 42 | /** 43 | * @param {T} fields 44 | * @template T 45 | */ 46 | function header(fields) 47 | { 48 | const result = class 49 | { 50 | #fields; 51 | /** @param {T} config */ 52 | constructor(config) 53 | { 54 | for(const name of Object.keys(fields)) 55 | this[name] = config[name] ?? 0; 56 | this.#fields = fields; 57 | } 58 | 59 | dump(wordSize) 60 | { 61 | const data = Buffer.alloc(result.size(wordSize)); 62 | for(const name of Object.keys(this.#fields)) 63 | { 64 | let [offset, size] = fields[name], number = this[name]; 65 | if(Array.isArray(offset)) offset = offset[wordSize >> 6]; 66 | if(Array.isArray(size)) size = size[wordSize >> 6]; 67 | 68 | switch(size) 69 | { 70 | case 1: data.writeInt8(number, offset); break; 71 | case 2: data.writeInt16LE(number, offset); break; 72 | case 4: data.writeInt32LE(number, offset); break; 73 | case 8: data.writeBigInt64LE(BigInt(number), offset); break; 74 | } 75 | } 76 | return data; 77 | } 78 | 79 | static size(wordSize) 80 | { 81 | let length = 0; 82 | for(let [offset, size] of Object.values(fields)) 83 | { 84 | if(Array.isArray(offset)) offset = offset[wordSize >> 6]; 85 | if(Array.isArray(size)) size = size[wordSize >> 6]; 86 | length = Math.max(length, offset + size); 87 | } 88 | return length; 89 | } 90 | }; 91 | return result; 92 | } 93 | 94 | export const ELFHeader = header({ 95 | /** 0x7F followed by ELF(45 4c 46) in ASCII; these four bytes constitute the magic number. */ 96 | EI_MAG: [0x00, 4], 97 | /** This byte is set to either 1 or 2 to signify 32- or 64-bit format, respectively. */ 98 | EI_CLASS: [0x04, 1], 99 | /** This byte is set to either 1 or 2 to signify little or big endianness, respectively. This affects interpretation of multi-byte fields starting with offset 0x10. */ 100 | EI_DATA: [0x05, 1], 101 | /** Set to 1 for the original and current version of ELF. */ 102 | EI_VERSION: [0x06, 1], 103 | /** Identifies the target operating system ABI. It is often set to 0 regardless of the target platform. */ 104 | EI_OSABI: [0x07, 1], 105 | /** Further specifies the ABI version. Its interpretation depends on the target ABI. Linux kernel (after at least 2.6) has no definition of it, so it is ignored for statically-linked executables. */ 106 | EI_ABIVERSION: [0x08, 1], 107 | /** currently unused, should be filled with zeros. */ 108 | EI_PAD: [0x09, 7], 109 | /** Identifies object file type. */ 110 | e_type: [0x10, 2], 111 | /** Specifies target instruction set architecture. */ 112 | e_machine: [0x12, 2], 113 | /** Set to 1 for the original version of ELF. */ 114 | e_version: [0x14, 4], 115 | /** This is the memory address of the entry point from where the process starts executing. */ 116 | e_entry: [0x18, [4, 8]], 117 | /** Points to the start of the program header table. */ 118 | e_phoff: [[0x1C, 0x20], [4, 8]], 119 | /** Points to the start of the section header table. */ 120 | e_shoff: [[0x20, 0x28], [4, 8]], 121 | /** Interpretation of this field depends on the target architecture. */ 122 | e_flags: [[0x24, 0x30], 4], 123 | /** Contains the size of this header. */ 124 | e_ehsize: [[0x28, 0x34], 2], 125 | /** Contains the size of a program header table entry. */ 126 | e_phentsize: [[0x2A, 0x36], 2], 127 | /** Contains the number of entries in the program header table. */ 128 | e_phnum: [[0x2C, 0x38], 2], 129 | /** Contains the size of a section header table entry. */ 130 | e_shentsize: [[0x2E, 0x3A], 2], 131 | /** Contains the number of entries in the section header table. */ 132 | e_shnum: [[0x30, 0x3C], 2], 133 | /** Contains index of the section header table entry that contains the section names. */ 134 | e_shstrndx: [[0x32, 0x3E], 2] 135 | }); 136 | 137 | export const ProgramHeader = header({ 138 | /** Identifies the type of the segment. */ 139 | p_type: [0x00, 4], 140 | /** Segment-dependent flags. */ 141 | p_flags: [[0x18, 0x04], 4], 142 | /** Offset of the segment in the file image. */ 143 | p_offset: [[0x04, 0x08], [4, 8]], 144 | /** Virtual address of the segment in memory. */ 145 | p_vaddr: [[0x08, 0x10], [4, 8]], 146 | /** On systems where physical address is relevant, reserved for segment's physical address. */ 147 | p_paddr: [[0x0C, 0x18], [4, 8]], 148 | /** Size in bytes of the segment in the file image. May be 0. */ 149 | p_filesz: [[0x10, 0x20], [4, 8]], 150 | /** Size in bytes of the segment in memory. May be 0. */ 151 | p_memsz: [[0x14, 0x28], [4, 8]], 152 | /** 0 and 1 specify no alignment. Otherwise should be a positive, integral power of 2, with p_vaddr equating p_offset modulus p_align. */ 153 | p_align: [[0x1C, 0x30], [4, 8]], 154 | }); 155 | 156 | export const SectionHeader = header({ 157 | /** An offset to a string in the .shstrtab section that represents the name of this section. */ 158 | sh_name: [0x00, 4], 159 | /** Identifies the type of this header. */ 160 | sh_type: [0x04, 4], 161 | /** Identifies the attributes of the section. */ 162 | sh_flags: [0x08, [4, 8]], 163 | /** Virtual address of the section in memory, for sections that are loaded. */ 164 | sh_addr: [[0x0C, 0x10], [4, 8]], 165 | /** Offset of the section in the file image. */ 166 | sh_offset: [[0x10, 0x18], [4, 8]], 167 | /** Size in bytes of the section in the file image. May be 0. */ 168 | sh_size: [[0x14, 0x20], [4, 8]], 169 | /** Contains the section index of an associated section. This field is used for several purposes, depending on the type of section. */ 170 | sh_link: [[0x18, 0x28], 4], 171 | /** Contains extra information about the section. This field is used for several purposes, depending on the type of section. */ 172 | sh_info: [[0x1C, 0x2C], 4], 173 | /** Contains the required alignment of the section. This field must be a power of two. */ 174 | sh_addralign: [[0x20, 0x30], [4, 8]], 175 | /** Contains the size, in bytes, of each entry, for sections that contain fixed-size entries. Otherwise, this field contains zero. */ 176 | sh_entsize: [[0x24, 0x38], [4, 8]], 177 | }); 178 | 179 | export class ELFSection 180 | { 181 | /** 182 | * @param {Object} config 183 | * @param {string} config.name 184 | * @param {StringTable} config.shstrtab 185 | * @param {number} config.type 186 | * @param {Buffer} config.buffer 187 | * @param {number} config.flags 188 | * @param {number} config.address 189 | * @param {number} config.entrySize 190 | * @param {32 | 64} config.bitness 191 | * @param {import('@defasm/core/sections.js').Section} config.section */ 192 | constructor({ type = 0, buffer = Buffer.alloc(0), flags = 0, address = 0, entrySize = 0, link = 0, info = 0, align = 1, linkSection = null, infoSection = null, section = null, bitness = 64 } = {}) 193 | { 194 | this.buffer = buffer; 195 | this.header = new SectionHeader({ 196 | sh_type: type, 197 | sh_flags: flags, 198 | sh_addr: address, 199 | sh_addralign: align, 200 | sh_size: buffer.length, 201 | sh_link: link, 202 | sh_entsize: entrySize, 203 | sh_info: info, 204 | }); 205 | this.entrySize = entrySize; 206 | this.section = section; 207 | this.linkSection = linkSection; 208 | this.infoSection = infoSection; 209 | this.bitness = bitness; 210 | } 211 | 212 | add(buffer) 213 | { 214 | this.buffer = Buffer.concat([this.buffer, buffer]); 215 | this.header.sh_size = this.buffer.length; 216 | } 217 | 218 | /** 219 | * @param {string} name 220 | * @param {StringTable} shstrtab 221 | */ 222 | name(name, shstrtab) 223 | { 224 | this.header.sh_name = shstrtab.getIndex(name); 225 | } 226 | 227 | setIndices(sections) 228 | { 229 | if(this.infoSection) 230 | this.header.sh_info = sections.indexOf(this.infoSection) + 1; 231 | if(this.linkSection) 232 | this.header.sh_link = sections.indexOf(this.linkSection) + 1; 233 | this.header.sh_size = this.buffer.length; 234 | } 235 | } 236 | 237 | export class StringTable extends ELFSection 238 | { 239 | constructor(config) 240 | { 241 | super({ ...config, type: 0x3, buffer: Buffer.alloc(1) }); 242 | this.indices = { "": 0 }; 243 | } 244 | 245 | getIndex(string) 246 | { 247 | if(!this.indices.hasOwnProperty(string)) 248 | { 249 | this.indices[string] = this.buffer.length; 250 | this.add(Buffer.from(string + '\0')); 251 | } 252 | return this.indices[string]; 253 | } 254 | } 255 | 256 | export class SymbolTable extends ELFSection 257 | { 258 | /** 259 | * @param {import("@defasm/core/symbols").Symbol[]} symbols 260 | * @param {StringTable} strtab */ 261 | constructor(config, symbols, strtab) 262 | { 263 | super({ ...config, type: 0x2, entrySize: config.bitness === 64 ? 0x18 : 0x10, info: 1, align: 8 }); 264 | this.symbolCount = 1; 265 | this.strtab = strtab; 266 | this.symbols = symbols.filter(symbol => symbol.value.section != pseudoSections.UND || !symbol.value.symbol); 267 | } 268 | 269 | setIndices(sections) 270 | { 271 | this.buffer = Buffer.alloc(this.entrySize * (this.symbols.length + 1)); 272 | let index = this.entrySize, i = 1; 273 | for(const symbol of this.symbols) 274 | { 275 | const val = symbol.value.flatten(); 276 | 277 | const st_name = this.strtab.getIndex(symbol.name); 278 | const st_info = (symbol.type ?? 0) | (symbol.bind || (val.section == pseudoSections.UND ? 1 : 0)) << 4; 279 | const st_other = symbol.visibility ?? 0; 280 | const st_shndx = val.section.index; 281 | const st_value = val.addend; 282 | const st_size = symbol.size ?? 0; 283 | 284 | if(this.bitness === 32) // Elf32_Sym 285 | { 286 | this.buffer.writeUInt32LE(st_name, index + 0x0); 287 | this.buffer.writeUInt32LE(Number(st_value), index + 0x4); 288 | this.buffer.writeUInt32LE(st_size, index + 0x8); 289 | this.buffer.writeUInt8(st_info, index + 0xC); 290 | this.buffer.writeUInt8(st_other, index + 0xD); 291 | this.buffer.writeUInt16LE(st_shndx, index + 0xE); 292 | } 293 | else // Elf64_Sym 294 | { 295 | this.buffer.writeUInt32LE(st_name, index + 0x0); 296 | this.buffer.writeUInt8(st_info, index + 0x4); 297 | this.buffer.writeUInt8(st_other, index + 0x5); 298 | this.buffer.writeUInt16LE(st_shndx, index + 0x6); 299 | this.buffer.writeBigUInt64LE(st_value, index + 0x8); 300 | this.buffer.writeBigUInt64LE(BigInt(st_size), index + 0x10); 301 | } 302 | 303 | if(!symbol.bind) 304 | this.header.sh_info = i + 1; 305 | 306 | this.symbolCount++; 307 | index += this.entrySize; 308 | i++; 309 | } 310 | this.buffer = this.buffer.subarray(0, index); 311 | super.setIndices(sections); 312 | } 313 | } 314 | 315 | export class RelocationSection extends ELFSection 316 | { 317 | /** 318 | * @param {import('@defasm/core/relocations').RelocEntry[]} relocations 319 | * @param {SymbolTable} symtab */ 320 | constructor(config, relocations, symtab) 321 | { 322 | let entrySize = config.bitness === 64 ? 0x18 : 0xC; 323 | super({ ...config, type: 0x4, entrySize, buffer: Buffer.alloc(entrySize * relocations.length), linkSection: symtab }); 324 | this.relocations = relocations; 325 | 326 | for(const reloc of relocations) 327 | if(reloc.symbol && !symtab.symbols.includes(reloc.symbol)) 328 | symtab.symbols.push(reloc.symbol); 329 | } 330 | 331 | setIndices(sections) 332 | { 333 | let index = 0, symtab = this.linkSection; 334 | const relocTypes = this.bitness === 64 ? relocTypesAMD64 : relocTypes386; 335 | for(const reloc of this.relocations) 336 | { 337 | const type = relocTypes[(reloc.pcRelative ? reloc.functionAddr ? 'PLT' : 'PC' : '') + reloc.size + (reloc.signed ? 'S' : '')]; 338 | const r_offset = reloc.offset; 339 | const r_info_sym = reloc.symbol ? symtab.symbols.indexOf(reloc.symbol) + 1 : 0; 340 | const r_info_type = type; 341 | const r_addend = reloc.addend; 342 | 343 | if(this.bitness === 32) // Elf32_Rela 344 | { 345 | this.buffer.writeUint32LE(r_offset, index + 0x00); 346 | this.buffer.writeUint32LE((r_info_sym << 8) + r_info_type, index + 0x04); 347 | this.buffer.writeInt32LE(Number(r_addend), index + 0x08); 348 | } 349 | else // Elf64_Rela 350 | { 351 | this.buffer.writeBigUInt64LE(BigInt(r_offset), index + 0x00); 352 | this.buffer.writeBigUInt64LE((BigInt(r_info_sym) << 32n) + BigInt(r_info_type), index + 0x8); 353 | this.buffer.writeBigInt64LE(BigInt(r_addend), index + 0x10); 354 | } 355 | index += this.entrySize; 356 | } 357 | super.setIndices(sections); 358 | } 359 | } -------------------------------------------------------------------------------- /cli/files.js: -------------------------------------------------------------------------------- 1 | import { AssemblyState } from "@defasm/core"; 2 | import { ELFHeader, ELFSection, ProgramHeader, RelocationSection, SectionHeader, StringTable, SymbolTable } from "./elf.js"; 3 | import fs from "fs"; 4 | import { pseudoSections, sectionFlags, STT_SECTION } from "@defasm/core/sections.js"; 5 | 6 | var fd = 0; 7 | 8 | function write(buffer, position) 9 | { 10 | fs.writeSync(fd, buffer, 0, buffer.length, position); 11 | } 12 | 13 | /** 14 | * @param {String} filename 15 | * @param {AssemblyState} state 16 | */ 17 | export function createObject(filename, state) 18 | { 19 | fd = fs.openSync(filename, 'w'); 20 | 21 | /** @type {import("@defasm/core/symbols").Symbol[]} */ 22 | let recordedSymbols = []; 23 | for(const fileSymbol of state.fileSymbols) 24 | { 25 | recordedSymbols.push({ 26 | type: STT_FILE, 27 | bind: 0, 28 | name: fileSymbol, 29 | size: 0, 30 | visibility: 0, 31 | value: { section: pseudoSections.ABS, addend: 0n } 32 | }); 33 | } 34 | state.symbols.forEach(symbol => { 35 | if(symbol.type != STT_SECTION) 36 | recordedSymbols.push(symbol); 37 | }); 38 | 39 | const strtab = new StringTable(); 40 | let shstrtab = new StringTable(), symtab = new SymbolTable({ linkSection: strtab, bitness: state.bitness }, recordedSymbols, strtab); 41 | let sections = []; 42 | for(const section of state.sections) 43 | { 44 | section.index = sections.length + 1; 45 | let newSection = new ELFSection({ 46 | type: section.type, 47 | buffer: section.head.dump(), 48 | flags: section.flags, 49 | section, 50 | entrySize: section.entrySize, 51 | bitness: state.bitness 52 | }); 53 | sections.push(newSection); 54 | const relocs = section.getRelocations(); 55 | if(relocs.length > 0) 56 | { 57 | const relocSection = new RelocationSection({ infoSection: newSection, flags: 0x40, align: 8, bitness: state.bitness }, relocs, symtab); 58 | relocSection.name('.rela' + section.name, shstrtab); 59 | sections.push(relocSection); 60 | } 61 | } 62 | 63 | if(symtab.symbols.length > 0) 64 | { 65 | symtab.name('.symtab', shstrtab); 66 | strtab.name('.strtab', shstrtab); 67 | 68 | sections.push(symtab, strtab); 69 | } 70 | sections.push(shstrtab); 71 | shstrtab.name('.shstrtab', shstrtab); 72 | 73 | 74 | // Finalizing 75 | let fileOffset = ELFHeader.size(state.bitness); 76 | 77 | symtab.symbols.sort((a, b) => (a.bind ?? 0) - (b.bind ?? 0) || (b.type ?? 0) - (a.type ?? 0)); 78 | 79 | for(const section of sections) 80 | { 81 | section.setIndices(sections); 82 | const align = section.header.sh_addralign; 83 | if(align) 84 | fileOffset = Math.ceil(fileOffset / align) * align; 85 | 86 | section.header.sh_offset = fileOffset; 87 | fileOffset += section.buffer.length; 88 | if(section.section) 89 | section.name(section.section.name, shstrtab); 90 | } 91 | 92 | // 8-byte alignment 93 | let alignedFileOffset = Math.ceil(fileOffset / 8) * 8; 94 | 95 | 96 | /* Outputting */ 97 | write(new ELFHeader({ 98 | EI_MAG: 0x46_4C_45_7F, 99 | EI_CLASS: state.bitness >> 5, 100 | EI_DATA: 1, 101 | EI_VERSION: 1, 102 | EI_OSABI: 0, 103 | 104 | e_type: 1, // ET_REL 105 | e_machine: state.bitness === 64 ? 0x3E : 0x03, 106 | e_version: 1, 107 | e_shoff: alignedFileOffset, 108 | e_ehsize: ELFHeader.size(state.bitness), 109 | e_shentsize: SectionHeader.size(state.bitness), 110 | e_shnum: sections.length + 1, 111 | e_shstrndx: sections.indexOf(shstrtab) + 1 112 | }).dump(state.bitness), 0); 113 | 114 | // Writing the section buffers 115 | for(const section of sections) 116 | write(section.buffer, section.header.sh_offset); 117 | 118 | // Writing the headers 119 | let index = alignedFileOffset + SectionHeader.size(state.bitness); 120 | for(const section of sections) 121 | { 122 | write(section.header.dump(state.bitness), index); 123 | index += SectionHeader.size(state.bitness); 124 | } 125 | fs.closeSync(fd); 126 | } 127 | 128 | /** 129 | * @param {String} filename 130 | * @param {AssemblyState} state 131 | */ 132 | export function createExecutable(filename, state) 133 | { 134 | fd = fs.openSync(filename, 'w', 0o755); 135 | 136 | let entryPoint = 0, entrySection = state.sections.find(section => section.name == '.text'); 137 | let programHeaders = [], fileOffset = Math.ceil(ELFHeader.size(state.bitness) / 0x1000) * 0x1000, memoryOffset = 0x400000; 138 | let sections = [...state.sections]; 139 | /** @type {import("@defasm/core/symbols").Symbol[]} */ 140 | let commonSymbols = []; 141 | 142 | state.symbols.forEach((symbol, name) => { 143 | if(name == '_start' && symbol.bind == 1) 144 | { 145 | entryPoint = Number(symbol.value.absoluteValue()); 146 | entrySection = symbol.value.section; 147 | } 148 | if(symbol.value.section == pseudoSections.UND) 149 | throw `Can't assemble executable: unknown symbol ${symbol.name}`; 150 | 151 | if(symbol.value.section == pseudoSections.COM) 152 | commonSymbols.push(symbol); 153 | }); 154 | 155 | for(const section of sections) 156 | { 157 | const data = section.head.dump(); 158 | write(data, fileOffset); 159 | const header = new ProgramHeader({ 160 | p_type: 1, 161 | p_flags: 162 | (section.flags & sectionFlags.a ? 4 : 0) | 163 | (section.flags & sectionFlags.w ? 2 : 0) | 164 | (section.flags & sectionFlags.x ? 1 : 0), 165 | p_offset: fileOffset, 166 | p_vaddr: memoryOffset, 167 | p_paddr: memoryOffset, 168 | p_filesz: data.length, 169 | p_memsz: data.length 170 | }); 171 | programHeaders.push(header); 172 | if(section == entrySection) 173 | entryPoint += memoryOffset; 174 | section.programHeader = header; 175 | header.section = section; 176 | 177 | let length = data.length || 1; 178 | fileOffset = Math.ceil((fileOffset + length) / 0x1000) * 0x1000; 179 | memoryOffset = Math.ceil((memoryOffset + length) / 0x1000) * 0x1000; 180 | } 181 | 182 | const bss = sections.find(section => section.name == '.bss').programHeader; 183 | if(commonSymbols.length > 0) 184 | { 185 | let sectionSize = bss.p_memsz; 186 | for(const symbol of commonSymbols) 187 | { 188 | const alignment = Number(symbol.value.addend) || 1; 189 | sectionSize = Math.ceil(sectionSize / alignment) * alignment; 190 | symbol.address = sectionSize; 191 | sectionSize += Number(symbol.size); 192 | } 193 | bss.p_memsz = sectionSize; 194 | } 195 | 196 | write(new ELFHeader({ 197 | EI_MAG: 0x46_4C_45_7F, 198 | EI_CLASS: state.bitness >> 5, 199 | EI_DATA: 1, 200 | EI_VERSION: 1, 201 | EI_OSABI: 0, 202 | 203 | e_type: 2, // ET_EXEC 204 | e_machine: state.bitness === 64 ? 0x3E : 0x03, 205 | e_version: 1, 206 | e_entry: entryPoint, 207 | e_phoff: fileOffset, 208 | e_ehsize: ELFHeader.size(state.bitness), 209 | e_phentsize: ProgramHeader.size(state.bitness), 210 | e_phnum: programHeaders.length 211 | }).dump(state.bitness), 0); 212 | 213 | // Writing the program headers 214 | for(const header of programHeaders) 215 | { 216 | write(header.dump(state.bitness), fileOffset); 217 | fileOffset += ProgramHeader.size(state.bitness); 218 | } 219 | 220 | // Applying the relocations 221 | for(const section of state.sections) for(const reloc of section.getRelocations()) 222 | { 223 | const offset = section.programHeader.p_vaddr + reloc.offset; 224 | const buffer = Buffer.alloc(reloc.size / 8); 225 | let value = reloc.addend + (reloc.symbol.value.section == pseudoSections.COM ? 226 | BigInt(reloc.symbol.address + bss.p_vaddr) 227 | : 228 | reloc.symbol?.value ? 229 | reloc.symbol.value.absoluteValue() + BigInt(reloc.symbol.value.section.programHeader.p_vaddr) 230 | : 231 | 0n 232 | ); 233 | 234 | if(reloc.pcRelative) 235 | value -= BigInt(offset); 236 | let bigInt = reloc.size == 64; 237 | value = value & (1n << BigInt(reloc.size)) - 1n; 238 | buffer[`write${bigInt ? 'Big' : ''}${reloc.signed ? '' : 'U'}Int${reloc.size}${reloc.size > 8 ? 'LE' : ''}`](bigInt ? value : Number(value)); 239 | 240 | write(buffer, section.programHeader.p_offset + reloc.offset); 241 | } 242 | 243 | fs.closeSync(fd); 244 | } -------------------------------------------------------------------------------- /cli/main.js: -------------------------------------------------------------------------------- 1 | export { createExecutable, createObject } from "./files.js"; 2 | export { debug } from "./debug.js"; -------------------------------------------------------------------------------- /cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@defasm/cli", 3 | "version": "1.3.0", 4 | "description": "Command-line utility for DefAssembler", 5 | "main": "main.js", 6 | "scripts": { 7 | "build": "node cli.js -m64 -x debug.s -o debug && node cli.js -m32 -x debug32.s -o debug32", 8 | "prepare": "npm run build" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/NewDefectus/defasm.git" 13 | }, 14 | "keywords": [ 15 | "assembly", 16 | "x86", 17 | "x64", 18 | "cli" 19 | ], 20 | "author": "Defectus", 21 | "type": "module", 22 | "license": "ISC", 23 | "bugs": { 24 | "url": "https://github.com/NewDefectus/defasm/issues" 25 | }, 26 | "homepage": "https://github.com/NewDefectus/defasm/tree/master/cli#readme", 27 | "dependencies": { 28 | "@defasm/core": "" 29 | }, 30 | "bin": { 31 | "defasm": "./cli.js" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /codemirror/README.md: -------------------------------------------------------------------------------- 1 | # DefAssembler - CodeMirror Extension 2 | A [CodeMirror 6](https://codemirror.net/6/) extension that highlights Assembly code and assembles it incrementally. 3 | 4 | This package uses the [DefAssembler core package](https://www.npmjs.com/package/@defasm/core) to generate binary dumps from Assembly code. For a demonstration of the plugin in action, I recommend checking out the [GitHub Pages site](https://newdefectus.github.io/defasm/), or alternatively, the [Code Golf editor](https://code.golf/fizz-buzz#assembly), where you can also run your programs and submit them to the site. 5 | 6 | # Usage 7 | The package exports the `assembly()` function, which returns an extension that can be added to the editor. The configuration object passed to the function may include the following boolean fields: 8 | * `byteDumps` - whether to display the results of the assembly on the side of the editor 9 | * `debug` - whether to enable toggling debug mode when pressing F3 10 | * `errorMarking` - whether to draw a red underline beneath segments of code that cause errors 11 | * `errorTooltips` - whether to show a tooltip on these segments explaining the cause of the error 12 | * `highlighting` - whether to enable syntax highlighting using a [`LanguageSupport`](https://codemirror.net/6/docs/ref/#language.LanguageSupport) object 13 | 14 | By default, all of these fields, except for `debug`, are set to `true`. Additionally, an `assemblyConfig` field may be provided to be passed into the `AssemblyState` constructor. 15 | 16 | The `AssemblyState` object associated with the editor may be accessed through the `ASMStateField` state field, as such: 17 | 18 | ```js 19 | import { assembly, ASMStateField } from "@defasm/codemirror"; 20 | new EditorView({ 21 | dispatch: tr => { 22 | const result = editor.update([tr]); 23 | const state = editor.state.field(ASMStateField); 24 | byteCount = state.head.length(); 25 | return result; 26 | }, 27 | state: EditorState.create({ extensions: [assembly()] }) 28 | }); 29 | ``` -------------------------------------------------------------------------------- /codemirror/assembly.grammar: -------------------------------------------------------------------------------- 1 | @top Program { statement ((';' | '\n') statement)* } 2 | 3 | @external tokens tokenizer from "./tokenizer" { 4 | Register, 5 | Directive, 6 | Comment, 7 | Opcode, IOpcode, 8 | RelOpcode, IRelOpcode, 9 | Prefix, 10 | word, 11 | Ptr, 12 | Offset, 13 | symEquals, 14 | SymbolName, 15 | VEXRound, 16 | number, 17 | immPrefix, 18 | SpecialWord, 19 | None 20 | } 21 | 22 | @skip {Space} 23 | 24 | @tokens { 25 | Space { std.whitespace } 26 | charString { "'" (![\\'] | "\\" _)* "'"? } 27 | FullString { '"' (![\\"] | "\\" _)* '"'? } 28 | unary { '+' | '-' | '~' | '!' } 29 | operator { $[+\-/*%|&^<>!] | '||' | '&&' | '>>' | '<<' | '<>' | '==' | '!=' | '>=' | '<='} 30 | @precedence { '\n', Space } 31 | } 32 | 33 | statement { 34 | ( 35 | Prefix | 36 | LabelDefinition | 37 | InstructionStatement | 38 | DirectiveStatement | 39 | SymbolDefinition | 40 | None)* Comment? 41 | } 42 | 43 | LabelDefinition { SymbolName ':' } 44 | SymbolDefinition { SymbolName symEquals Expression } 45 | 46 | 47 | InstructionStatement { 48 | (Opcode VEXRound? commaSep<(Register | Immediate | Relative | Memory) VEXRound? VEXMask? VEXRound?>) 49 | | 50 | (IOpcode commaSep) 51 | | 52 | (RelOpcode (('*' (Register | Relative | Memory)) | Register | Expression | Memory)) 53 | | 54 | (IRelOpcode (Register | Expression | IMemory)) 55 | } 56 | 57 | DirectiveStatement { 58 | SymbolName? Directive commaSep 59 | } 60 | 61 | Immediate { 62 | immPrefix Expression 63 | } 64 | 65 | Memory { 66 | Relative? '(' ("" | Register ("," (Register | number)?)*) ')' 67 | } 68 | 69 | IMemory { 70 | Relative? '[' iMemExpr ']' 71 | } 72 | 73 | VEXMask { '{' Register '}' } 74 | 75 | IImmediate[@dynamicPrecedence=1] { (unary | '(')* (number | charString) ')'* (operator IImmediate)? ~immSplit } 76 | Relative { (unary | '(')* (number | charString | word) ')'* (operator Relative)? ~immSplit } 77 | Expression { (unary | '(')* (number | charString | word) ')'* (operator Expression)? } 78 | iMemExpr { (unary | '(')* (Register | number | charString | word) ')'* (operator iMemExpr)? } 79 | 80 | commaSep { 81 | "" | content (',' content?)* 82 | } -------------------------------------------------------------------------------- /codemirror/assembly.js: -------------------------------------------------------------------------------- 1 | import { ASMStateField, ASMLanguageData, byteDumper, ASMColorFacet, SectionColors, ASMFlush } from "./compilerPlugin.js"; 2 | import { errorMarker, errorTooltipper } from "./errorPlugin.js"; 3 | import { parser } from "./parser.js"; 4 | import { debugPlugin } from "./debugPlugin.js"; 5 | import { ShellcodePlugin, ShellcodeField } from "./shellcodePlugin.js"; 6 | import { ctxTracker } from "./tokenizer.js"; 7 | import { LRLanguage, LanguageSupport } from '@codemirror/language'; 8 | import { styleTags, tags } from '@lezer/highlight'; 9 | import { AssemblyState } from '@defasm/core'; 10 | 11 | const assemblyLang = LRLanguage.define({ 12 | parser: parser.configure({ 13 | props: [ 14 | styleTags({ 15 | Opcode: tags.operatorKeyword, 16 | IOpcode: tags.operatorKeyword, 17 | RelOpcode: tags.operatorKeyword, 18 | IRelOpcode: tags.operatorKeyword, 19 | Prefix: tags.operatorKeyword, 20 | Register: tags.className, 21 | Directive: tags.meta, 22 | Comment: tags.lineComment, 23 | SymbolName: tags.definition(tags.labelName), 24 | Immediate: tags.literal, 25 | IImmediate: tags.literal, 26 | Memory: tags.regexp, 27 | IMemory: tags.regexp, 28 | Relative: tags.regexp, 29 | Expression: tags.literal, 30 | FullString: tags.string, 31 | VEXRound: tags.modifier, 32 | VEXMask: tags.modifier, 33 | Offset: tags.emphasis, 34 | Ptr: tags.emphasis, 35 | SpecialWord: tags.annotation 36 | }) 37 | ] 38 | }) 39 | }); 40 | 41 | /** Create a CodeMirror extension utilizing DefAssembler 42 | * @param {Object} config 43 | * @param {import("@defasm/core/compiler").AssemblyConfig} config.assemblyConfig The config object passed to the `AssemblyState` constructor 44 | * @param {boolean} config.byteDumps Whether to display the results of the assembly on the side of the editor 45 | * @param {boolean} config.debug Whether to enable toggling debug mode when pressing F3 46 | * @param {boolean} config.errorMarking Whether to draw a red underline beneath segments of code that cause errors 47 | * @param {boolean} config.errorTooltips Whether to show a tooltip on these segments explaining the cause of the error 48 | * @param {boolean} config.highlighting Whether to enable syntax highlighting using a [`LanguageSupport`](https://codemirror.net/6/docs/ref/#language.LanguageSupport) object 49 | */ 50 | export function assembly({ 51 | assemblyConfig = { syntax: { intel: false, prefix: true }, bitness: 64 }, 52 | byteDumps = true, 53 | debug = false, 54 | errorMarking = true, 55 | errorTooltips = true, 56 | highlighting = true, 57 | } = {}) 58 | { 59 | const plugins = [ 60 | ASMStateField.init(state => { 61 | const asm = new AssemblyState(assemblyConfig); 62 | asm.compile(state.sliceDoc()); 63 | return asm; 64 | }), 65 | ASMLanguageData 66 | ]; 67 | if(byteDumps) plugins.push(SectionColors, byteDumper); 68 | if(debug) plugins.push(debugPlugin); 69 | if(errorMarking) plugins.push(errorMarker); 70 | if(errorTooltips) plugins.push(errorTooltipper); 71 | 72 | if(highlighting) 73 | return new LanguageSupport(assemblyLang.configure({ 74 | contextTracker: ctxTracker(assemblyConfig.syntax, assemblyConfig.bitness) 75 | }), plugins); 76 | return plugins; 77 | } 78 | 79 | export { ASMStateField, ASMColorFacet, ASMFlush, ShellcodePlugin, ShellcodeField }; -------------------------------------------------------------------------------- /codemirror/compilerPlugin.js: -------------------------------------------------------------------------------- 1 | import { EditorView, ViewPlugin, ViewUpdate, Decoration, WidgetType } from '@codemirror/view'; 2 | import { EditorState, StateField, Facet, RangeValue, RangeSet, RangeSetBuilder, StateEffect } from '@codemirror/state'; 3 | import { AssemblyState, Range } from '@defasm/core'; 4 | 5 | /** @type {StateField} */ 6 | export const ASMStateField = StateField.define({ 7 | create: state => { 8 | const asm = new AssemblyState(); 9 | asm.compile(state.sliceDoc()); 10 | return asm; 11 | }, 12 | update: (state, transaction) => { 13 | if(!transaction.docChanged) 14 | return state; 15 | 16 | /* In case there are multiple changed ranges, we'll compile each 17 | range separately and only run the second pass on the final state. */ 18 | let offset = 0; 19 | transaction.changes.iterChanges( 20 | (fromA, toA, fromB, toB) => { 21 | state.compile(transaction.state.sliceDoc(fromB, toB), { range: new Range(fromA + offset, toA - fromA), doSecondPass: false }); 22 | offset += toB - fromB - (toA - fromA); 23 | } 24 | ); 25 | 26 | state.secondPass(); 27 | return state; 28 | } 29 | }); 30 | 31 | export const ASMLanguageData = EditorState.languageData.of((state, pos, side) => { 32 | pos = state.doc.lineAt(pos).from; 33 | 34 | var asmState = state.field(ASMStateField); 35 | var lastInstr = null; 36 | 37 | asmState.iterate(instr => { 38 | if(instr.range.start < pos) 39 | lastInstr = instr; 40 | }); 41 | return [{ 42 | commentTokens: { line: 43 | (lastInstr ? lastInstr.syntax : asmState.defaultSyntax).intel ? ';' : '#' } 44 | }]; 45 | }) 46 | 47 | export class ASMColor extends RangeValue 48 | { 49 | /** @param {String} color */ 50 | constructor(color) 51 | { 52 | super(); 53 | this.color = color; 54 | } 55 | eq(other) 56 | { 57 | return this.color == other.color; 58 | } 59 | } 60 | 61 | /** @type {Facet>} */ 62 | export const ASMColorFacet = Facet.define(); 63 | 64 | export const SectionColors = ASMColorFacet.compute( 65 | ['doc'], 66 | state => { 67 | let assemblyState = state.field(ASMStateField), offset = 0; 68 | /** @type {RangeSetBuilder} */ 69 | let builder = new RangeSetBuilder(); 70 | assemblyState.iterate((instr, line) => { 71 | let sectionName = instr.section.name; 72 | let color = sectionName == ".text" ? "#666" : 73 | sectionName == ".data" ? "#66A" : 74 | sectionName == ".bss" ? "#6A6" : 75 | sectionName == ".rodata" ? "#AA6" : 76 | "#A66"; 77 | builder.add(offset, offset += instr.length, new ASMColor(color)); 78 | }); 79 | return builder.finish(); 80 | } 81 | ); 82 | 83 | class AsmDumpWidget extends WidgetType 84 | { 85 | /** 86 | * @param {Uint8Array} bytes 87 | * @param {Number} offset 88 | * @param {RangeSet} colors 89 | */ 90 | constructor(bytes, offset, colors) 91 | { 92 | super(); 93 | this.bytes = bytes; 94 | this.offset = offset; 95 | this.colors = colors; 96 | } 97 | 98 | toDOM() 99 | { 100 | let node = document.createElement('span'); 101 | node.setAttribute('aria-hidden', 'true'); 102 | node.className = 'cm-asm-dump'; 103 | node.style.marginLeft = this.offset + 'px'; 104 | let colorCursor = this.colors.iter(); 105 | 106 | for(let i = 0; i < this.bytes.length; i++) 107 | { 108 | let text = this.bytes[i].toString(16).toUpperCase().padStart(2, '0'); 109 | let span = document.createElement('span'); 110 | 111 | while(colorCursor.to <= i) 112 | colorCursor.next(); 113 | 114 | if(colorCursor.from <= i && i < colorCursor.to) 115 | span.style.color = colorCursor.value.color; 116 | 117 | span.innerText = text; 118 | node.appendChild(span); 119 | } 120 | 121 | return node; 122 | } 123 | 124 | eq(widget) 125 | { 126 | if(this.offset != widget.offset || this.bytes.length != widget.bytes.length) 127 | return false; 128 | 129 | for(let i = 0; i < this.bytes.length; i++) 130 | if(this.bytes[i] != widget.bytes[i]) 131 | return false; 132 | 133 | // RangeSet.eq doesn't work for some reason 134 | let oldCursor = widget.colors.iter(), newCursor = this.colors.iter(); 135 | while(true) 136 | { 137 | if(newCursor.value === null) 138 | { 139 | if(oldCursor.value === null) 140 | break; 141 | return false; 142 | } 143 | 144 | if(!newCursor.value.eq(oldCursor.value) || 145 | newCursor.from !== oldCursor.from || 146 | newCursor.to !== oldCursor.to) 147 | return false; 148 | 149 | oldCursor.next(); newCursor.next(); 150 | } 151 | 152 | return true; 153 | } 154 | 155 | /** @param {HTMLElement} node */ 156 | updateDOM(node) 157 | { 158 | node.style.marginLeft = this.offset + 'px'; 159 | let colorCursor = this.colors.iter(); 160 | 161 | for(let i = 0; i < this.bytes.length; i++) 162 | { 163 | while(colorCursor.to <= i) 164 | colorCursor.next(); 165 | 166 | let text = this.bytes[i].toString(16).toUpperCase().padStart(2, '0'); 167 | if(i < node.childElementCount) 168 | { 169 | let span = node.children.item(i); 170 | if(span.innerText !== text) 171 | span.innerText = text; 172 | if(colorCursor.from <= i && i < colorCursor.to) 173 | { 174 | if(colorCursor.value.color !== span.style.color) 175 | span.style.color = colorCursor.value.color; 176 | } 177 | else 178 | span.style.color = ""; 179 | } 180 | else 181 | { 182 | let span = document.createElement('span'); 183 | if(colorCursor.value !== null) 184 | span.style.color = colorCursor.value.color; 185 | 186 | span.innerText = text; 187 | node.appendChild(span); 188 | } 189 | while(colorCursor.to < i) 190 | colorCursor.next(); 191 | } 192 | while(node.childElementCount > this.bytes.length) 193 | node.removeChild(node.lastChild); 194 | 195 | return true; 196 | } 197 | } 198 | 199 | /* Convert tabs to spaces, for proper width measurement */ 200 | function expandTabs(text, tabSize) 201 | { 202 | let result = "", i = tabSize; 203 | for(let char of text) 204 | { 205 | if(char == '\t') 206 | { 207 | result += ' '.repeat(i); 208 | i = tabSize; 209 | } 210 | else 211 | { 212 | result += char; 213 | i = i - 1 || tabSize; 214 | } 215 | } 216 | return result; 217 | } 218 | 219 | export const ASMFlush = StateEffect.define(); 220 | 221 | export const byteDumper = [ 222 | EditorView.baseTheme({ 223 | '.cm-asm-dump' : { fontStyle: "italic" }, 224 | '.cm-asm-dump > span': { marginRight: "1ch" }, 225 | '&dark .cm-asm-dump' : { color: "#AAA" } 226 | }), 227 | ViewPlugin.fromClass(class { 228 | /** @param {EditorView} view */ 229 | constructor(view) 230 | { 231 | this.ctx = document.createElement('canvas').getContext('2d'); 232 | this.lineWidths = []; 233 | 234 | this.decorations = Decoration.set([]); 235 | 236 | // This timeout is required to let the content DOM's style be calculated 237 | setTimeout(() => { 238 | let style = window.getComputedStyle(view.contentDOM); 239 | 240 | this.ctx.font = `${ 241 | style.getPropertyValue('font-style') 242 | } ${ 243 | style.getPropertyValue('font-variant') 244 | } ${ 245 | style.getPropertyValue('font-weight') 246 | } ${ 247 | style.getPropertyValue('font-size') 248 | } ${ 249 | style.getPropertyValue('font-family') 250 | }`; 251 | 252 | 253 | this.updateWidths(0, view.state.doc.length, 0, view.state); 254 | this.makeAsmDecorations(view.state); 255 | view.dispatch(); 256 | }, 1); 257 | } 258 | 259 | /** @param {ViewUpdate} update */ 260 | update(update) 261 | { 262 | if(!update.docChanged && !update.transactions.some( 263 | tr => tr.effects.some( 264 | effect => effect.is(ASMFlush) 265 | ) 266 | )) 267 | return; 268 | 269 | let state = update.view.state; 270 | 271 | update.changes.iterChangedRanges( 272 | (fromA, toA, fromB, toB) => { 273 | let removedLines = 274 | update.startState.doc.lineAt(toA).number 275 | - 276 | update.startState.doc.lineAt(fromA).number; 277 | this.updateWidths(fromB, toB, removedLines, state); 278 | } 279 | ); 280 | 281 | this.makeAsmDecorations(update.state); 282 | } 283 | 284 | updateWidths(from, to, removedLines, { doc, tabSize }) 285 | { 286 | let start = doc.lineAt(from).number; 287 | let end = doc.lineAt(to).number; 288 | let newWidths = []; 289 | 290 | for(let i = start; i <= end; i++) 291 | newWidths.push(this.ctx.measureText(expandTabs(doc.line(i).text, tabSize)).width); 292 | 293 | this.lineWidths.splice(start - 1, removedLines + 1, ...newWidths); 294 | } 295 | 296 | /** @param {EditorState} state */ 297 | makeAsmDecorations(state) 298 | { 299 | let doc = state.doc; 300 | let maxOffset = Math.max(...this.lineWidths) + 50; 301 | let widgets = []; 302 | 303 | let asmColors = state.facet(ASMColorFacet); 304 | let assemblyState = state.field(ASMStateField); 305 | let i = 0; 306 | 307 | assemblyState.bytesPerLine((bytes, line) => { 308 | if(bytes.length > 0) 309 | { 310 | /** @type {RangeSetBuilder} */ 311 | let builder = new RangeSetBuilder(); 312 | RangeSet.spans(asmColors, i, i + bytes.length, { 313 | span: (from, to, active) => { 314 | if(active.length > 0) 315 | builder.add(from - i, to - i, active[active.length - 1]); 316 | } 317 | }); 318 | let colors = builder.finish(); 319 | i += bytes.length; 320 | 321 | widgets.push(Decoration.widget({ 322 | widget: new AsmDumpWidget( 323 | bytes, 324 | maxOffset - this.lineWidths[line - 1], 325 | colors 326 | ), side: 2 327 | }).range(doc.line(line).to)); 328 | } 329 | }); 330 | 331 | this.decorations = Decoration.set(widgets); 332 | } 333 | 334 | }, { decorations: plugin => plugin.decorations }) 335 | ]; -------------------------------------------------------------------------------- /codemirror/debugPlugin.js: -------------------------------------------------------------------------------- 1 | import { EditorView, ViewPlugin, ViewUpdate, Decoration, hoverTooltip } from '@codemirror/view'; 2 | import { EditorState, ChangeSet } from '@codemirror/state'; 3 | import { ASMStateField } from './compilerPlugin.js'; 4 | 5 | var debugEnabled = false; 6 | 7 | export const debugPlugin = [ 8 | EditorView.baseTheme({ 9 | '.red': { background: 'lightcoral' }, 10 | '.blue': { background: 'lightblue' }, 11 | '.cm-asm-debug-compiled .red, .red .cm-asm-debug-compiled': { background: 'indianred' }, 12 | '.cm-asm-debug-compiled .blue, .blue .cm-asm-debug-compiled': { background: 'dodgerblue' }, 13 | '.cm-asm-debug-compiled': { background: '#ddd' } 14 | }), 15 | hoverTooltip((view, pos) => { 16 | if(!debugEnabled) 17 | return null; 18 | const node = view.state.field(ASMStateField).head.find(pos); 19 | if(!node) 20 | return null; 21 | const instr = node.statement; 22 | return { 23 | pos: instr.range.start, 24 | end: Math.min(instr.range.end, view.state.doc.length), 25 | above: true, 26 | create: view => { 27 | let dom = document.createElement('div'); 28 | dom.textContent = `${instr.constructor.name} (#${instr.id})`; 29 | dom.className = 'cm-asm-error-tooltip'; 30 | return { dom }; 31 | } 32 | } 33 | }), 34 | EditorView.domEventHandlers({ 35 | mousedown: (event, view) => { 36 | if(debugEnabled && event.ctrlKey) 37 | { 38 | console.log(view.state.field(ASMStateField).head.find(view.posAtCoords(event))); 39 | return true; 40 | } 41 | }, 42 | keydown: (event, view) => { 43 | if(event.key == 'F3') 44 | { 45 | debugEnabled = !debugEnabled; 46 | view.dispatch(ChangeSet.empty(0)); 47 | return true; 48 | } 49 | } 50 | }), 51 | ViewPlugin.fromClass( 52 | class 53 | { 54 | /** @param {EditorView} view */ 55 | constructor(view) 56 | { 57 | this.markInstructions(view.state); 58 | } 59 | 60 | /** @param {ViewUpdate} update */ 61 | update(update) 62 | { 63 | this.markInstructions(update.state); 64 | } 65 | 66 | /** @param {EditorState} state */ 67 | markInstructions(state) 68 | { 69 | if(!debugEnabled) 70 | { 71 | this.decorations = Decoration.set([]); 72 | return; 73 | } 74 | let instrMarks = []; 75 | let i = 0; 76 | state.field(ASMStateField).iterate(instr => { 77 | instrMarks.push(Decoration.mark({ 78 | class: i++ % 2 ? 'blue' : 'red' 79 | }).range(instr.range.start, instr.range.end)) 80 | }); 81 | let compiledRange = state.field(ASMStateField).compiledRange; 82 | if(compiledRange.length > 0) 83 | instrMarks.push(Decoration.mark({ 84 | class: 'cm-asm-debug-compiled' 85 | }).range(compiledRange.start, compiledRange.end)); 86 | this.decorations = Decoration.set(instrMarks, true); 87 | } 88 | }, 89 | { decorations: plugin => plugin.decorations } 90 | ) 91 | ]; -------------------------------------------------------------------------------- /codemirror/errorPlugin.js: -------------------------------------------------------------------------------- 1 | import { EditorView, ViewPlugin, Decoration, WidgetType, hoverTooltip } from '@codemirror/view'; 2 | import { ASMStateField } from "./compilerPlugin"; 3 | import { EditorState } from '@codemirror/state'; 4 | 5 | class EOLError extends WidgetType 6 | { 7 | constructor() 8 | { 9 | super(); 10 | } 11 | 12 | toDOM() 13 | { 14 | let node = document.createElement('span'); 15 | node.setAttribute('aria-hidden', 'true'); 16 | node.className = 'cm-asm-error'; 17 | node.style.position = 'absolute'; 18 | node.innerText = ' '; 19 | return node; 20 | } 21 | } 22 | 23 | export const errorMarker = [ 24 | EditorView.baseTheme({ 25 | '.cm-asm-error': { 26 | textDecoration: "underline red" 27 | } 28 | }), 29 | ViewPlugin.fromClass( 30 | class 31 | { 32 | constructor(view) { this.markErrors(view.state); } 33 | update(update) { if(update.docChanged) this.markErrors(update.state); } 34 | 35 | /** @param {EditorState} state */ 36 | markErrors(state) 37 | { 38 | this.marks = Decoration.set(state.field(ASMStateField).errors.map(error => { 39 | let content = state.sliceDoc(error.range.start, error.range.end); 40 | if(content == '\n' || !content) 41 | return Decoration.widget({ 42 | widget: new EOLError(), 43 | side: 1 44 | }).range(error.range.start); 45 | 46 | return Decoration.mark({ 47 | class: 'cm-asm-error' 48 | }).range(error.range.start, error.range.end); 49 | })); 50 | } 51 | }, 52 | { decorations: plugin => plugin.marks } 53 | ) 54 | ]; 55 | 56 | export const errorTooltipper = [ 57 | EditorView.baseTheme({ 58 | '.cm-asm-error-tooltip': { 59 | fontFamily: "monospace", 60 | borderRadius: ".25em", 61 | padding: ".1em .25em", 62 | color: "#eee", 63 | backgroundColor: "black !important", 64 | "&:before": { 65 | position: "absolute", 66 | content: '""', 67 | left: ".3em", 68 | marginLeft: "-.1em", 69 | bottom: "-.3em", 70 | borderLeft: ".3em solid transparent", 71 | borderRight: ".3em solid transparent", 72 | borderTop: ".3em solid black" 73 | } 74 | }, 75 | '&dark .cm-asm-error-tooltip': { 76 | color: "black", 77 | backgroundColor: "#eee !important", 78 | "&:before": { 79 | borderTop: ".3em solid #eee" 80 | } 81 | } 82 | }), 83 | hoverTooltip((view, pos) => { 84 | for(let { range, message } of view.state.field(ASMStateField).errors) 85 | if(range.start <= pos && range.end >= pos) 86 | return { 87 | pos: range.start, 88 | end: Math.min(range.end, view.state.doc.length), 89 | above: true, 90 | create: view => { 91 | let dom = document.createElement('div'); 92 | dom.textContent = message; 93 | dom.className = 'cm-asm-error-tooltip'; 94 | return { dom }; 95 | } 96 | } 97 | 98 | return null; 99 | }) 100 | ]; -------------------------------------------------------------------------------- /codemirror/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@defasm/codemirror", 3 | "version": "1.4.4", 4 | "description": "CodeMirror 6 extension utilizing DefAssembler", 5 | "main": "assembly.js", 6 | "scripts": { 7 | "build": "lezer-generator assembly.grammar --output parser", 8 | "prepare": "npm run build" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/NewDefectus/defasm.git" 13 | }, 14 | "keywords": [ 15 | "assembly", 16 | "x86", 17 | "x64", 18 | "codemirror" 19 | ], 20 | "author": "Defectus", 21 | "type": "module", 22 | "license": "ISC", 23 | "bugs": { 24 | "url": "https://github.com/NewDefectus/defasm/issues" 25 | }, 26 | "homepage": "https://github.com/NewDefectus/defasm/tree/master/codemirror#readme", 27 | "dependencies": { 28 | "@codemirror/language": "", 29 | "@codemirror/state": "", 30 | "@codemirror/view": "", 31 | "@defasm/core": "", 32 | "@lezer/common": "", 33 | "@lezer/highlight": "", 34 | "@lezer/lr": "" 35 | }, 36 | "devDependencies": { 37 | "@lezer/generator": "" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /codemirror/parser.js: -------------------------------------------------------------------------------- 1 | // This file was generated by lezer-generator. You probably shouldn't edit it. 2 | import {LRParser} from "@lezer/lr" 3 | import {tokenizer} from "./tokenizer" 4 | export const parser = LRParser.deserialize({ 5 | version: 14, 6 | states: "8SOVQROOO}QRO'#CmO#gQRO'#CnO$SQRO'#CnO%hQRO'#CnO%rQRO'#CnO%|QRO'#CvOOQP'#Cy'#CyO'uQRO'#DXOOQO'#DX'#DXQ(VQQOOOOQP,59X,59XO%|QRO,59bO(_QRO,59dOOQP'#Cz'#CzO(pQRO'#CzO)XQRO'#CqO(_QRO'#CoO)jQTO'#CqO+xQRO'#DZO*wQRO'#DZOOQP,59Y,59YO!YQRO,59YO,PQRO'#CqO,bQTO'#CqO-lQRO'#CuO.QQQO'#CuO.VQRO'#DdO(_QRO'#DdO/WQRO'#DdO/bQRO'#CpO0nQTO'#CpO0{QQO'#CrO1cQRO,59YO1jQRO'#CpO1{QTO'#CpO2]QRO'#DiOOQP,59b,59bOOQP-E6w-E6wOOQO,59s,59sOVQRO'#DQQ(VQQOOOOQP1G.|1G.|OOQP1G/O1G/OOOQP,59^,59^O3WQQO,59^OOQP-E6x-E6xO3`QTO,59]OOQP,59Z,59ZOOQP'#C{'#C{O3`QTO,59]O4mQRO,59]O5OQRO,59^O5WQRO'#CsO5]QRO'#C}O6`QRO,59uO7ZQRO,59uO7eQRO,59uOOQP1G.t1G.tO7lQTO,59]O7lQTO,59]O8vQRO,59]O9XQRO'#DfO9mQSO'#DfO9xQQO,59aO-lQRO,59aO9}QRO'#DOO;TQRO,5:OOQQRO1G.tO>{QTO,59[O>{QTO,59[O(_QRO,59[O@SQRO'#DPOAVQRO,5:TOOQO,59l,59lOOQO-E7O-E7OOBQQRO'#C|OB`QQO1G.xOOQP1G.x1G.xOBhQTO1G.wO4mQRO1G.wOOQP-E6y-E6yOOQP1G.w1G.wOB`QQO1G.xOCuQQO,59_OD{QRO,59iOCzQRO,59iOOQP-E6{-E6{OESQRO1G/aOESQRO1G/aOE}QRO1G/aOFUQTO1G.wO8vQRO1G.wOOQP1G.z1G.zOG`QSO,5:QOG`QSO,5:QO-lQRO,5:QOOQP1G.{1G.{OGkQQO1G.{OGpQRO,59jO(_QRO,59jOHqQRO,59jOOQP-E6|-E6|OH{QRO1G/jOH{QRO1G/jOIvQRO1G/jOI}QRO1G/jOJXQRO1G/jOK^QTO1G.vO=oQRO1G.vOOQP1G.v1G.vOKkQTO1G.vO(_QRO1G.vOOQP,59k,59kOOQP-E6}-E6}OOQO,59h,59hOOQO-E6z-E6zOOQP7+$d7+$dO4mQRO7+$cOOQP7+$c7+$cOK{QQO7+$dOOQP1G.y1G.yOMRQRO1G/TOLTQRO1G/TOMYQRO7+${OMYQRO7+${O8vQRO7+$cOOQP7+$f7+$fONTQSO1G/lO-lQRO1G/lOOQO1G/l1G/lOOQP7+$g7+$gO! ^QRO1G/UON`QRO1G/UO! eQRO1G/UO(_QRO1G/UO!!fQRO7+%UO!!fQRO7+%UO!#aQRO7+%UO!#hQRO7+%UO=oQRO7+$bOOQP7+$b7+$bO(_QRO7+$bOOQP<[O!)xQROAN>[OOQPAN=vAN=vO!*sQROAN=vO!+qQROG23vOOQPG23bG23b", 7 | stateData: "!,l~O_OS~OQUORXOSQOTROUSOVTOWVOZPO^VOu{P!^{P!_{P~OQ[Ow]O|ZO~OPdOvbOxbOyaO!O^O!P_O!QbOQ}PR}PS}PT}PU}PV}PW}PZ}P^}Pu}P!^}P!_}P~O[fO~P!YOvbOxhO!O^O!P^O!QhO!XiO~OPkOXmOYlOQ!WPR!WPS!WPT!WPU!WPV!WPW!WPZ!WP^!WPu!WP!^!WP!_!WP~P#nOPeOvoOxoO!O^O!QoO~O!P_O![qO~P%VO!P^O!XiO~P%VO]tOktOvsOxsO!O^O!P^O!QsOQ!]PR!]PS!]PT!]PU!]PV!]PW!]PZ!]P^!]Pu!]P!^!]P!_!]P~OQUOSQOTROUSOVTOWVOZPO^VO~ORwOu{X!^{X!_{X~P'ZO!^xO!_xO~OvsOxsO!O^O!P^O!QsO~OP}O!R|OvnXxnX!OnX!PnX!QnX~Ov!POx!PO!O^O!P^O!Q!PO~O!R!RO!S!TOQeXReXSeXTeXUeXVeXWeXZeX[eX^eXueX!PeX!TeX!UeX!^eX!_eX!XeX~O[!YO!T!WO!U!VOQ}XR}XS}XT}XU}XV}XW}XZ}X^}Xu}X!^}X!_}X~O!P!UO~P*wOv!POx!]O!O^O!P^O!Q!]O~O!R!RO!S!_OQhXRhXShXThXUhXVhXWhXZhX[hX^hXuhX!ThX!UhX!XeX!^hX!_hX~OP!aOv!aOx!aO!O^O!P^O!Q!aO~O!X!cO~O[!fO!T!dO!U!VOQ!WXR!WXS!WXT!WXU!WXV!WXW!WXZ!WX^!WXu!WX!^!WX!_!WX~OP!hOY!iO~P#nOv!jOx!jO!O^O!P^O!Q!jO~O!R!ROQdXRdXSdXTdXUdXVdXWdXZdX^dXudX!^dX!_dX~O!S!lO!PeX!XeX~P/sO!P!UO~OvbOxbO!O^O!P_O!QbO~OP![O~P1QOv!nOx!nO!O^O!P^O!Q!nO~O!S!pO!TdX[dX!UdX~P/sO!T!qOQ!]XR!]XS!]XT!]XU!]XV!]XW!]XZ!]X^!]Xu!]X!^!]X!_!]X~O!R!wO!T!uO~O!R!RO!S!yOQeaReaSeaTeaUeaVeaWeaZea[ea^eauea!Pea!Tea!Uea!^ea!_ea!Xea~OvbOxbO!O^O!P^O!QbO~OP!|O!R!wO~OP!}O~OP#POyaOQqXRqXSqXTqXUqXVqXWqXZqX^qXuqX!TqX!^qX!_qX~P1QO!T!WOQ}aR}aS}aT}aU}aV}aW}aZ}a^}au}a!^}a!_}a~O[#SO!U!VO~P6`O[#SO~P6`O!R!RO!S#VOQhaRhaShaThaUhaVhaWhaZha[ha^hauha!Tha!Uha!Xea!^ha!_ha~OvbOxhO!O^O!P^O!QhO~OP#XOv#XOx#XO!O^O!P^O!Q#XO~O!R!RO!S#ZO!Z!YX~O!Z#[O~OP#^OX#`OY#_OQrXRrXSrXTrXUrXVrXWrXZrX^rXurX!TrX!^rX!_rX~P#nO!T!dOQ!WaR!WaS!WaT!WaU!WaV!WaW!WaZ!Wa^!Wau!Wa!^!Wa!_!Wa~O[#cO!U!VO~P;TO[#cO~P;TO[#eO!U!VO~P;TO!R!RO!S#hOQdaRdaSdaTdaUdaVdaWdaZda^dauda!Pea!^da!_da!Xea~OvoOxoO!O^O!P^O!QoO~O!P!UOQbiRbiSbiTbiUbiVbiWbiZbi^biubi!^bi!_bi~O!R!RO!S#kOQdaRdaSdaTdaUdaVdaWdaZda^dauda!Tda!^da!_da[da!Uda~O]#lOk#lOQsXRsXSsXTsXUsXVsXWsXZsX^sXusX!TsX!^sX!_sX~P(_O!T!qOQ!]aR!]aS!]aT!]aU!]aV!]aW!]aZ!]a^!]au!]a!^!]a!_!]a~OP#nOx#nO!RpX!TpX~O!R#pO!T!uO~O!R!RO!S#qOQeiReiSeiTeiUeiVeiWeiZei[ei^eiuei!Pei!Tei!Uei!^ei!_ei!Xei~O!V#tO~O[#uO!U!VOQqaRqaSqaTqaUqaVqaWqaZqa^qauqa!Tqa!^qa!_qa~O!P!UO~PCzO!T!WOQ}iR}iS}iT}iU}iV}iW}iZ}i^}iu}i!^}i!_}i~O[#xO~PESO!R!RO!S#yOQhiRhiShiThiUhiVhiWhiZhi[hi^hiuhi!Thi!Uhi!Xei!^hi!_hi~O!R!RO!S#|O!Z!Ya~O!Z$OO~O[$PO!U!VOQraRraSraTraUraVraWraZra^raura!Tra!^ra!_ra~OP$ROY$SO~P#nO!T!dOQ!WiR!WiS!WiT!WiU!WiV!WiW!WiZ!Wi^!Wiu!Wi!^!Wi!_!Wi~O[$UO~PH{O[$UO!U!VO~PH{O[$WO!U!VO~PH{O!R!ROQdiRdiSdiTdiUdiVdiWdiZdi^diudi!^di!_di~O!S$XO!Pei!Xei~PJcO!S$ZO!Tdi[di!Udi~PJcO!R$]O!T!uO~O[$^OQqiRqiSqiTqiUqiVqiWqiZqi^qiuqi!Tqi!^qi!_qi~O!U!VO~PLTO!T!WOQ}qR}qS}qT}qU}qV}qW}qZ}q^}qu}q!^}q!_}q~O!R!RO!S$bO!Z!Yi~O[$dOQriRriSriTriUriVriWriZri^riuri!Tri!^ri!_ri~O!U!VO~PN`O[$fO!U!VOQriRriSriTriUriVriWriZri^riuri!Tri!^ri!_ri~O!T!dOQ!WqR!WqS!WqT!WqU!WqV!WqW!WqZ!Wq^!Wqu!Wq!^!Wq!_!Wq~O[$iO~P!!fO[$iO!U!VO~P!!fO[$lOQqqRqqSqqTqqUqqVqqWqqZqq^qquqq!Tqq!^qq!_qq~O!T!WOQ}yR}yS}yT}yU}yV}yW}yZ}y^}yu}y!^}y!_}y~O[$nOQrqRrqSrqTrqUrqVrqWrqZrq^rqurq!Trq!^rq!_rq~O!U!VO~P!%kO[$pO!U!VOQrqRrqSrqTrqUrqVrqWrqZrq^rqurq!Trq!^rq!_rq~O!T!dOQ!WyR!WyS!WyT!WyU!WyV!WyW!WyZ!Wy^!Wyu!Wy!^!Wy!_!Wy~O[$rO~P!'qO[$sOQryRrySryTryUryVryWryZry^ryury!Try!^ry!_ry~O!U!VO~P!(sO!T!dOQ!W!RR!W!RS!W!RT!W!RU!W!RV!W!RW!W!RZ!W!R^!W!Ru!W!R!^!W!R!_!W!R~O[$vOQr!RRr!RSr!RTr!RUr!RVr!RWr!RZr!R^r!Rur!R!Tr!R!^r!R!_r!R~O!T!dOQ!W!ZR!W!ZS!W!ZT!W!ZU!W!ZV!W!ZW!W!ZZ!W!Z^!W!Zu!W!Z!^!W!Z!_!W!Z~O", 8 | goto: "*y!^PPPPPPPPPPPPPPPPP!_!_!d!k#f$Z$h%d%y!_P!_&Y&a'h(d(n)T)w)}PPPPPP*TP*ZPPPPPPPP*aP*dPP*sVVOWxSdQfR#P!WSeSTStU[Q{]Q!QaQ!hlQ#f!iS#i!l!pQ#l!qQ$R#_S$Y#h#kQ$g$ST$k$X$ZScQfYjRTm!d#`QpSQ!mqU!{!T!_!lQ#O!WU#r!y#V#hV$[#q#y$XSdQfQeSQ![qR#P!WS!ZcdQ!gkQ#T!YS#d!f!hS#v#O#PQ$Q#^S$V#e#fQ$_#uS$e$P$RQ$j$WS$o$f$gR$t$pQkRQ!hmQ#W!_Q#^!dQ#z#VQ$R#`R$a#yQeTQkRQ!hmQ#^!dR$R#`SWOxRvW^`Qfq!T!W!y#q^gRm!_!d#V#`#yYnST!l#h$XhrU[]al!i!p!q#_#k$S$ZY!O`gnr!`Z!`i!c#Z#|$bQ!SbQ!^hQ!koQ!osQ!x!Pd!z!S!^!k!o!x#U#Y#g#j#{Q#U!]Q#Y!aQ#g!jQ#j!nR#{#XQ!v}S#o!v#sR#s!|S!XcdW#Q!X#R#w$`S#R!Y!ZS#w#S#TR$`#xQ!ek[#a!e#b$T$h$q$uU#b!f!g!hW$T#c#d#e#fU$h$U$V$WS$q$i$jR$u$rQ!rtR#m!rQyYR!tyQYOR!sxQeQR![fReRQ!biQ#]!cQ#}#ZQ$c#|R$m$bQuURz[", 9 | nodeNames: "⚠ Register Directive Comment Opcode IOpcode RelOpcode IRelOpcode Prefix Ptr Offset SymbolName VEXRound SpecialWord None Space Program LabelDefinition InstructionStatement Immediate Expression Relative Memory VEXMask IImmediate IMemory DirectiveStatement FullString SymbolDefinition", 10 | maxTerm: 61, 11 | skippedNodes: [0,15], 12 | repeatNodeCount: 8, 13 | tokenData: "'s~RtXY#cYZ#hZ^#cpq#cqr#ors$Ouv#yvw$owx$wxy%hyz%mz{%r{|%y|}&Q}!O%y!P!Q#y![!]&V!]!^&[!^!_&a!_!`&o!`!a&u!}#O'Q#P#Q'V#Q#R#y#o#p'[#p#q'a#q#r'i#r#s'n#y#z#c$f$g#c#BY#BZ#c$IS$I_#c$I|$JO#c$JT$JU#c$KV$KW#c&FU&FV#c~#hO_~~#oO!_~_~R#vP!SQ!OP!_!`#yQ$OO!SQ~$TTk~Or$Ors$ds#O$O#O#P$i#P~$O~$iOk~~$lPO~$OQ$tP!SQvw#y~$|T!Q~Ow$wwx%]x#O$w#O#P%b#P~$w~%bO!Q~~%ePO~$w~%mO!P~~%rO!R~R%yO![P!SQR&QO!SQ!OP~&VO!T~~&[O|~~&aO!^~Q&fR!SQ!^!_#y!_!`#y!`!a#yQ&rP!_!`#yQ&zQ!SQ!_!`#y!`!a#y~'VO!X~~'[O!Z~~'aO!U~Q'fP!SQ#p#q#y~'nO!V~P'sO!OP", 14 | tokenizers: [tokenizer, 0, 1], 15 | topRules: {"Program":[0,16]}, 16 | dynamicPrecedences: {"24":1}, 17 | tokenPrec: 0 18 | }) 19 | -------------------------------------------------------------------------------- /codemirror/parser.terms.js: -------------------------------------------------------------------------------- 1 | // This file was generated by lezer-generator. You probably shouldn't edit it. 2 | export const 3 | Register = 1, 4 | Directive = 2, 5 | Comment = 3, 6 | Opcode = 4, 7 | IOpcode = 5, 8 | RelOpcode = 6, 9 | IRelOpcode = 7, 10 | Prefix = 8, 11 | word = 38, 12 | Ptr = 9, 13 | Offset = 10, 14 | symEquals = 39, 15 | SymbolName = 11, 16 | VEXRound = 12, 17 | number = 40, 18 | immPrefix = 41, 19 | SpecialWord = 13, 20 | None = 14, 21 | Space = 15, 22 | Program = 16, 23 | LabelDefinition = 17, 24 | InstructionStatement = 18, 25 | Immediate = 19, 26 | Expression = 20, 27 | Relative = 21, 28 | Memory = 22, 29 | VEXMask = 23, 30 | IImmediate = 24, 31 | IMemory = 25, 32 | DirectiveStatement = 26, 33 | FullString = 27, 34 | SymbolDefinition = 28 35 | -------------------------------------------------------------------------------- /codemirror/shellcodePlugin.js: -------------------------------------------------------------------------------- 1 | import { StateField, RangeSet, RangeSetBuilder } from '@codemirror/state'; 2 | import { ASMStateField, ASMColorFacet, ASMColor } from './compilerPlugin.js'; 3 | 4 | /** 5 | * @typedef {Object} ShellcodeState 6 | * @property {RangeSet} colors Invalid or extended UTF-8 sequences 7 | * @property {String} code The shellcode generated by the assembly dumps 8 | */ 9 | 10 | /** 11 | * @param {Uint8Array} bytes 12 | * @returns {ShellcodeState} 13 | */ 14 | function getShellcode(bytes) 15 | { 16 | let uniSeqLen = 0, uniSeqStart = -1; 17 | let code = ""; 18 | /** @type {RangeSetBuilder} */ 19 | let builder = new RangeSetBuilder(); 20 | 21 | for(let i = 0; true; i++) 22 | { 23 | let byte = bytes[i] ?? 0; 24 | try { 25 | if(byte < 0x80) 26 | { 27 | if(uniSeqStart >= 0) 28 | throw [uniSeqStart, i]; 29 | } 30 | else if(byte < 0xC0) 31 | { 32 | if(uniSeqStart < 0) 33 | throw [i, i + 1]; 34 | 35 | if(i - uniSeqStart == uniSeqLen) 36 | { 37 | try { 38 | let uriSeq = ""; 39 | for(let j = uniSeqStart; j <= i; j++) 40 | uriSeq += '%' + bytes[j].toString(16); 41 | code += decodeURIComponent(uriSeq); 42 | } 43 | catch(e) { 44 | throw [uniSeqStart, i + 1]; 45 | } 46 | builder.add(uniSeqStart, i + 1, new ASMColor("#00F")); 47 | uniSeqStart = -1; 48 | } 49 | } 50 | else 51 | { 52 | if(uniSeqStart >= 0) 53 | throw [uniSeqStart, i]; 54 | } 55 | } catch([from, to]) { 56 | builder.add(from, to, new ASMColor("#F00")); 57 | for(let i = from; i < to; i++) 58 | code += '\\' + bytes[i].toString(8); 59 | uniSeqStart = -1; 60 | } 61 | 62 | if(i == bytes.length) 63 | break; 64 | 65 | if(byte < 0x80) 66 | { 67 | let char; 68 | if(byte == 0) 69 | char = '\\0'; 70 | else if(byte == 13) 71 | char = '\\15'; 72 | else 73 | { 74 | char = String.fromCharCode(byte); 75 | if(char == '\\' || char == '"') 76 | char = '\\' + char; 77 | } 78 | code += char; 79 | } 80 | else if(byte >= 0xC0) 81 | { 82 | uniSeqStart = i; 83 | uniSeqLen = (byte < 0xE0 ? 1 : 84 | byte < 0xF0 ? 2 : 3); 85 | } 86 | } 87 | return { code, colors: builder.finish() }; 88 | } 89 | 90 | /** @type {StateField} */ 91 | export const ShellcodeField = StateField.define({ 92 | create: state => { 93 | return getShellcode(state.field(ASMStateField).head.dump()); 94 | }, 95 | update: (state, transaction) => { 96 | if(!transaction.docChanged) 97 | return state; 98 | 99 | return getShellcode(transaction.state.field(ASMStateField).head.dump()); 100 | } 101 | }); 102 | 103 | const ShellcodeColors = ASMColorFacet.compute( 104 | [ShellcodeField], 105 | state => state.field(ShellcodeField).colors 106 | ); 107 | 108 | export const ShellcodePlugin = [ShellcodeField.extension, ShellcodeColors]; -------------------------------------------------------------------------------- /codemirror/tokenizer.js: -------------------------------------------------------------------------------- 1 | import { 2 | fetchMnemonic, isSizeHint, prefixes, isRegister, 3 | isDirective, scanIdentifier 4 | } from '@defasm/core'; 5 | import { ContextTracker, ExternalTokenizer, InputStream } from '@lezer/lr'; 6 | 7 | import * as Terms from './parser.terms.js'; 8 | 9 | var tok, pureString; 10 | 11 | /** @param {InputStream} input */ 12 | function next(input) 13 | { 14 | tok = ''; 15 | let char; 16 | 17 | while(input.next >= 0 && input.next != 10 && String.fromCharCode(input.next).match(/\s/)) 18 | input.advance(); 19 | if(input.next >= 0 && !(char = String.fromCharCode(input.next)).match(/[.$\w]/)) 20 | { 21 | tok = char; 22 | input.advance(); 23 | pureString = false; 24 | } 25 | else 26 | while(input.next >= 0 && (char = String.fromCharCode(input.next)).match(/[.$\w]/)) 27 | { 28 | tok += char; 29 | input.advance(); 30 | } 31 | 32 | tok = tok.toLowerCase() || '\n'; 33 | 34 | return tok; 35 | } 36 | 37 | /** @param {InputStream} input */ 38 | function peekNext(input) 39 | { 40 | let i = 0, char; 41 | while((char = input.peek(i)) >= 0 && char != 10 && String.fromCharCode(char).match(/\s/)) 42 | i++; 43 | if((char = input.peek(i)) >= 0 && !(char = String.fromCharCode(char)).match(/[.$\w]/)) 44 | return char; 45 | 46 | let result = ''; 47 | while((char = input.peek(i)) >= 0 && (char = String.fromCharCode(char)).match(/[.$\w]/)) 48 | { 49 | result += char; 50 | i++; 51 | } 52 | return result.toLowerCase() || '\n'; 53 | } 54 | 55 | const 56 | STATE_SYNTAX_INTEL = 1, 57 | STATE_SYNTAX_PREFIX = 2, 58 | STATE_IN_INSTRUCTION = 4, 59 | STATE_ALLOW_IMM = 8, 60 | STATE_SYNTAX_X86 = 16; 61 | 62 | /** @param {import('@defasm/core/parser.js').Syntax} initialSyntax */ 63 | export const ctxTracker = (initialSyntax, bitness) => new ContextTracker({ 64 | start: 65 | (initialSyntax.intel * STATE_SYNTAX_INTEL) | 66 | (initialSyntax.prefix * STATE_SYNTAX_PREFIX) | 67 | ((bitness == 32) * STATE_SYNTAX_X86), 68 | shift: (ctx, term, stack, input) => { 69 | if(term == Terms.Opcode) 70 | ctx |= STATE_IN_INSTRUCTION | STATE_ALLOW_IMM; 71 | else if(term == Terms.RelOpcode || term == Terms.IOpcode 72 | || term == Terms.IRelOpcode || term == Terms.symEquals 73 | || term == Terms.Directive) 74 | ctx |= STATE_IN_INSTRUCTION; 75 | else if((ctx & STATE_IN_INSTRUCTION) && term != Terms.Space) 76 | { 77 | if(input.next == ','.charCodeAt(0)) 78 | ctx |= STATE_ALLOW_IMM; 79 | else 80 | ctx &= ~STATE_ALLOW_IMM; 81 | } 82 | 83 | if(input.next == '\n'.charCodeAt(0) || input.next == ';'.charCodeAt(0)) 84 | ctx &= ~STATE_IN_INSTRUCTION; 85 | if(term != Terms.Directive) 86 | return ctx; 87 | let result = ctx, syntax = next(input); 88 | if(syntax == ".intel_syntax") 89 | { 90 | result |= STATE_SYNTAX_INTEL; 91 | result &= ~STATE_SYNTAX_PREFIX; 92 | } 93 | else if(syntax == ".att_syntax") 94 | { 95 | result &= ~STATE_SYNTAX_INTEL; 96 | result |= STATE_SYNTAX_PREFIX; 97 | } 98 | else 99 | return ctx; 100 | const pref = next(input); 101 | if(pref == 'prefix') 102 | result |= STATE_SYNTAX_PREFIX; 103 | else if(pref == 'noprefix') 104 | result &= ~STATE_SYNTAX_PREFIX; 105 | else if(pref != '\n' && pref != ';' && ((result & STATE_SYNTAX_INTEL) || pref != '#')) 106 | return ctx; 107 | 108 | return result; 109 | }, 110 | hash: ctx => ctx, 111 | strict: false 112 | }); 113 | 114 | /** @param {InputStream} input */ 115 | function tokenize(ctx, input) 116 | { 117 | const intel = ctx & STATE_SYNTAX_INTEL, 118 | prefix = ctx & STATE_SYNTAX_PREFIX, 119 | bitness = (ctx & STATE_SYNTAX_X86) ? 32 : 64; 120 | if(tok == (intel ? ';' : '#')) 121 | { 122 | while(input.next >= 0 && input.next != '\n'.charCodeAt(0)) 123 | input.advance(); 124 | return Terms.Comment; 125 | } 126 | 127 | if(!(ctx & STATE_IN_INSTRUCTION)) 128 | { 129 | const nextTok = peekNext(input); 130 | if(nextTok == '=' || nextTok == ':' || intel && (nextTok == 'equ' || isDirective(nextTok, true))) 131 | return Terms.SymbolName; 132 | 133 | if(tok == '%' && intel) 134 | return isDirective('%' + next(input), true) ? Terms.Directive : null; 135 | 136 | if(isDirective(tok, intel)) 137 | return Terms.Directive; 138 | 139 | if(intel && tok == 'offset') 140 | return Terms.Offset; 141 | 142 | if(prefixes.hasOwnProperty(tok)) 143 | return Terms.Prefix; 144 | 145 | if(tok == '=' || intel && tok == 'equ') 146 | return Terms.symEquals; 147 | 148 | let opcode = tok, interps = fetchMnemonic(opcode, intel, !intel, bitness); 149 | if(interps.length > 0) 150 | return interps[0].relative 151 | ? 152 | intel ? Terms.IRelOpcode : Terms.RelOpcode 153 | : 154 | intel ? Terms.IOpcode : Terms.Opcode; 155 | return null; 156 | } 157 | if((ctx & STATE_ALLOW_IMM) && tok[0] == '$') 158 | { 159 | input.pos -= tok.length - 1; 160 | return Terms.immPrefix; 161 | } 162 | 163 | if(tok == '@') 164 | { 165 | next(input); 166 | return Terms.SpecialWord; 167 | } 168 | 169 | if(tok == '%' && prefix) 170 | return isRegister(next(input), bitness) ? Terms.Register : null; 171 | 172 | if(tok == '{') 173 | { 174 | if((!prefix || next(input) == '%') && isRegister(next(input), bitness)) 175 | return null; 176 | while(tok != '\n' && tok != '}') 177 | next(input); 178 | return Terms.VEXRound; 179 | } 180 | 181 | if(intel && isSizeHint(tok)) 182 | { 183 | let prevEnd = input.pos; 184 | if(",;\n{:".includes(next(input))) 185 | { 186 | input.pos = prevEnd; 187 | return Terms.word; 188 | } 189 | 190 | if(tok == 'ptr') 191 | { 192 | let nextPrevEnd = input.pos; 193 | input.pos = ",;\n{:".includes(next(input)) ? prevEnd : nextPrevEnd; 194 | return Terms.Ptr; 195 | } 196 | 197 | input.pos = prevEnd; 198 | return Terms.Ptr; 199 | } 200 | 201 | const idType = scanIdentifier(tok, intel); 202 | if(idType === null) 203 | return null; 204 | if(!prefix && isRegister(tok, bitness)) 205 | return Terms.Register; 206 | if(idType == 'symbol') 207 | return Terms.word; 208 | return Terms.number; 209 | } 210 | export const tokenizer = new ExternalTokenizer( 211 | (input, stack) => { 212 | if(input.next < 0 || String.fromCharCode(input.next).match(/\s/)) 213 | return; 214 | 215 | pureString = true; 216 | next(input); 217 | const type = tokenize(stack.context, input); 218 | if(type !== null || pureString) 219 | input.acceptToken(type ?? Terms.None); 220 | 221 | }, { 222 | contextual: false 223 | } 224 | ); -------------------------------------------------------------------------------- /core/README.md: -------------------------------------------------------------------------------- 1 | # DefAssembler - Core 2 | This package holds the core of [DefAssembler](https://github.com/NewDefectus/defasm#readme), an incremental x86-64 assembler, for use both in browsers and as a Node.js package. 3 | 4 | DefAssembler is built from scratch without the use of any additional libraries, making it relatively lightweight and fast. For a quick demonstration, I recommend checking out the [GitHub Pages site](http://newdefectus.github.io/defasm) showcasing the [@defasm/codemirror](https://www.npmjs.com/package/@defasm/codemirror) package, which utilizes this assembler. Alternatively, you can try it out with the [Code Golf editor](https://code.golf/fizz-buzz#assembly), where you can also run your programs and submit them to the site. 5 | 6 | # JavaScript types 7 | ## `AssemblyState` 8 | This is the primary export of the assembler; an object of this type is needed to run it. Its constructor takes in an optional config object, which can be used to set the initial parsing syntax to Intel or AT&T (defaults to AT&T) and whether or not the .text section is writable. 9 | 10 | The class has the following properties: 11 | * `compiledRange` - the range of text parsed by the compiler during the last call to `compile` (used for debugging) 12 | * `errors` - a list of all errors generated by the program (updated by `secondPass`) 13 | * `head` - a [statement list](#statementnode) containing all the instructions of the program 14 | * `sections` - a list of [`Section`](#section)s that appear in the program 15 | * `source` - the source code of the program 16 | * `symbols` - a map from a symbol name to the symbol's definition (if it exists) and its references 17 | 18 | `AssemblyState` has 6 methods: 19 | * `compile()` - usually assembles a given string of source code and discards the previous state; however, it can also be configured to replace parts of the code and update the state appropriately. This is done by passing a [`Range`](#range) object to the configuration parameter. The assembler aims to perform as few recompilations as possible, so you can rest assured this function is quite efficient. 20 | * `secondPass()` - performs a second pass on the state, resolving symbol references and reporting errors. You typically won't need to use this (it's called automatically after `compile` unless configured otherwise with `doSecondPass`); it's available in case you wish to make multiple changes at once before executing the second pass, which is more efficient. 21 | * `line()` - creates a `Range` object that spans a given line (useful for replacing/inserting lines in `compile`) 22 | * `iterate()` - iterates over the instructions using a given callback, passing the instruction's line as a second parameter (for instructions that span multiple lines, this will be their first line). 23 | * `bytesPerLine()` - iterate over each line in the program using a given callback, sending a line number and a `Uint8Array`. Data directives spanning multiple lines may be sent multiple times. Empty instructions and lines are skipped. 24 | 25 | ## `Section` 26 | An object corresponding to an [ELF](https://en.wikipedia.org/wiki/Executable_and_Linkable_Format) section. Its properties include: 27 | * `head` - a [statement list](statementnode) containing all the instructions belonging to the section 28 | * `name` - the name of the section (`.text`, `.data`, `.bss`, etc.) 29 | * `entryPoints` - a list of all the `.section` directives that refer to this section 30 | * `type`, `flags`, `entrySize` - numbers that are relevant to ELF file generation 31 | 32 | ## `StatementNode` 33 | `StatementNode` objects form linked lists that hold statements (instructions, directives, comments, etc.). A list usually begins with a dummy node. 34 | 35 | `StatementNode`s have the following functions: 36 | * `find()` - given an index in the source code, find the `StatementNode` in the list whose statement's range encompasses that index. 37 | * `length()` - calculate the total number of bytes of instructions in the list 38 | * `dump()` - generate a `Buffer` (or `Uint8Array`, depending on which is available) containing all the bytes of the instructions in the list 39 | 40 | ## `Range` 41 | `Range` objects are used within the compiler to keep track of each instruction's span. A `Range` object may be passed to the `compile` method of `AssemblyState` to specify the range in the code to replace. It can be created using the constructor, which receives the range's start index and length in characters, or using `AssemblyState`'s `line` method described above. 42 | 43 | There are also a number of functions exported by the package that identify or collect information about assembly keywords. These are mostly there for syntax highlighting. 44 | 45 | ## Example 46 | 47 | The following Node.js script illustrates the assembler's basic capabilities: 48 | ```js 49 | import("@defasm/core").then(core => { 50 | const { AssemblyState } = core; 51 | let state = new AssemblyState(); 52 | 53 | 54 | /* Compile just the "nop" instruction */ 55 | state.compile('nop'); 56 | console.log(state.head.dump()); // 57 | 58 | 59 | /* Insert a "mov $4, %ax" instruction on line 4 */ 60 | state.compile('mov $4, %ax', { range: state.line(4) }); 61 | console.log(state.head.dump()); // 62 | 63 | 64 | /* Insert "jmp lab" on line 3. Note that "lab" is not defined yet, so this 65 | instruction will contain a "null" value */ 66 | state.compile('jmp lab', { range: state.line(3) }); 67 | console.log(state.head.dump()); // 68 | 69 | 70 | /* Define "lab" as a label on line 2. This will cause the aforementioned 71 | "jmp lab" instruction to recompile, now that the "lab" symbol has been defined */ 72 | state.compile('lab:', { range: state.line(2) }); 73 | console.log(state.head.dump()); // 74 | 75 | 76 | /* Replace lines 1-4 with "sub $lab, %dl". Note that among the 77 | deleted instructions was the definition of "lab"; now that it has been removed, 78 | the symbol is once again undefined, so this instruction will have a "null" value */ 79 | state.compile('sub $lab, %dl', { range: state.line(1).until(state.line(4)) }); 80 | console.log(state.head.dump()); // 81 | 82 | 83 | /* Redefining the symbol will prompt the previous instruction to recompile */ 84 | state.compile('lab = 27', { range: state.line(2) }); 85 | console.log(state.head.dump()); // 86 | 87 | 88 | /* Replace the previous code with this code, which assembles some data 89 | into different sections */ 90 | state.compile( 91 | `.data 92 | .ascii "Hello, world!" 93 | .bss 94 | .long 0, 0 95 | .text 96 | push %rax 97 | pop %rsi 98 | .section custom, "wax", @progbits 99 | jmp . + 4 100 | `, { range: state.line(1).until(state.line(2)) }); 101 | 102 | /* Take a look at the data generated by each of the sections */ 103 | for(const section of state.sections) 104 | console.log(`${section.name}:`, section.head.dump()); 105 | // .text: 106 | // .data: 107 | // .bss: 108 | // custom: 109 | 110 | 111 | /* Removing the third line, which contains the .bss directive, will cause 112 | the data in the .bss section to be moved over to the .data section */ 113 | state.compile('', { range: state.line(3) }); 114 | for(const section of state.sections) 115 | console.log(`${section.name}:`, section.head.dump()); 116 | // .text: 117 | // .data: 118 | // .bss: 119 | // custom: 120 | 121 | process.exit(); 122 | }); 123 | ``` -------------------------------------------------------------------------------- /core/bitfield.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a bitfield class with accessible names 3 | * @param {string[]} fieldNames names of the fields in the bitfield 4 | * @returns a class 5 | */ 6 | export function createBitfieldClass(fieldNames) 7 | { 8 | let prototype = {}; 9 | 10 | for(let i = 0; i < fieldNames.length; i++) 11 | { 12 | let fieldValue = 1 << i; 13 | Object.defineProperty(prototype, fieldNames[i], { 14 | get() 15 | { 16 | return (this.bits & fieldValue) != 0; 17 | }, 18 | set(value) 19 | { 20 | if(value) 21 | this.bits |= fieldValue; 22 | else 23 | this.bits &= ~fieldValue; 24 | return value; 25 | } 26 | }) 27 | }; 28 | 29 | prototype.add = function(field) { 30 | this.bits |= field.bits; 31 | }; 32 | 33 | return class { 34 | constructor() 35 | { 36 | this.bits = 0; 37 | Object.setPrototypeOf(this, prototype) 38 | } 39 | }; 40 | } -------------------------------------------------------------------------------- /core/compiler.js: -------------------------------------------------------------------------------- 1 | import { ASMError, token, next, match, loadCode, currRange, currSyntax, setSyntax, prevRange, line, comment, Range, startAbsRange, RelativeRange, ungetToken } from "./parser.js"; 2 | import { isDirective, makeDirective } from "./directives.js"; 3 | import { Instruction } from "./instructions.js"; 4 | import { SymbolDefinition, recompQueue, queueRecomp, loadSymbols, symbols } from "./symbols.js"; 5 | import { Statement, StatementNode } from "./statement.js"; 6 | import { loadSections, Section, sectionFlags, sections } from "./sections.js"; 7 | 8 | /** @type {StatementNode} */ var prevNode = null; 9 | /** @type {Section} */ export var currSection = null; 10 | /** @type {32 | 64} */ export var currBitness; 11 | 12 | var addr = 0; 13 | 14 | /** @param {Section} section */ 15 | function setSection(section) 16 | { 17 | currSection = section; 18 | const prevInstr = section.cursor.prev.statement; 19 | return prevInstr.address + prevInstr.length; 20 | } 21 | 22 | /** @param {Statement} instr */ 23 | function addInstruction(instr, seekEnd = true) 24 | { 25 | if(instr.section !== currSection) 26 | instr.address = setSection(instr.section); 27 | 28 | prevNode = prevNode.next = new StatementNode(instr); 29 | currSection.cursor.prev = currSection.cursor.prev.next = instr.sectionNode; 30 | setSyntax(instr.syntax); 31 | 32 | if(seekEnd && token != '\n' && token != ';') 33 | { 34 | // Special case: this error should appear but not remove the instruction's bytes 35 | instr.error = new ASMError("Expected end of line"); 36 | while(token != '\n' && token != ';') 37 | next(); 38 | } 39 | 40 | addr = instr.address + instr.length; 41 | instr.range.length = (seekEnd ? currRange.start : currRange.end) - instr.range.start; 42 | } 43 | 44 | /** 45 | * @typedef {Object} AssemblyConfig 46 | * @property {import('@defasm/core/parser.js').Syntax} config.syntax The initial syntax to use 47 | * @property {boolean} config.writableText Whether the .text section should be writable 48 | * @property {32|64} config.bitness Whether to use the 64- or 32-bit instruction set 49 | */ 50 | 51 | export class AssemblyState 52 | { 53 | /** @param {AssemblyConfig} */ 54 | constructor({ 55 | syntax = { 56 | intel: false, 57 | prefix: true 58 | }, 59 | writableText = false, 60 | bitness = 64 61 | } = {}) 62 | { 63 | this.defaultSyntax = syntax; 64 | this.bitness = bitness; 65 | 66 | /** @type {Map} */ 67 | this.symbols = new Map(); 68 | /** @type {string[]} */ 69 | this.fileSymbols = []; 70 | 71 | setSyntax(syntax); 72 | loadSymbols(this.symbols, this.fileSymbols); 73 | currBitness = bitness; 74 | 75 | /** @type {Section[]} */ 76 | this.sections = [ 77 | new Section('.text'), 78 | new Section('.data'), 79 | new Section('.bss') 80 | ]; 81 | 82 | if(writableText) 83 | this.sections[0].flags |= sectionFlags.w; 84 | 85 | this.head = new StatementNode(); 86 | 87 | /** @type {string} */ 88 | this.source = ''; 89 | 90 | /** @type {Range} */ 91 | this.compiledRange = new Range(); 92 | 93 | /** @type {ASMError[]} */ 94 | this.errors = []; 95 | } 96 | 97 | /** Compile Assembly from source code into machine code 98 | * @param {string} source 99 | */ 100 | compile(source, { 101 | haltOnError = false, 102 | range: replacementRange = new Range(0, this.source.length), 103 | doSecondPass = true } = {}) 104 | { 105 | this.source = 106 | /* If the given range is outside the current 107 | code's span, fill the in-between with newlines */ 108 | this.source.slice(0, replacementRange.start).padEnd(replacementRange.start, '\n') + 109 | source + 110 | this.source.slice(replacementRange.end); 111 | 112 | loadSymbols(this.symbols, this.fileSymbols); 113 | loadSections(this.sections, replacementRange); 114 | currBitness = this.bitness; 115 | 116 | let { head, tail } = this.head.getAffectedArea(replacementRange, true, source.length); 117 | 118 | setSyntax(head.statement ? head.statement.syntax : this.defaultSyntax); 119 | addr = setSection(head.statement ? head.statement.section : this.sections[0]); 120 | loadCode(this.source, replacementRange.start); 121 | 122 | prevNode = head; 123 | 124 | while(match) 125 | { 126 | let range = startAbsRange(); 127 | try 128 | { 129 | if(token != '\n' && token != ';') 130 | { 131 | let name = token; 132 | next(); 133 | if(token == ':') // Label definition 134 | addInstruction(new SymbolDefinition({ addr, name, range, isLabel: true }), false); 135 | else if(token == '=' || currSyntax.intel && token.toLowerCase() == 'equ') // Symbol definition 136 | addInstruction(new SymbolDefinition({ addr, name, range })); 137 | else 138 | { 139 | let isDir = false; 140 | if(currSyntax.intel) 141 | { 142 | if(name[0] == '%') 143 | { 144 | isDir = true; 145 | name += token.toLowerCase(); 146 | next(); 147 | } 148 | else 149 | isDir = isDirective(name, true); 150 | } 151 | else 152 | isDir = name[0] == '.'; 153 | 154 | if(isDir) // Assembler directive 155 | addInstruction(makeDirective({ addr, range }, currSyntax.intel ? name : name.slice(1))); 156 | else if(currSyntax.intel && isDirective(token, true)) // "