From: Jonas Hvid Date: Mon, 9 Mar 2020 14:13:34 +0000 (+0100) Subject: Make it run on UEFI! X-Git-Url: https://git.rrq.au/?a=commitdiff_plain;h=55d4cc10bbfa7bc445948b222b6a72d2d4b127a2;p=rrq%2Fjonasforth.git Make it run on UEFI! The program is now usable, albeit barely, running inside QEMU, with the only dependency being UEFI! Very cool! Right now, no output is printed to the screen when you type characters, newlines aren't printed correctly, and the program doesn't accept Enter as valid whitespace when reading input. --- diff --git a/.gitignore b/.gitignore index 95811e0..5557c76 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -/main +/out +/OVMF_CODE.fd +/OVMF_VARS.fd diff --git a/Makefile b/Makefile index cb7bff3..ff474b0 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,23 @@ -.PHONY: run -run: main - cat sys.f - | ./main +.PHONY: qemu +qemu: out/main OVMF_CODE.fd OVMF_VARS.fd + # Based on https://wiki.osdev.org/UEFI#Emulation_with_QEMU_and_OVMF + qemu-system-x86_64 -cpu qemu64 \ + -drive if=pflash,format=raw,unit=0,file=OVMF_CODE.fd,readonly=on \ + -drive if=pflash,format=raw,unit=1,file=OVMF_VARS.fd \ + -net none \ + -drive format=raw,file=fat:rw:out \ + -display type=gtk,zoom-to-fit=on -main: main.asm impl.asm bootstrap.asm sys.f - fasm $< $@ +# Assuming 'ovmf' package on Arch Linux is installed. +OVMF_CODE.fd: /usr/share/ovmf/x64/OVMF_CODE.fd + cp $< $@ +OVMF_VARS.fd: /usr/share/ovmf/x64/OVMF_VARS.fd + cp $< $@ + +out/main: main.asm impl.asm bootstrap.asm sys.f uefi.asm + mkdir -p out + fasm $< out/main .PHONY: clean clean: - rm -f main + rm -rf out OVMF_CODE.fd OVMF_VARS.fd diff --git a/impl.asm b/impl.asm index 659d98e..ea6c04e 100644 --- a/impl.asm +++ b/impl.asm @@ -1,6 +1,6 @@ ;; vim: syntax=fasm -segment readable executable +section '.text' code readable executable macro printlen msg, len { push rsi @@ -231,7 +231,7 @@ parse_number: newline sys_terminate 100 -segment readable writable +section '.data' readable writable find.search_length dq ? find.search_buffer dq ? diff --git a/main.asm b/main.asm index 691e9f3..4ed478d 100644 --- a/main.asm +++ b/main.asm @@ -1,6 +1,6 @@ ;; vim: syntax=fasm -format ELF64 executable +include "uefi.asm" ;; "Syscalls" {{{ @@ -22,10 +22,15 @@ format ELF64 executable ;; ;; Clobbers: RAX, RCX, R11, RDI, RSI macro sys_print_string { - mov rax, 1 - mov rdi, 1 - mov rsi, rcx - syscall + 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. @@ -38,16 +43,25 @@ macro sys_print_string { ;; ;; Clobbers: RAX, RCX, R11, RDI, RSI, RDX macro sys_read_char { - mov rax, 0 - mov rdi, 0 - mov rdx, 1 - syscall + 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, $3C - mov rdi, code - syscall + mov rax, code + call uefi_terminate } ;; }}} @@ -109,9 +123,7 @@ macro forth_asm label, name, immediate { .start: } -segment readable executable - -entry main +section '.text' code readable executable include "impl.asm" ; Misc. subroutines include "bootstrap.asm" ; Forth words encoded in Assembly @@ -120,6 +132,8 @@ main: cld ; Clear direction flag so LODSQ does the right thing. mov rbp, return_stack_top ; Initialize return stack + call uefi_initialize + mov rax, MAIN jmp qword [rax] @@ -597,7 +611,7 @@ forth INPUT_LENGTH, 'INPUT-LENGTH' dq LIT, input_buffer_length dq EXIT -segment readable writable +section '.data' readable writable ;; 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 @@ -637,8 +651,6 @@ here_top rq $4000 rq $2000 return_stack_top: -segment readable - ;; 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. diff --git a/uefi.asm b/uefi.asm new file mode 100644 index 0000000..6584315 --- /dev/null +++ b/uefi.asm @@ -0,0 +1,165 @@ +;; 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 +;; +;; [TODO] Handle newlines correctly. (I.e. translate '\n' to '\r\n'.) +uefi_print_string: + mov r8, rcx + mov r9, rdx + + mov r10, r9 + add r10, r10 + + ; 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 rcx, 0 + mov rsi, 0 +.copy_byte: + cmp rcx, r10 + je .done + + mov al, byte [r8 + rsi] + lea rdx, [.output_buffer + rcx] + mov byte [rdx], al + inc rcx + inc rsi + + lea rdx, [.output_buffer + rcx] + mov byte [rdx], 0 + inc rcx + + jmp .copy_byte +.done: + lea rdx, [.output_buffer + r10] + mov byte [rdx], 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) +;; +;; [TODO] Show the user's input on screen while they are typing. +;; [TODO] Handle enter key correctly (should return '\n'). +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 + + 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 diff --git a/uefi/.gitignore b/uefi/.gitignore deleted file mode 100644 index 5557c76..0000000 --- a/uefi/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/out -/OVMF_CODE.fd -/OVMF_VARS.fd diff --git a/uefi/Makefile b/uefi/Makefile deleted file mode 100644 index aeff045..0000000 --- a/uefi/Makefile +++ /dev/null @@ -1,27 +0,0 @@ -.PHONY: qemu -qemu: out/main out/hello OVMF_CODE.fd OVMF_VARS.fd - # Based on https://wiki.osdev.org/UEFI#Emulation_with_QEMU_and_OVMF - qemu-system-x86_64 -cpu qemu64 \ - -drive if=pflash,format=raw,unit=0,file=OVMF_CODE.fd,readonly=on \ - -drive if=pflash,format=raw,unit=1,file=OVMF_VARS.fd \ - -net none \ - -drive format=raw,file=fat:rw:out \ - -display type=gtk,zoom-to-fit=on - -# Assuming 'ovmf' package on Arch Linux is installed. -OVMF_CODE.fd: /usr/share/ovmf/x64/OVMF_CODE.fd - cp $< $@ -OVMF_VARS.fd: /usr/share/ovmf/x64/OVMF_VARS.fd - cp $< $@ - -out/main: main.asm - mkdir -p out - fasm $< out/main - -out/hello: - mkdir -p out - echo -e "Hello!\nThis is a test." > out/hello - -.PHONY: clean -clean: - rm -f main diff --git a/uefi/main.asm b/uefi/main.asm deleted file mode 100644 index 431a64c..0000000 --- a/uefi/main.asm +++ /dev/null @@ -1,180 +0,0 @@ -;; vim: syntax=fasm - -format pe64 dll efi -entry main - -;; [TODO] We need to provide the following: -;; - [X] Print a string of a given length -;; - [ ] Print a single character -;; - [ ] Terminate the program (? - What should this do?) -;; - [X] Read a single character -;; - This should allow the user to type in a string, and then feed the -;; buffer to us one character at a time. -;; - [ ] We want to show the user's input on the screen while reading -;; - [ ] Read a file that was bundled with the program -;; - It looks like we can use EFI_LOAD_FILE_PROTOCOL.LoadFile() to load -;; a file into a buffer. In order to be able to use this, we need to -;; have some way of interpreting a static buffer instead of reading as -;; we go. - -;; 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 - -main: - ; At program startup, RDX contains an EFI_SYSTEM_TABLE*. - mov [system_table], rdx - - mov rcx, hello_string - mov rdx, hello_string.len - call print_string - - mov rcx, char_buffer - call read_char - - mov rcx, char_buffer - mov rdx, 1 - call print_string - - mov rcx, hello_string - mov rdx, hello_string.len - call print_string - - ret - -;; Print a string of the given length. -;; -;; Inputs: -;; - RCX = String buffer -;; - RDX = String length -print_string: - mov r8, rcx - mov r9, rdx - - mov r10, r9 - add r10, r10 - - ; 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 rcx, 0 - mov rsi, 0 -.copy_byte: - cmp rcx, r10 - je .done - - mov al, byte [r8 + rsi] - lea rdx, [.output_buffer + rcx] - mov byte [rdx], al - inc rcx - inc rsi - - lea rdx, [.output_buffer + rcx] - mov byte [rdx], 0 - inc rcx - - jmp .copy_byte -.done: - lea rdx, [.output_buffer + r10] - mov byte [rdx], 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) -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 - - ret - -section '.data' readable writable - -system_table dq ? ; EFI_SYSTEM_TABLE* - -hello_string db 'Hello, world!', 0xD, 0xA, 'Here is some more text.', 0xD, 0xA -.len = $ - hello_string - -print_string.output_buffer rq 0x400 - -char_buffer db ? - -input_key EFI_INPUT_KEY