From f49939b72b2157e67f4be6befb5ba6409f85969a Mon Sep 17 00:00:00 2001 From: janis Date: Sun, 7 Jun 2026 00:11:58 +0200 Subject: [PATCH] parse registers --- stages/as0/Makefile | 12 ++ stages/as0/as0.asm | 357 ++++++++++++++++++++++++++++++++++++++++++++ stages/as0/test.rs | 148 ++++++++++++++++++ 3 files changed, 517 insertions(+) create mode 100644 stages/as0/Makefile create mode 100644 stages/as0/as0.asm create mode 100644 stages/as0/test.rs diff --git a/stages/as0/Makefile b/stages/as0/Makefile new file mode 100644 index 0000000..279ee68 --- /dev/null +++ b/stages/as0/Makefile @@ -0,0 +1,12 @@ + +test.bin: test.rs as0.o + rustc -Clink-arg=-fuse-ld=mold -Clink-arg=as0.o --edition=2024 --test -g $< -o $@ + +as0.o: as0.asm + nasm -g -f elf64 -o as0.o as0.asm + +test: test.bin + ./test.bin + +clean: + rm -f as0.o test.bin diff --git a/stages/as0/as0.asm b/stages/as0/as0.asm new file mode 100644 index 0000000..f6e0e50 --- /dev/null +++ b/stages/as0/as0.asm @@ -0,0 +1,357 @@ +;; This assembler is a 2-pass compiler, permitting forward and backward label references. +;; It's also the first stage in this bootstrapping experiment that will +;; consume proper mnemonic assembly, rather than hex input. + +section .data + _start_lbl db "_start", 0 + +;; supported instructions: +;; mov +;; test +;; xor +;; and +;; shl +;; shr +;; inc +;; dec +;; neg +;; ret +;; not +;; add +;; sub +;; imul +;; cmp +;; lea +;; push, pop +;; cmovcc +;; xchg +;; bts +;; bsf +;; bt + +;; syscall +;; jcc rel32 +;; jmp rel32 +;; call rel32 + +section .text +instr_table: + ; mnemonic, num_operands, handler +_mov: + dq "mov" + db 2 + dq instr_table - _mov_handler + +;; @param lhs: (rdi, rsi) +;; @param rhs: (rdx, rcx) +;; @return al +strcmp: + cmp rcx, rsi + cmovb rsi, rcx ; if rhs is shorter, use its length for the loop + xor eax, eax +.strcmp_loop: + cmp rsi, rax + je .strcmp_equal + movzx ecx, byte [rdx + rax] + cmp byte [rdi + rax], cl + lea rax, [rax + 1] + je .strcmp_loop + seta al ; al = lhs > rhs + sbb al, 0 ; al = al - CF + ret +.strcmp_equal: + xor eax, eax + ret + +memcpy: + xor rax, rax +.memcpy_loop: + cmp rax, rdx + je .memcpy_done + mov al, byte [rdi + rax] + mov byte [rsi + rax], al + lea rax, [rax + 1] + jmp .memcpy_loop +.memcpy_done: + ret + + ;; read from input file +read_file: + +; ;; returns the next byte in the input stream without advancing the read position. +; peekc: + +; ;; returns the next byte in the input stream and advances the read position. + ; getc: +extern peekc +extern getc + + ;; converts char $dil to a digit with radix $rsi, returning it in $edx. $al is set to 1 if the char is a valid digit, and 0 otherwise. +to_digit: + lea eax, [rsi - 2] + cmp eax, 35 + jae .invalid + movzx rdi, dil + lea edx, [rdi - 65] ; 'A' = 65 + and edx, -33 ; convert to uppercase + add edx, 10 ; 'A' should map to 10 + lea eax, [rdi - 48] ; '0' = 48 + cmp esi, 11 + cmovb edx, eax ; if radix <= 10, then take the difference from '0' + cmp edi, 58 + cmovb edx, eax ; or if char < '9', then take the difference from '0' + xor eax, eax + cmp edx, esi + setb al ; al = edx < radix + ret +.invalid: + xor eax, eax + ret + +;; reads from $rdi and parses digits with radix $rsi until a non-digit is encountered. +parse_digits: + sub rsp, 24 + mov qword [rsp + 16], rsi ; save the radix + mov qword [rsp + 8], rdi ; save the source iterator + mov qword [rsp], 0 + .loop: + mov rdi, qword [rsp + 8] ; restore the source iterator + call peekc + mov dil, al + mov rsi, qword [rsp + 16] ; restore the radix + call to_digit + test al, al + jz .done + mov rax, qword [rsp] + mov rsi, qword [rsp + 16] ; restore the radix + imul rax, rsi + add rax, rdx + mov qword [rsp], rax + mov rdi, qword [rsp + 8] ; restore the source iterator + call getc + jmp .loop + + .done: + mov rax, [rsp] + add rsp, 24 + ret + +;; enum Register { + ; A = 0, + ; B, + ; C, + ; D, + ; Src, + ; Dst, + ; Sp, + ; Bp, + ; R8, + ; R9, + ; R10, + ; R11, + ; R12, + ; R13, + ; R14, + ; R15, +;; } + +;; parses a non-extended GPR operand (e.g. a register name that isn't r8-r15) +;; returns the register number in rax and the width in rdx +;; if $rdx == 0, then parsing failed (invalid register name) +;; $rdi = source +;; $rsi = prefix char (e.g. 'r' or 'e' or 0) +global parse_gpr +parse_gpr: + sub rsp, 24 + mov qword [rsp + 16], -1 ; register number, initialized to -1 (invalid) + mov qword [rsp + 8], rdi ; source iterator + mov qword [rsp], rsi ; prefix char + call getc + + ; check for a,b,c,d,s + movzx rax, al + sub rax, 'a' ; convert 'a' to 0, 'b' to 1, ..., 's' to 4 + cmp rax, 's' - 'a' + ja .invalid ; if it's greater than 's', it's invalid + mov rcx, 4 + cmovz rax, rcx ; if it's 's', set rax to 4 + cmp rax, 4 + ja .invalid ; if it's greater than 4, it's invalid + lea rcx, [rel .jt] ; jump table for a,b,c,d + movsxd rdx, dword [rcx + rax * 4] ; get the offset of the handler if a-d + add rcx, rdx ; calculate the address of the handler + jmp rcx + +.suffix: + mov byte [rsp + 16], al ; save the register number for a,b,c,d,s + mov rdi, qword [rsp + 8] ; restore the source iterator + call peekc + cmp al, 'l' + jne .not_l + mov rdi, qword [rsp + 8] ; restore the source iterator + call getc ; consume the suffix + mov rdx, 1 ; width = 1 for al, bl, cl, dl, sil, dil + jmp .done +.not_l: ; x is only valid for a,b,c,d, not sp, bp, si or di + mov rdx, 2 + cmp byte [rsp + 16], 4 + jae .done ; for sp, bp, si, di, the width is 2 if not 'l' + cmp al, 'x' + jne .not_x + mov rdi, qword [rsp + 8] ; restore the source iterator + call getc ; consume the suffix + mov rdx, 2 ; width = 2 for ax, bx, cx, dx + jmp .done +.not_x: ; x is only valid for a,b,c,d + cmp al, 'h' + jne .invalid + mov rdx, 3 ; width = 3 for ah, bh, ch, dh +.done: + movzx rax, byte [rsp + 16] ; move the register number into rax + add rsp, 24 + ret +.invalid: + xor rax, rax ; rax = 0 indicates invalid register + xor rdx, rdx ; rdx = 0 indicates invalid register + jmp .done + +.a: + mov rax, 0 + jmp .suffix +.b: + mov rdi, qword [rsp + 8] ; restore the source iterator + call peekc + cmp al, 'p' + mov rax, 1 + jne .suffix + mov rdi, qword [rsp + 8] ; restore the source iterator + call getc + mov rax, 5 + jmp .suffix +.c: + mov rax, 2 + jmp .suffix +.d: + mov rdi, qword [rsp + 8] ; restore the source iterator + call peekc + cmp al, 'i' + mov rax, 3 + jne .suffix + mov rdi, qword [rsp + 8] ; restore the source iterator + call getc + mov rax, 7 + jmp .suffix +.s: + mov rdi, qword [rsp + 8] ; restore the source iterator + call getc + mov cl, al + cmp cl, 'i' + mov rax, 6 + je .suffix + cmp cl, 'p' + mov rax, 4 + je .suffix + jmp .invalid + +.jt: + dd .a-.jt + dd .b-.jt + dd .c-.jt + dd .d-.jt + dd .s-.jt + + + + +;; parses an extended GPR operand (e.g. r8-r15) +;; $rdi = source, leading r is already consumed +parse_egpr: + sub rsp, 16 + mov qword [rsp + 8], rdi ; source iterator + mov rsi, 10 + call parse_digits + cmp rax, 8 + jb .invalid + cmp rax, 15 + ja .invalid + mov qword [rsp], rax +.suffix: + mov rdi, qword [rsp + 8] ; restore the source iterator + call peekc + mov rcx, 1 + cmp al, 'b' + je .done_getc + mov rcx, 2 + cmp al, 'w' + je .done_getc + mov rcx, 4 + cmp al, 'd' + je .done_getc + mov rcx, 8 ; default width is 8 for r8-r15 + jmp .done +.done_getc: + mov rdi, qword [rsp + 8] ; restore the source iterator + push rcx + call getc + pop rcx +.done: + mov rdx, rcx + mov rax, qword [rsp] ; move the register number into rax + add rsp, 16 + ret +.invalid: + mov qword [rsp], 0 + xor rcx, rcx ; rdx = 0 indicates invalid register + jmp .done + +;; (rdi, rsi): source string + global parse_reg +parse_reg: + sub rsp, 16 + mov qword [rsp + 8], rdi ; source iterator + mov byte [rsp], 0 ; x1 + call peekc + cmp al, 'r' + mov rdi, qword [rsp + 8] ; restore the source iterator + je .r + cmp al, 'e' + jne .gpr + mov byte [rsp], 1 ; x2 + call getc ; consume the 'e' + mov rdi, qword [rsp + 8] ; restore the source iterator + jmp .gpr +.r: + mov byte [rsp], 2 ; x4 + call getc ; consume the 'r' + mov rdi, qword [rsp + 8] ; restore the source iterator + call peekc + mov dil, al + mov rsi, 10 + call to_digit + test al, al + mov rdi, qword [rsp + 8] ; restore the source iterator + jz .gpr + call parse_egpr + jmp .done +.gpr: + call parse_gpr + mov cl, byte [rsp] + shl rdx, cl +.done: + add rsp, 16 + ret + + +;; enum Operand { +;; Reg(Reg, width: u8), +;; Imm(u32, width: u8), +;; Mem(width: u8, base: Option, index: Option<(Reg, scale: u8)>, disp: Option), +;; } + +;; mov r/m, r +;; mov r, r/m +;; mov r/m, imm +_mov_handler: + ret + + diff --git a/stages/as0/test.rs b/stages/as0/test.rs new file mode 100644 index 0000000..58c30cb --- /dev/null +++ b/stages/as0/test.rs @@ -0,0 +1,148 @@ +unsafe extern "C" { + unsafe fn parse_gpr(src: *mut Source, prefix: u8) -> (u64, u64); + unsafe fn parse_reg(src: *mut Source) -> (u64, u64); +} + +struct Source<'a> { + iter: core::iter::Peekable>, +} + +impl<'a> From<&'a [u8]> for Source<'a> { + fn from(value: &'a [u8]) -> Self { + Self { + iter: value.iter().peekable(), + } + } +} + +#[unsafe(no_mangle)] +extern "C" fn peekc(src: *mut Source) -> u8 { + unsafe { (*src).iter.peek().cloned().unwrap_or(&0).clone() } +} + +#[unsafe(no_mangle)] +extern "C" fn getc(src: *mut Source) -> u8 { + unsafe { (*src).iter.next().unwrap_or(&0).clone() } +} + +#[test] +fn test_parse_reg() { + let cases = [ + (&b"rax"[..], (0, 8)), + (&b"rbx"[..], (1, 8)), + (&b"rcx"[..], (2, 8)), + (&b"rdx"[..], (3, 8)), + (&b"rsp"[..], (4, 8)), + (&b"rbp"[..], (5, 8)), + (&b"rsi"[..], (6, 8)), + (&b"rdi"[..], (7, 8)), + (&b"eax"[..], (0, 4)), + (&b"ebx"[..], (1, 4)), + (&b"ecx"[..], (2, 4)), + (&b"edx"[..], (3, 4)), + (&b"esp"[..], (4, 4)), + (&b"ebp"[..], (5, 4)), + (&b"esi"[..], (6, 4)), + (&b"edi"[..], (7, 4)), + (&b"r8"[..], (8, 8)), + (&b"r9"[..], (9, 8)), + (&b"r10"[..], (10, 8)), + (&b"r11"[..], (11, 8)), + (&b"r12"[..], (12, 8)), + (&b"r13"[..], (13, 8)), + (&b"r14"[..], (14, 8)), + (&b"r15"[..], (15, 8)), + (&b"r8d"[..], (8, 4)), + (&b"r9d"[..], (9, 4)), + (&b"r10d"[..], (10, 4)), + (&b"r11d"[..], (11, 4)), + (&b"r12d"[..], (12, 4)), + (&b"r13d"[..], (13, 4)), + (&b"r14d"[..], (14, 4)), + (&b"r15d"[..], (15, 4)), + (&b"r8w"[..], (8, 2)), + (&b"r9w"[..], (9, 2)), + (&b"r10w"[..], (10, 2)), + (&b"r11w"[..], (11, 2)), + (&b"r12w"[..], (12, 2)), + (&b"r13w"[..], (13, 2)), + (&b"r14w"[..], (14, 2)), + (&b"r15w"[..], (15, 2)), + (&b"r8b"[..], (8, 1)), + (&b"r9b"[..], (9, 1)), + (&b"r10b"[..], (10, 1)), + (&b"r11b"[..], (11, 1)), + (&b"r12b"[..], (12, 1)), + (&b"r13b"[..], (13, 1)), + (&b"r14b"[..], (14, 1)), + (&b"r15b"[..], (15, 1)), + (&b"ax"[..], (0, 2)), + (&b"bx"[..], (1, 2)), + (&b"cx"[..], (2, 2)), + (&b"dx"[..], (3, 2)), + (&b"sp"[..], (4, 2)), + (&b"bp"[..], (5, 2)), + (&b"si"[..], (6, 2)), + (&b"di"[..], (7, 2)), + (&b"al"[..], (0, 1)), + (&b"bl"[..], (1, 1)), + (&b"cl"[..], (2, 1)), + (&b"dl"[..], (3, 1)), + (&b"ah"[..], (0, 3)), + (&b"bh"[..], (1, 3)), + (&b"ch"[..], (2, 3)), + (&b"dh"[..], (3, 3)), + (&b"sil"[..], (6, 1)), + (&b"dil"[..], (7, 1)), + (&b"asd"[..], (0, 0)), + ]; + + for (text, result) in cases { + let mut src = Source::from(text); + unsafe { + assert_eq!( + parse_reg(&mut src), + result, + "Failed to parse '{}'", + std::str::from_utf8(text).unwrap() + ); + } + } +} + +#[test] +fn test_parse_gpr() { + let cases = [ + (&b"ax"[..], (0, 2)), + (&b"bx"[..], (1, 2)), + (&b"cx"[..], (2, 2)), + (&b"dx"[..], (3, 2)), + (&b"sp"[..], (4, 2)), + (&b"bp"[..], (5, 2)), + (&b"si"[..], (6, 2)), + (&b"di"[..], (7, 2)), + (&b"al"[..], (0, 1)), + (&b"bl"[..], (1, 1)), + (&b"cl"[..], (2, 1)), + (&b"dl"[..], (3, 1)), + (&b"ah"[..], (0, 3)), + (&b"bh"[..], (1, 3)), + (&b"ch"[..], (2, 3)), + (&b"dh"[..], (3, 3)), + (&b"sil"[..], (6, 1)), + (&b"dil"[..], (7, 1)), + (&b"asd"[..], (0, 0)), + ]; + + for (text, result) in cases { + let mut src = Source::from(text); + unsafe { + assert_eq!( + parse_gpr(&mut src, b'r'), + result, + "Failed to parse '{}'", + std::str::from_utf8(text).unwrap() + ); + } + } +}