Make it run on UEFI!
authorJonas Hvid <mail@johv.dk>
Mon, 9 Mar 2020 14:13:34 +0000 (15:13 +0100)
committerJonas Hvid <mail@johv.dk>
Mon, 9 Mar 2020 14:13:34 +0000 (15:13 +0100)
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.

.gitignore
Makefile
impl.asm
main.asm
uefi.asm [new file with mode: 0644]
uefi/.gitignore [deleted file]
uefi/Makefile [deleted file]
uefi/main.asm [deleted file]

index 95811e0016a4979f9518a1ad8e6bfd8e3482e95f..5557c762e3873e91cb01d4cca24df6bcc8110247 100644 (file)
@@ -1 +1,3 @@
-/main
+/out
+/OVMF_CODE.fd
+/OVMF_VARS.fd
index cb7bff3854cd40502f74373033635311c96e5f55..ff474b0b073405bea8aa8a32171a7cdda2fbd61a 100644 (file)
--- 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
index 659d98ed7258b90b82e7a74061b6364948c15a8a..ea6c04ebd42155a2dd5f495ac57874308ad2fbe4 100644 (file)
--- 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 ?
index 691e9f363d352fc95f351beabb2a30f05b14615a..4ed478d0743f7f75d584f2632cc377e454fa98f8 100644 (file)
--- 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 (file)
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 (file)
index 5557c76..0000000
+++ /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 (file)
index aeff045..0000000
+++ /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 (file)
index 431a64c..0000000
+++ /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