From: Jonas Hvid Date: Sun, 4 Oct 2020 01:08:51 +0000 (+0200) Subject: Add alternative Linux backend X-Git-Url: https://git.rrq.au/?a=commitdiff_plain;h=9b08decf6a8f5d454c90dbcbf12d42cb8954a022;hp=afb6afb35d212499a691c0168ad3f1e479792e18;p=rrq%2Fjonasforth.git Add alternative Linux backend You can now run: make main && ./main To run the interpreter without needing to boot up a virtual machine. Note that this mode uses per-line buffering by default, so you may have to press enter where you would normally only need a space. --- diff --git a/.gitignore b/.gitignore index 5557c76..cef3077 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /out +/main /OVMF_CODE.fd /OVMF_VARS.fd diff --git a/Makefile b/Makefile index ff474b0..c812dcc 100644 --- a/Makefile +++ b/Makefile @@ -14,9 +14,12 @@ OVMF_CODE.fd: /usr/share/ovmf/x64/OVMF_CODE.fd OVMF_VARS.fd: /usr/share/ovmf/x64/OVMF_VARS.fd cp $< $@ -out/main: main.asm impl.asm bootstrap.asm sys.f uefi.asm +out/main: main.asm impl.asm bootstrap.asm sys.f os/uefi.asm mkdir -p out - fasm $< out/main + OS_INCLUDE=os/uefi.asm fasm $< $@ + +main: main.asm impl.asm bootstrap.asm sys.f os/linux.asm + OS_INCLUDE=os/linux.asm fasm $< $@ .PHONY: clean clean: diff --git a/impl.asm b/impl.asm index e8cd883..67d11b5 100644 --- a/impl.asm +++ b/impl.asm @@ -1,6 +1,4 @@ -;; vim: syntax=fasm - -section '.text' code readable executable +os_code_section macro printlen msg, len { push rsi @@ -8,7 +6,7 @@ macro printlen msg, len { mov rcx, msg mov rdx, len - sys_print_string + call os_print_string sub rsp, 8 pop rsi @@ -183,9 +181,10 @@ parse_number: pop rdi printlen rdi, [.length] newline - sys_terminate 100 + mov rax, 100 + call os_terminate -section '.data' readable writable +os_data_section find.search_length dq ? find.search_buffer dq ? diff --git a/main.asm b/main.asm index 43eef7a..93e576f 100644 --- a/main.asm +++ b/main.asm @@ -1,70 +1,31 @@ ;; vim: syntax=fasm -include "uefi.asm" - -;; "Syscalls" {{{ - -;; [NOTE] Volatile registers Linux (syscalls) vs UEFI +;; At compile-time we load the module given by the environment variable +;; OS_INCLUDE. All of the following these procedures should preserve the value +;; of RSI and RSP. They may use other registers as they like. ;; -;; Linux syscalls: RAX, RCX, R11 -;; UEFI: RAX, RCX, R11, RDX, R8, R9, R10 - -;; We are in the process of replacing our dependency on Linux with a dependency -;; on UEFI. The following macros attempt to isolate what would be syscalls in -;; Linux; thus, we will be able to replace these with UEFI-based implementations, -;; and in theory we should expect the program to work. - -;; Print a string of a given length. +;; The module should provide the following: ;; -;; Input: -;; - RCX = Pointer to buffer -;; - RDX = Buffer length +;; os_code_section +;; Macro to start the text segment. ;; -;; Clobbers: RAX, RCX, R11, RDI, RSI -macro sys_print_string { - push r8 - push r9 - push r10 - - call uefi_print_string - - pop r10 - pop r9 - pop r8 -} - -;; Read a character from the user into the given buffer. +;; os_data_section +;; Macro to start the data segment. ;; -;; Input: -;; - RSI = Character buffer +;; os_initialize +;; Called at initialization. ;; -;; Output: -;; - BYTE [RSI] = Character +;; os_print_string +;; Takes a string buffer in RCX and the length in RDX, and prints the string +;; to the console. ;; -;; Clobbers: RAX, RCX, R11, RDI, RSI, RDX -macro sys_read_char { - push rbx - push r8 - push r9 - push r10 - push r15 - - mov rcx, rsi - call uefi_read_char - - pop r15 - pop r10 - pop r9 - pop r8 - pop rbx -} - -macro sys_terminate code { - mov rax, code - call uefi_terminate -} - -;; }}} +;; os_read_char +;; Wait for the user to type a key, and then put the corresponding ASCII byte +;; into RAX. +;; +;; os_terminate +;; Shut down the system, returning the error code given in RAX. +include '%OS_INCLUDE%' ;; The code in this macro is placed at the end of each Forth word. When we are ;; executing a definition, this code is what causes execution to resume at the @@ -123,16 +84,16 @@ macro forth_asm label, name, immediate { .start: } -section '.text' code readable executable - include "impl.asm" ; Misc. subroutines include "bootstrap.asm" ; Forth words encoded in Assembly +os_code_section + main: cld ; Clear direction flag so LODSQ does the right thing. mov rbp, return_stack_top ; Initialize return stack - call uefi_initialize + call os_initialize mov rax, MAIN jmp qword [rax] @@ -235,7 +196,7 @@ forth_asm EMIT, 'EMIT' lea rcx, [rsp] mov rdx, 1 - sys_print_string + call os_print_string add rsp, 8 popr rax @@ -260,12 +221,7 @@ forth_asm KEY, 'KEY' jne .from_buffer ;; Reading user input - push rsi - mov rsi, .buffer - sys_read_char - pop rsi - - movzx rax, byte [.buffer] + call os_read_char ret .from_buffer: @@ -336,7 +292,7 @@ forth_asm TELL, 'TELL' pop rdx ; Length pop rcx ; Buffer - sys_print_string + call os_print_string popr rsi popr rax @@ -344,7 +300,8 @@ forth_asm TELL, 'TELL' ;; Exit the program cleanly. forth_asm TERMINATE, 'TERMINATE' - sys_terminate 0 + mov rax, 0 + call os_terminate ;; Duplicate a pair of elements. forth_asm PAIRDUP, '2DUP' @@ -433,7 +390,7 @@ forth_asm DOTU, '.U' ;; Print the buffer mov rcx, .buffer mov rdx, [.printed_length] - sys_print_string + call os_print_string ;; Restore RSI and continue execution pop rsi @@ -506,10 +463,7 @@ forth_asm READ_STRING, 'S"' mov [.length], 0 .read_char: - mov rsi, .char_buffer - sys_read_char - - mov al, [.char_buffer] + call os_read_char cmp al, '"' je .done @@ -661,7 +615,7 @@ forth INPUT_LENGTH, 'INPUT-LENGTH' dq LIT, input_buffer_length dq EXIT -section '.data' readable writable +os_data_section ;; The LATEST variable holds a pointer to the word that was last added to the ;; dictionary. This pointer is updated as new words are added, and its value is @@ -709,6 +663,8 @@ return_stack_top: ;; We store some Forth code in sys.f that defined common words that the user ;; would expect to have available at startup. To execute these words, we just ;; include the file directly in the binary, and then interpret it at startup. -sysf file 'sys.f' +sysf: +file 'sys.f' +file 'example.f' sysf.len = $ - sysf diff --git a/os/linux.asm b/os/linux.asm new file mode 100644 index 0000000..2ea3d16 --- /dev/null +++ b/os/linux.asm @@ -0,0 +1,45 @@ +format ELF64 executable +entry main + +macro os_code_section { + segment readable executable +} + +macro os_data_section { + segment readable writable +} + +os_code_section + +os_initialize: + ret + +os_print_string: + push rsi + mov rax, 1 + mov rdi, 1 + mov rsi, rcx + syscall + pop rsi + ret + +os_read_char: + push rsi + mov rax, 0 + mov rdi, 0 + mov rsi, .buffer + mov rdx, 1 + syscall + pop rsi + movzx rax, byte [.buffer] + ret + +os_terminate: + mov rdi, rax + mov rax, $3C + syscall + +os_data_section + +os_read_char.buffer db ? + diff --git a/os/uefi.asm b/os/uefi.asm new file mode 100644 index 0000000..10c7627 --- /dev/null +++ b/os/uefi.asm @@ -0,0 +1,192 @@ +;; vim: syntax=fasm + +format pe64 dll efi +entry main + +;; EFI struct definitions {{{ + +EFI_NOT_READY = 0x8000_0000_0000_0000 or 6 + +;; Based on https://wiki.osdev.org/Uefi.inc +macro struct name { + virtual at 0 + name name + end virtual +} + +struc EFI_TABLE_HEADER { + dq ? + dd ? + dd ? + dd ? + dd ? +} + +struc EFI_SYSTEM_TABLE { + .Hdr EFI_TABLE_HEADER + .FirmwareVendor dq ? ; CHAR16* + .FirmwareRevision dd ? ; UINT32 + align 8 + .ConsoleInHandle dq ? ; EFI_HANDLE + .ConIn dq ? ; EFI_SIMPLE_TEXT_INPUT_PROTOCOL* + .ConsoleOutHandle dq ? ; EFI_HANDLE + .ConOut dq ? ; EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL* + ; ... +} +struct EFI_SYSTEM_TABLE + +struc EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL { + .Reset dq ? ; EFI_TEXT_RESET + .OutputString dq ? ; EFI_TEXT_STRING + ; ... +} +struct EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL + +struc EFI_SIMPLE_TEXT_INPUT_PROTOCOL { + .Reset dq ? ; EFI_INPUT_RESET + .ReadKeyStroke dq ? ; EFI_INPUT_READ_KEY + ; ... +} +struct EFI_SIMPLE_TEXT_INPUT_PROTOCOL + +struc EFI_INPUT_KEY { + .ScanCode dw ? ; UINT16 + .UnicodeChar dw ? ; CHAR16 + align 8 +} +struct EFI_INPUT_KEY + +;; }}} + +macro os_code_section { + section '.text' code readable executable +} + +macro os_data_section { + section '.data' readable writable +} + +section '.text' code executable readable + +os_initialize: + ; At program startup, RDX contains an EFI_SYSTEM_TABLE*. + mov [system_table], rdx + ret + +os_print_string: + ;; We take an input string of bytes without any terminator. We need to turn + ;; this string into a string of words, terminated by a null character. + + mov rdi, .output_buffer ; Current location in output string + +.copy_byte: + ;; When there are no characters left in the input string, we are done. + cmp rdx, 0 + je .done + + ;; Load byte from input string + mov al, byte [rcx] + + ;; Copy byte to output string + + cmp al, $A + jne .not_newline +.newline: + ;; It's a newline; replace it with '\r\n' in output string. + mov byte [rdi], $D + inc rdi + mov byte [rdi], 0 + inc rdi + mov byte [rdi], $A + inc rdi + mov byte [rdi], 0 + inc rdi + jmp .pop + +.not_newline: + ;; Not a newline, proceed as normal: + mov byte [rdi], al + inc rdi + + ;; The output string has words rather than bytes for charactesr, so we need + ;; to add an extra zero: + mov byte [rdi], 0 + inc rdi + +.pop: + ;; We finished copying character to output string, so pop it from the input + ;; string. + inc rcx + dec rdx + + jmp .copy_byte +.done: + ;; Append a final null-word: + mov word [rdi], 0 + + ; At this point we have our null-terminated word-string at .output_buffer. Now + ; we just need to print it. + + mov rcx, [system_table] ; EFI_SYSTEM_TABLE* rcx + mov rcx, [rcx + EFI_SYSTEM_TABLE.ConOut] ; EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL* rcx + mov rdx, .output_buffer + mov rbx, [rcx + EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL.OutputString] ; EFI_TEXT_STRING rbx + sub rsp, 32 + call rbx + add rsp, 32 + ret + +os_read_char: +.read_key: + mov rcx, [system_table] ; EFI_SYSTEM_TABLE* rcx + mov rcx, [rcx + EFI_SYSTEM_TABLE.ConIn] ; EFI_SIMPLE_TEXT_INPUT_PROTOCOL* rcx + mov rbx, [rcx + EFI_SIMPLE_TEXT_INPUT_PROTOCOL.ReadKeyStroke] ; EFI_INPUT_READ_KEY rbx + mov rdx, input_key ; EFI_INPUT_KEY* rdx + sub rsp, 32 + call rbx + add rsp, 32 + + mov r8, EFI_NOT_READY + cmp rax, r8 + je .read_key + + movzx rax, word [input_key.UnicodeChar] + + ;; Special handling of enter (UEFI gives us '\r', but we want '\n'.) + cmp ax, $D + jne .no_enter + mov al, $A +.no_enter: + + push rax + ;; Print the character + mov [char_buffer], al + mov rcx, char_buffer + mov rdx, 1 + call os_print_string + pop rax + + ret + +;; Terminate with the given error code. +;; +;; Inputs: +;; - RCX = Error code +os_terminate: + mov rcx, terminated_msg + mov rdx, terminated_msg.len + call os_print_string + jmp $ + +section '.data' readable writable + +system_table dq ? ; EFI_SYSTEM_TABLE* + +terminated_msg db 0xD, 0xA, '(The program has terminated.)', 0xD, 0xA +.len = $ - terminated_msg + +os_print_string.output_buffer rq 0x400 + +char_buffer db ? + +input_key EFI_INPUT_KEY diff --git a/uefi.asm b/uefi.asm deleted file mode 100644 index d116b2c..0000000 --- a/uefi.asm +++ /dev/null @@ -1,192 +0,0 @@ -;; vim: syntax=fasm - -format pe64 dll efi -entry main - -;; EFI struct definitions {{{ - -EFI_NOT_READY = 0x8000_0000_0000_0000 or 6 - -;; Based on https://wiki.osdev.org/Uefi.inc -macro struct name { - virtual at 0 - name name - end virtual -} - -struc EFI_TABLE_HEADER { - dq ? - dd ? - dd ? - dd ? - dd ? -} - -struc EFI_SYSTEM_TABLE { - .Hdr EFI_TABLE_HEADER - .FirmwareVendor dq ? ; CHAR16* - .FirmwareRevision dd ? ; UINT32 - align 8 - .ConsoleInHandle dq ? ; EFI_HANDLE - .ConIn dq ? ; EFI_SIMPLE_TEXT_INPUT_PROTOCOL* - .ConsoleOutHandle dq ? ; EFI_HANDLE - .ConOut dq ? ; EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL* - ; ... -} -struct EFI_SYSTEM_TABLE - -struc EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL { - .Reset dq ? ; EFI_TEXT_RESET - .OutputString dq ? ; EFI_TEXT_STRING - ; ... -} -struct EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL - -struc EFI_SIMPLE_TEXT_INPUT_PROTOCOL { - .Reset dq ? ; EFI_INPUT_RESET - .ReadKeyStroke dq ? ; EFI_INPUT_READ_KEY - ; ... -} -struct EFI_SIMPLE_TEXT_INPUT_PROTOCOL - -struc EFI_INPUT_KEY { - .ScanCode dw ? ; UINT16 - .UnicodeChar dw ? ; CHAR16 - align 8 -} -struct EFI_INPUT_KEY - -;; }}} - -section '.text' code executable readable - -uefi_initialize: - ; At program startup, RDX contains an EFI_SYSTEM_TABLE*. - mov [system_table], rdx - ret - -;; Print a string of the given length. -;; -;; Inputs: -;; - RCX = String buffer -;; - RDX = String length -uefi_print_string: - ;; We take an input string of bytes without any terminator. We need to turn - ;; this string into a string of words, terminated by a null character. - - mov rdi, .output_buffer ; Current location in output string - -.copy_byte: - ;; When there are no characters left in the input string, we are done. - cmp rdx, 0 - je .done - - ;; Load byte from input string - mov al, byte [rcx] - - ;; Copy byte to output string - - cmp al, $A - jne .not_newline -.newline: - ;; It's a newline; replace it with '\r\n' in output string. - mov byte [rdi], $D - inc rdi - mov byte [rdi], 0 - inc rdi - mov byte [rdi], $A - inc rdi - mov byte [rdi], 0 - inc rdi - jmp .pop - -.not_newline: - ;; Not a newline, proceed as normal: - mov byte [rdi], al - inc rdi - - ;; The output string has words rather than bytes for charactesr, so we need - ;; to add an extra zero: - mov byte [rdi], 0 - inc rdi - -.pop: - ;; We finished copying character to output string, so pop it from the input - ;; string. - inc rcx - dec rdx - - jmp .copy_byte -.done: - ;; Append a final null-word: - mov word [rdi], 0 - - ; At this point we have our null-terminated word-string at .output_buffer. Now - ; we just need to print it. - - mov rcx, [system_table] ; EFI_SYSTEM_TABLE* rcx - mov rcx, [rcx + EFI_SYSTEM_TABLE.ConOut] ; EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL* rcx - mov rdx, .output_buffer - mov rbx, [rcx + EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL.OutputString] ; EFI_TEXT_STRING rbx - sub rsp, 32 - call rbx - add rsp, 32 - ret - -;; Read a character as an ASCII byte into the given buffer. -;; -;; Inputs: -;; - RCX = Character buffer (1 byte) -uefi_read_char: - mov r15, rcx -.read_key: - mov rcx, [system_table] ; EFI_SYSTEM_TABLE* rcx - mov rcx, [rcx + EFI_SYSTEM_TABLE.ConIn] ; EFI_SIMPLE_TEXT_INPUT_PROTOCOL* rcx - mov rbx, [rcx + EFI_SIMPLE_TEXT_INPUT_PROTOCOL.ReadKeyStroke] ; EFI_INPUT_READ_KEY rbx - mov rdx, input_key ; EFI_INPUT_KEY* rdx - sub rsp, 32 - call rbx - add rsp, 32 - - mov r8, EFI_NOT_READY - cmp rax, r8 - je .read_key - - mov ax, [input_key.UnicodeChar] - mov [r15], al - - ;; Special handling of enter (UEFI gives us '\r', but we want '\n'.) - cmp ax, $D - jne .no_enter - mov byte [r15], $A -.no_enter: - - ;; Print the character - mov rcx, r15 - mov rdx, 1 - call uefi_print_string - - ret - -;; Terminate with the given error code. -;; -;; Inputs: -;; - RCX = Error code -uefi_terminate: - mov rcx, terminated_msg - mov rdx, terminated_msg.len - call uefi_print_string - jmp $ - -section '.data' readable writable - -system_table dq ? ; EFI_SYSTEM_TABLE* - -terminated_msg db 0xD, 0xA, '(The program has terminated.)', 0xD, 0xA -.len = $ - terminated_msg - -uefi_print_string.output_buffer rq 0x400 - -char_buffer db ? - -input_key EFI_INPUT_KEY