├── .gitattributes ├── .github └── workflows │ └── lisp.yaml ├── .gitignore ├── LICENSE ├── README.md ├── boot ├── boot.bat ├── bootloader ├── bootsect-footer.asm ├── bootsect-header.asm └── bootsect.asm ├── bootstrap-hex.asm ├── bootstrap-lisp.asm ├── debug ├── images └── v0.4-hex-2x.gif ├── keyboard-layouts └── dvorak.asm ├── lisp ├── data-for-test.lisp ├── lisp.asm └── tester.asm ├── reference_manuals ├── add_bookmarks_to_bios_manual.tex └── get_docs ├── test └── text-editor.asm /.gitattributes: -------------------------------------------------------------------------------- 1 | *.bat eol=crlf 2 | -------------------------------------------------------------------------------- /.github/workflows/lisp.yaml: -------------------------------------------------------------------------------- 1 | name: lisp 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | checks: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Set up dependencies 17 | run: sudo apt update && sudo apt install -y qemu-system-x86 nasm 18 | 19 | - name: Test 20 | run: script -q -e -c "./test -DHEADLESS lisp/tester.asm" 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | reference_manuals/*.pdf 3 | gdbconf/ 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2020 Andy Kallmeyer 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the “Software”), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons 7 | to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or 10 | substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 13 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 14 | PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 15 | FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 17 | DEALINGS IN THE SOFTWARE. 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bootstrap-os 2 | 3 | A bootable x86 code editor that lets you run the code you type in directly on 4 | real hardware. Enough to bootstrap anything with no other tools. 5 | 6 | ![bootstrap-hex demo](images/v0.4-hex-2x.gif) 7 | 8 | Above is v0.4 (bootstrap-hex.asm), the latest release. Technically it is all you 9 | need to create anything you want. For the full bootstrapping experience you'd 10 | have to write your assembly on paper and hand-assemble it before typing it in 11 | carefully. 12 | 13 | Since you'd probably write a text editor and an assembler first thing, I think 14 | it makes sense to just start there. I'm 15 | [currently working on](https://github.com/fsmv/bootstrap-os/tree/assembler) 16 | exactly that. 17 | 18 | ## Compatibility 19 | 20 | This code should run correctly on any x86 PC compatible machine made between 21 | 1984 and 2020. I believe the earilest machine that will run it is the IBM PC AT 22 | with the EGA card expansion released in October 1984. 23 | 24 | If it doesn't boot on your machine that supports BIOS, please let me know. I'd 25 | love to find out why. 26 | 27 | ### Compatibility details 28 | 29 | This code is 16 bit real mode x86 assembly which Intel has kept available on all 30 | x86 CPUs since the original 8086 processor from 1976. The assembly code itself 31 | is compatible with the 8086. 32 | 33 | It also depends on the IBM PC BIOS hardware interface standard started in 1981 34 | which modern computers still implement. Sadly Intel has partially ended this 35 | incredible nealy 40 year backwards compatibility story by officially 36 | [ending support for BIOS](https://www.bleepingcomputer.com/news/hardware/intel-plans-to-end-legacy-bios-support-by-2020/) 37 | as of 2020, so now there are some machines on the market that only support UEFI 38 | booting. 39 | 40 | The latest BIOS call used here is int 0x10 with ah = 0x13 for printing a string. 41 | This was introduced with PC AT although the earlier XT machines got this feature 42 | later with a BIOS update. Additionally it uses BIOS functions to set the 43 | overscan color and blink bit behavior which where added with the EGA card, so it 44 | is technically not compatible with the CGA card although it will mostly work. 45 | 46 | If we had a replacement for BIOS print string based on the print character 47 | interrupts, technically it could be compatible with the original PC from 1981. 48 | However the lisp interpreter code is more than the 8 sectors per track the PC 49 | supported and the bootsector code expects to have multi-track read support which 50 | the PC didn't have originially (maybe you could get a BIOS that does have it). 51 | 52 | Additionally the text editor requires 256k of RAM which was the maximum possible 53 | PC configuration. 54 | 55 | ## Why? 56 | 57 | Mainly I'm jealous of the people who grew up with computers that booted to a 58 | basic editor and had computer magazines with code to type in. 59 | 60 | I hope people will use this to learn more about the computers we use at a low 61 | level and have fun running real assembly directly on the CPU with no other code 62 | involved. Also I hope that some college classes might try teaching assembly with 63 | real x86 using this instead of using a mips simulator (because I for one have 64 | never seen a mips CPU). 65 | 66 | I think modern software has gotten too far from the hardware. Also I thought it 67 | would be fun to escape 68 | [The 30 Million Line Problem](https://www.youtube.com/watch?v=kZRE7HIO3vk) 69 | for a while. 70 | 71 | ## Booting it 72 | 73 | ### On Real Hardware 74 | 75 | 1. Get the binary from the releases or use the boot script (assemble it with 76 | `nasm`) 77 | 2. Write it to a usb drive: `dd if=bin/bootstrap-hex.bin of=/dev/sdb && sync` 78 | - Warning: make sure to change the output to the right path for your system. 79 | Use `fdisk -l` to list them. 80 | - You can backup the first couple sectors if you don't want to reformat the 81 | drive later with: `dd if=/dev/sdb of=sdb-10-sectors.bak bs=512 count=10` 82 | 3. Keep the USB plugged in and boot it on your computer in BIOS mode by pressing 83 | F2 or F11 during startup to get the system menu (also USB booting sometimes 84 | needs to be enabled in the setup menu) 85 | 86 | ### In QEMU 87 | 88 | #### Linux / MacOS 89 | 90 | 1. Install `qemu` and `nasm` with your package manager of choice. 91 | 2. `./boot bootstrap-lisp.asm` 92 | 93 | #### Debugging on Linux / MacOS 94 | 95 | You can also run `./debug bootstrap-lisp.asm` to attach gdb to qemu with a 96 | special config for 16 bit real mode and it has symbols properly loaded and 97 | everything. 98 | 99 | In `gdbconf/gdbinit_real_mode.txt` several new gdb commands are defined. The 100 | most important is `stepo` which steps over a call instruction skipping the body of 101 | the call, but you can open up that file and try out the other commands too. 102 | 103 | #### Windows 104 | 105 | 1. Install qemu: https://www.qemu.org/download/#windows 106 | 2. Install nasm: https://www.nasm.us/pub/nasm/snapshots/latest/win64/ 107 | 3. Set the PATH variable: 108 | 1. In the start menu, search for and open the environment variables editor 109 | 2. Choose either the user or system-wide Path variable and add both `C:\Program Files\NASM` & `C:\Program Files\qemu` 110 | 4. `boot.bat bootstrap-lisp.asm` (alternatively you can use the bash script in the git bash prompt) 111 | 112 | #### Android 113 | 114 | 1. Install [Termux](https://play.google.com/store/apps/details/id=com.termux) 115 | from the play store (or f-droid) 116 | 117 | 2. Install qemu and the assembler, and git to clone the repo 118 | `pkg install x11-repo && pkg install qemu-system-x86-64 nasm git` 119 | 120 | 3. (Optional) Set up X11 for graphical display 121 | 122 | 1. Install the Termux X11 apk from 123 | https://github.com/termux/termux-x11/releases (you most likely need the 124 | arm64 one), or from f-droid 125 | 2. In the Termux X11 app settings configure the output display resolution 126 | mode to custom and the resolution to 720x480 127 | 3. Install the necessary packages in the main Termux app: 128 | `pkg install termux-x11-nightly` 129 | 130 | 4. Clone bootstrap-os `git clone https://github.com/fsmv/bootstrap-os.git && cd bootstrap-os` 131 | 132 | 5. To run in Termux: 133 | 1. `export QEMU_ARGS="-display gtk,show-menubar=off -L /data/data/com.termux/files/usr/share/qemu"` 134 | If you like you can put this in `~/.bashrc` to save it 135 | 2. `termux-x11 :0 -xstartup "./boot bootstrap-lisp.asm"` 136 | - Alternatively (if you didn't install the X11 app) you can run: 137 | `./test -DHEADLESS lisp/tester.asm` 138 | it will print the test results using the qemu debug console feature. 139 | 140 | I tried to run the debugger but I wasn't able to install a version of gdb that 141 | understands x86 in Termux (it only does arm) so debugging won't work. 142 | 143 | ## Reference Manuals 144 | 145 | This project depends on the 146 | [Intel Architecture Manual](https://software.intel.com/sites/default/files/managed/39/c5/325462-sdm-vol-1-2abcd-3abcd.pdf) 147 | and the [1988 IBM BIOS Manual](http://bitsavers.trailing-edge.com/pdf/ibm/pc/ps2/15F0306_PS2_and_PC_BIOS_Interface_Technical_Reference_May88.pdf). 148 | There's also the [1992 IBM VGA Card Manual](http://bitsavers.trailing-edge.com/pdf/ibm/pc/cards/IBM_VGA_XGA_Technical_Reference_Manual_May92.pdf) 149 | which isn't really used in this project so far but would be useful for anyone 150 | who wants to do graphics programming after BIOS booting. 151 | 152 | Modern x86 computers still implement these standards and it makes me happy to 153 | use the original documentation. Unfortunately to actually boot on modern 154 | computers you have to comply with some manufacturer expectations added over the 155 | years which I learned about on https://wiki.osdev.org/ 156 | 157 | ### Script Instructions 158 | 159 | `reference_manuals/get_docs` is script Bash users can run to download these 160 | manuals and also optionally adds bookmarks to the IBM BIOS manual (which I 161 | manually typed out) using `pdflatex` (you'll need to install that to add the 162 | bookmarks). 163 | 164 | Windows users could run it in WSL or copy the pdflatex commands out of the 165 | script. 166 | 167 | ## Make Your Own Bootable Code 168 | 169 | You can simply `%include` the `util/bootsect-header.asm` at the top of your 170 | bootsector code and `util/bootsect-footer.asm` at the bottom. Feel free to 171 | customize it and check out the comments for details. These two files contain all 172 | the details to allow you to BIOS boot on real hardware. 173 | 174 | Once you go over the single sector size nasm will give you an error. At that 175 | point you'll have to put any additional code after `util/bootsect-footer.asm` 176 | and write some code inside the bootsector to load any additional sectors from 177 | disk (since the BIOS won't do it for you). Check out the bootsector code I'm 178 | using for an example. 179 | -------------------------------------------------------------------------------- /boot: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$#" -eq 0 ]; then 4 | printf "Usage: $0 [filename] [any other nasm args].\nEx: $0 bootstrap-asm.asm -DDVORAK\n" 5 | exit 1 6 | fi 7 | 8 | file=${@: -1} 9 | name="${file%%.*}" 10 | binfile="bin/$name.bin" 11 | 12 | mkdir -p `dirname $binfile` 13 | nasm $file -f bin -o $binfile ${@:1:$(($#-1))} && qemu-system-x86_64 $QEMU_ARGS -drive file=$binfile,format=raw,if=ide 14 | -------------------------------------------------------------------------------- /boot.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | set file=%~1 3 | set name=%~n1 4 | set binfile=bin\%name%.bin 5 | 6 | mkdir bin 2> NUL 7 | nasm %file% -f bin -o %binfile% && qemu-system-x86_64 -drive file=%binfile%,format=raw,if=ide -------------------------------------------------------------------------------- /bootloader/bootsect-footer.asm: -------------------------------------------------------------------------------- 1 | ; Provided under the MIT License: http://mit-license.org/ 2 | ; Copyright (c) 2020 Andy Kallmeyer 3 | 4 | ; The subtractions account for the data below this line, since the zeros need to 5 | ; be above those consntants. The $-$$ is the size of whatever is above this line 6 | times SECTOR_SIZE-4-2-0x40-2-($-$$) db 0 ; Pad with zeros up to 512 bytes 7 | dd 0xDEADC0DE ; Windows reserved unique drive number 8 | dw 0x0000 ; Just extra padding 9 | times 0x40 db 0 ; Empty partition table 10 | dw 0xAA55 ; BIOS bootable drive magic number 11 | -------------------------------------------------------------------------------- /bootloader/bootsect-header.asm: -------------------------------------------------------------------------------- 1 | ; Provided under the MIT License: http://mit-license.org/ 2 | ; Copyright (c) 2020 Andy Kallmeyer 3 | 4 | ; All the voodoo required to boot on modern hardware. Mostly it is spaces and 5 | ; magic numbers for compatibilty with various extensions to BIOS that became 6 | ; de-facto standard over time. 7 | ; 8 | ; A lot of this came from https://wiki.osdev.org/Problems_Booting_From_USB_Flash 9 | 10 | ; Set the stack to the ~30k area between the reserved BIOS Data Area and the 11 | ; automatically loaded bootsector segment. i.e. between 0x500 and 0x7C00. 12 | ; 13 | ; The stack pointer is set to the highest available address so you know when the 14 | ; stack is out of space when sp is zero. 15 | ; 16 | ; If you change this you also need to change how sp is set. I expect if you're 17 | ; changing this you're not putting the ss between the BDA and the CODE_SEGMENT 18 | ; so it's not possible to write code below that automatically sets sp correctly 19 | ; if you change this. If you have a whole 64k segment free set sp to 0xFFFF. 20 | %define STACK_SEGMENT 0x050 21 | ; This is where the BIOS always loads the code, you can't change that. 22 | ; 23 | ; In bootsect.asm there's code to load additional sectors from disk immediately 24 | ; after this first one in memory that is always loaded. 25 | ; 26 | ; This bootsect-header.asm file contains code that set the cs register to this 27 | ; value so that you can write your code at org 0. So technically you can change 28 | ; this constant because this is just what we set the cs register to. 29 | %define CODE_SEGMENT 0x7C0 30 | ; The distance between non-overlapping segment in the segment register address 31 | ; space (i.e. it's 0x10000 bytes but you only add 0x1000 to the segment register) 32 | %define NEXT_SEGMENT 0x1000 33 | ; The last segment in the unmapped memary block after 0x7C0 that can be used 34 | ; with the full 16bit address range (0x7FFFF is the last safe address before the 35 | ; Extended BIOS Data Area). 36 | ; 37 | ; If you set a segment register higher than this value you might overflow into 38 | ; the reserved memory areas. 39 | ; 40 | ; See: https://wiki.osdev.org/Memory_Map_(x86) 41 | %define LAST_SEGMENT 0x7000 42 | ; The last valid sp value given that we set ss to 0x0050 43 | ; 44 | ; This puts the stack in the ~30k area of free memory between the BIOS data and 45 | ; the start of this code (0x7C00) 46 | %define MAX_STACK_POINTER 0x2BFF 47 | 48 | ; The size of a hard drive sector in bytes 49 | %define SECTOR_SIZE 0x200 ; 512 in decimal 50 | 51 | ; Calculate the number of sectors a given span of code takes up from the passed 52 | ; in label to the line NUM_SECTORS is applied on. This is useful for calculating 53 | ; how much code to load off disk in your bootloader. 54 | ; 55 | ; The -1 is because we don't want the next sector until we have 512+1 bytes 56 | ; e.g. for exactly 512 bytes we want 1 extra sector not 2. 57 | ; The +1 is because int division does floor() and we want ceil(). 58 | %define NUM_SECTORS(label) (($-label-1)/SECTOR_SIZE + 1) 59 | 60 | ; Jump past the BIOS Parameter block, which is needed for USB boots 61 | ; 62 | ; Apparently some BIOS implementations check for this instruction, so I think 63 | ; we can't just set the cs register here right away 64 | jmp start 65 | nop ; Take up the remaning byte before the BIOS parameter block 66 | 67 | ; The BIOS parameter block. This stores information about the floppy disk the 68 | ; code is on. In USB boots like we're set up for, the BIOS just overwrites this 69 | ; section in memory with the right data. 70 | db ' ASK ',0 ; OEM Name (8 bytes). I've seen 'MSDOS5.0' here in some bootloaders. 71 | ; Space for the BPB 72 | times 0x33 db 0 73 | start: 74 | 75 | ; Set the code segment to 0x7C0, optional if there's no absolute jumps 76 | ; The bootsector code is always loaded at 0x7C00 by the BIOS 77 | ; 78 | ; This has never changed and is documented in the BIOS manual under int 19H 79 | jmp CODE_SEGMENT:start2 80 | start2: 81 | 82 | ; Set up the stack 83 | mov ax, STACK_SEGMENT ; It is not physically possible to set ss directly (on the 8086) 84 | mov ss, ax ; Stack top at 0x0500, the start of the 30k block after the BIOS Data Area 85 | mov sp, 0x7BFF-0x500 ; ~30K stack, push subtracts from this (torwards ss). 0x7BFF start (right before the CODE_SEGMENT) 86 | mov bp, sp 87 | 88 | ; Set the data segment (and es) start at 0x7C00 so you can write your assembly at org 0 89 | mov ax, CODE_SEGMENT 90 | mov ds, ax 91 | mov es, ax 92 | 93 | cld ; Set the direction flag to forward (for string instructions) 94 | 95 | ; Must not overwrite dl which is the boot drive number 96 | -------------------------------------------------------------------------------- /bootloader/bootsect.asm: -------------------------------------------------------------------------------- 1 | ; This assembles to a 512 byte BIOS bootloader sector that works on real 2 | ; hardware as long as it has an x86 CPU with BIOS support. 3 | ; 4 | ; Loads NUM_EXTRA_SECTORS of code after the bootsector on disk into memory 5 | ; directly after the bootsector code. Also sets the video mode to some nice 6 | ; text mode defaults and sets up the stack and other segment registers. 7 | ; 8 | ; After that it JMPs to _start 9 | 10 | ; To use this: 11 | ; 12 | ; 1. Include this bootloader/bootsect.asm file at the top of your code before 13 | ; any other bytes (this file includes the header and footer) 14 | ; 2. Put a label at the top right below the include of this file Ex: 15 | ; extra_sectors_start: 16 | ; 3. Paste at the bottom after all of your application code is written/included 17 | ; 18 | ; NUM_EXTRA_SECTORS: equ NUM_SECTORS(extra_sectors_start) 19 | ; 20 | ; You can write all of your code with org 0 (the default) since we set cs so 21 | ; that the bootloader code and then your code will start at 0. 22 | ; 23 | ; This will only work up to 64k of code. Once you're beyond that size you will 24 | ; need to set the cs register correctly as you go because it doesn't all fit 25 | ; within the 16 bit address space if you keep cs fixed. You would have to 26 | ; separate your code into 64k sized modules so internal references work and move 27 | ; cs as needed to call the different modules, or implement some other kind of 28 | ; dynamic loading of chunks of code system. 29 | ; 30 | ; So NUM_EXTRA_SECTORS can be at most 0x7F. I can't add a %if %error %endif 31 | ; check here because nasm requires that to go after NUM_EXTRA_SECTORS is 32 | ; defined. 33 | 34 | ; NOTE: The write string BIOS call only works for: 35 | ; PC XT BIOS dated 1/10/86 and after, AT, EGA, PC Convertible, 36 | ; and Personal System/2 products 37 | %define BIOS_PRINT_STRING 0x13 38 | %define BIOS_PRINT_CHAR 0x0E ; Works on the original PC 39 | %define QEMU_DEBUG_PORT 0xE9 40 | 41 | %include "bootloader/bootsect-header.asm" 42 | 43 | mov [BOOT_DISK], dl ; Save the boot disk number 44 | 45 | ; NOTE: the PC with CGA card has int 0x10 ah = 0 to 0xF the rest are ignored 46 | 47 | ; TODO: Do some sort of video mode detection. Error if only mono is supported 48 | ; unless it turns out the attribute bytes are somehow compatible. 49 | ; It may not be necessary to do the full detection procedure although it would 50 | ; be nice to know if we have EGA support at least. 51 | 52 | ; Set video mode, 16 color 80x25 chars 53 | ; This is supported by CGA cards and PCjr 54 | ; 55 | ; The IBM BIOS manual describes a long procedure for determining which video 56 | ; modes are supported and all the possible options for supporting both mono and 57 | ; color. Sometimes mono isn't supported if the PC supports color modes. So, I'm 58 | ; just going to assume all hardware this is used on supports color modes. 59 | mov ax, 0x0003 60 | int 0x10 61 | 62 | ; Make the 8th bit of the colors bg-intensity instead of blink 63 | ; This works on EGA and VGA 64 | ; TODO: support doing the out port address for CGA and detect CGA 65 | mov ax, 0x1003 66 | mov bl, 0 67 | int 0x10 68 | 69 | ; Make the 4th bit of the colors fg-intensity instead of font selection 70 | ; Use block 0 for the font. 71 | ; This means we only have 256 characters instead of 512. 72 | ; Apparently this is the default in EGA and VGA so maybe we don't need it 73 | mov ax, 0x1103 74 | mov bl, 0 75 | int 0x10 76 | 77 | ; Color byte is now: bg-intensity,bg-r,bg-g,bg-b ; fg-intensity,fg-r,fg-g,fg-b 78 | 79 | ; Set cursor type to an underline 80 | mov ax, 0x0100 81 | mov cx, 0x0607 82 | int 0x10 83 | 84 | ; Set the keyboard repeat speed 85 | ; For AT BIOS dated 11/15/85 and after, PC XT Model 286, and Personal System/2 products 86 | ; The original PC ignores this 87 | mov ax, 0x0305 88 | ;mov bx, 0x0107 ; 500 ms delay before repeat ; 16 characters per second 89 | mov bx, 0x0100 ; 500 ms delay before repeat ; 30 characters per second 90 | int 0x16 91 | ; fallthrough 92 | 93 | load_extra_sectors: 94 | ; int 0x13 shouldn't be called with 0 for the amount to read. 95 | ; Can't use preprocessor for this unfortunately because it's a label 96 | mov al, NUM_EXTRA_SECTORS ; number of sectors to read 97 | test al, al 98 | je start_ 99 | 100 | ; TODO: do the proper thing and detect the max sectors per track and don't 101 | ; assume multi-track read support. 102 | 103 | ; Load the code from the extra sectors 104 | ; Try to do the full read every time to take advantage of possible BIOS support 105 | ; for multi-track reads if NUM_EXTRA_SECTORS > 63 106 | ; 107 | ; This worked on the original PC, but the max sectors to read was 8 (max track was 39, head 1) 108 | mov ah, 0x02 109 | mov bx, SECTOR_SIZE ; es:bx is address to write to. es = cs, so write directly after the boot sector 110 | mov dl, [BOOT_DISK] ; Drive number 111 | xor dh, dh ; Head number 112 | mov cx, 0x0002 ; Read from Cylinder (track) 0; Sector 2 (1 is the boot sector) 113 | int 0x13 114 | 115 | jnc start_ ; if there was no error, jump to the loaded user code 116 | ; Otherwise handle errors 117 | 118 | push ax ; push the error code 119 | 120 | ; TODO: maybe make a print_string function that can also write to QEMU 121 | ; maybe I could even have a fallback for if ah = 13 isn't available, but I'm not 122 | ; totally sure how to detect. Maybe just always replace it for CGA cards. 123 | 124 | ; Print the error message 125 | mov ah, BIOS_PRINT_STRING 126 | mov al, 1 ; no attr bytes, move cursor 127 | mov bp, disk_error_msg ; String pointer in es:bp (es is at code start from bootsect-header.asm) 128 | mov cx, disk_error_msg_len ; String length 129 | xor dx, dx ; top left 130 | mov bx, 0x004F ; bh = 0 (page number); bl = color (white on red) 131 | int 0x10 132 | 133 | pop cx ; pop the error code 134 | call print_hex ; print the error code 135 | 136 | mov ah, BIOS_PRINT_STRING 137 | mov al, 1 ; no attr bytes, move cursor 138 | mov bp, retry_msg ; String pointer in es:bp (es is at code start from bootsect-header.asm) 139 | mov cx, retry_msg_len ; String length 140 | mov dx, 0x0100 ; second line; left edge 141 | mov bx, 0x004F ; bh = 0 (page number); bl = color (white on red) 142 | int 0x10 143 | 144 | mov cl, [READ_RETRIES] 145 | call print_hex 146 | 147 | cmp byte [READ_RETRIES], 0 148 | je .no_more_tries 149 | 150 | ; Reset the disk and retry 151 | ; Works on the original PC 152 | dec byte [READ_RETRIES] 153 | xor ax, ax 154 | mov dl, [BOOT_DISK] ; Drive number 155 | int 0x13 156 | jmp load_extra_sectors 157 | 158 | .no_more_tries: 159 | jmp $ ; stop forever 160 | 161 | ; Prints the ascii hex character which represents the integer value of al 162 | ; Only accepts 0x0 <= al <= 0xF, anything else is garbage output 163 | ; e.g. al = 12 prints "C" 164 | ; clobbers ax, and bx 165 | ; Works on the original PC 166 | print_hex_char: 167 | mov ah, BIOS_PRINT_CHAR ; Scrolling teletype BIOS routine (used with int 0x10) 168 | xor bx, bx ; Clear bx. bh = page, bl = color 169 | 170 | cmp al, 9 171 | ja .over_9 172 | 173 | add al, '0' 174 | %ifdef DEBUGCON 175 | out QEMU_DEBUG_PORT, al 176 | %endif 177 | int 0x10 178 | ret 179 | 180 | .over_9: 181 | sub al, 10 182 | add al, 'A' 183 | %ifdef DEBUGCON 184 | out QEMU_DEBUG_PORT, al 185 | %endif 186 | int 0x10 187 | ret 188 | 189 | ; cx = two bytes to write at current cursor 190 | ; clobbers ax, and bx 191 | ; Works on the original PC 192 | print_hex: 193 | ; Nibble 0 (most significant) 194 | mov al, ch 195 | shr al, 4 196 | call print_hex_char 197 | ; Nibble 1 198 | mov al, ch 199 | and al, 0x0F 200 | call print_hex_char 201 | ; Nibble 2 202 | mov al, cl 203 | shr al, 4 204 | call print_hex_char 205 | ; Nibble 3 206 | mov al, cl 207 | and al, 0x0F 208 | call print_hex_char 209 | 210 | ret 211 | 212 | ; cl = byte to write at current cursor 213 | ; clobbers ax, and bx 214 | print_hex_byte: 215 | ; Nibble 1 216 | mov al, cl 217 | shr al, 4 218 | call print_hex_char 219 | ; Nibble 2 220 | mov al, cl 221 | and al, 0x0F 222 | call print_hex_char 223 | 224 | ret 225 | 226 | ; === Bootsector data area === 227 | 228 | disk_error_msg: db `Error reading from disk: ` 229 | disk_error_msg_len: equ $-disk_error_msg 230 | retry_msg: db `Retry attempts remaining: ` 231 | retry_msg_len: equ $-retry_msg 232 | 233 | BOOT_DISK: db 0x00 ; value is filled first thing 234 | READ_RETRIES: db 5 235 | 236 | %include "bootloader/bootsect-footer.asm" 237 | -------------------------------------------------------------------------------- /bootstrap-hex.asm: -------------------------------------------------------------------------------- 1 | ; Provided under the MIT License: http://mit-license.org/ 2 | ; Copyright (c) 2020 Andy Kallmeyer 3 | %define SECTOR_SIZE 0x200 4 | 5 | ; Segment register value (so the actual start is at the address 0x10*this) 6 | ; This is the first sector after the editor's code 7 | %define USER_CODE_LOC (CODE_SEGMENT+(SECTOR_SIZE/0x10)*(NUM_EXTRA_SECTORS+1)) 8 | ; Max value for di and si in typing_loop (i.e. the user code buffer) 9 | ; 10 | ; There's a lot of extra memory still after this but I don't want to move around 11 | ; the segment register so this is the limit. 12 | ; 13 | ; Allows 64k of code 14 | %define USER_CODE_MAX 0xFFFF 15 | 16 | ; Values in ax after the keyboard read BIOS call 17 | ; See Figure 4-3 of the 1987 BIOS manual. (page 195) 18 | %define LEFT_ARROW 0x4B00 19 | %define RIGHT_ARROW 0x4D00 20 | %define EOT 0x2004 ; Ctrl+D 21 | 22 | %define MAIN_COLOR 0x17 23 | %define BORDER_COLOR 0x91 24 | 25 | %define MAIN_TOP_LEFT 0x0210 ; row = 2, col = 16 26 | %define MAIN_BOTTOM_RIGHT 0x163F ; row = 22, col = 79-16 27 | %define START_ROW 0x02 28 | %define START_COL 0x10 29 | %define END_ROW 0x16 30 | %define END_COL 0x3F 31 | 32 | %define LINE_NUM_TOP_LEFT 0x020B ; row = 2, col = 11 33 | %define LINE_NUM_BOTTOM_RIGHT 0x160F ; row = 22, col = 15 34 | %define LINE_NUM_COL 0x0B 35 | 36 | ; Bitfield values for ch in typing_loop 37 | %define CURSOR_ON_SECOND_NIBBLE 0b01 38 | %define SAVED_NIBBLE_AT_END 0b10 39 | 40 | %include "bootloader/bootsect.asm" 41 | 42 | ; This is also the start for calculating NUM_EXTRTA_SECTORS 43 | ; There must be nothing other than the bootsector above this label 44 | start_: 45 | 46 | ; Set the border color (by clearing the whole screen) 47 | mov ax, 0x0600 48 | xor cx, cx ; row = 0, col = 0 49 | mov dx, 0x184F ; row = 24, col = 79 50 | mov bh, BORDER_COLOR 51 | int 0x10 52 | 53 | ; Set the background color (by clearing just the middle) 54 | ;mov ax, 0x0600 55 | mov cx, MAIN_TOP_LEFT 56 | mov dx, MAIN_BOTTOM_RIGHT 57 | mov bh, MAIN_COLOR 58 | int 0x10 59 | 60 | ; Print the starting greeting 61 | mov ax, 0x1301 ; Write String, move cursor mode in al 62 | mov bx, BORDER_COLOR ; bh = 0 (page number); bl = color 63 | mov bp, greeting ; String pointer in es:bp (es is at code start from bootsect-header.asm) 64 | mov cx, greeting_len ; Streng length 65 | mov dx, 0x0010 ; row = 0, col = 16 66 | int 0x10 67 | 68 | ; Print the row header (column byte numbers) 69 | mov bp, row_header ; String pointer in es:bp 70 | mov cx, row_header_len ; Streng length 71 | mov dx, 0x0110 ; row = 1, col = 16 72 | int 0x10 73 | 74 | ; Print the run instructions 75 | mov bp, run_instr ; String pointer in es:bp 76 | mov cx, run_instr_len ; Streng length 77 | mov dx, 0x1710 ; row = END_ROW+1, col = 16 78 | int 0x10 79 | 80 | ; Move the cursor for printing the code segment location 81 | mov dh, START_ROW 82 | mov dl, LINE_NUM_COL-4-1 ; Space for "FFFF:" 83 | mov ax, 0x0200 84 | xor bh, bh 85 | int 0x10 86 | ; Print the USER_CODE_LOC 87 | mov cx, USER_CODE_LOC 88 | call print_hex 89 | ; Print the ":" 90 | mov ah, 0x0E 91 | mov al, ':' 92 | xor bh, bh 93 | int 0x10 94 | 95 | ; Print the line numbers 96 | ; cx = full address for user code 97 | xor cx, cx 98 | print_line_nums: 99 | ; Move the cursor to the next line number position 100 | mov ax, 0x0200 101 | xor bh, bh 102 | mov dl, LINE_NUM_COL 103 | int 0x10 104 | 105 | call print_hex ; print cx 106 | add cx, 0x10 ; Add 16 bytes to the line number 107 | 108 | inc dh ; Add one row to the cursor 109 | cmp dh, END_ROW+1 110 | jne print_line_nums 111 | 112 | ; Set cursor position to the start 113 | mov ax, 0x0200 114 | mov dx, MAIN_TOP_LEFT 115 | xor bh, bh ; page 0 116 | int 0x10 117 | 118 | ; --- typing_loop global register variables --- 119 | ; 120 | ; dx - cursor position (set above) 121 | ; [es:di] - the current position to write to in the user code buffer (di=0 is the beginning) 122 | ; [es:si] - the end of the user code buffer (will possibly be only half a byte) 123 | ; cl - storage for the current byte the cursor is on (2 chars per byte) 124 | ; ch - bitfield for state flags about half-bytes (CURSOR_ON_SECOND_NIBBLE | SAVED_NIBBLE_AT_END) 125 | mov ax, USER_CODE_LOC 126 | mov es, ax 127 | xor di, di 128 | xor si, si 129 | xor cx, cx 130 | 131 | ; Note: all of the jumps in typing_loop loop back here (except for run_code) 132 | ; 133 | ; I thought it would be fun to save the call and ret instructions since the 134 | ; entire program is just this loop (after the setup). 135 | typing_loop: 136 | ; Read keyboard 137 | mov ah, 0x00 138 | int 0x16 139 | ; ah = key code, al = ascii value 140 | 141 | ; For the consecutive range checks below, we save an add instruction by 142 | ; doing a bit of algebra and packing it into the next sub instruction 143 | ; (letting the assembler do the work statically). 144 | 145 | sub al, 'a' 146 | cmp al, 'f'-'a' 147 | jbe save_and_print_hex_letter ; jump if al is between 'a' and 'f' 148 | 149 | sub al, 'A'-'a' ; -'a' compensates for the sub al, 'a' instruction above 150 | cmp al, 'F'-'A' 151 | jbe save_and_print_hex_letter 152 | 153 | sub al, '0'-'A' 154 | cmp al, '9'-'0' 155 | jbe save_and_print_hex_number 156 | add al, '0' ; We're done with the range checks so we don't need the offset 157 | 158 | cmp al, `\b` ; Backspace, shift backspace, Ctrl+H 159 | je move_left 160 | 161 | cmp ax, LEFT_ARROW 162 | je move_left 163 | 164 | cmp ax, RIGHT_ARROW 165 | je move_right 166 | 167 | cmp ax, EOT ; Ctrl+D 168 | je run_code 169 | 170 | jmp typing_loop ; Doesn't match anything above 171 | 172 | ; ==== typing_loop internal helpers === 173 | 174 | ; Takes an ascii char A-F in al then saves and prints it and continues typing_loop 175 | save_and_print_hex_letter: 176 | mov bl, al 177 | add bl, 0xA ; bl = nibble value (al = 0x0 if input was 'A' or 'a') 178 | add al, 'A' ; al = the character to print 179 | jmp _save_and_print_nibble 180 | 181 | ; Takes an ascii char 0-9 in al then saves and prints it and continues typing_loop 182 | save_and_print_hex_number: 183 | mov bl, al ; bl = nibble value 184 | add al, '0' ; al = the character to print 185 | ; fallthrough 186 | ; Takes a nibble value in bl and ascii hex character in al 187 | ; then saves and prints it and continues typing_loop 188 | _save_and_print_nibble: 189 | ; Print the nibble ascii char (in al) 190 | mov ah, 0x0E ; Write teletype character 191 | xor bh, bh ; Note: cannot clear bl here 192 | int 0x10 193 | 194 | test ch, CURSOR_ON_SECOND_NIBBLE 195 | jnz .save_second_nibble 196 | ; .save_first_nibble 197 | shl bl, 4 ; Move the nibble into the high half 198 | and cl, 0x0F ; Clear the high half of the temp byte 199 | or cl, bl ; Put the nibble into the high half of cl 200 | 201 | cmp di, si 202 | jne move_right ; move_right if it's not the end 203 | ; If we're at the end, mark the bit that we have a nibble 204 | or ch, SAVED_NIBBLE_AT_END 205 | jmp move_right 206 | 207 | .save_second_nibble: 208 | ; Save the byte 209 | and cl, 0xF0 ; clear the low half of the temp byte 210 | or cl, bl ; put the nibble in the low half of the temp byte 211 | 212 | cmp di, si 213 | jne move_right ; move_right if we're not at the end 214 | ; If we're at the end 215 | 216 | cmp si, USER_CODE_MAX 217 | je typing_loop 218 | 219 | inc si ; move the end of the buffer forward so we can move the cursor (only user typing can do this) 220 | and ch, ~SAVED_NIBBLE_AT_END ; Clear the bit, we don't have a nibble anymore 221 | jmp move_right 222 | 223 | ; Moves the cursor to the value in dx and continues typing_loop 224 | set_cursor_and_continue: 225 | mov ah, 0x02 226 | xor bh, bh 227 | int 0x10 ; dx is the cursor position 228 | jmp typing_loop 229 | 230 | ; Moves the cursor left one nibble then continues typing_loop 231 | ; - Saves the byte in cl when moving to a new byte, also loads the existing 232 | ; data into cl if applicable 233 | ; - Calls move_up when moving to the next line 234 | move_left: 235 | test ch, CURSOR_ON_SECOND_NIBBLE 236 | jz .previous_byte 237 | ; We're moving left past the first nibble of a byte, 238 | ; never need to change lines because of this 239 | 240 | and ch, ~CURSOR_ON_SECOND_NIBBLE 241 | dec dl 242 | jmp set_cursor_and_continue 243 | 244 | .previous_byte: 245 | ; Don't do anything if we're already at the beginning of the buffer 246 | test di, di 247 | jz typing_loop 248 | 249 | or ch, CURSOR_ON_SECOND_NIBBLE ; We stepped past the second nibble 250 | 251 | ; store the temp byte in the destination memory 252 | mov [es:di], cl 253 | dec di 254 | 255 | mov cl, [es:di] ; Load the previous byte 256 | 257 | cmp dl, 0x29 ; Column of the first char of the 9th byte 258 | je .extra_space 259 | 260 | cmp dl, START_COL 261 | je .previous_line 262 | 263 | ; Normal case, the char plus 1 space 264 | sub dl, 2 265 | jmp set_cursor_and_continue 266 | 267 | .extra_space: ; Move past the two spaces in the middle 268 | sub dl, 3 269 | jmp set_cursor_and_continue 270 | 271 | .previous_line: 272 | mov dl, END_COL 273 | jmp _move_up 274 | 275 | ; Moves the cursor right one nibble then continues typing_loop 276 | ; - Saves the byte in cl when moving to a new byte, also loads the existing 277 | ; data into cl if applicable 278 | ; - Calls move_down when moving to the next line 279 | move_right: 280 | test ch, CURSOR_ON_SECOND_NIBBLE 281 | jnz .next_byte 282 | ; We're moving right to the second nibble 283 | ; Never have to change lines 284 | 285 | cmp di, si 286 | jne .move_one_char_forward ; if we're not at the end 287 | ; we're at the end 288 | test ch, SAVED_NIBBLE_AT_END 289 | jnz .move_one_char_forward ; allowed to move one more if there's a nibble 290 | jmp typing_loop ; this is the end, no move moving right 291 | 292 | .move_one_char_forward: 293 | or ch, CURSOR_ON_SECOND_NIBBLE ; set the bit 294 | inc dl 295 | jmp set_cursor_and_continue 296 | 297 | .next_byte: 298 | cmp di, si ; note: when the user types a character this is never equal 299 | je typing_loop ; do nothing if we're already at the end 300 | 301 | ; store the temp byte in the destination memory 302 | mov [es:di], cl 303 | inc di 304 | 305 | ; Set the cl value for the next byte 306 | cmp di, si 307 | jne .load_byte ; if we moved right and we're not at the end, load the data already entered 308 | 309 | test ch, SAVED_NIBBLE_AT_END ; if we moved to the end and there's a saved nibble there 310 | jnz .load_byte ; just load the whole byte (the second nibble can be user data if we hit the USER_CODE_MAX) 311 | 312 | ; If we don't take either of the above jumps 313 | xor cl, cl ; Clear the temp storage for the next byte (maybe not really necessary) 314 | jmp .move_cursor_to_next_byte 315 | 316 | .load_byte: 317 | mov cl, [es:di] 318 | ; fallthrough 319 | .move_cursor_to_next_byte: 320 | and ch, ~CURSOR_ON_SECOND_NIBBLE ; Clear this bit in the bitfield 321 | 322 | cmp dl, 0x26 ; The column of the last char of the 8th byte 323 | je .extra_space 324 | 325 | cmp dl, END_COL 326 | je .new_line 327 | 328 | ; Normal case, the char printed plus 1 space 329 | add dl, 2 330 | jmp set_cursor_and_continue 331 | 332 | .extra_space: ; Put two spaces in the middle 333 | add dl, 3 334 | jmp set_cursor_and_continue 335 | 336 | .new_line: 337 | mov dl, START_COL 338 | jmp _move_down 339 | 340 | ; Move the cursor up one line and keep the same column 341 | ; - Does not update the di pointer or cx state 342 | ; - Does not check bounds 343 | ; - Scrolls if necessary 344 | ; - Prints existing data if scolling to a part of the buffer with data 345 | _move_up: 346 | cmp dh, START_ROW 347 | je .scroll_down 348 | 349 | dec dh 350 | jmp set_cursor_and_continue 351 | 352 | .scroll_down: 353 | mov ah, 0x07 ; BIOS scroll down, means make room at the top 354 | jmp _scroll_and_continue 355 | 356 | 357 | ; Move the cursor down one line and keep the same column 358 | ; - Does not update the di pointer or cx state 359 | ; - Does not check bounds 360 | ; - Scrolls if necessary 361 | ; - Prints existing data if scolling to a part of the buffer with data 362 | _move_down: 363 | cmp dh, END_ROW ; if we're at the bottom 364 | je .scroll_up 365 | 366 | inc dh ; Next row 367 | jmp set_cursor_and_continue 368 | 369 | .scroll_up: 370 | mov ah, 0x06 ; BIOS scroll up, means make room at the bottom 371 | jmp _scroll_and_continue 372 | 373 | ; Scrolls the text up or down one row printing any existing data in the text 374 | ; buffer when it does it 375 | ; - ah = 0x06 for down; ah = 0x07 for up; ah = anything else for hacks 376 | _scroll_and_continue: 377 | push dx ; save the cursor position 378 | push di ; save the write pointer 379 | push cx ; save the current byte storage (must be last) 380 | 381 | mov al, 1 ; scroll one line (shared between the two calls) 382 | 383 | ; Scroll the user code text area 384 | mov cx, MAIN_TOP_LEFT 385 | mov dx, MAIN_BOTTOM_RIGHT 386 | mov bh, MAIN_COLOR 387 | int 0x10 388 | 389 | ; Scroll the line numbers 390 | mov cx, LINE_NUM_TOP_LEFT 391 | mov dx, LINE_NUM_BOTTOM_RIGHT 392 | mov bh, BORDER_COLOR 393 | int 0x10 394 | 395 | ; Set the cursor to the start of the new line number 396 | cmp ah, 0x06 397 | jne .went_up 398 | mov dh, END_ROW 399 | jmp .went_down 400 | .went_up: 401 | mov dh, START_ROW 402 | .went_down: 403 | mov dl, LINE_NUM_COL 404 | mov ah, 0x02 405 | xor bh, bh 406 | int 0x10 407 | 408 | ; Move the current buffer pos pointer to the beginning of the line 409 | and di, 0xFFF0 410 | 411 | ; Print the new line number 412 | mov cx, di 413 | call print_hex 414 | 415 | ; Move the cursor forward to the start of the line 416 | add dl, 5 417 | mov ah, 0x02 418 | xor bh, bh 419 | int 0x10 420 | 421 | ; Prints a whole hex line from the [es:di] up to the end of the line or [es:si] 422 | ; Also checks the SAVED_NIBBLE_AT_END flag and prints it 423 | pop cx ; restore the global cx value (we need the ch flags) 424 | .print_line_loop: 425 | cmp di, si 426 | jne .print_whole_byte 427 | cmp si, USER_CODE_MAX 428 | je .print_whole_byte ; Just always print the last byte even if the user didn't type there yet 429 | test ch, SAVED_NIBBLE_AT_END 430 | jnz .print_one_nibble 431 | jmp .done 432 | 433 | .print_one_nibble: 434 | mov al, [es:di] 435 | shr al, 4 436 | call print_hex_char 437 | jmp .done ; no need to skip spaces 438 | 439 | .print_whole_byte: 440 | mov al, [es:di] 441 | shr al, 4 442 | call print_hex_char 443 | 444 | mov al, [es:di] 445 | and al, 0x0F 446 | call print_hex_char 447 | 448 | ;.skip_spaces: 449 | ; if (di & 0xF) = 7 450 | mov ax, di 451 | and ax, 0xF 452 | cmp al, 7 453 | jne .one_space 454 | ;two_spaces: 455 | inc dl 456 | ; fallthrough 457 | .one_space: 458 | add dl, 3 459 | mov ah, 0x02 460 | xor bh, bh 461 | int 0x10 462 | 463 | ; If we printed the last byte of the line, we're done 464 | mov ax, di 465 | and ax, 0xF 466 | cmp ax, 0xF 467 | je .done 468 | 469 | inc di 470 | jmp .print_line_loop 471 | 472 | .done: 473 | pop di ; restore the write pointer 474 | pop dx ; restore the cursor position 475 | jmp set_cursor_and_continue 476 | 477 | run_code: 478 | ; Set video mode, 16 color 80x25 chars 479 | mov ax, 0x0003 480 | int 0x10 481 | 482 | ; Reset the data segement to the user buffer 483 | mov ax, USER_CODE_LOC 484 | mov ds, ax 485 | 486 | jmp USER_CODE_LOC:0x00 487 | 488 | ; ==== Data area ==== 489 | 490 | greeting: db `Write your x86 (16-bit real mode) hex here:` 491 | greeting_len: equ $-greeting 492 | 493 | row_header: db ` 0 1 2 3 4 5 6 7 8 9 A B C D E F` 494 | row_header_len: equ $-row_header 495 | 496 | run_instr: db `Press Ctrl+D to run your code immediately.` 497 | run_instr_len: equ $-run_instr 498 | 499 | NUM_EXTRA_SECTORS: equ NUM_SECTORS(start_) 500 | -------------------------------------------------------------------------------- /bootstrap-lisp.asm: -------------------------------------------------------------------------------- 1 | ; Provided under the MIT License: http://mit-license.org/ 2 | ; Copyright (c) 2020 Andy Kallmeyer 3 | ; 4 | ; See lisp/lisp.asm for more details (the top comment). 5 | 6 | %include "bootloader/bootsect.asm" 7 | 8 | ; This is the start for calculating NUM_EXTRA_SECTORS 9 | ; There must be nothing other than the bootsector above this label 10 | extra_sectors_start: 11 | 12 | %define RUN_CODE ; tell the text editor we have defined run_code 13 | %include "text-editor.asm" 14 | %include "lisp/lisp.asm" 15 | 16 | %ifdef DEBUG_TEXT 17 | debug_text: 18 | ;db "(define elm2 (lambda (l) (car (cdr l)))) (elm2 '(foo bar baz))", 0 19 | ;db "(quote ((l) . (car (cdr l))))",`\n`,'((l) . (car (cdr l)))", 0 20 | ;db "(define append1 (lambda (s t) (cond ((pair? s) (cons (car s) (append1 (cdr s) t))) (#t t) ))) (append1 '(this is a) '(test))", 0 21 | 22 | ;db "(define append1 (lambda (s t)",`\n` 23 | ;db " (cond",`\n` 24 | ;db " ((pair? s) (cons (car s) (append1 (cdr s) t)))",`\n` 25 | ;db " (#t t)",`\n` 26 | ;db " )",`\n` 27 | ;db "))",`\n`,`\n` 28 | ;db "(define greeting (lambda name",`\n` 29 | ;db " (append1 '(Hello, ) name)",`\n` 30 | ;db "))",`\n`,`\n` 31 | ;db "(greeting 'lisp)",0 32 | 33 | ;db "(define append (lambda (t . args)",`\n` 34 | ;db " (cond",`\n` 35 | ;db " ((pair? args) (append1 t (append . args)))",`\n` 36 | ;db " (#t t)",`\n` 37 | ;db " )",`\n` 38 | ;db "))",`\n`,`\n` 39 | 40 | ;db "(define 0 ())",`\n` 41 | ;db "(define succ (lambda (x) (cons x 0)))",`\n` 42 | ;db "(define 1 (succ 0))"," " 43 | ;db "(define 2 (succ 1))"," " 44 | ;db "(define 3 (succ 2))",`\n` 45 | ;db "(define dec (lambda (x) (car x)))",`\n` 46 | ;db "(define rep (lambda (x c)",`\n` 47 | ;db " (cond",`\n` 48 | ;db " ((eq? c 0) ())",`\n` 49 | ;db " (#t (cons x (rep x (dec c)) ))",`\n` 50 | ;db " )",`\n` 51 | ;db "))",`\n` 52 | ;;db "(rep 'lisp 3)",`\n`,`\n` 53 | ; 54 | ;db "(define add (lambda (a b)",`\n` 55 | ;db " (cond",`\n` 56 | ;db " ((eq? b 0) a)",`\n` 57 | ;db " (#t (succ (add a (dec b))))",`\n` 58 | ;db " )",`\n` 59 | ;db "))",`\n` 60 | ;db "(define 5 (add 3 2))",`\n` 61 | ;db "(rep 'lisp 5)",0 62 | 63 | ;db "(define env",`\n` 64 | ;; Closures are ((v . x) . e) 65 | ;; the global env is set only if there's a defined value in the env 66 | ;; So we just need to pop off the defined value and the closure stuff 67 | ;db " (cdr (cdr ((lambda (x) (lambda (y) x)) 'a)))",`\n` 68 | ;db ")",`\n` 69 | ;db "env",0 70 | 71 | db "(define list (lambda args args))",`\n` 72 | 73 | db "(define not (lambda (x)",`\n` 74 | db " (eq? x ())",`\n` 75 | db "))",`\n`,`\n` 76 | 77 | ;db "(define or1 (lambda (x)",`\n` 78 | ;db " (cond",`\n` 79 | ;db " ((not (pair? x)) x)",`\n` 80 | ;db " ((car x) (car x))",`\n` 81 | ;db " (#t (or1 (cdr x)))",`\n` 82 | ;db " )",`\n` 83 | ;db "))",`\n`,`\n` 84 | ; 85 | ;db "(define or (lambda x",`\n` 86 | ;db " (or1 x)",`\n` 87 | ;db "))",`\n` 88 | 89 | db "(define orq1 (lambda (x)",`\n` 90 | db " (cond",`\n` 91 | db " ((not (pair? x)) x)",`\n` 92 | db " ((eval (car x)) (eval (car x)))",`\n` 93 | db " (#t (orq1 (cdr x)))",`\n` 94 | db " )",`\n` 95 | db "))",`\n`,`\n` 96 | 97 | db "(define orq (lambda x",`\n` 98 | db " (orq1 x)",`\n` 99 | db "))",`\n` 100 | 101 | db "(define andq1 (lambda (x)",`\n` 102 | db " (cond",`\n` 103 | db " ((eq? x ()) #t)",`\n` 104 | db " ((not (pair? x)) x)",`\n` 105 | db " ((not (eval (car x))) ())",`\n` 106 | ;db " ((eq? (cdr x) ()) (eval (car x)))",`\n` 107 | db " (#t (andq1 (cdr x)))",`\n` 108 | db " )",`\n` 109 | db "))",`\n` 110 | 111 | ; I think you could avoid doing this by doing (apply (cons and (cdr x))) 112 | db "(define andq (lambda x",`\n` 113 | db " (andq1 x)",`\n` 114 | db "))",`\n`,`\n` 115 | 116 | ;db "(define and1 (lambda (x)",`\n` 117 | ;db " (cond",`\n` 118 | ;db " ((not (pair? x)) x)",`\n` 119 | ;db " ((not (car x)) ())",`\n` 120 | ;db " ((eq? (cdr x) ()) (car x))",`\n` 121 | ;db " (#t (and1 (cdr x)))",`\n` 122 | ;db " )",`\n` 123 | ;db "))",`\n` 124 | ; 125 | ;; I think you could avoid doing this by doing (apply (cons and (cdr x))) 126 | ;db "(define and (lambda x",`\n` 127 | ;db " (and1 x)",`\n` 128 | ;db "))",`\n`,`\n` 129 | 130 | ;db "(and 'a)",`\n` 131 | ;db "(and1 (cdr '(a b)))",`\n` 132 | ;db "(and 'a 'b 'c)",`\n` 133 | ;db "(and 'a 'b () 'c)",`\n` 134 | ;db "(and1 'a)",`\n` 135 | ;db "(andq '(eq? 'b 'b) '(eq? 'a 'a))",`\n` 136 | ;db "(or () (eq? 'a 'b) 'd)",`\n` 137 | ;db "(or '() '(eq? 'a 'a) '())",`\n` 138 | 139 | db "(define inner_equal? (lambda (x y)",`\n` 140 | db " (andq ",`\n` 141 | db " (list 'pair? (list 'quote x))",`\n` 142 | db " (list 'pair? (list 'quote y))",`\n` 143 | db " (list 'equal? (list 'quote (car x)) (list 'quote (car y)))",`\n` 144 | db " (list 'equal? (list 'quote (cdr x)) (list 'quote (cdr y)))",`\n` 145 | db " )",`\n` 146 | db "))",`\n` 147 | 148 | db "(define equal?",`\n` 149 | db " (lambda (x y)",`\n` 150 | db " (orq",`\n` 151 | db " (list 'eq? (list 'quote x) (list 'quote y))",`\n` 152 | db " (list 'inner_equal? (list 'quote x) (list 'quote y))",`\n` 153 | db " )",`\n` 154 | db "))",`\n` 155 | db "(equal? '((a b) c) '((a b) c) )" 156 | 157 | ;db "(define if (lambda (x y z)",`\n` 158 | ;db " (cond",`\n` 159 | ;db " (x y)",`\n` 160 | ;db " (#t z)",`\n` 161 | ;db " )",`\n` 162 | ;db "))",`\n` 163 | ;db "(if (eq? () ()) 'true 'false)",`\n` 164 | 165 | db 0 166 | 167 | ;db "(eq? '(a b c) '(cons a (b c)))",0 168 | 169 | ;db "(eq? 'a 'a) (eq? 'a 'b) (eq? #t #t) (eq? () ()) (define foo '(test me)) (eq? foo foo) (eq? '(foo) '(foo))", 0 170 | %endif 171 | 172 | NUM_EXTRA_SECTORS: equ NUM_SECTORS(extra_sectors_start) 173 | -------------------------------------------------------------------------------- /debug: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Thanks to https://stackoverflow.com/a/65442462 and https://stackoverflow.com/a/32960272 3 | 4 | if [ "$#" -eq 0 ]; then 5 | printf "Usage: $0 [filename] [any other nasm args].\nEx: $0 bootstrap-asm.asm -DDVORAK" 6 | 7 | printf "\n\nThe debugger will automatically break at the start of the code after" 8 | printf "\nthe bootloader. Unfortunately I couldn't get debug symbols to work, so" 9 | printf "\nyou'll have to manually compare the disassembly with the source code." 10 | 11 | printf "\n\nWhen you set breakpoints use the addresses listed in the disassembly" 12 | printf "\noutput. e.g. \`b *0x7E00\`. If you just type \`b\` it won't put the" 13 | printf "\nbreakpoint on the right address because it won't include the cs register" 14 | printf "\nvalue. To break on the current address use \`b *\$rip\`. You can also" 15 | printf "\nstep through and look at jump/call addresses in the disassembly to find" 16 | printf "\nthe addresses of your labels to set breakpoints on.\n" 17 | exit 1 18 | fi 19 | 20 | # This is a fix to make qemu not force the gdb architecture back to 32 bit x86 21 | mkdir -p gdbconf 22 | if ! stat gdbconf/i386-32bit.xml >/dev/null 2>&1; then 23 | echo "Setting up a patch to fix the disassembly..." 24 | echo 'i8086' > gdbconf/target.xml 25 | wget -nc https://raw.githubusercontent.com/qemu/qemu/master/gdb-xml/i386-32bit.xml -O gdbconf/i386-32bit.xml 26 | fi 27 | 28 | # This is a nice config that provides the automatic disassembly and memory 29 | # printing and several nice functions 30 | # Mostly use stepi and stepo (skip a function call) from this when you debug 31 | GDB_REAL_CONF=gdbconf/gdbinit_real_mode.txt 32 | if ! stat $GDB_REAL_CONF >/dev/null 2>&1; then 33 | echo "Downloading a gdb config to make this nicer... (provides auto disassembly, functions: stepo, find_in_mem, etc)" 34 | wget -nc https://raw.githubusercontent.com/mhugo/gdb_init_real_mode/843e3b530971aeed9cfa1d12f585132e0d651c15/gdbinit_real_mode.txt -O $GDB_REAL_CONF 35 | # Patch in using the above work-around 36 | sed -i '/set architecture i8086/a\ 37 | set tdesc filename gdbconf/target.xml' $GDB_REAL_CONF 38 | # Fix the stepo address, not sure why this is wasn't right already... 39 | sed -i 's/set $_nextaddress = $eip + $offset + $noffset/set $_nextaddress = $rip + $offset + $noffset/' $GDB_REAL_CONF 40 | # Also tbreak isn't working for us so we have to clear it in stepo 41 | sed -i '/# else we just single step/i\ 42 | clear *$_nextaddress' $GDB_REAL_CONF 43 | fi 44 | 45 | file="${@: -1}" 46 | name="${file%%.*}" 47 | 48 | binfile="bin/$name.bin" 49 | elffile="bin/$name.o.elf" 50 | bit16file="bin/$name.bits.asm" 51 | mkdir -p `dirname $binfile` 52 | 53 | if ! nasm $file -f bin -o $binfile ${@:1:$(($#-1))}; then 54 | exit $? 55 | fi 56 | # Add "bits 16" as the first line which is only needed when making an elf file 57 | sed -e '1ibits 16\' $file > $bit16file 58 | # Make the debug symbols elf file. The extensions is needed for 16 bit to work. 59 | nasm $bit16file -f elf32 -F dwarf -g -w+gnu-elf-extensions -o $elffile ${@:1:$(($#-1))} 60 | rm $bit16file 61 | 62 | if ! which qemu-system-i386 > /dev/null; then 63 | printf "\nYou need to install qemu-system-i386 (qemu-arch-extra package in some distributions) to use the debugger\n" 64 | exit 127 65 | fi 66 | 67 | qemu-system-i386 $QEMU_ARGS -s -S -drive file=$binfile,format=raw,if=ide & 68 | 69 | # Note: If you turn off the offset for the add-symbol-file command then gdb can 70 | # load the source code line numbers correctly but the fancy real-mode-gdb config 71 | # doesn't work because it actually understand the offset correctly. 72 | gdb -ix $GDB_REAL_CONF \ 73 | -ex 'target remote localhost:1234' \ 74 | -ex "add-symbol-file $elffile -s .text 0x7c00" \ 75 | -ex 'b start_' -ex 'c' -ex 'delete breakpoints' # break at the start of the code after the bootloader 76 | -------------------------------------------------------------------------------- /images/v0.4-hex-2x.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsmv/bootstrap-os/167b76894e68d0155bffb217a1a8ec487ad42fa4/images/v0.4-hex-2x.gif -------------------------------------------------------------------------------- /keyboard-layouts/dvorak.asm: -------------------------------------------------------------------------------- 1 | %ifdef DVORAK 2 | 3 | ; Used to type with the dvorak layout (see convert_keyboard_layout) 4 | ; 5 | ; Instructions to make a new map are at the %include location 6 | keyboard_map: 7 | db ` !_#$%&-()*}w[vz0123456789SsW]VZ@AXJE>UIDCHTNMBRL"POYGK 3 | ; 4 | ; Many thanks to the BSD licensed 5 | ; tinylisp 6 | ; 7 | ; This code was heavily inspired by tinylisp. Mainly the datastructure, a stack 8 | ; of typed objects, and the way that the logic is built around using that 9 | ; datastructure internally. Also I'm not sure if it's possible to have a 10 | ; different eval function and still call it lisp. I have made some important 11 | ; changes though, a possibly not exhaustive list: 12 | ; 13 | ; - I'm using integers not doubles and I didn't do the NaN trick to pack the 14 | ; type information in, and I decided to just use two words instead of 15 | ; splitting the bits of one word and having smaller integers. 16 | ; - I didn't use the heap side of the memory block at all, I just read the 17 | ; strings out of the code directly instead of copying them. This meant I 18 | ; needed to add new type tags. 19 | ; - I decided to set the bit values of my types differently so my flag checking 20 | ; code is different. I used separate bits and some have two bits if the code 21 | ; for one type should also apply to another. 22 | ; - I simplified a lot of things in parsing. For one thing I didn't use a 23 | ; buffer for the tokens, I just point into the code. Several of the functions 24 | ; in tinylisp are just inlined in my code, etc. 25 | ; - I changed some names of things to hopefully make it more clear and don't 26 | ; have all of the same functions because of the above changes 27 | ; - Several changes in the list of primitives. I wanted mine to look more like 28 | ; McCarthy's paper and be extremely minimal. 29 | ; - I left out if, and, or as a primitives, you can define those in lisp, 30 | ; well to get shortcutting you would need macros but technically you can 31 | ; quote everything and use eval to simulate macros even though it's not 32 | ; efficient. 33 | ; - I also left out let* which doesn't allow you to accomplish anything you 34 | ; couldn't also do with lambdas (although let* is nicer) 35 | ; - I could remove the pair? primitive, which was in McCarthy's paper as the 36 | ; inverse atom?. In tinylisp and this interpreter you can actually 37 | ; implement pair? and atom? within lisp by checking if cdr/car returns an 38 | ; error for the object. I didn't like the idea of this so I made a more 39 | ; direct primitive check. It felt important to allow writing error-free 40 | ; lisp programs. 41 | ; - This lisp currently doesn't support any form of numbers, or strings for 42 | ; that matter, everything is lists even numbers and strings! 43 | 44 | ; Run the lisp interpreter! 45 | ; 46 | ; Input: 47 | ; 48 | ; [es:si] - the code to run 49 | ; [ds:0] - an unused memory block 50 | ; di - output segment address; starts at 0 offset; null terminated 51 | ; 52 | ; Global register variables: 53 | ; 54 | ; [es:si] - the code input pointer (null terminated) 55 | ; [ds:di] - The object stack pointer, holds the typed objects. The low end of the ds memory block. 56 | ; 57 | ; The object stack is a list of 2 word objects. The first word is the type and 58 | ; the second word is the value. 59 | ; 60 | ; Segments: 61 | ; 62 | ; All of the lisp REPL functions required all 4 memory segments to be 63 | ; unmodified. There is no more available address space to use more memory 64 | ; without using extended pointers in the lisp stack and moving the ds segment 65 | ; around as needed. 66 | ; 67 | ; Each segment is limited to 0xFFFF bytes or 64k 68 | ; 69 | ; ds - The lisp memory, a stack of type tagged objects with internal pointers 70 | ; es - the input code, we point directly into the code for the symbol strings 71 | ; instead of copying them into the lisp memory 72 | ; cs - The code segment, builtin strings are stored here (and the interpreter) 73 | ; ss - the usual stack, needed for function calls in the interpreter 74 | run_code: 75 | ; save the output location 76 | mov [cs:output_seg], di 77 | mov word [cs:output_addr], 0 78 | ; Resetting the stack to the env tip acts as a garbage collector 79 | mov di, [cs:env] 80 | 81 | test di, di 82 | jnz .env_exists 83 | call setup_env 84 | .env_exists: 85 | 86 | .repl: 87 | call repl_step 88 | test cx, cx 89 | jnz .repl 90 | ret 91 | 92 | repl_step: 93 | call scan 94 | 95 | cmp cx, 0 96 | ja .not_end 97 | ; null terminate the output and return 98 | ; this is the only return location from .repl 99 | mov es, [cs:output_seg] 100 | mov bx, [cs:output_addr] 101 | mov byte [es:bx], 0 102 | ret 103 | .not_end: 104 | 105 | call parse 106 | 107 | cmp dx, type.ERROR 108 | je .parse_error 109 | 110 | mov cx, [cs:env] 111 | call eval 112 | 113 | .parse_error: 114 | 115 | ; Print the eval result to the output buffer location 116 | push es 117 | push di 118 | push si 119 | mov si, es 120 | mov di, [cs:output_addr] 121 | mov es, [cs:output_seg] 122 | call print 123 | ; TODO: check for overflow in print 124 | cmp di, [cs:output_addr] 125 | je .empty_out 126 | ; Add a newline at the end and null terminate it 127 | mov byte [es:di], `\n` 128 | inc di 129 | .empty_out: 130 | mov byte [es:di], 0 131 | mov [cs:output_addr], di ; save the end of the output 132 | ; Restore ds:di for the next lisp expression 133 | pop si 134 | pop di 135 | pop bx 136 | mov es, bx 137 | 138 | mov cx, 1 139 | ret 140 | 141 | ; Use the code segment instead of stack locals like C would do because I didn't 142 | ; preserve the bp register like the C calling convention 143 | output_seg: dw 0 144 | output_addr: dw 0 145 | 146 | ; Constants with subvalues so I can type objarg2.type 147 | objsize: equ 4 148 | objarg1: 149 | .value: equ 4 150 | .type: equ 2 151 | objarg2: 152 | .value: equ 8 153 | .type: equ 6 154 | 155 | ; Objects on the lisp stack are two words, the value and then the type. 156 | ; This is the type enum. 157 | ; 158 | ; The values are bit flags and some flags apply to multiple types. For example: 159 | ; a CLOS counts as a CONS, so car and cdr work on CLOS. 160 | type: 161 | .ATOM: equ 0x0001 ; String pointer on es, eg [es:val]; dh is set to the length. 162 | .CSATOM: equ 0x0011 ; String pointer on cs, eg [cs:val]; null terminated. 163 | .ERROR: equ 0x0091 ; String pointer on cs, this is a CSATOM 164 | .CONS: equ 0x0002 ; Pointer on ds; eg [ds:val-4] is the second elm [ds:val-8] is the first elm 165 | .NIL: equ 0x0004 ; Value is always 0 166 | .INT: equ 0x0008 ; Value is a 16 bit integer 167 | .PRIM: equ 0x0020 ; Function pointer on cs; eg call [cs:val] 168 | .CLOS: equ 0x0042 ; A CONS object that should be run as a function with an env 169 | 170 | ; The global environment of defined symbols in the lisp interpreter. 171 | ; A list of CONS pairs e.g. (("gcd" . {CLOS}) . ((t . t) . nil)) 172 | ; 173 | ; This is just the value part of the object, it's assumed to be type.CONS 174 | ; 175 | ; Define expressions add symbols to the global environment and local 176 | ; environments created by extending env with bound variables are used for 177 | ; closures. 178 | env: dw 0 179 | 180 | ; Builtin atoms, I just fill the lisp object values every time I use them 181 | ; They have type.CSATOM and the value is the pointer to the string 182 | atoms: 183 | .t: db '#t', 0 184 | .err: db 'ERROR', 0 185 | .lookup_err: db 'no-matching-symbol-error', 0 186 | .not_fn_err: db 'not-a-function-error', 0 187 | .open_list_err: db 'missing-right-paren-error', 0 188 | .no_list_err: db 'extra-right-paren-error', 0 189 | .symbol_too_long_err: db 'symbol-too-long-max-is-0xFF', 0 190 | .empty: db 0 191 | .quote: equ atom_quote 192 | 193 | ; Data array of null terminated function names then the function pointer. 194 | ; The array itself is then null terminated. 195 | ; 196 | ; This list is searched in order from bottom to top so it's best to put the most 197 | ; frequently used primitives last. 198 | ; 199 | ; All primitives have tho following call signature: 200 | ; 201 | ; Input: 202 | ; ax,dx - the arguments (everything but the prim name) 203 | ; cx - the environment pointer, assumed to be type.CONS 204 | ; 205 | ; Output: 206 | ; ax,dx - the result object 207 | primitives: 208 | db 'cons', 0 209 | dw prim_cons 210 | db 'eval', 0 211 | dw prim_eval 212 | db 'pair?', 0 213 | dw prim_is_pair 214 | ; Note: the CSATOM label for quote must be the exact same string as in the 215 | ; primitives so we can avoid writing another string equals function. This is 216 | ; because when parsing 'test inserts (quote . (test . nil)) using quote_atom. 217 | atom_quote: db 'quote',0 218 | dw car ; this primitive is the only one that doesn't need eval or anything! 219 | db 'lambda', 0 220 | dw prim_lambda 221 | db 'define', 0 222 | dw prim_define 223 | db 'eq?', 0 224 | dw prim_eq 225 | db 'cond', 0 226 | dw prim_cond 227 | db 'car', 0 228 | dw prim_car 229 | db 'cdr', 0 230 | dw prim_cdr 231 | db 0 232 | 233 | prim_cons: 234 | call eval_each 235 | mov bp, sp 236 | .orig: equ 4 237 | push ax 238 | push dx 239 | 240 | ; The second arg to cons 241 | call cdr 242 | call car 243 | push ax 244 | push dx 245 | 246 | ; The first arg to cons 247 | mov ax, [bp-.orig+2] 248 | mov dx, [bp-.orig] 249 | call car 250 | call cons 251 | add sp, 2*objsize 252 | ret 253 | 254 | prim_eval: 255 | call car 256 | call eval 257 | jmp eval 258 | 259 | ; TODO should there be an error if there's more than one argument? A: yes in CL 260 | prim_car: 261 | call car ; get the first argument 262 | call eval ; eval the argument 263 | jmp car ; run car on the argument 264 | 265 | prim_cdr: 266 | call car ; get the first argument 267 | call eval ; eval the argument 268 | jmp cdr ; run cdr on the argument 269 | 270 | prim_is_pair: 271 | call car ; get the first argument 272 | call eval ; eval the argument 273 | 274 | test dx, type.CONS 275 | jz .false 276 | mov ax, atoms.t 277 | mov dx, type.CSATOM 278 | ret 279 | .false: 280 | mov ax, 0 281 | mov dx, type.NIL 282 | ret 283 | 284 | prim_eq: 285 | call eval_each 286 | mov bp, sp 287 | .orig: equ 4 288 | push ax 289 | push dx 290 | 291 | ; The second arg 292 | call cdr 293 | call car 294 | push ax 295 | push dx 296 | .rhs_type: equ 8 297 | 298 | ; The first arg 299 | mov ax, [bp-.orig+2] 300 | mov dx, [bp-.orig] 301 | call car 302 | 303 | ; ATOM bits need special treatment because there are multiple string types 304 | test dx, type.ATOM 305 | jnz .lhs_atom 306 | 307 | ; If it's not an atom just check the type and value 308 | call obj_equal 309 | jmp .equal_result 310 | 311 | .lhs_atom: 312 | mov bx, [bp-.rhs_type] 313 | test bx, type.ATOM 314 | jnz .both_atom 315 | jmp .not_equal 316 | .both_atom: 317 | 318 | call atom_equal 319 | ; jmp .equal_result 320 | ; fallthrough 321 | 322 | .equal_result: 323 | test bx, bx 324 | jz .not_equal 325 | 326 | ; return true 327 | add sp, 2*objsize 328 | mov ax, atoms.t 329 | mov dx, type.CSATOM 330 | ret 331 | 332 | .not_equal: 333 | add sp, 2*objsize 334 | mov ax, 0 335 | mov dx, type.NIL 336 | ret 337 | 338 | ; Executes an expression like: 339 | ; (cond 340 | ; (first_condition first_result) 341 | ; (second_condition second_result) 342 | ; ... 343 | ; ) 344 | prim_cond: 345 | ; TODO: check the formatting of the arguments and return errors otherwise! 346 | .orig: equ objsize 347 | push ax 348 | push dx 349 | 350 | .loop: 351 | cmp dx, type.NIL 352 | je .no_match 353 | 354 | call car 355 | call car 356 | call eval ; bp clobbered 357 | ; TODO: just change the offsets to skip doing the add instruction 358 | mov bp, sp 359 | add bp, objsize ; restore bp (we skipped setting it the first time) 360 | cmp dx, type.NIL 361 | jne .found_match 362 | 363 | mov ax, [bp-.orig+2] 364 | mov dx, [bp-.orig] 365 | call cdr 366 | mov [bp-.orig+2], ax 367 | mov [bp-.orig], dx 368 | 369 | jmp .loop 370 | 371 | .no_match: 372 | ; TODO: maybe make this an error. LISP 1 and 2 and tinylisp and scheme do it. 373 | add sp, objsize 374 | ret ; return the NIL, technically the result is "undefined" 375 | 376 | .found_match: 377 | mov ax, [bp-.orig+2] 378 | mov dx, [bp-.orig] 379 | add sp, objsize 380 | ; fallthrough 381 | call car 382 | call cdr 383 | call car 384 | jmp eval 385 | 386 | ; Creates a closure object which looks like 387 | ; ((v . x) . e) where e is replaced with nil if it was cs:env 388 | ; from the defined (lambda v x). 389 | ; 390 | ; v is a list of argument names. 391 | ; x is a lisp expression which will have the argument defined in the env when called 392 | ; 393 | ; For example: 394 | ; (lambda (l) (car (cdr l))) 395 | ; evaluates to 396 | ; ( ( (l) . (car (cdr l)) ) . nil ) 397 | ; which is printed as: (because there's no way to distingish a cons from a list start) 398 | ; (((l) car (cdr l))) 399 | prim_lambda: 400 | ; TODO: should error if there's more than one value not ignore it 401 | mov bp, sp 402 | .orig: equ 4 403 | push ax 404 | push dx 405 | 406 | ; Conditionally set the env arg for add_env 407 | ; Start with the bound env or nil 408 | cmp cx, [cs:env] 409 | je .push_nil 410 | push cx 411 | push type.CONS 412 | jmp .env_set 413 | .push_nil: 414 | push word 0 415 | push type.NIL 416 | .env_set: 417 | 418 | ; Extract the function body and push it for add_env 419 | mov ax, [bp-.orig+2] 420 | mov dx, [bp-.orig] 421 | call cdr 422 | call car 423 | push ax 424 | push dx 425 | 426 | ; Extract the argument names 427 | mov ax, [bp-.orig+2] 428 | mov dx, [bp-.orig] 429 | call car 430 | ; Creates ((arg_names . function_body) . cs:env or nil) 431 | call add_env ; TODO: probably better to just inline the cons calls here 432 | mov dx, type.CLOS 433 | add sp, 3*objsize 434 | ret 435 | 436 | ; Adds an entry to the current environment 437 | prim_define: 438 | mov bp, sp 439 | .orig: equ 4 440 | push ax 441 | push dx 442 | 443 | push word [cs:env] 444 | push type.CONS 445 | 446 | ; Evaluate the values 447 | call cdr 448 | call car 449 | call eval 450 | push ax 451 | push dx 452 | 453 | mov bp, sp 454 | add bp, 3*objsize 455 | mov ax, [bp-.orig+2] 456 | mov dx, [bp-.orig] 457 | call car ; get the name 458 | call add_env ; add the name mapped to the value onto the global env 459 | mov [cs:env], ax 460 | 461 | add sp, 3*objsize ; drop everything we pushed 462 | 463 | %ifdef PRINT_ENV_ON_DEFINE 464 | ret ; add_env returns the env in ax, dx 465 | %endif 466 | 467 | ; Return empty string 468 | mov ax, atoms.empty 469 | mov dx, type.CSATOM 470 | ret 471 | 472 | ; Set the initial state of the lisp global environment 473 | ; - Adds the true symbol (so it doesn't need to be quoted) 474 | ; - Adds all the primitive function handlers 475 | ; 476 | ; Saves in the global env pointer to a type.CONS 477 | setup_env: 478 | ; Start the global environment with ((t . t) . nil) 479 | push word 0 480 | push word type.NIL 481 | push word atoms.t 482 | push word type.CSATOM 483 | mov ax, atoms.t 484 | mov dx, type.CSATOM 485 | call add_env 486 | add sp, 2*objsize ; clear the stack 487 | 488 | ; Add each primitive function to the env 489 | push si 490 | mov si, primitives 491 | .prim_loop: 492 | cmp byte [cs:si], 0 493 | jz .end_prim 494 | 495 | ; env arg 496 | push ax 497 | push dx 498 | ; the name arg 499 | mov ax, si 500 | mov dx, type.CSATOM 501 | ; Move si forward to the end of the string 502 | .endstr_loop: 503 | cmp byte [cs:si], 0 504 | jz .endstr 505 | inc si 506 | jmp .endstr_loop 507 | .endstr: 508 | ; value arg, the actual primitive function pointer 509 | push word [cs:si+1] 510 | push type.PRIM 511 | call add_env 512 | add sp, 2*objsize ; clear the stack 513 | add si, 3 ; null terminator plus the 2 byte function pointer 514 | jmp .prim_loop 515 | .end_prim: 516 | pop si 517 | 518 | mov word [cs:env], ax ; save the new head of the env list 519 | ret 520 | 521 | ; Creates a pair of elements, the fundamental data type of lisp. 522 | ; 523 | ; The notation for a cons pair is (a . b) and it is used to create linked lists 524 | ; that look like: (a . (b . (c. ()))) for the list (a b c). 525 | ; 526 | ; Internally the elements are any of the typed objects. A CONS type is just the 527 | ; two object elements pushed onto a lisp stack and then a returned pointer to 528 | ; those two elements wrapped in an object with type CONS. 529 | ; 530 | ; The new CONS object is not itself pushed onto the lisp stack. 531 | ; 532 | ; Input: 533 | ; ax,dx - first object element 534 | ; push, push - second object element (push ax, then push dx) 535 | ; 536 | ; Output: 537 | ; ax,dx - the new type.CONS object pointing to the pair 538 | cons: 539 | mov bp, sp 540 | ; An internal version used by higher-order functions to forward their stack to 541 | ; cons instead of pushing it again 542 | ; 543 | ; Input: 544 | ; ax,dx - first object element 545 | ; [bp+4], [bp+2] - second object element 546 | _cons: 547 | ; Push the two values of the CONS pair 548 | mov word [ds:di+0], ax 549 | mov word [ds:di+2], dx 550 | mov ax, [bp+objarg1.value] 551 | mov dx, [bp+objarg1.type] 552 | mov word [ds:di+4], ax 553 | mov word [ds:di+6], dx 554 | add di, 8 555 | 556 | ; TODO: check for lisp stack overflow 557 | 558 | mov ax, di 559 | mov dx, type.CONS 560 | ret 561 | 562 | ; The same as cons but takes the arguments in the opposite order. That way you 563 | ; don't have to do a complicated swap sequence to call it. 564 | ; 565 | ; Input: 566 | ; push - first element object value 567 | ; push - first element object type 568 | ; ax,dx - second object element 569 | reverse_cons: 570 | mov bp, sp 571 | ; Push the two values of the CONS pair 572 | mov word [ds:di+4], ax 573 | mov word [ds:di+6], dx 574 | mov ax, [bp+objarg1.value] 575 | mov dx, [bp+objarg1.type] 576 | mov word [ds:di+0], ax 577 | mov word [ds:di+2], dx 578 | add di, 8 579 | 580 | ; TODO: check for lisp stack overflow 581 | 582 | mov ax, di 583 | mov dx, type.CONS 584 | ret 585 | 586 | _err: 587 | mov ax, atoms.err 588 | mov dx, type.CSATOM 589 | ret 590 | 591 | ; Return the first element of a linked list or cons pair 592 | ; Returns an error if the type is not one of those. 593 | ; 594 | ; Must not clobber cx for some callers 595 | ; 596 | ; Input: ax, dx - the cons or linked list (which is also a cons) 597 | ; Output: ax, dx - the lisp object that was the first element 598 | car: 599 | test dx, type.CONS 600 | jz _err 601 | 602 | mov bx, ax 603 | mov ax, [ds:bx-8] 604 | mov dx, [ds:bx-6] 605 | ret 606 | 607 | ; Return the second element cons, which is the tail of a linked list after 608 | ; cutting off the first element. 609 | ; Returns an error if the type is not cons or a linked list. 610 | ; 611 | ; Must not clobber cx for some callers 612 | ; 613 | ; Input: ax, dx - the cons or linked list (which is also a cons) 614 | ; Output: ax, dx - the lisp object that was the second element 615 | cdr: 616 | test dx, type.CONS 617 | jz _err 618 | 619 | mov bx, ax 620 | mov ax, [ds:bx-4] 621 | mov dx, [ds:bx-2] 622 | ret 623 | 624 | ; Input: 625 | ; push, push - left hand side 626 | ; ax, dx - right hand side 627 | ; Output: bx non-zero if true, zero if false 628 | ; 629 | ; Registers: 630 | ; - does not clobber cx, or bp 631 | obj_equal: 632 | mov bx, sp 633 | cmp [ss:bx+objarg1.value], ax 634 | jne .not_equal 635 | cmp [ss:bx+objarg1.type], dx 636 | jne .not_equal 637 | mov bx, 1 638 | ret 639 | .not_equal: 640 | xor bx, bx 641 | ret 642 | 643 | ; Compare any two objects that have the type.ATOM bits (ATOM or CSATOM) 644 | ; 645 | ; Because we have 2 string types there are 4 possible combinations we might have 646 | ; to compare. This function picks the right routine for each. 647 | ; 648 | ; Note: does not work with type.ERROR 649 | ; 650 | ; Input: 651 | ; push, push - type.ATOM or type.CSATOM 652 | ; ax, dx - type.ATOM or type.CSATOM 653 | ; Output: bx non-zero if true, zero if false 654 | ; 655 | ; Registers: 656 | ; - does not clobber cx (needed by lookup_env) 657 | ; - clobbers ax, dx 658 | atom_equal: 659 | mov bp, sp 660 | mov bx, [bp+objarg1.type] ; memory arg type 661 | 662 | ; Check the basic ATOM bit first 663 | test dx, type.ATOM 664 | jz .not_atom 665 | test bx, type.ATOM 666 | jz .not_atom 667 | 668 | cmp dx, type.CSATOM 669 | je .reg_csatom 670 | ; dx is ATOM 671 | cmp bx, type.CSATOM 672 | je _atom_csatom_equal 673 | jmp _atom_atom_equal 674 | 675 | .reg_csatom: 676 | cmp bx, type.CSATOM 677 | je obj_equal ; both CSATOMs, they always have the same pointer for the string 678 | jmp _csatom_atom_equal 679 | 680 | .not_atom: 681 | ; TODO return error 682 | ret 683 | 684 | ; Input: 685 | ; push, push - type.ATOM 686 | ; ax, dx - type.ATOM 687 | ; Output: bx non-zero if true, zero if false 688 | ; 689 | ; Registers: 690 | ; - does not clobber cx (needed by lookup_env) 691 | ; - clobbers ax, dx 692 | _atom_atom_equal: 693 | mov bp, sp 694 | 695 | ; Check the lengths first 696 | mov bx, [bp+objarg1.type] 697 | cmp bh, dh 698 | jne .not_equal 699 | 700 | ; Size is equal, now check the characters 701 | mov bx, [bp+objarg1.value] 702 | mov bp, ax 703 | .loop: 704 | test dh, dh 705 | jz .equal 706 | 707 | mov byte ah, [es:bp] 708 | mov byte al, [es:bx] 709 | cmp ah, al 710 | jne .not_equal 711 | 712 | inc bx 713 | inc bp 714 | dec dh 715 | 716 | jmp .loop 717 | 718 | .equal: 719 | mov bx, 1 720 | ret 721 | 722 | .not_equal: 723 | xor bx, bx 724 | ret 725 | 726 | 727 | ; Does a string match to compare a CSATOM and a ATOM. 728 | ; Assumes the types were already checked. 729 | ; 730 | ; Input: 731 | ; push, push - type.ATOM 732 | ; ax, dx - type.CSATOM 733 | ; Output: bx non-zero if true, zero if false 734 | ; 735 | ; Registers: 736 | ; - does not clobber cx (needed by lookup_env) 737 | ; - clobbers ax, dx 738 | _csatom_atom_equal: 739 | mov bp, sp 740 | mov bx, [bp+objarg1.value] ; the ATOM pointer 741 | mov dx, [bp+objarg1.type] ; the ATOM length, in dh (and type in dl) 742 | mov bp, ax ; the CSATOM pointer 743 | 744 | .loop: 745 | test dh, dh 746 | jz .end_of_atom 747 | cmp byte [cs:bp], 0 748 | je .not_equal ; since it was not the end of the ATOM 749 | 750 | mov byte ah, [cs:bp] ; CSATOM char 751 | mov byte al, [es:bx] ; ATOM char 752 | cmp ah, al 753 | jne .not_equal 754 | 755 | inc bx 756 | inc bp 757 | dec dh 758 | jmp .loop 759 | 760 | .end_of_atom: 761 | cmp byte [cs:bp], 0 762 | je .equal 763 | ;fallthrough 764 | .not_equal: 765 | xor bx, bx 766 | ret 767 | 768 | .equal: 769 | mov bx, 1 770 | ret 771 | 772 | ; Input: 773 | ; push, push - type.CSATOM 774 | ; ax, dx - type.ATOM 775 | ; Output: bx non-zero if true, zero if false 776 | _atom_csatom_equal: 777 | ; Note: this currently only happens if there's a user-defined symbol in the 778 | ; env and a csatom in the code, which only happens for quote. So this 779 | ; comparison only happens if the user re-defines quote. 780 | ; 781 | ; So just swap the args and call the function we already have. 782 | mov bp, sp 783 | push ax 784 | push dx 785 | mov ax, [bp+objarg1.value] 786 | mov dx, [bp+objarg1.type] 787 | call _csatom_atom_equal 788 | add sp, objsize ; no need to restore ax,dx 789 | ret 790 | 791 | ; Adds a defined pair to an environment, which is a list of pairs. 792 | ; Effectively: takes 3 objects and returns a cons object of ((a.b).c) 793 | ; 794 | ; For the environment the 3 objects are ((name . value) . env) 795 | ; 796 | ; Input: 797 | ; push, push - The env object (push ax then dx) 798 | ; push, push - The value object (push ax then dx, after the env obj) 799 | ; ax,dx - The name object 800 | ; 801 | ; Output: ax, dx - a CONS object for the new env 802 | add_env: 803 | mov bp, sp 804 | call _cons 805 | add bp, objsize 806 | jmp _cons ; takes over the stack frame 807 | 808 | ; Perform a key-value lookup in the env 809 | ; 810 | ; Input: 811 | ; cx - the env to lookup in (assumed to be CONS) 812 | ; ax, dx - the key to lookup 813 | ; 814 | ; Output: ax, dx - the looked-up value from the key 815 | lookup_env: 816 | push cx ; save the original env arg so we don't clobber it 817 | 818 | ; Unfortunately we have to swap the input args, so we can do operations on the 819 | ; env. If we didn't swap here we'd have to swap in both places it is called. 820 | push ax 821 | push dx 822 | mov ax, cx 823 | mov dx, type.CONS 824 | 825 | ; Search the env, linked list of pairs 826 | .loop: 827 | test dx, type.CONS 828 | jz .not_found 829 | 830 | call car ; first pair in env 831 | call car ; first key in env 832 | call atom_equal ; key == search_key 833 | 834 | ; restore the env object 835 | mov ax, cx 836 | mov dx, type.CONS ; type was clobbered by car(car(env)) we tested it was cons 837 | 838 | test bx, bx 839 | jnz .found_match ; end if the key matched 840 | 841 | ; key didn't match, try the next pair in the env 842 | call cdr ; env = cdr(env) 843 | mov cx, ax 844 | 845 | jmp .loop 846 | 847 | .found_match: 848 | call car ; the matching pair 849 | call cdr ; the value of the pair 850 | add sp, objsize ; remove the key from the stack 851 | pop cx ; restore the original env pointer 852 | ret 853 | 854 | .not_found: 855 | ; The lookup key is still on the stack from the swap at the beginning 856 | mov ax, atoms.lookup_err 857 | mov dx, type.CSATOM 858 | call cons ; return (lookup_err . key) 859 | add sp, objsize ; remove the key from the stack 860 | pop cx ; restore the original env pointer 861 | ret 862 | 863 | ; Advances the code input and reads a lisp token which is: 864 | ; - ( 865 | ; - ) 866 | ; - ' 867 | ; - a symbol string 868 | ; 869 | ; Skips all whitespace, which is not part of the token. 870 | ; Length of the token may be 0 if it was the end of the code. 871 | ; 872 | ; Input: 873 | ; [es:si] - the code input 874 | ; 875 | ; Output: 876 | ; [es:bx] - the token string 877 | ; cx - length of the token 878 | scan: 879 | xor cx, cx 880 | 881 | .skip_spaces: 882 | cmp byte [es:si], ' ' 883 | ja .past_spaces ; all ascii chars less than ' ' are whitespace 884 | cmp byte [es:si], 0 885 | je .at_end 886 | inc si 887 | jmp .skip_spaces 888 | 889 | .past_spaces: 890 | mov bx, si 891 | 892 | cmp byte [es:si], '(' 893 | je .special_char 894 | cmp byte [es:si], ')' 895 | je .special_char 896 | cmp byte [es:si], "'" 897 | je .special_char 898 | 899 | .symbol_loop: 900 | inc cx 901 | inc si 902 | cmp byte [es:si], '(' 903 | je .symbol_done 904 | cmp byte [es:si], ')' 905 | je .symbol_done 906 | cmp byte [es:si], ' ' 907 | jbe .symbol_done 908 | jmp .symbol_loop 909 | .symbol_done: 910 | ret 911 | 912 | .special_char: 913 | mov cx, 1 914 | inc si 915 | ret 916 | 917 | .at_end: 918 | mov bx, si 919 | ret 920 | 921 | ; Recursively parse an entire cons pair or a linked list. 922 | ; Assumes that the starting ( was already scanned, and immediately skips it. 923 | ; 924 | ; Input: 925 | ; [es:bx] - The scanned ( token 926 | ; cx - 1, the length of the scanned ( token 927 | ; 928 | ; Output: 929 | ; ax, dx - The object pointing to the parsed list or cons 930 | _parse_list: 931 | call scan ; read the element 932 | cmp byte [es:bx], 0 933 | jne .not_error 934 | mov ax, atoms.open_list_err 935 | mov dx, type.ERROR 936 | ret 937 | .not_error: 938 | cmp byte [es:bx], ')' 939 | jne .not_end 940 | mov ax, 0 941 | mov dx, type.NIL 942 | ret 943 | .not_end: 944 | 945 | cmp byte [es:bx], '.' 946 | jne .not_cons 947 | cmp cx, 1 948 | jne .not_cons 949 | call scan ; skip the . 950 | call parse ; parse the second element 951 | cmp dx, type.ERROR 952 | jne .not_cons_parse_error 953 | ret 954 | .not_cons_parse_error: 955 | call scan 956 | cmp byte [es:bx], ')' 957 | je .correct_cons_ending 958 | mov ax, atoms.open_list_err 959 | mov dx, type.ERROR 960 | .correct_cons_ending: 961 | ret 962 | .not_cons: 963 | 964 | ; Parse an element of the list 965 | call parse 966 | cmp dx, type.ERROR 967 | jne .not_elm_parse_error 968 | ret 969 | .not_elm_parse_error: 970 | push ax 971 | push dx 972 | call _parse_list ; Parse the rest of the list 973 | cmp dx, type.ERROR 974 | je .error 975 | call reverse_cons ; return (first_element . rest_of_the_list) 976 | .error: 977 | add sp, 4 978 | ret 979 | 980 | ; Parses a single lisp expression from the input code. Returns the lisp object 981 | ; for the top-level of the parse tree. E.g. for (a b c) it returns the CONS 982 | ; object that is the start of the linked list. 983 | ; 984 | ; Expects that scan was already called to consume the first token. It sometimes 985 | ; recursively calls itself with or without scan. It parses a full expression, 986 | ; scanning when needed internally, not just the first token. 987 | ; 988 | ; Input: 989 | ; [es:bx] - The first token to parse (result from scan) 990 | ; cx - The length of the first token (result from scan) 991 | ; 992 | ; Output: 993 | ; ax, dx - The object pointing to the parsed lisp expression (not always a cons) 994 | parse: 995 | cmp byte [es:bx], ')' 996 | jne .not_end_error 997 | mov ax, atoms.no_list_err 998 | mov dx, type.ERROR 999 | ret 1000 | .not_end_error: 1001 | 1002 | cmp byte [es:bx], '(' 1003 | jne .not_list 1004 | jmp _parse_list ; takes over our stack frame 1005 | .not_list: 1006 | 1007 | cmp byte [es:bx], "'" 1008 | jne .not_quote 1009 | ; Return (quote . (parse() . nil)) i.e. (quote parse()) 1010 | ; Inner pair (parse() . nil) 1011 | call scan ; skip the ' 1012 | call parse 1013 | cmp dx, type.ERROR 1014 | jne .not_quote_parse_error 1015 | ret 1016 | .not_quote_parse_error: 1017 | push word 0 1018 | push word type.NIL 1019 | call cons 1020 | add sp, objsize 1021 | ; Outer pair (quote . above-result) 1022 | push ax 1023 | push dx 1024 | mov ax, atoms.quote 1025 | mov dx, type.CSATOM 1026 | call cons 1027 | add sp, objsize 1028 | ret 1029 | .not_quote: 1030 | 1031 | %if 0 1032 | cmp byte [es:bx], '0' 1033 | jb .not_int 1034 | cmp byte [es:bx], '9' 1035 | ja .not_int 1036 | ; TODO 1037 | .not_int: 1038 | %endif 1039 | 1040 | cmp cx, 0xFF 1041 | jbe .not_range_error 1042 | mov ax, atoms.symbol_too_long_err 1043 | mov dx, type.ERROR 1044 | ret 1045 | .not_range_error: 1046 | 1047 | mov ax, bx 1048 | mov dh, cl 1049 | mov dl, type.ATOM 1050 | ret 1051 | 1052 | ; Adds entries to the given environment for each of the variable names in the 1053 | ; list mapping them to the evaluated result of the values. 1054 | ; 1055 | ; This is used to give closure arguments values when running the function body. 1056 | ; 1057 | ; Input: 1058 | ; ax, dx - the variable names to bind 1059 | ; cx - the env pointer to extend 1060 | ; push, push - the values to bind to the names 1061 | ; Output: cx - the newly bound env 1062 | bind_variables: 1063 | cmp dx, type.NIL 1064 | je .nil 1065 | 1066 | test dx, type.CONS 1067 | jnz .cons 1068 | mov bp, sp 1069 | 1070 | ; End of the recursion 1071 | ; TODO what should the type be? 1072 | push cx 1073 | push type.CONS 1074 | push word [bp+objarg1.value] 1075 | push word [bp+objarg1.type] 1076 | call add_env 1077 | add sp, 2*objsize 1078 | mov cx, ax 1079 | ret 1080 | 1081 | .nil: 1082 | ret ; just return the env we started with 1083 | 1084 | .cons: 1085 | mov bp, sp 1086 | .orig: equ 4 1087 | push ax 1088 | push dx 1089 | 1090 | push cx 1091 | push type.CONS 1092 | 1093 | mov ax, [bp+objarg1.value] 1094 | mov dx, [bp+objarg1.type] 1095 | call car 1096 | push ax 1097 | push dx 1098 | 1099 | mov ax, [bp-.orig+2] 1100 | mov dx, [bp-.orig] 1101 | call car 1102 | call add_env 1103 | mov cx, ax ; env arg to bind_variables is the add_env result 1104 | add sp, 2*objsize ; remove the add_env args, keeping .orig 1105 | 1106 | mov bp, sp 1107 | add bp, objsize ; restore bp after add_env 1108 | 1109 | mov ax, [bp+objarg1.value] 1110 | mov dx, [bp+objarg1.type] 1111 | call cdr 1112 | push ax 1113 | push dx 1114 | 1115 | mov ax, [bp-.orig+2] 1116 | mov dx, [bp-.orig] 1117 | call cdr 1118 | ; TODO: check for x86 stack overflow 1119 | call bind_variables 1120 | add sp, 2*objsize ; remove bind_variables arg and .orig 1121 | ret 1122 | 1123 | ; Run a function on arguments, it can be a closure or a primitive 1124 | ; 1125 | ; Input: 1126 | ; ax, dx - the function to apply 1127 | ; cx - the env pointer 1128 | ; push, push - the arguments to apply the function to 1129 | ; Output: ax, dx - the result of the function call 1130 | ; 1131 | ; TODO: I think this could be more efficient if we used a native structure in 1132 | ; memory for the 3 parts of the closure instead of using lisp objects. 1133 | apply: 1134 | mov bp, sp 1135 | 1136 | cmp dx, type.PRIM 1137 | jne .not_primitive 1138 | 1139 | mov bx, ax ; the primitive function pointer 1140 | ; the arguments are the first arg to the primitive 1141 | mov ax, [bp+objarg1.value] 1142 | mov dx, [bp+objarg1.type] 1143 | ; the env is in cx already 1144 | jmp bx ; call the prim function ; call ret 1145 | 1146 | .not_primitive: 1147 | 1148 | cmp dx, type.CLOS 1149 | jne .not_closure 1150 | ; It's a closure, run it. This section could be called "reduce" 1151 | 1152 | .orig: equ 4 1153 | push ax 1154 | push dx 1155 | 1156 | ; Evaluate the arguments to get the values to bind to the closure 1157 | mov ax, [bp+objarg1.value] 1158 | mov dx, [bp+objarg1.type] 1159 | call eval_each 1160 | mov bp, sp 1161 | add bp, objsize ; restore bp after eval_each 1162 | push cx ; save the environment so we don't clobber it 1163 | ; push the evaluated arguments for calling bind_variables 1164 | push ax 1165 | push dx 1166 | 1167 | ; For bind_variables, start with the closure env provided (if this is a lambda 1168 | ; inside a lambda, it gets access to the outer lambda arguments as well) 1169 | ; or the global env if there is no outer environment 1170 | mov ax, [bp-.orig+2] 1171 | mov dx, [bp-.orig] 1172 | call cdr 1173 | ; TODO: Am I sure that it's the right thing to do to store nil and then 1174 | ; retrieve cs:env here again? Shouldn't I bind [cs:env] at the time lambda was 1175 | ; called? This way I think there is a difference if you define a lambda in a 1176 | ; lamda then defines made after won't be available in the inner lambda body 1177 | ; but a lambda made at global scope has access to everything defined after. 1178 | cmp dx, type.NIL 1179 | je .use_global_env 1180 | ; TODO: make sure dx is a CONS? 1181 | mov cx, ax 1182 | jmp .env_set 1183 | .use_global_env: 1184 | mov cx, [cs:env] 1185 | .env_set: 1186 | 1187 | ; Bind the argument values to the input variable names 1188 | mov ax, [bp-.orig+2] 1189 | mov dx, [bp-.orig] 1190 | call car 1191 | call car 1192 | call bind_variables 1193 | 1194 | add sp, objsize ; drop the bind_variables arguments 1195 | mov bp, sp 1196 | add bp, 2+objsize ; restore bp so we can get the args 1197 | 1198 | mov ax, [bp-.orig+2] 1199 | mov dx, [bp-.orig] 1200 | 1201 | call car 1202 | call cdr 1203 | call eval ; Evaluate the function body with the bound variables env 1204 | 1205 | pop cx 1206 | add sp, objsize ; drop the saved original ax,dx argument 1207 | ret 1208 | 1209 | .not_closure: 1210 | 1211 | mov ax, atoms.not_fn_err 1212 | mov dx, type.CSATOM 1213 | ret 1214 | 1215 | ; Evaluates each element of the input list (e.g. a list of arguments) 1216 | ; 1217 | ; This is different from eval because the first element is not assumed to be a 1218 | ; function, instead all of the elements are just independent expressions that 1219 | ; get eval'd in order. 1220 | ; 1221 | ; Input: 1222 | ; ax,dx - a list of objects to eval 1223 | ; cx - the env pointer 1224 | ; 1225 | ; Output: 1226 | ; ax,dx - the list of evaluated result objects 1227 | eval_each: ; TODO: switch to the loop implementation 1228 | cmp dx, type.CONS 1229 | je .cons 1230 | 1231 | ; TODO: Are atoms are allowed for single argument functions?? 1232 | test dx, type.ATOM 1233 | jnz .atom 1234 | 1235 | ; TODO: invalid arguments! eval_each should only be passed a list or atom 1236 | ;cmp dx, type.CLOS 1237 | ;cmp dx, type.PRIM 1238 | ; or actually just check != NIL to cover all future types 1239 | ; 1240 | ; How do we know at all the call sites that we are always passing in a list? 1241 | 1242 | ; This ends the recursion since the end of the list is nil 1243 | ; TODO: if we check for errors we don't have to set NIL here 1244 | mov ax, 0 1245 | mov dx, type.NIL 1246 | ret 1247 | 1248 | .atom: 1249 | jmp lookup_env 1250 | 1251 | .cons: 1252 | 1253 | ; Save the original argument (the list to eval) 1254 | ; 1255 | ; 2*objsize because we will push 2 lisp objects onto the stack before we set 1256 | ; bp = sp and then 2 and 4 are the offsets for ax and dx 1257 | .val: equ 2*objsize - 2 1258 | push ax 1259 | .typ: equ 2*objsize - 4 1260 | push dx 1261 | 1262 | ; recursively eval_each the rest of the list 1263 | call cdr 1264 | ; TODO: check for x86 stack overflow 1265 | call eval_each ; bp clobbered 1266 | ; save the result of eval_each, the tail of the result list 1267 | push ax 1268 | push dx 1269 | 1270 | ; Restore the original input list argument 1271 | mov bp, sp 1272 | mov ax, [bp+.val] 1273 | mov dx, [bp+.typ] 1274 | call car ; Get the first element of the list 1275 | call eval ; Evaluate the first element 1276 | 1277 | call cons ; append the first result to the rest of the results 1278 | add sp, 2*objsize 1279 | ret 1280 | 1281 | ; Primarily interprets the first element of a list as a function (closure 1282 | ; actually) and then applies it returning the result. 1283 | ; 1284 | ; eval does this by first recursively evaluating it, which either looks it up in 1285 | ; the env if it's a name, or does the same eval apply until it ends in name 1286 | ; lookups in the env. Having computed the function body, either a closure 1287 | ; created with the lambda primitive or a primitive function pointer, it applies 1288 | ; the function body on the arguments (after first eval_each-ing the arguments), 1289 | ; and returns the result as a replacement for the function call input. 1290 | ; 1291 | ; Also, as mentioned in the paragraph, for an ATOM it just does a lookup in the 1292 | ; env and then returns the stored result. This ends the recursion. 1293 | ; 1294 | ; Input: 1295 | ; ax,dx - the object to eval 1296 | ; cx - the environment pointer, assumed to be type.CONS 1297 | ; 1298 | ; Output: 1299 | ; ax,dx - the evaluated result object 1300 | eval: 1301 | ; TODO: check for x86 stack overflow 1302 | test dx, type.ATOM 1303 | jnz .atom 1304 | test dx, type.CONS 1305 | jnz .cons 1306 | ret ; return the input for all other types 1307 | 1308 | .atom: 1309 | jmp lookup_env ; call ret 1310 | 1311 | .cons: ; all cons are treated as function calls 1312 | ; So we can use the two local variables. 1313 | ; We only need it before we call eval again so no need to save it 1314 | mov bp, sp 1315 | 1316 | ; Save the original input object 1317 | .orig: equ 4 1318 | push ax 1319 | push dx 1320 | 1321 | ; Extract the arguments of function call 1322 | call cdr 1323 | .args: equ 8 ; unused, just to remember what it is 1324 | push ax 1325 | push dx 1326 | 1327 | ; Extract the function name 1328 | mov ax, [bp-.orig+2] 1329 | mov dx, [bp-.orig] 1330 | call car 1331 | ; env is still in cx 1332 | call eval ; evaluate the function name to get the definition (bp is clobbered now) 1333 | call apply ; run the function on the arguments (with bound variables) 1334 | add sp, 2*objsize ; remove the locals we pushed 1335 | ret 1336 | 1337 | ; Print a 16 bit integer in hex to the output buffer 1338 | ; 1339 | ; Input: 1340 | ; ax - the word to print 1341 | ; [ds:di] - the output buffer 1342 | _write_hex_word: 1343 | mov cl, ah 1344 | shr cl, 4 1345 | call .write_hex_char 1346 | mov cl, ah 1347 | and cl, 0x0F 1348 | call .write_hex_char 1349 | mov cl, al 1350 | shr cl, 4 1351 | call .write_hex_char 1352 | mov cl, al 1353 | and cl, 0x0F 1354 | ; fallthrough 1355 | 1356 | ; arg is cl which must be less than 0x10 1357 | .write_hex_char: 1358 | cmp cl, 9 1359 | ja .over_9 1360 | 1361 | add cl, '0' 1362 | mov byte [es:di], cl 1363 | inc di 1364 | ret 1365 | 1366 | .over_9: 1367 | sub cl, 10 1368 | add cl, 'A' 1369 | mov byte [es:di], cl 1370 | inc di 1371 | ret 1372 | 1373 | ; Prints a lisp list or cons object (ax, dx as always) 1374 | _print_list: 1375 | mov byte [es:di], '(' 1376 | inc di 1377 | 1378 | .loop: 1379 | ; Print the first element of the list 1380 | push ax 1381 | push dx 1382 | call car 1383 | call print 1384 | pop dx 1385 | pop ax 1386 | 1387 | ; Handle the remaining elements 1388 | call cdr 1389 | test dx, type.NIL 1390 | jnz .done 1391 | 1392 | cmp dx, type.CONS 1393 | je .next_element 1394 | 1395 | mov word [es:di], " ." 1396 | mov byte [es:di+2], ' ' 1397 | add di, 3 1398 | call print 1399 | jmp .done 1400 | 1401 | .next_element: 1402 | mov byte [es:di], ' ' 1403 | inc di 1404 | jmp .loop 1405 | 1406 | .done: 1407 | mov byte [es:di], ')' 1408 | inc di 1409 | ret 1410 | 1411 | ; Writes a string version of a lisp object 1412 | ; 1413 | ; This still requires all 4 segment registers to be unmodified. So the output 1414 | ; buffer must be at the end of the lisp memory. Print needs to be able to find 1415 | ; the strings and walk the list pointers which are spread among the segments. 1416 | ; 1417 | ; Input: 1418 | ; ax, dx - the lisp object to print 1419 | ; [es:di] - the output string location 1420 | ; si - the input code segment (needed for ATOM strings) 1421 | ; 1422 | ; Output: 1423 | ; [es:di] - one past the end of the string printed 1424 | print: 1425 | cmp dx, type.NIL 1426 | jne .not_nil 1427 | mov word [es:di], "()" 1428 | add di, 2 1429 | ret 1430 | .not_nil: 1431 | 1432 | cmp dx, type.ERROR 1433 | je .cs_atom 1434 | cmp dx, type.CSATOM 1435 | jne .not_csatom 1436 | .cs_atom: 1437 | mov bx, ax 1438 | .copy_cs_str: 1439 | mov al, [cs:bx] 1440 | cmp al, 0 1441 | je .end_cs_str 1442 | mov [es:di], al 1443 | inc di 1444 | inc bx 1445 | jmp .copy_cs_str 1446 | .end_cs_str: 1447 | ret 1448 | .not_csatom: 1449 | 1450 | test dx, type.ATOM 1451 | jz .not_atom 1452 | ; ATOM strings are in the input code location, we need to swap out lisp stack 1453 | ; segment temporarily so we can use more memory. 1454 | push ds 1455 | mov ds, si 1456 | mov bx, ax 1457 | .copy_str_len: 1458 | test dh, dh 1459 | jz .end_str_len 1460 | mov al, [ds:bx] 1461 | mov [es:di], al 1462 | inc di 1463 | inc bx 1464 | dec dh 1465 | jmp .copy_str_len 1466 | .end_str_len: 1467 | ; restore the lisp stack 1468 | pop bx 1469 | mov ds, bx 1470 | ret 1471 | .not_atom: 1472 | 1473 | ; TODO: should I just print the prim name, then the print output will actually 1474 | ; be valid lisp code with this interpreter. The problem is we don't actually 1475 | ; parse {HEX} syntax. Or maybe we should parse that later and allow users to 1476 | ; define their own primitives. 1477 | cmp dx, type.PRIM 1478 | jne .not_prim 1479 | mov byte [es:di], '{' 1480 | inc di 1481 | call _write_hex_word 1482 | mov byte [es:di], '}' 1483 | inc di 1484 | ret 1485 | .not_prim: 1486 | 1487 | cmp dx, type.CONS 1488 | jne .not_cons 1489 | jmp _print_list ; call ret 1490 | .not_cons: 1491 | 1492 | cmp dx, type.CLOS 1493 | jne .not_clos 1494 | jmp _print_list ; TODO print closure 1495 | .not_clos: 1496 | 1497 | ret 1498 | -------------------------------------------------------------------------------- /lisp/tester.asm: -------------------------------------------------------------------------------- 1 | %include "bootloader/bootsect.asm" 2 | 3 | extra_sectors_start: 4 | 5 | %include "lisp/lisp.asm" 6 | 7 | %define LISP_DATA_LOC (CODE_SEGMENT+(SECTOR_SIZE/0x10)*(NUM_EXTRA_SECTORS+1)) 8 | %define OUTPUT_LOC LISP_DATA_LOC+NEXT_SEGMENT 9 | 10 | start_: 11 | ; set [es:si] to the lisp code to run 12 | mov ax, CODE_SEGMENT 13 | mov es, ax 14 | mov si, input_code 15 | ; Set the output buffer location 16 | mov word [cs:output_seg], OUTPUT_LOC 17 | mov word [cs:output_addr], 0 18 | xor di, di 19 | ; Set the lisp data location 20 | mov ax, LISP_DATA_LOC 21 | mov ds, ax 22 | 23 | call setup_env 24 | 25 | .test_loop: 26 | mov ax, [cs:output_addr] 27 | push ax 28 | 29 | .continue: 30 | call repl_step 31 | test cx, cx 32 | jz .end_of_file 33 | mov bp, sp 34 | mov cx, [ss:bp] 35 | cmp cx, [cs:output_addr] 36 | je .continue 37 | 38 | ; TODO: stolen from scan: 39 | .skip_spaces: 40 | cmp byte [es:si], ' ' 41 | ja .past_spaces ; all ascii chars less than ' ' are whitespace 42 | cmp byte [es:si], 0 43 | je .end_of_file 44 | inc si 45 | jmp .skip_spaces 46 | .past_spaces: 47 | 48 | cmp byte [es:si], 'w' 49 | jne .test_file_error 50 | cmp byte [es:si+1], 'a' 51 | jne .test_file_error 52 | cmp byte [es:si+2], 'n' 53 | jne .test_file_error 54 | cmp byte [es:si+3], 't' 55 | jne .test_file_error 56 | cmp byte [es:si+4], ':' 57 | jne .test_file_error 58 | cmp byte [es:si+5], ' ' 59 | jne .test_file_error 60 | add si, 6 61 | 62 | ; Compare [es:si] with the output at [ds:di] 63 | pop di 64 | push si 65 | push di 66 | mov ax, OUTPUT_LOC 67 | mov ds, ax 68 | .compare_loop: 69 | 70 | mov byte al, [es:si] 71 | cmp byte al, [ds:di] 72 | jne .fail 73 | 74 | cmp byte [ds:di], `\n` 75 | je .match 76 | cmp byte [ds:di], 0 77 | je .match 78 | 79 | inc di 80 | inc si 81 | jmp .compare_loop 82 | 83 | .fail: 84 | 85 | ; common print_string args 86 | xor dx, dx 87 | mov bx, 7 ; black background ; grey text 88 | 89 | mov bp, fail_str 90 | mov cx, fail_str_len 91 | call print_string 92 | add dx, fail_str_len 93 | 94 | ; Print the got string 95 | mov ax, ds 96 | mov es, ax 97 | pop di 98 | mov bp, di 99 | .str_len: 100 | inc bp 101 | cmp byte [es:bp], `\n` 102 | jne .str_len 103 | mov cx, bp 104 | sub cx, di 105 | mov bp, di 106 | call print_string 107 | add dx, cx 108 | 109 | mov ax, CODE_SEGMENT 110 | mov es, ax 111 | mov bp, want_str 112 | mov cx, want_str_len 113 | call print_string 114 | add dx, want_str_len 115 | 116 | pop si 117 | mov bp, si 118 | .str_len2: 119 | inc bp 120 | cmp byte [es:bp], `\n` 121 | je .found_len 122 | cmp byte [es:bp], 0 123 | je .found_len 124 | jmp .str_len2 125 | .found_len: 126 | mov cx, bp 127 | sub cx, si 128 | mov bp, si 129 | call print_string 130 | 131 | mov al, `\n` 132 | call print_char 133 | 134 | jmp halt 135 | 136 | ; fallthrough 137 | .match: 138 | pop ax ; clear the stack 139 | 140 | ; Reset and go again 141 | mov ax, LISP_DATA_LOC 142 | mov ds, ax 143 | xor di, di 144 | call setup_env 145 | cmp byte [es:si], 0 146 | jne .test_loop 147 | .end_of_file: 148 | 149 | jmp pass 150 | 151 | 152 | 153 | .test_file_error: ; TODO 154 | mov bp, fail_str 155 | mov cx, fail_str_len 156 | call print_string 157 | add dx, fail_str_len 158 | 159 | mov al, [es:si] 160 | call print_char 161 | mov al, `\n` 162 | call print_char 163 | 164 | ;fallthrough 165 | 166 | halt: 167 | %ifdef HEADLESS 168 | ; qemu shutdown 169 | mov ax, 0x2000 170 | mov dx, 0x604 171 | out dx, ax 172 | ; older qemu shutdown 173 | mov dx, 0xB004 174 | out dx, ax 175 | %endif 176 | jmp $ 177 | 178 | print_char: 179 | %ifdef DEBUGCON 180 | out QEMU_DEBUG_PORT, al 181 | %endif 182 | 183 | mov ah, BIOS_PRINT_CHAR 184 | int 0x10 185 | ret 186 | 187 | ; [es:bp] = string to print 188 | ; cx = length to print 189 | ; dx = cursor position to print (doesn't affect qemu) 190 | ; bx = color (doesn't affect qemu) 191 | print_string: 192 | %ifdef DEBUGCON 193 | push cx 194 | push si 195 | xor si, si 196 | .string_loop: 197 | mov byte al, [es:bp+si] 198 | out QEMU_DEBUG_PORT, al 199 | dec cx 200 | inc si 201 | test cx, cx 202 | jnz .string_loop 203 | pop si 204 | pop cx 205 | %endif 206 | 207 | mov ah, BIOS_PRINT_STRING 208 | mov al, 1 ; no attr bytes, move cursor 209 | int 0x10 210 | ret 211 | 212 | pass: 213 | mov ax, cs 214 | mov es, ax 215 | mov bp, pass_str 216 | mov cx, pass_str_len 217 | mov dx, pass_pos 218 | mov bl, 0x02 ; black bg green text 219 | xor bh, bh 220 | call print_string 221 | 222 | ; Hide the cursor by moving it off screen 223 | mov ah, 0x02 224 | mov dx, 0xFFFF 225 | xor bh, bh 226 | int 0x10 ; dx is the cursor position 227 | 228 | jmp halt 229 | 230 | fail_str: db "FAIL! got: " 231 | fail_str_len: equ $-fail_str 232 | 233 | want_str: db " want: " 234 | want_str_len: equ $-want_str 235 | 236 | ; 21 spaces to let us center the 38 chars wide str in 80 chars with one print 237 | pass_str: 238 | db " ######## ######## ######## ########",`\n\r` 239 | db " ## ## ## ## ## ## ",`\n\r` 240 | db " ## ## ## ## ## ## ",`\n\r` 241 | db " ######## ######## ######## ########",`\n\r` 242 | db " ## ## ## ## ##",`\n\r` 243 | db " ## ## ## ## ##",`\n\r` 244 | db " ## ## ## ## ##",`\n\r` 245 | db " ## ## ## ######## ########",`\n` 246 | pass_str_len: equ $-pass_str 247 | pass_pos: equ 0x0800 ; row 8 column 0 to get it centered 248 | 249 | input_code: 250 | incbin "lisp/data-for-test.lisp" 251 | db 0 252 | 253 | NUM_EXTRA_SECTORS: equ NUM_SECTORS(extra_sectors_start) 254 | -------------------------------------------------------------------------------- /reference_manuals/add_bookmarks_to_bios_manual.tex: -------------------------------------------------------------------------------- 1 | % To run: pdflatex "\\def\\filename{$FILENAME}\\input{$THIS_FILE}" 2 | % Huge thanks to https://web.archive.org/web/20120616001456/https://michaelgoerz.net/blog/2011/04/pdf-bookmarks-with-latex/ 3 | % which this entire file is templated on 4 | \documentclass{article} 5 | \usepackage[utf8]{inputenc} 6 | \usepackage{pdfpages} 7 | \usepackage[ 8 | pdfpagelabels=true, 9 | pdftitle={PS/2 and PC BIOS Interface Technical Reference}, 10 | pdfauthor={IBM} 11 | ]{hyperref} 12 | \usepackage{bookmark} 13 | 14 | \begin{document} 15 | \includepdf[pages=1-]{\filename} 16 | 17 | \bookmark[page=01,level=0]{BIOS} 18 | \bookmark[page=08,level=1]{Contents} 19 | \bookmark[page=14,level=1]{Introduction} 20 | \bookmark[page=18,level=1]{Interrupts} 21 | \bookmark[page=22,level=2]{int 02H - Nonmaskable Interrupt (NMI)} 22 | \bookmark[page=24,level=2]{int 05H - Print Screen} 23 | \bookmark[page=25,level=2]{int 08H - System Timer} 24 | \bookmark[page=26,level=2]{int 09H - Keyboard} 25 | \bookmark[page=28,level=2]{int 10H - Video} 26 | \bookmark[page=29,level=3]{ah = 00H - Set Mode} 27 | \bookmark[page=33,level=3]{ah = 01H - Set Cursor Type} 28 | \bookmark[page=33,level=3]{ah = 02H - Set Cursor Position} 29 | \bookmark[page=33,level=3]{ah = 03H - Read Cursor Position} 30 | \bookmark[page=34,level=3]{ah = 04H - Read Light Pen Position} 31 | \bookmark[page=34,level=3]{ah = 05H - Select Active Display Page} 32 | \bookmark[page=35,level=3]{ah = 06H - Scroll Active Page Up} 33 | \bookmark[page=35,level=3]{ah = 07H - Scroll Active Page Down} 34 | \bookmark[page=35,level=3]{ah = 08H - Read Attr/Char at Current Cursor Position} 35 | \bookmark[page=35,level=3]{ah = 09H - Write Attr/Char at Current Cursor Position} 36 | \bookmark[page=36,level=3]{ah = 0AH - Write Character at Current Cursor Position} 37 | \bookmark[page=36,level=3]{ah = 0BH - Set Color Palette} 38 | \bookmark[page=37,level=3]{ah = 0CH - Write Dot} 39 | \bookmark[page=37,level=3]{ah = 0DH - Read Dot} 40 | \bookmark[page=37,level=3]{ah = 0EH - Write Teletype to Active Page} 41 | \bookmark[page=38,level=3]{ah = 0FH - Read Current Video State} 42 | \bookmark[page=38,level=3]{ah = 10H - Set Palette Registers} 43 | \bookmark[page=42,level=3]{ah = 11H - Character Generator} 44 | \bookmark[page=50,level=3]{ah = 12H - Alternate Select} 45 | \bookmark[page=54,level=3]{ah = 13H - Write String} 46 | \bookmark[page=55,level=3]{ah = 14H - Load LCD Character Font} 47 | \bookmark[page=55,level=3]{ah = 15H - Return Physical Display Parameters for Active Display} 48 | \bookmark[page=56,level=3]{ah = 1AH - Read/Write Display Combination Code} 49 | \bookmark[page=58,level=3]{ah = 1BH - Return Functionality/State Information} 50 | \bookmark[page=62,level=3]{ah = 1CH - Save/Restore Video State} 51 | \bookmark[page=63,level=2]{int 11H - Equipment Determination} 52 | \bookmark[page=64,level=2]{int 12H - Memory Size Determination} 53 | \bookmark[page=65,level=2]{int 13H - Diskette} 54 | \bookmark[page=66,level=3]{ah = 00H - Read Diskette System} 55 | \bookmark[page=66,level=3]{ah = 01H - Read Status of Last Operation} 56 | \bookmark[page=67,level=3]{ah = 02H - Read Desired Sectors into Memory} 57 | \bookmark[page=67,level=3]{ah = 03H - Write Desired Sectors from Memory} 58 | \bookmark[page=68,level=3]{ah = 04H - Verify Desired Sectors} 59 | \bookmark[page=68,level=3]{ah = 05H - Format Desired Track} 60 | \bookmark[page=69,level=3]{ah = 08H - Read Drive Parameters} 61 | \bookmark[page=71,level=3]{ah = 15H - Read DASD Type} 62 | \bookmark[page=71,level=3]{ah = 16H - Diskette Change Line Status} 63 | \bookmark[page=72,level=3]{ah = 17H - Set DASD Type for Format} 64 | \bookmark[page=73,level=3]{ah = 18H - Set Media Type for Format} 65 | \bookmark[page=75,level=2]{int 13H - Fixed Disk} 66 | \bookmark[page=76,level=3]{ah = 00H - Reset Disk System} 67 | \bookmark[page=77,level=3]{ah = 01H - Read Status of Last Operation} 68 | \bookmark[page=77,level=3]{ah = 02H - Read Desired Sectors into Memory} 69 | \bookmark[page=78,level=3]{ah = 03H - Write Desired Sectors from Memory} 70 | \bookmark[page=78,level=3]{ah = 04H - Verify Desired Sectors} 71 | \bookmark[page=79,level=3]{ah = 05H - Format Desired Cylinder} 72 | \bookmark[page=80,level=3]{ah = 06H - Format Desired Cylinder and Set Bad Sector Flags} 73 | \bookmark[page=80,level=3]{ah = 07H - Format Drive Starting at Desired Cylinder} 74 | \bookmark[page=81,level=3]{ah = 08H - Read Drive Parameters} 75 | \bookmark[page=81,level=3]{ah = 09H - Initialize Drive Pair Characteristics} 76 | \bookmark[page=82,level=3]{ah = 0CH - Seek} 77 | \bookmark[page=83,level=3]{ah = 0DH - Alternate Disk Reset} 78 | \bookmark[page=83,level=3]{ah = 10H - Test Drive Ready} 79 | \bookmark[page=83,level=3]{ah = 11H - Recalibrate} 80 | \bookmark[page=84,level=3]{ah = 15H - Read DASO Type} 81 | \bookmark[page=84,level=3]{ah = 19H - Park Heads} 82 | \bookmark[page=84,level=3]{ah = 1AH - Format Unit} 83 | \bookmark[page=86,level=2]{int 14H - Asynchronous Communications} 84 | \bookmark[page=86,level=3]{ah = 00H - Initialize the Communications Port} 85 | \bookmark[page=87,level=3]{ah = 01H - Send Character} 86 | \bookmark[page=87,level=3]{ah = 02H - Receive Character} 87 | \bookmark[page=88,level=3]{ah = 03H - Read Status} 88 | \bookmark[page=88,level=3]{ah = 04H - Extended Initialize} 89 | \bookmark[page=89,level=3]{ah = 05H - Extended Communications Port Control} 90 | \bookmark[page=90,level=2]{int 15H - System Services} 91 | \bookmark[page=091,level=3]{ah = 00H - Turn Cassette Motor On} 92 | \bookmark[page=091,level=3]{ah = 01H - Turn Cassette Motor Off} 93 | \bookmark[page=091,level=3]{ah = 02H - Read Blocks from Cassette} 94 | \bookmark[page=092,level=3]{ah = 03H - Write Blocks to Cassette} 95 | \bookmark[page=092,level=3]{ah = 0FH - Format Unit Periodic Interrupt} 96 | \bookmark[page=093,level=3]{ah = 21H - Power-On Self-Test Error Log} 97 | \bookmark[page=094,level=3]{ah = 40H - Read/Modify Profiles} 98 | \bookmark[page=095,level=3]{ah = 41H - Wait for External Event} 99 | \bookmark[page=095,level=3]{ah = 42H - Request System Power-Off} 100 | \bookmark[page=096,level=3]{ah = 43H - Read System Status} 101 | \bookmark[page=097,level=3]{ah = 44H - Activate/Deactivate Internal Modem Power} 102 | \bookmark[page=097,level=3]{ah = 4FH - Keyboard Intercept} 103 | \bookmark[page=098,level=3]{ah = 80H - Device Open} 104 | \bookmark[page=099,level=3]{ah = 81H - Device Close} 105 | \bookmark[page=099,level=3]{ah = 82H - Program Termination} 106 | \bookmark[page=099,level=3]{ah = 83H - Event Wait} 107 | \bookmark[page=100,level=3]{ah = 84H - Joystick Support} 108 | \bookmark[page=101,level=3]{ah = 85H - System Request Key Pressed} 109 | \bookmark[page=102,level=3]{ah = 86H - Wait} 110 | \bookmark[page=102,level=3]{ah = 87H - Move Block} 111 | \bookmark[page=104,level=3]{ah = 88H - Extended Memory Size Determine} 112 | \bookmark[page=105,level=3]{ah = 89H - Switch Processor to Protected Mode} 113 | \bookmark[page=108,level=3]{ah = 90H - Device Busy} 114 | \bookmark[page=109,level=3]{ah = 91H - Interrupt Complete} 115 | \bookmark[page=109,level=3]{ah = C0H - Return System Configuration Parameters} 116 | \bookmark[page=111,level=3]{ah = C1H - Return Extended BIOS Data Area Segment Address} 117 | \bookmark[page=112,level=3]{ah = C2H - Pointing Device BIOS Interface} 118 | \bookmark[page=116,level=3]{ah = C3H - Enable/Disable Watchdog Time-Out} 119 | \bookmark[page=117,level=3]{ah = C4H - Programmable Option Select} 120 | \bookmark[page=119,level=2]{int 16H - Keyboard} 121 | \bookmark[page=121,level=3]{ah = 00H - Keyboard Read} 122 | \bookmark[page=121,level=3]{ah = 01H - Keystroke Status} 123 | \bookmark[page=122,level=3]{ah = 02H - Shift Status} 124 | \bookmark[page=122,level=3]{ah = 03H - Set Typematic Rate} 125 | \bookmark[page=123,level=3]{ah = 04H - Keyboard Click Adjustment} 126 | \bookmark[page=123,level=3]{ah = 05H - Keyboard Write} 127 | \bookmark[page=124,level=3]{ah = 09H - Keyboard Functionality Determination} 128 | \bookmark[page=125,level=3]{ah = 10H - Extended Keyboard Read} 129 | \bookmark[page=125,level=3]{ah = 11H - Extended Keystroke Status} 130 | \bookmark[page=126,level=3]{ah = 12H - Extended Shift Status} 131 | \bookmark[page=127,level=2]{int 17H - Printer} 132 | \bookmark[page=127,level=3]{ah = 00H - Print Character} 133 | \bookmark[page=127,level=3]{ah = 01H - Initialize the Printer Port} 134 | \bookmark[page=128,level=3]{ah = 02H - Read Status} 135 | \bookmark[page=130,level=2]{int 19H - Bootstrap Loader} 136 | \bookmark[page=131,level=2]{int 1AH - System-Timer and Real-Time Clock Services} 137 | \bookmark[page=131,level=3]{ah = 00H - Read System-Timer Time Counter} 138 | \bookmark[page=132,level=3]{ah = 01H - Set System-Timer Time Counter} 139 | \bookmark[page=132,level=3]{ah = 02H - Read Real-Time Clock Time} 140 | \bookmark[page=133,level=3]{ah = 03H - Set Real-Time Clock Time} 141 | \bookmark[page=133,level=3]{ah = 04H - Read Real-Time Clock Date} 142 | \bookmark[page=133,level=3]{ah = 05H - Set Real-Time Clock Date} 143 | \bookmark[page=134,level=3]{ah = 06H - Set Real-Time Clock Alarm} 144 | \bookmark[page=134,level=3]{ah = 07H - Reset Real-Time Clock Alarm} 145 | \bookmark[page=135,level=3]{ah = 08H - Set Real-Time Clock Activated Power-On Mode} 146 | \bookmark[page=135,level=3]{ah = 09H - Read Real-Time Clock Alarm Time and Status} 147 | \bookmark[page=136,level=3]{ah = 0AH - Read System-Timer Day Counter} 148 | \bookmark[page=136,level=3]{ah = 0BH - Set System-Timer Day Counter} 149 | \bookmark[page=137,level=3]{ah = 80H - Set Up Sound Multiplexer} 150 | \bookmark[page=138,level=2]{int 70H - Real-Time Clock Interrupt} 151 | \bookmark[page=140,level=1]{Data Areas and ROM Tables} 152 | \bookmark[page=142,level=2]{BIOS Data Area} 153 | \bookmark[page=156,level=2]{Extended BIOS Data Area} 154 | \bookmark[page=157,level=2]{ROM Tables} 155 | \bookmark[page=157,level=3]{Fixed Disk Drive Parameter Table} 156 | \bookmark[page=165,level=3]{Diskette Drive Parameter Table} 157 | \bookmark[page=166,level=1]{Additional Information} 158 | \bookmark[page=168,level=2]{Interrupt Sharing} 159 | \bookmark[page=177,level=2]{Adapter ROM} 160 | \bookmark[page=179,level=2]{Video Function Compatibility} 161 | \bookmark[page=181,level=2]{Multitasking Provisions} 162 | \bookmark[page=183,level=2]{System Identification} 163 | \bookmark[page=184,level=2]{Application Guidelines} 164 | \bookmark[page=189,level=2]{Scan Code/Character Code Combinations} 165 | \bookmark[page=200,level=1]{Index} 166 | 167 | \bookmark[page=210,level=0]{Advanced BIOS} 168 | \bookmark[page=210,level=1]{Contents} 169 | \bookmark[page=214,level=1]{Introduction} 170 | \bookmark[page=216,level=2]{Introduction} 171 | \bookmark[page=217,level=2]{Data Structures} 172 | \bookmark[page=218,level=2]{Initialization} 173 | \bookmark[page=219,level=2]{Transfer Conventions} 174 | \bookmark[page=220,level=2]{Interrupt Processing} 175 | \bookmark[page=221,level=2]{Extending ABIOS} 176 | \bookmark[page=222,level=1]{Data Structures} 177 | \bookmark[page=224,level=2]{Introduction} 178 | \bookmark[page=224,level=2]{Common Data Area} 179 | \bookmark[page=228,level=2]{Function Transfer Table} 180 | \bookmark[page=230,level=2]{Device Block} 181 | \bookmark[page=236,level=1]{Initialization} 182 | \bookmark[page=238,level=2]{Introduction} 183 | \bookmark[page=239,level=2]{Build System Parameters Table - Operating System} 184 | \bookmark[page=239,level=2]{Build System Parameters Table - BIOS} 185 | \bookmark[page=241,level=2]{Build Initialization Table - Operating System} 186 | \bookmark[page=241,level=2]{Build Initialization Table - BIOS} 187 | \bookmark[page=244,level=2]{Build Common Data Area - Operating System} 188 | \bookmark[page=245,level=2]{Initialize Pointers - Operating System} 189 | \bookmark[page=246,level=2]{Initialize Data Structures - ABIOS} 190 | \bookmark[page=248,level=2]{Logical ID 2 Initialization} 191 | \bookmark[page=249,level=2]{Build Protected Mode Tables} 192 | \bookmark[page=252,level=1]{Transfer Conventions} 193 | \bookmark[page=254,level=2]{Introduction} 194 | \bookmark[page=254,level=2]{Request Block} 195 | \bookmark[page=256,level=3]{Functional Parameters} 196 | \bookmark[page=256,level=3]{Service Specific Parameters} 197 | \bookmark[page=265,level=2]{ABIOS Transfer Convention} 198 | \bookmark[page=267,level=2]{Operating System Transfer Convention} 199 | \bookmark[page=270,level=1]{Additional Information} 200 | \bookmark[page=272,level=2]{Interrupt Processing} 201 | \bookmark[page=272,level=3]{Interrupt Flow} 202 | \bookmark[page=272,level=3]{Interrupt Sharing} 203 | \bookmark[page=274,level=3]{Default Interrupt Handler} 204 | \bookmark[page=275,level=2]{Adding, Patching, Extending, and Replacing} 205 | \bookmark[page=276,level=3]{Adapter ROM Structure} 206 | \bookmark[page=278,level=3]{RAM Extension Structure} 207 | \bookmark[page=281,level=3]{Adding} 208 | \bookmark[page=281,level=3]{Patching} 209 | \bookmark[page=283,level=3]{Extending} 210 | \bookmark[page=285,level=3]{Replacing} 211 | \bookmark[page=287,level=3]{Considerations for RAM Extensions} 212 | \bookmark[page=289,level=2]{Operating Systems Implementation Considerations} 213 | \bookmark[page=289,level=3]{ABIOS Rules} 214 | \bookmark[page=291,level=3]{Considerations for Bimodal Implementations} 215 | \bookmark[page=296,level=1]{Interfaces} 216 | \bookmark[page=298,level=2]{Introduction} 217 | \bookmark[page=300,level=2]{Diskette} 218 | \bookmark[page=313,level=3]{Return Codes} 219 | \bookmark[page=313,level=3]{Programming Considerations} 220 | \bookmark[page=316,level=2]{Disk} 221 | \bookmark[page=324,level=3]{Return Codes} 222 | \bookmark[page=326,level=3]{Programming Considerations} 223 | \bookmark[page=328,level=2]{Video} 224 | \bookmark[page=343,level=3]{Return Codes} 225 | \bookmark[page=343,level=3]{Video Modes} 226 | \bookmark[page=344,level=2]{Keyboard} 227 | \bookmark[page=353,level=3]{Return Codes} 228 | \bookmark[page=353,level=3]{Programming Considerations} 229 | \bookmark[page=356,level=2]{Parallel Port} 230 | \bookmark[page=361,level=3]{Return Codes} 231 | \bookmark[page=362,level=3]{Programming Considerations} 232 | \bookmark[page=364,level=2]{Asynchronous Communications} 233 | \bookmark[page=386,level=3]{Return Codes} 234 | \bookmark[page=386,level=3]{Programming Considerations} 235 | \bookmark[page=392,level=2]{System Timer} 236 | \bookmark[page=393,level=3]{Return Codes} 237 | \bookmark[page=393,level=3]{Programming Considerations} 238 | \bookmark[page=394,level=2]{Real-Time Clock} 239 | \bookmark[page=402,level=3]{Return Codes} 240 | \bookmark[page=402,level=3]{Programming Considerations} 241 | \bookmark[page=404,level=2]{System Services} 242 | \bookmark[page=407,level=3]{Return Codes} 243 | \bookmark[page=408,level=2]{Nonmaskable Interrupt (NMI)} 244 | \bookmark[page=410,level=3]{Return Codes} 245 | \bookmark[page=412,level=2]{Pointing Device} 246 | \bookmark[page=418,level=3]{Return Codes} 247 | \bookmark[page=419,level=3]{Programming Considerations} 248 | \bookmark[page=420,level=2]{Nonvolatile Random Access Memory (NVRAM)} 249 | \bookmark[page=423,level=3]{Return Codes} 250 | \bookmark[page=423,level=3]{Programming Considerations} 251 | \bookmark[page=424,level=2]{Direct Memory Access (DMA)} 252 | \bookmark[page=431,level=3]{Return Codes} 253 | \bookmark[page=431,level=3]{Programming Considerations} 254 | \bookmark[page=434,level=2]{Programmable Option Select (POS)} 255 | \bookmark[page=439,level=3]{Return Codes} 256 | \bookmark[page=440,level=2]{Keyboard Security} 257 | \bookmark[page=445,level=3]{Return Codes} 258 | \bookmark[page=446,level=1]{Index} 259 | \bookmark[page=462,level=1]{Asynchronous Communications Supplement} 260 | \bookmark[page=464,level=1]{Programmable Option Select Supplement} 261 | 262 | \end{document} 263 | -------------------------------------------------------------------------------- /reference_manuals/get_docs: -------------------------------------------------------------------------------- 1 | BIOS_DOC_NAME=IBM_BIOS_Manual_1988.pdf 2 | VGA_DOC_NAME=IBM_VGA_Manual_1992.pdf 3 | 4 | echo -n "The official Intel Software Developer's Manual and the Optimization Manual can be downloaded from:" 5 | echo -n "https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html" 6 | echo -n "If the links haven't moved this script will download them automatically." 7 | 8 | wget -nc --no-check-certificate https://cdrdv2-public.intel.com/789583/325462-sdm-vol-1-2abcd-3abcd-4.pdf -O Intel_Software_Developers_Manual.pdf 9 | wget -nc http://bitsavers.trailing-edge.com/pdf/ibm/pc/ps2/15F0306_PS2_and_PC_BIOS_Interface_Technical_Reference_May88.pdf -O $BIOS_DOC_NAME 10 | 11 | echo -n "Do you want the Intel Optimization Manual? [y/n]: " 12 | read ans 13 | if [[ $ans =~ ^[Yy] ]]; then 14 | wget -nc --no-check-certificate https://cdrdv2-public.intel.com/671488/248966-Software-Optimization-Manual-V1-048.pdf -O Intel_Optimization_Manual.pdf 15 | fi 16 | 17 | VGA_MANUAL=0 18 | echo -n "Do you want the IBM VGA (Graphics Adapter) Manual? (not used in this repo) [y/n]: " 19 | read ans 20 | if [[ $ans =~ ^[Yy] ]]; then 21 | VGA_MANUAL=1 22 | wget -nc http://bitsavers.trailing-edge.com/pdf/ibm/pc/cards/IBM_VGA_XGA_Technical_Reference_Manual_May92.pdf -O $VGA_DOC_NAME 23 | fi 24 | 25 | echo -n "Add bookmarks to the IBM BIOS manual? (using pdflatex) [y/n]: " 26 | read ans 27 | if [[ $ans =~ ^[Yy] ]]; then 28 | echo 29 | echo "Uses the packages: inputenc, pdfpages, bookmark, hyperref" 30 | echo "You may have to install them and run this script again" 31 | echo 32 | 33 | pdflatex --jobname=temp --interaction=batchmode \ 34 | "\\def\\filename{$BIOS_DOC_NAME}\\input{add_bookmarks_to_bios_manual.tex}" \ 35 | && rm temp.{log,aux} \ 36 | && rm $BIOS_DOC_NAME && mv temp.pdf $BIOS_DOC_NAME 37 | #if [ $VGA_MANUAL -eq 1 ]; then 38 | # pdflatex --jobname=temp --interaction=batchmode \ 39 | # "\\def\\filename{$VGA_DOC_NAME}\\input{add_bookmarks_to_vga_manual.tex}" \ 40 | # && rm temp.{log,aux} \ 41 | # && rm $VGA_DOC_NAME && mv temp.pdf $VGA_DOC_NAME 42 | #fi 43 | fi 44 | -------------------------------------------------------------------------------- /test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$#" -eq 0 ]; then 4 | printf "Usage: $0 [filename] [any other nasm args].\nEx: $0 bootstrap-asm.asm -DDVORAK\n" 5 | exit 1 6 | fi 7 | 8 | file=${@: -1} 9 | name="${file%%.*}" 10 | binfile="bin/$name.bin" 11 | 12 | HEADLESS_ARGS="" 13 | if [ "$1" = "-DHEADLESS" ]; then 14 | HEADLESS_ARGS="-display none" 15 | fi 16 | 17 | PID=$$ 18 | compile() { 19 | nasm $file -f bin -o $binfile -DDEBUGCON ${@:2:$(($#-2))} && \ 20 | qemu-system-x86_64 $QEMU_ARGS $HEADLESS_ARGS -debugcon stdio -drive file=$binfile,format=raw,if=ide 21 | # Kill the process if we had a compile error so that we exit with non-zero 22 | # status even though we used a pipe which exits with the tee status 23 | ERR=$? 24 | if [ $ERR -ne 0 ]; then 25 | echo "Exit code: $ERR" 26 | kill $PID 27 | sleep 1 28 | fi 29 | return $ERR 30 | } 31 | 32 | mkdir -p `dirname $binfile` 33 | OUTPUT=$(compile ERROR $@ | tee /dev/tty) 34 | 35 | if grep -q "FAIL" - <<< "$OUTPUT"; then 36 | exit 1 37 | fi 38 | -------------------------------------------------------------------------------- /text-editor.asm: -------------------------------------------------------------------------------- 1 | ; Provided under the MIT License: http://mit-license.org/ 2 | ; Copyright (c) 2020 Andy Kallmeyer 3 | 4 | ; This is a text editor (uses a gap buffer) which calls the run_code function 5 | ; when you press ctrl+D. You use it by %define RUN_CODE before you include the 6 | ; text editor, and then define your run_code function after that. 7 | ; 8 | ; You may also define a debug_text label pointing to a null terminated string 9 | ; which will be pre-loaded into the editor on startup if DEBUG_TEXT is defined. 10 | ; 11 | ; Search for run_code: below to see the API for it. 12 | 13 | 14 | ; Color byte is: bg-intensity,bg-r,bg-g,bg-b ; fg-intensity,fg-r,fg-g,fg-b 15 | %define MAIN_COLOR 0x17 16 | %define BORDER_COLOR 0x97 17 | %define ERROR_COLOR 0x47 18 | ; This is a different color space from the others. It's 6 bits. 19 | ; secondary r,g,b then primary r,g,b 20 | ; So 0x39 is 1 for all secondary values and 1 for b 21 | %define OVERSCAN_COLOR 0x39 22 | 23 | ; Note that the code requires that there's at least one character of border 24 | %define MAIN_TOP_LEFT 0x0204 ; row = 2, col = 4 25 | %define MAIN_BOTTOM_RIGHT 0x164B ; row = 22, col = 79-4 26 | %define START_ROW 0x02 27 | %define START_COL 0x04 28 | %define END_ROW 0x16 29 | %define END_COL 0x4B 30 | 31 | ; TODO: detect the RAM and give an error if we don't have 256k which is 0x40000 32 | ; which is exactly the 4 segments we use (the 4th is the code segment) 33 | 34 | ; Segment register value (so the actual start is at the address 0x10*this) 35 | ; This is the first sector after the editor's code 36 | ; 37 | ; This offset won't be exactly a multiple of NEXT_SEGMENT but it is at most 38 | ; NEXT_SEGMENT because we don't have a dynamic code segment switching setup so 39 | ; NUM_EXTRA_SECTORS is limited to 0x7F. So since there are 6 40 | ; non-overlapping complete segments available and we're only using 4 here 41 | ; (including the code segment), it's impossible for COMPILER_OUT_LOC to be 42 | ; greater than LAST_SEGMENT. 43 | %define USER_CODE_LOC (CODE_SEGMENT+(SECTOR_SIZE/0x10)*(NUM_EXTRA_SECTORS+1)) 44 | %define COMPILER_DATA_LOC USER_CODE_LOC+NEXT_SEGMENT 45 | %define COMPILER_OUT_LOC COMPILER_DATA_LOC+NEXT_SEGMENT 46 | 47 | ; Maxiumum size for the code buffer 48 | ; 49 | ; There's a lot of extra memory still after this but I don't want to move around 50 | ; the segment register so this is the limit. 51 | ; 52 | ; Allows 64k of code 53 | %define USER_CODE_MAX 0xFFFF 54 | 55 | ; Values in ax after the keyboard read BIOS call 56 | ; See Figure 4-3 of the 1988 BIOS manual. (page 195) 57 | %define LEFT_ARROW 0x4B00 58 | %define RIGHT_ARROW 0x4D00 59 | %define UP_ARROW 0x4800 60 | %define DOWN_ARROW 0x5000 61 | %define EOT 0x2004 ; Ctrl+D 62 | 63 | %define ROW_COUNT (END_ROW-START_ROW+1) ; 0x15, 21 64 | %define ROW_LENGTH (END_COL-START_COL+1) ; 0x48, 72 65 | %define NUM_PRINTS_PER_ROW (ROW_LENGTH/5) ; 5 = 4 hex chars + 1 space 66 | 67 | ; Size of the gap buffer when we reset it 68 | ; Must be less than USER_CODE_MAX 69 | ; 70 | ; The user code is stored using a gap buffer to allow inserting at the cursor 71 | ; location without having to shift the trailing data in memory. The system 72 | ; maintains a single gap in the code buffer at the cursor position which shrinks 73 | ; as the usertypes continuously and must periodically be reset by shifting the 74 | ; data. When the user deletes the gap grows. When the cursor hits the end of the 75 | ; buffer the gap can be reset for free. 76 | %define GAP_SIZE (8*ROW_LENGTH) 77 | 78 | ; start_ should be the first bytes in the file because that's where I have 79 | ; configured gdb to breakpoint 80 | start_: 81 | 82 | ; TODO: CGA support 83 | ; Set the overscan color to the border color 84 | mov ax, 0x1001 85 | mov bh, OVERSCAN_COLOR 86 | int 0x10 87 | 88 | ; Set the border color (by clearing the whole screen) 89 | mov ax, 0x0600 90 | xor cx, cx ; row = 0, col = 0 91 | mov dx, 0x184F ; row = 24, col = 79 92 | mov bh, BORDER_COLOR 93 | int 0x10 94 | 95 | ; Set the background color (by clearing just the middle) 96 | mov ax, 0x0600 97 | mov cx, MAIN_TOP_LEFT 98 | mov dx, MAIN_BOTTOM_RIGHT 99 | mov bh, MAIN_COLOR 100 | int 0x10 101 | 102 | ; Set cursor position to the start 103 | mov ax, 0x0200 104 | mov dx, MAIN_TOP_LEFT 105 | xor bh, bh ; page 0 106 | int 0x10 107 | 108 | ; Memory map: 109 | ; 110 | ; Each segment gets a full non-overlapping 64k block of memory and we don't move 111 | ; the segment so 64k is the limit for each part. 112 | ; 113 | ; SS: 0x050 114 | ; - Leaves ~30k for the stack between 0x0500 and 0x7C00, the start of the code. 115 | ; CS: CODE_SEGMENT (0x7C0) 116 | ; - The assembly code 117 | ; ES: USER_CODE_LOC or COMPILER_OUT_LOC if we're printing the output 118 | ; - The code that the user types into the editor 119 | ; - The printing routines all use es so we use es for printing 120 | ; DS: COMPILER_DATA_LOC 121 | ; - Extra memory passed to the plugin program that runs the code you typed. 122 | ; Only set when ctrl+D is pressed and passed to the plugin. 123 | ; - Normally the default for memory address reads and our bootloader leaves 124 | ; this set to CODE_SEGMENT for that. But this code always uses cs: to 125 | ; reference strings stored in the binary. 126 | 127 | ; --- typing_loop global register variables --- 128 | ; 129 | ; The user code is an (almost) contiguous null-terminated buffer starting at 130 | ; es:0 with a single gap of garbage data in it (which lets us do inserts without 131 | ; shifting the tail of the data every time to make space). 132 | ; 133 | ; dx - cursor position (set above) 134 | ; [es:di] - the first garbage character in the gap (current position to write to) 135 | ; [es:si] - the first code character after the gap 136 | ; 137 | ; Note: We must have at least one char of gap i.e. si-di >= 1 at all times 138 | ; (because we use it as scratch space when printing sometimes) 139 | ; 140 | ; Additionally cx,bp are callee save while ax,bx are caller save (clobbered) 141 | mov ax, USER_CODE_LOC 142 | mov es, ax 143 | mov ax, COMPILER_DATA_LOC 144 | mov ds, ax 145 | xor di, di 146 | mov si, GAP_SIZE 147 | mov byte [es:si], 0 ; null-terminate the string 148 | 149 | %ifndef DEBUG_TEXT 150 | 151 | jmp typing_loop 152 | 153 | %else 154 | 155 | ; Move the debug_text into the segment used for the text editor code 156 | mov di, debug_text 157 | copy_debug_text: 158 | mov al, [cs:di] 159 | mov [es:si], al 160 | cmp byte [cs:di], 0 161 | je done_copy_debug 162 | inc di 163 | inc si 164 | jmp copy_debug_text 165 | done_copy_debug: 166 | 167 | ; Print it nicely in the editor 168 | mov si, GAP_SIZE 169 | mov di, GAP_SIZE 170 | mov dh, START_ROW 171 | mov cx, ROW_COUNT 172 | call print_text 173 | 174 | ; Scroll the printed text to the top corner where the cursor starts 175 | cmp cx, 0 176 | je .skip_scolling 177 | mov bx, cx 178 | mov al, 0 179 | mov dx, MAIN_TOP_LEFT 180 | call scroll_text 181 | .skip_scolling: 182 | 183 | ; Reset and run the typing_loop 184 | xor di, di 185 | mov si, GAP_SIZE 186 | mov dx, MAIN_TOP_LEFT 187 | jmp set_cursor_and_continue 188 | 189 | %endif 190 | 191 | ; Optionally change the keyboard layout. 192 | ; 193 | ; To use a layout: look in the file you want for the %ifdef label and -D define 194 | ; it on the commandline. Ex: ./boot bootstrap-asm.asm -DDVORAK 195 | ; 196 | ; It's easy to make a new layout! 197 | ; 198 | ; Simply pull up an ascii table (my favorite is `man ascii`) then type it out 199 | ; starting from ' ' to '~' using your qwerty keyboard labels using the desired 200 | ; layout in your OS (note: both \ and ` must be escaped for the assembler) 201 | %include "keyboard-layouts/dvorak.asm" 202 | ; TODO: is the default layout qwerty or hardware specific? My map is qwerty->dvorak only 203 | 204 | no_more_room_msg: db `No more room in the code buffer` 205 | no_more_room_msg_len: equ $-no_more_room_msg 206 | 207 | num_debug_prints: db 0x00 208 | 209 | ; Prints a hex word in the top margin for debugging purposes 210 | ; Supports printing as many as will fit in before it runs into the text area 211 | ; 212 | ; cx = two bytes to write at current cursor 213 | ; clobbers ax, and bx 214 | debug_print_hex: 215 | push dx ; Save the cursor position 216 | 217 | ; One row above the top of the text area 218 | mov dl, START_COL 219 | xor dh, dh 220 | 221 | ; Calculate where to print based on number of words we have printed 222 | xor ax, ax 223 | mov byte al, [cs:num_debug_prints] 224 | mov bl, NUM_PRINTS_PER_ROW 225 | div bl 226 | ; al = (num_debug_prints)/(num_per_row); ah = (num_debug_prints) % (num_per_row) 227 | 228 | add dh, al ; row += the row to print on 229 | 230 | ; Reset the count so we overwrite at the beginning 231 | cmp dh, START_ROW 232 | jne .no_reset 233 | mov byte [cs:num_debug_prints], 0 234 | xor ax, ax 235 | xor dh, dh 236 | .no_reset: 237 | 238 | inc byte [cs:num_debug_prints] ; remember that we printed 239 | 240 | ; column += modulus * (hex+" " string length) 241 | mov al, ah 242 | xor ah, ah 243 | mov bl, 5 244 | mul bl 245 | add dl, al ; note: the mul result technically could be more than 2^8 (in ax) but we know it is less than ROW_LENGTH 246 | 247 | ; set cursor to the location to print 248 | mov ah, 0x02 249 | xor bh, bh 250 | int 0x10 ; dx is the cursor position 251 | 252 | ; Print cx 253 | call print_hex 254 | ; don't bother printing a space because the margin is blank 255 | 256 | ; reset the cursor 257 | pop dx 258 | mov ah, 0x02 259 | xor bh, bh 260 | int 0x10 ; dx is the cursor position 261 | 262 | ret 263 | 264 | user_code_start: dw 0 265 | 266 | ; Note: all of the jumps in typing_loop loop back here (except for run_code) 267 | ; 268 | ; I thought it would be fun to save the call and ret instructions since the 269 | ; entire program is just this loop (after the setup). 270 | typing_loop: 271 | ; Read keyboard 272 | mov ah, 0x00 273 | int 0x16 274 | ; ah = key code, al = ascii value 275 | 276 | cmp al, ' ' 277 | jb .non_printable 278 | cmp al, '~' 279 | ja .non_printable 280 | jmp save_and_print_char 281 | 282 | .non_printable: 283 | 284 | cmp al, `\b` ; Backspace, shift backspace, Ctrl+H 285 | je backspace 286 | 287 | cmp al, `\r` ; Enter/Return key 288 | je save_new_line 289 | 290 | cmp ax, LEFT_ARROW 291 | je move_left 292 | 293 | cmp ax, RIGHT_ARROW 294 | je move_right 295 | 296 | cmp ax, UP_ARROW 297 | je move_up 298 | 299 | cmp ax, DOWN_ARROW 300 | je move_down 301 | 302 | cmp ax, EOT ; Ctrl+D 303 | je prepare_and_run_code 304 | 305 | jmp typing_loop ; Doesn't match anything above 306 | 307 | %ifndef RUN_CODE 308 | ; This is run when ctrl+D is pressed and passed the code and memory locations 309 | ; for compiler/interpreter purposes and finally a separate output text buffer 310 | ; which will be displayed in the editor after run_code returns. 311 | ; 312 | ; Input: 313 | ; [ds:si] - the code to run 314 | ; [es:0] - the code runner memory block. 315 | ; This memory is preserved between successive runs of run_code. 316 | ; di - output segment address; starts at 0 offset; null terminated 317 | ; This memory is preserved between successive runs of run_code. 318 | run_code: 319 | mov es, di 320 | mov byte [es:0], "!" 321 | mov byte [es:1], 0 322 | ret 323 | %endif 324 | 325 | prepare_and_run_code: 326 | call clear_error ; In case this isn't the first try 327 | 328 | ; Close the gap so we have a contiguous buffer 329 | .close_gap: 330 | mov byte al, [es:si] 331 | mov byte [es:di], al 332 | ; Check for \0 after moving because we want to copy it over 333 | cmp byte [es:si], 0 334 | je .gap_closed 335 | inc di 336 | inc si 337 | jmp .close_gap 338 | .gap_closed: 339 | 340 | ; Clear the text from the screen 341 | mov dx, MAIN_TOP_LEFT 342 | mov bl, END_COL-START_COL 343 | call scroll_text 344 | ; Set the cursor position to the start of the text area 345 | mov ah, 0x02 346 | xor bh, bh 347 | int 0x10 348 | 349 | push si 350 | 351 | ; source code is [es:si], dest data is [ds:di] 352 | ; set the source [es:si] for the assembler 353 | mov di, COMPILER_OUT_LOC 354 | mov si, [cs:user_code_start] 355 | call run_code 356 | 357 | mov ax, COMPILER_OUT_LOC 358 | mov es, ax 359 | xor si, si 360 | xor di, di ; set es:si to the same as es:di so the gap buffer code works 361 | mov dh, START_ROW 362 | call print_text 363 | 364 | ; Reset the state to continue the editor 365 | ; 366 | ; TODO: make an option to save the input text instead of resetting the buffer 367 | ; with an empty null terminated string. Not sure what I want the controls to 368 | ; be. Would probably want to reset the lisp memory too at that point. 369 | pop si 370 | mov [cs:user_code_start], si 371 | mov di, si 372 | add si, GAP_SIZE 373 | mov byte [es:si], 0 ; null-terminate the string 374 | mov ax, USER_CODE_LOC 375 | mov es, ax 376 | 377 | ; Read keyboard, so they can read the output 378 | mov ah, 0x00 379 | int 0x16 380 | 381 | ; Clear the text from the screen 382 | mov dx, MAIN_TOP_LEFT 383 | mov bl, END_COL-START_COL 384 | call scroll_text 385 | 386 | mov dx, MAIN_TOP_LEFT 387 | jmp set_cursor_and_continue 388 | 389 | ; Print a body of text into the text editor respecting newlines and the screen 390 | ; boundaries. Always starts printing at the left edge. 391 | ; 392 | ; Args: 393 | ; [es:si] - the null terminated text to print 394 | ; cx - max lines to print, 0 for unlimited 395 | ; Returns: 396 | ; cx - the number of lines remaining in the max 397 | ; (or negative lines printed if cx was 0) 398 | ; i.e. cx = (input cx) - lines_printed 399 | ; So if you set cx non-zero this is the number of lines to scroll up 400 | print_text: 401 | push cx 402 | jmp .skip_scroll 403 | .print_loop: 404 | ; scroll up by one line to make room for the next line to print, do this at 405 | ; the top so it doesn't happen after the last line 406 | mov al, 0 407 | mov bl, 1 408 | mov dx, MAIN_TOP_LEFT 409 | call scroll_text 410 | .skip_scroll: 411 | 412 | ; Find the end of the line 413 | mov bp, si 414 | call scan_forward 415 | 416 | ; Check if we found the end of the line, and set the right scroll marker 417 | mov ax, 0x0000 ; scroll markers: set to off ; right margin 418 | cmp cx, ROW_LENGTH 419 | jbe .shorter_than_row 420 | mov cx, ROW_LENGTH ; only print up to the edge of the screen 421 | mov ax, 0x0100 ; scroll markers: set to on ; right margin 422 | ; fallthrough 423 | .shorter_than_row: 424 | 425 | ; move bp to the end of the next line 426 | cmp byte [es:bp], 0 427 | je .end_of_buffer 428 | inc bp ; skip the \n 429 | ; fallthrough 430 | 431 | .end_of_buffer: 432 | ; ax was set above 433 | mov dh, END_ROW 434 | call set_line_scroll_marker 435 | 436 | ; swap bp and si 437 | ; after: bp = beginning of line, si = start of next line 438 | xor si, bp 439 | xor bp, si 440 | xor si, bp 441 | 442 | mov dh, END_ROW 443 | mov dl, START_COL 444 | call print_line ; prints cx chars from bp 445 | 446 | ; Keep track of the lines printed 447 | pop cx 448 | dec cx 449 | 450 | ; If we have run out of our line limit, stop 451 | ; If cx was 0 for unlimited then this will be negative and not equal to 0 452 | cmp cx, 0 453 | je .end 454 | ; If we hit the end of the buffer, stop 455 | cmp byte [es:bp], 0 456 | je .end 457 | ; If we're still looping push line_count again so we can pop it above 458 | push cx 459 | jmp .print_loop 460 | 461 | .end: 462 | ret 463 | 464 | ; ==== typing_loop internal helpers that continue the loop ==== 465 | 466 | ; Moves the cursor to the value in dx and continues typing_loop 467 | set_cursor_and_continue: 468 | mov ah, 0x02 469 | xor bh, bh 470 | int 0x10 ; dx is the cursor position 471 | jmp typing_loop 472 | 473 | ; Takes an ascii char in al then saves and prints it and continues typing_loop 474 | save_and_print_char: 475 | lea bx, [si-1] 476 | cmp di, bx ; disallow typing anywhere when we're out of buffer space 477 | je typing_loop 478 | cmp di, USER_CODE_MAX ; Leave a \0 at the end 479 | je typing_loop 480 | 481 | ; Convert the keyboard layout if one is %included 482 | %ifdef CONVERT_LAYOUT 483 | call convert_keyboard_layout 484 | %endif 485 | 486 | ; Print the ascii char (in al) 487 | mov ah, 0x0E ; Write teletype character 488 | xor bh, bh 489 | int 0x10 490 | 491 | mov byte [es:di], al ; Write the typed char to the buffer 492 | inc di 493 | 494 | call maybe_reset_gap 495 | 496 | cmp dl, END_COL 497 | je _scroll_line_right ; same if we're inserting or at the end of the line 498 | ; We're inserting in the middle of the row, repaint the tail of the line 499 | 500 | inc dl 501 | 502 | mov bp, si 503 | call scan_forward 504 | 505 | ; bx = remaining space in the row from the current cursor position 506 | mov bx, (ROW_LENGTH+START_COL) 507 | sub bl, dl 508 | 509 | cmp cx, bx 510 | jbe .not_cut_off 511 | mov cx, bx ; clip the amount to repaint to the current row 512 | 513 | mov ax, 0x0100 ; set to on ; right margin 514 | call set_line_scroll_marker 515 | ; fallthrough 516 | .not_cut_off: 517 | mov bp, si 518 | call print_line 519 | 520 | ; The cursor did move from the typing interrupt but lets just set it anyway, 521 | ; if we didn't the cursor breaks when putting in a debug print in some places 522 | jmp set_cursor_and_continue 523 | 524 | save_new_line: 525 | lea bx, [si-1] 526 | cmp di, bx ; disallow typing anywhere when we're out of buffer space 527 | je typing_loop 528 | cmp di, USER_CODE_MAX ; Leave a \0 at the end 529 | je typing_loop 530 | 531 | ; Write the `\n` into the buffer 532 | mov byte [es:di], `\n` 533 | inc di 534 | 535 | call maybe_reset_gap 536 | ; fallthrough 537 | .reset_current_line: 538 | 539 | ; If we put a \n in the first character, so there's no string to scan 540 | mov bx, [cs:user_code_start] 541 | inc bx 542 | cmp di, bx 543 | ja .not_empty_first_line 544 | ; Clear it incase we were scolled. We're not doing a scan_forward to check 545 | mov ax, 0x0000 ; set to off ; right margin 546 | call set_line_scroll_marker 547 | jmp .clear_tail 548 | .not_empty_first_line: 549 | 550 | lea bp, [di-2] 551 | call scan_backward 552 | 553 | ; Set the scroll markers 554 | mov ax, 0x0001 ; set to off ; left margin 555 | call set_line_scroll_marker 556 | ; Set right to on or off always 557 | mov ax, 0x0000 ; set to off ; right margin 558 | cmp cx, ROW_LENGTH 559 | jbe .right_off 560 | 561 | mov cx, ROW_LENGTH ; also clip the length for printing (reuse the cmp) 562 | 563 | mov ax, 0x0100 ; set to on ; right margin 564 | .right_off: 565 | call set_line_scroll_marker 566 | 567 | ; Repaint the line 568 | mov dl, START_COL 569 | call print_line 570 | add dl, cl ; move the cursor to the end of the string (for .clear_tail) 571 | 572 | cmp cx, ROW_LENGTH 573 | je .move_down ; skip clearing the tail if we just repainted everything 574 | ; fallthrough 575 | 576 | .clear_tail: 577 | cmp byte [es:si], `\n` 578 | je .move_down ; no need to clear if we were at the end of the line already 579 | cmp byte [es:si], 0 580 | je .move_down ; nothing to clear at the end of the buffer 581 | 582 | ; Write spaces to clear the rest of the line since it's on the next line now 583 | ; Note: this call uses the actual set cursor position not dx 584 | mov ah, 0x09 585 | mov al, ' ' 586 | xor bh, bh 587 | mov bl, MAIN_COLOR 588 | ; cx = 1 + END_COL - curr_cursor_col = num spaces to write 589 | mov cx, END_COL+1 590 | sub cl, dl 591 | int 0x10 592 | ; fallthrough 593 | 594 | ; Do a partial screen scroll to make room for the new line in the middle 595 | .move_down: 596 | mov dl, START_COL ; start at the beginning on the next line 597 | ; If there's no text after our tail, we don't want to scroll anything 598 | mov bp, si 599 | call scan_forward ; Note we need to save the return (cx) until .print_new_line 600 | 601 | cmp dh, END_ROW 602 | jne .make_space_in_middle 603 | ; Move the text one line up to make an empty line at the bottom 604 | mov dh, START_ROW ; scroll the whole screen 605 | mov al, 0 606 | mov bl, 1 ; scroll one line 607 | call scroll_text 608 | mov dh, END_ROW ; restore the cursor row 609 | jmp .print_new_line 610 | 611 | .make_space_in_middle: 612 | inc dh 613 | cmp byte [es:bp], 0 ; if this is the last line, we already have space 614 | je .no_scroll 615 | mov al, 1 ; shift the text down to leave a space 616 | mov bl, 1 ; scroll one line 617 | call scroll_text 618 | .no_scroll: 619 | ; fallthrough 620 | 621 | ; Print the tail of the line after the new \n if there is any 622 | ; (uses cx from scan_forward above) 623 | .print_new_line: 624 | ; re-use the saved result of scan_forward from the check above 625 | cmp cx, ROW_LENGTH 626 | jbe .not_cut_off 627 | mov cx, ROW_LENGTH ; clamp to the row length 628 | mov ax, 0x0100 ; set to on ; right margin 629 | call set_line_scroll_marker 630 | ; fallthrough 631 | .not_cut_off: 632 | mov bp, si 633 | call print_line 634 | 635 | jmp set_cursor_and_continue 636 | 637 | backspace: 638 | cmp di, [cs:user_code_start] ; Can't delete past the beginning of the buffer 639 | je typing_loop 640 | 641 | lea bx, [si-1] 642 | cmp di, bx ; disallow typing anywhere when we're out of buffer space 643 | jne .not_full_buffer 644 | call clear_error 645 | ; fallthrough 646 | .not_full_buffer: 647 | 648 | cmp byte [es:di-1], `\n` ; check the char we just "deleted" (di-1 is usually the char before the cursor) 649 | je _join_lines 650 | 651 | ; If we're at the beginning of the line we don't have to update the view 652 | cmp dl, START_COL 653 | jne .delete_mid_line 654 | 655 | dec di ; delete the unseen char by pushing it into the gap 656 | 657 | ; Remove the scroll marker if we just hit the beginning of the line 658 | cmp di, [cs:user_code_start] 659 | je .clear_left_marker 660 | cmp byte [es:di-1], `\n` 661 | je .clear_left_marker 662 | jmp typing_loop 663 | 664 | .clear_left_marker: 665 | mov ax, 0x0001 ; set to off ; left margin 666 | call set_line_scroll_marker 667 | jmp typing_loop 668 | 669 | .delete_mid_line: 670 | dec di ; delete the char by pushing it into the gap 671 | dec dl ; Update the cursor position 672 | 673 | ; .repaint_tail: 674 | mov bp, si 675 | call scan_forward 676 | 677 | ; bx = maximum chars until the end of the row 678 | mov bx, (START_COL+ROW_LENGTH) 679 | sub bl, dl 680 | cmp cx, bx 681 | jae .clip_line ; in the equals case we don't want to print an extra space 682 | 683 | ; We're re-painting a line tail that's not cut off currently. So: 684 | ; Replace the line-ending char with a space for covering up the old last char 685 | push word [es:bp-1] ; save the line ending char in the high byte (you can only push whole words) 686 | ; Note: [es:bp+1] may not be safe to read while -1 is here. 687 | mov byte [es:bp], ' ' 688 | push bp ; save the pointer to the line ending 689 | 690 | ; Repaint the tail of the line 691 | inc cx ; print the space too 692 | mov bp, si 693 | call print_line 694 | 695 | ; Restore the line ending 696 | pop bp 697 | pop ax 698 | mov byte [es:bp], ah 699 | 700 | jmp set_cursor_and_continue 701 | 702 | .clip_line: 703 | ; Set the right marker on, unless our line exactly ends at the end of the row 704 | mov ax, 0x0000 ; set to off ; right margin 705 | cmp cx, bx 706 | je .no_marker 707 | mov ax, 0x0100 ; set to on ; right margin 708 | .no_marker: 709 | call set_line_scroll_marker 710 | 711 | ; Print only up to our maximum (recalculated from above since bx gets clobbered) 712 | mov cx, END_COL+1 713 | sub cl, dl 714 | mov bp, si 715 | call print_line 716 | 717 | jmp set_cursor_and_continue 718 | 719 | _repaint_bottom_line: 720 | push dx 721 | ; Note: the first iteration will just be the tail of the new joined line 722 | mov bp, si 723 | call scan_forward ; skip to the end of the new combined line 724 | .next_line_loop: 725 | cmp dh, END_ROW 726 | je .at_last_line 727 | 728 | ; If we hit the end of the code before the END_ROW, we didn't have a line cut off 729 | cmp byte [es:bp], 0 730 | je .no_line_to_print 731 | 732 | inc bp ; skip the \n to scan the next line 733 | call scan_forward 734 | inc dh 735 | 736 | jmp .next_line_loop 737 | .at_last_line: 738 | 739 | ; Set the right scroll marker 740 | mov ax, 0x0000 ; set to off ; right margin 741 | cmp cx, ROW_LENGTH 742 | jbe .right_off 743 | mov cx, ROW_LENGTH ; also clip the length for printing (reuse the cmp) 744 | mov ax, 0x0100 ; set to on ; right margin 745 | .right_off: 746 | call set_line_scroll_marker 747 | 748 | sub bp, cx 749 | call print_line ; if we hit the end row, paint the new last screen line 750 | ; fallthrough 751 | 752 | .no_line_to_print: 753 | pop dx 754 | ret 755 | 756 | _join_lines: 757 | dec di ; delete the char by pushing it into the gap 758 | 759 | ; Scroll the text below up (which clears the current line & markers for free) 760 | mov al, 0 ; shift the text up to cover the current line 761 | mov bl, 1 ; scroll one line 762 | call scroll_text 763 | 764 | dec dh ; Move the cursor onto the prev line (must be after scrolling) 765 | 766 | ; We might be bringing a line up from the bottom of the screen 767 | call _repaint_bottom_line 768 | 769 | .paint_joined_line: 770 | ; Need to skip scan_backward if we're at di == 0 because we can't check di-1 771 | xor cx, cx ; if the check below passes, we have length 0 772 | cmp di, [cs:user_code_start] 773 | je .print_the_tail 774 | 775 | ; Setup the new current line 776 | lea bp, [di-1] 777 | call scan_backward 778 | 779 | cmp cx, ROW_LENGTH 780 | jae .shift_line_right 781 | .print_the_tail: 782 | ; set the cursor column to one past the last char on the prev line 783 | mov dl, START_COL 784 | add dl, cl 785 | 786 | ; Find the length of the new tail 787 | mov bp, si 788 | call scan_forward 789 | 790 | ; Clip the printing to the end of the row 791 | ; bx = remaining space in the row from the current cursor position 792 | mov bx, (ROW_LENGTH+START_COL) 793 | sub bl, dl 794 | cmp cx, bx 795 | jb .no_clipping 796 | mov cx, bx 797 | 798 | mov ax, 0x0100 ; set to on ; right margin 799 | call set_line_scroll_marker 800 | .no_clipping: 801 | 802 | mov bp, si 803 | call print_line 804 | jmp set_cursor_and_continue 805 | .shift_line_right: 806 | ; Copy the last char in the row into the gap so we can print it 807 | mov byte al, [es:si] 808 | mov byte [es:di], al 809 | 810 | ; If we joined an empty line, we need to print a space 811 | ; Note: this is the garbage gap space so no need to restore the ending 812 | cmp byte [es:di], `\n` 813 | je .space 814 | cmp byte [es:di], 0 815 | je .space 816 | jmp .no_space 817 | .space: 818 | mov byte [es:di], ' ' 819 | ; If this happened we have nothing cut off, otherwise we just keep the 820 | ; existing marker (we know the line had some cut off on the right before this) 821 | mov ax, 0x0000 ; set to off ; right margin 822 | call set_line_scroll_marker 823 | .no_space: 824 | 825 | mov dl, START_COL ; print from the start column 826 | mov bp, di 827 | sub bp, ROW_LENGTH-1 828 | mov cx, ROW_LENGTH 829 | call print_line 830 | mov dl, END_COL ; leave the cursor on the last char (first from the joined line) 831 | 832 | jmp set_cursor_and_continue 833 | 834 | ; Moves the cursor left one nibble then continues typing_loop 835 | ; - Saves the byte in cl when moving to a new byte, also loads the existing 836 | ; data into cl if applicable 837 | ; - Calls move_up when moving to the next line 838 | move_left: 839 | ; Don't do anything if we're already at the beginning of the buffer 840 | cmp di, [cs:user_code_start] 841 | je typing_loop 842 | 843 | dec di 844 | dec si 845 | mov byte al, [es:di] 846 | mov byte [es:si], al 847 | 848 | cmp byte [es:si], `\n` 849 | je _prev_line 850 | 851 | cmp dl, START_COL 852 | je _scroll_line_left 853 | 854 | ; Normal case, just go back one char 855 | dec dl 856 | jmp set_cursor_and_continue 857 | 858 | _scroll_line_left: 859 | ; Just need to repaint the tail of the line and set scroll markers 860 | mov bp, si 861 | call scan_forward 862 | 863 | cmp cx, ROW_LENGTH 864 | jbe .no_clipping 865 | ; Set the right marker only when this is the first char we've cut off 866 | cmp cx, ROW_LENGTH+1 867 | jne .no_set_right_marker 868 | mov ax, 0x0100 ; set to on ; right margin 869 | call set_line_scroll_marker 870 | .no_set_right_marker: 871 | 872 | mov cx, ROW_LENGTH ; Print only up to our maximum 873 | ; fallthrough 874 | .no_clipping: 875 | mov bp, si 876 | call print_line 877 | 878 | ; Check if we're at the start of the line and clear the left marker if so 879 | cmp di, [cs:user_code_start] 880 | je .at_beginning_of_line 881 | cmp byte [es:di-1], `\n` ; -1 is safe because of the above check 882 | je .at_beginning_of_line 883 | jmp .keep_marker 884 | .at_beginning_of_line: 885 | mov ax, 0x0001 ; set to off ; left margin 886 | call set_line_scroll_marker 887 | .keep_marker: 888 | jmp set_cursor_and_continue 889 | 890 | _prev_line: 891 | xor cx, cx ; line length is 0 if we're at di==0 892 | cmp di, [cs:user_code_start] ; skip scanning if the first char is \n 893 | je .move_up 894 | 895 | lea bp, [di-1] ; scan from the char before the \n 896 | call scan_backward 897 | ; fallthrough 898 | 899 | ; Scroll the screen (and set the final cursor row position) 900 | .move_up: 901 | cmp dh, START_ROW 902 | jne .no_screen_scroll 903 | ; We have to make room for the previous line 904 | mov al, 1 905 | mov bl, 1 906 | call scroll_text 907 | jmp .paint_row 908 | .no_screen_scroll: 909 | ; Just move the cursor up 910 | dec dh 911 | cmp cx, ROW_LENGTH-1 912 | jbe .skip_repaint 913 | ; fallthrough 914 | 915 | .paint_row: 916 | ; Clip the string to [:min(strlen, ROW_LENGTH-1)] chars at the end 917 | ; (-1 to leave a space for the user to type at the end) 918 | mov dl, START_COL ; print from the beginning of the line 919 | cmp cx, 0 920 | je .skip_repaint 921 | cmp cx, ROW_LENGTH-1 922 | jbe .no_clipping 923 | ; Move the print pointer to the end of the line minus ROW_LENGTH-1 924 | add bp, cx 925 | mov cx, ROW_LENGTH ; num chars to print 926 | sub bp, ROW_LENGTH-1 ; leave a space at the end 927 | 928 | ; Put a space in the buffer so we print it and overwrite on the screen the 929 | ; last char the user typed. 930 | mov byte [es:di], ' ' 931 | 932 | ; Set the scroll markers since we have some cut off to the left 933 | mov ax, 0x0101 ; set to on ; left margin 934 | call set_line_scroll_marker 935 | mov ax, 0x0000 ; set to off ; right margin 936 | call set_line_scroll_marker 937 | 938 | call print_line 939 | dec cx ; so we set the cursor after the last char not after the extra space 940 | jmp .skip_repaint ; done 941 | 942 | .no_clipping: 943 | call print_line 944 | ; fallthrough 945 | 946 | .skip_repaint: 947 | ; Set the cursor column and finally move it 948 | mov dl, START_COL 949 | add dl, cl 950 | jmp set_cursor_and_continue 951 | 952 | ; Moves the cursor right one char (also wrapping lines) then continues typing_loop 953 | move_right: 954 | ; Stop the cursor at the end of the user's code 955 | cmp byte [es:si], 0 956 | je typing_loop 957 | 958 | mov byte al, [es:si] 959 | mov byte [es:di], al 960 | inc di 961 | inc si ; we know we are not at USER_CODE_MAX because of the \0 962 | 963 | ; If we just hit the end for the first time, do a free gap reset 964 | cmp byte [es:si], 0 965 | jne .no_reset_gap ; We don't want to copy every time we move the cursor 966 | call maybe_reset_gap ; This will do the check again and take the no-copy path 967 | .no_reset_gap: 968 | 969 | ; Move the cursor to the next line once we're past the \n so we can append 970 | ; (remember, di is garbarge space and di-1 is the character before the cursor) 971 | cmp byte [es:di-1], `\n` 972 | je _next_line 973 | 974 | cmp dl, END_COL 975 | je _scroll_line_right 976 | ; Normal case, just go right one 977 | inc dl 978 | jmp set_cursor_and_continue 979 | 980 | _scroll_line_right: 981 | lea bp, [di-(ROW_LENGTH-1)] ; print starting from the second char in the row 982 | mov cx, ROW_LENGTH ; number of chars to print 983 | 984 | ; Note: we know that bp >= 1 here because we scrolled right so we must have 985 | ; at least ROW_LENGTH characters in the buffer 986 | 987 | ; Print the left-side marker if this is the first time we've cut off a char 988 | mov bx, [cs:user_code_start] 989 | inc bx 990 | cmp bp, bx ; if the first char in the line is the first char in the buffer 991 | je .need_left_marker 992 | ; Now we know that bp >= 2 because bp != 1 993 | cmp byte [es:bp-2], `\n` ; if the char before the first char in the line is \n 994 | jne .already_have_left_marker 995 | .need_left_marker: 996 | mov ax, 0x0101 ; set to on ; left margin 997 | call set_line_scroll_marker 998 | .already_have_left_marker: 999 | 1000 | ; Check if we're at the end of the line and clear the right marker if so 1001 | cmp byte [es:si], 0 1002 | je .at_end_of_line 1003 | cmp byte [es:si], `\n` ; check the next char 1004 | je .at_end_of_line 1005 | jmp .not_at_end 1006 | 1007 | ; We need to leave a space for the next character to type 1008 | .at_end_of_line: 1009 | ; Put a space in the buffer so we print it and overwrite on the screen the 1010 | ; last char the user typed. 1011 | mov byte [es:di], ' ' 1012 | jmp .paint_line 1013 | 1014 | .not_at_end: 1015 | 1016 | ; If the current char ([es:si]) is the last char in the line, clear the marker 1017 | ; +1 is safe because we know the current char is not an end-of-line char 1018 | cmp byte [es:si+1], 0 1019 | je .clear_right_marker 1020 | cmp byte [es:si+1], `\n` ; check the next char 1021 | je .clear_right_marker 1022 | jmp .keep_right_marker 1023 | .clear_right_marker: 1024 | mov ax, 0x0000 ; set to off ; right side 1025 | call set_line_scroll_marker 1026 | .keep_right_marker: 1027 | 1028 | ; Copy the next char into the temp space so we have a contiguous buffer 1029 | mov byte al, [es:si] 1030 | mov byte [es:di], al 1031 | ; fallthrough 1032 | .paint_line: 1033 | ; Print starting from the beginning of the row and restore the cursor 1034 | push dx 1035 | mov dl, START_COL 1036 | call print_line 1037 | pop dx 1038 | jmp set_cursor_and_continue 1039 | 1040 | _next_line: 1041 | 1042 | ; Reset the current line to show the left side if it's longer than the screen. 1043 | mov bx, [cs:user_code_start] 1044 | inc bx 1045 | cmp di, bx 1046 | je .skip_resetting_line ; The first char was the \n, no string to scan 1047 | lea bp, [di-2] ; scan starting from the char before the \n 1048 | call scan_backward 1049 | mov dl, START_COL ; On the next line we're going to be at the start 1050 | 1051 | cmp cx, ROW_LENGTH 1052 | jb .skip_resetting_line 1053 | ; Note: equal is a special case to remove the extra space at the end of the 1054 | ; line for the user to type in. 1055 | je .skip_right_marker 1056 | mov ax, 0x0100 ; set to on ; right margin 1057 | call set_line_scroll_marker 1058 | .skip_right_marker: 1059 | mov ax, 0x0001 ; set to off ; left margin 1060 | call set_line_scroll_marker 1061 | 1062 | mov cx, ROW_LENGTH 1063 | call print_line 1064 | .skip_resetting_line: 1065 | ;fallthrough 1066 | 1067 | .move_down: 1068 | cmp dh, END_ROW ; if we're at the bottom 1069 | je .scroll_up 1070 | inc dh ; Next row 1071 | jmp .done 1072 | 1073 | .scroll_up: 1074 | mov dh, START_ROW ; scroll the whole screen 1075 | mov al, 0 1076 | mov bl, 1 1077 | call scroll_text 1078 | mov dh, END_ROW ; restore the cursor row 1079 | 1080 | mov bp, si 1081 | call scan_forward 1082 | 1083 | cmp cx, ROW_LENGTH 1084 | jbe .shorter_than_row 1085 | mov ax, 0x0100 ; set to on ; right side 1086 | call set_line_scroll_marker 1087 | mov cx, ROW_LENGTH ; clip to the row 1088 | ; fallthrough 1089 | .shorter_than_row: 1090 | 1091 | mov bp, si 1092 | call print_line ; cursor position is already correct 1093 | ; fallthrough 1094 | .done: 1095 | jmp set_cursor_and_continue 1096 | 1097 | move_up: 1098 | cmp di, [cs:user_code_start] 1099 | je typing_loop 1100 | 1101 | ; Find the start of the current line from the current gap buffer position 1102 | mov bp, di 1103 | xor cx, cx 1104 | cmp byte [es:di-1], `\n` 1105 | je .first_char 1106 | dec bp 1107 | call scan_backward 1108 | .first_char: 1109 | 1110 | cmp bp, [cs:user_code_start] 1111 | je typing_loop 1112 | 1113 | call _repaint_current_line_if_needed 1114 | 1115 | ; Move bp to the start of the line above 1116 | dec bp ; move bp to the \n that scan_backward found 1117 | cmp bp, [cs:user_code_start] ; if the first char in the buffer was a newline 1118 | je .empty_line_above 1119 | cmp byte [es:bp-1], `\n` 1120 | je .empty_line_above 1121 | dec bp ; skip back to the last character of the line above 1122 | .empty_line_above: ; scan_backward sets cx = 0 and returns 1123 | call scan_backward ; leaves cx = length of line above 1124 | 1125 | ; If the cursor is at the top of the editor window 1126 | cmp dh, START_ROW 1127 | je .top_row 1128 | dec dh ; move the cursor up one line 1129 | jmp .set_cursor_and_buffer 1130 | 1131 | .top_row: 1132 | ; Scroll the screen and paint the new top line 1133 | push dx 1134 | ; scroll the screen down one to make room 1135 | mov dx, MAIN_TOP_LEFT 1136 | mov al, 1 1137 | mov bl, 1 1138 | call scroll_text 1139 | 1140 | ; Set the scroll markers for printing the above line all the way at the left 1141 | mov ax, 0x0000 ; scroll markers: set to off ; right margin 1142 | cmp cx, ROW_LENGTH 1143 | jbe .shorter_than_row 1144 | mov cx, ROW_LENGTH ; only print up to the edge of the screen 1145 | mov ax, 0x0100 ; scroll markers: set to on ; right margin 1146 | ; fallthrough 1147 | .shorter_than_row: 1148 | call set_line_scroll_marker 1149 | 1150 | call print_line ; print cx chars from bp at dx (MAIN_TOP_LEFT) 1151 | 1152 | pop dx ; restore the cursor 1153 | ; fallthrough 1154 | 1155 | ; Choose the cursor and buffer location on the line above 1156 | ; 1157 | ; by .vars_set: 1158 | ; Set bp = position to leave the gap for the new cursor 1159 | ; and cx = length from the start of the above line to the new cursor 1160 | ; and dx = the position to leave the cursor in 1161 | ; bx = number of characters to left edge of the screen 1162 | .set_cursor_and_buffer: 1163 | mov bx, dx 1164 | xor bh, bh 1165 | sub bx, START_COL 1166 | cmp cx, bx 1167 | jae .above_line_is_long_enough 1168 | 1169 | ; set the cursor to the end of the shorter line 1170 | mov dl, cl 1171 | add dl, START_COL 1172 | add bp, cx 1173 | jmp .vars_set 1174 | 1175 | .above_line_is_long_enough: 1176 | ; set to current line prefix length into the above line 1177 | ; leave the cursor column where it is 1178 | add bp, bx 1179 | mov cx, bx 1180 | ; fallthrough 1181 | .vars_set: 1182 | 1183 | ; Move the gap buffer back to the character (bp) for the new cursor position 1184 | .move_gap_back_loop: 1185 | dec di 1186 | dec si 1187 | mov al, [es:di] 1188 | mov [es:si], al 1189 | cmp di, bp 1190 | ja .move_gap_back_loop 1191 | 1192 | jmp set_cursor_and_continue 1193 | 1194 | move_down: 1195 | mov bp, si 1196 | call scan_forward 1197 | 1198 | cmp byte [es:bp], 0 1199 | je typing_loop 1200 | 1201 | cmp di, [cs:user_code_start] 1202 | je .no_repaint_needed 1203 | push bp 1204 | lea bp, [di-1] 1205 | call scan_backward 1206 | call _repaint_current_line_if_needed 1207 | pop bp 1208 | .no_repaint_needed: 1209 | 1210 | ; Move bp to the start of the line below 1211 | inc bp ; skip past the \n that scan_forward found 1212 | push bp 1213 | call scan_forward 1214 | pop bp 1215 | 1216 | ; If the cursor is at the end of the editor window 1217 | cmp dh, END_ROW 1218 | je .end_row 1219 | inc dh ; move the cursor down one line 1220 | jmp .set_cursor_and_buffer 1221 | 1222 | .end_row: 1223 | ; Scroll the screen and paint the new bottom line 1224 | push dx 1225 | ; scroll the screen down one to make room 1226 | mov dx, MAIN_TOP_LEFT 1227 | mov al, 0 1228 | mov bl, 1 1229 | call scroll_text 1230 | 1231 | ; Set the scroll markers for printing the above line all the way at the left 1232 | mov ax, 0x0000 ; scroll markers: set to off ; right margin 1233 | cmp cx, ROW_LENGTH 1234 | jbe .shorter_than_row 1235 | mov cx, ROW_LENGTH ; only print up to the edge of the screen 1236 | mov ax, 0x0100 ; scroll markers: set to on ; right margin 1237 | ; fallthrough 1238 | .shorter_than_row: 1239 | call set_line_scroll_marker 1240 | 1241 | mov dh, END_ROW 1242 | call print_line ; print cx chars from bp at dx (bottow left corner) 1243 | 1244 | pop dx ; restore the cursor 1245 | ; fallthrough 1246 | 1247 | ; Choose the cursor and buffer location on the line above 1248 | ; 1249 | ; by .vars_set: 1250 | ; Set bp = position to leave the gap for the new cursor 1251 | ; and cx = length from the start of the below line to the new cursor 1252 | ; and dx = the position to leave the cursor in 1253 | ; bx = number of characters to left edge of the screen 1254 | .set_cursor_and_buffer: 1255 | mov bx, dx 1256 | xor bh, bh 1257 | sub bx, START_COL 1258 | cmp cx, bx 1259 | jae .below_line_is_long_enough 1260 | 1261 | ; set the cursor to the end of the shorter line 1262 | mov dl, cl 1263 | add dl, START_COL 1264 | add bp, cx 1265 | jmp .vars_set 1266 | 1267 | .below_line_is_long_enough: 1268 | ; set to current line prefix length into the above line 1269 | ; leave the cursor column where it is 1270 | add bp, bx 1271 | mov cx, bx 1272 | ; fallthrough 1273 | .vars_set: 1274 | 1275 | ; Move the gap buffer forward to the character (bp) for the new cursor position 1276 | .move_gap_forward_loop: 1277 | mov byte al, [es:si] 1278 | mov byte [es:di], al 1279 | inc di 1280 | inc si ; we know we are not at USER_CODE_MAX because of the \0 1281 | cmp si, bp 1282 | jb .move_gap_forward_loop 1283 | 1284 | jmp set_cursor_and_continue 1285 | 1286 | ; ==== typing_loop helpers that use ret ==== 1287 | 1288 | %ifdef CONVERT_LAYOUT 1289 | ; Takes an ascii char in al (typed in querty) and converts it to dvorak 1290 | ; Assumes al >= ' ' && al <= '~' 1291 | convert_keyboard_layout: 1292 | xor bh, bh 1293 | mov bl, al 1294 | sub bl, ' ' 1295 | mov al, [cs:bx + keyboard_map] 1296 | ret 1297 | %endif 1298 | 1299 | ; If the current line is scrolled over, repaint it unscrolled 1300 | ; 1301 | ; Expects that scan_backward has already been called and takes it's output as 1302 | ; input 1303 | _repaint_current_line_if_needed: 1304 | ; check the line prefix length against (cursor_col - START_COL) 1305 | ; because it's possible to be scrolled from moving left but the prefix is less 1306 | ; than ROW_LENGTH and in that case we still need to repaint 1307 | xor bh, bh 1308 | mov bl, dl 1309 | sub bl, START_COL 1310 | cmp cx, bx 1311 | jbe .current_line_not_scrolled 1312 | ; reprint the current line unscrolled 1313 | push dx 1314 | mov cx, ROW_LENGTH 1315 | mov dl, START_COL 1316 | call print_line 1317 | mov ax, 0x0001 ; scroll markers: set to on ; right margin 1318 | call set_line_scroll_marker 1319 | mov ax, 0x0100 ; scroll markers: set to on ; right margin 1320 | call set_line_scroll_marker 1321 | pop dx 1322 | .current_line_not_scrolled: 1323 | ret 1324 | 1325 | 1326 | ; Shifts the text up or down N rows in a block (leaving a blank line), 1327 | ; starting at the current cursor line (used to clear a line for save_new_line or 1328 | ; join lines in backspace) 1329 | ; 1330 | ; Saves bp 1331 | ; 1332 | ; Args: 1333 | ; dx : The top left of the block to scroll, goes until the bottom right of the 1334 | ; text area 1335 | ; al : 0 for up, non-zero for down 1336 | ; bl : number of rows to scroll 1337 | scroll_text: 1338 | push bp 1339 | push dx ; cursor position 1340 | push cx 1341 | 1342 | ; set ah = 6 for going up; 7 for going down 1343 | mov ah, 6; BIOS scroll up, means make room at the bottom 1344 | test al, al 1345 | jz .up 1346 | ; .down: 1347 | inc ah ; set the BIOS command for down 1348 | .up: 1349 | 1350 | mov ch, dh ; start at the cursor row for all 3 (and go until the bottom) 1351 | mov al, bl ; set the number of rows to scroll 1352 | 1353 | ; Save ax because BIOS doesn't preserve ax 1354 | push ax 1355 | mov bp, sp 1356 | 1357 | ; Scroll the user code text area 1358 | ;mov ax, [bp] 1359 | mov cl, START_COL 1360 | mov dx, MAIN_BOTTOM_RIGHT 1361 | mov bh, MAIN_COLOR ; Also clear bh because it's the page number for write string below 1362 | int 0x10 1363 | 1364 | ; Scroll the left side scroll markers (even if there's nothing there) 1365 | mov ax, [bp] 1366 | mov cl, START_COL - 1 ; left one column from the upper left corner (columns are the low bits, so we can save an instruction) 1367 | mov dh, END_ROW 1368 | mov dl, START_COL-1 1369 | mov bh, BORDER_COLOR ; Also clear bh because it's the page number for write string below 1370 | int 0x10 1371 | 1372 | ; Scroll the right side scroll markers 1373 | ; Note: I reset some registers to the value they should still have 1374 | ; just in case the BIOS clobbers them 1375 | mov ax, [bp] 1376 | mov cl, END_COL+1 1377 | mov dh, END_ROW 1378 | mov dl, END_COL+1 1379 | mov bh, BORDER_COLOR ; Also clear bh because it's the page number for write string below 1380 | int 0x10 1381 | 1382 | pop ax 1383 | pop cx 1384 | pop dx 1385 | pop bp 1386 | ret 1387 | 1388 | ; Print a line from the buffer at the starting at the current cursor position 1389 | ; Move the VGA cursor to the end of the print 1390 | ; 1391 | ; Args: 1392 | ; [es:bp] : pointer to print from 1393 | ; cx : number of characters to print 1394 | ; dx : cursor position to print from 1395 | print_line: 1396 | ; Print the line from the buffer 1397 | ; bp is already the pointer to print from 1398 | ; cx is number of chars to print 1399 | ; dx is the cursor position to print at 1400 | xor bh, bh ; page number 0 1401 | mov bl, MAIN_COLOR 1402 | mov ah, BIOS_PRINT_STRING ; Write String 1403 | mov al, 1 ; no attr bytes, move the cursor 1404 | int 0x10 1405 | ret 1406 | 1407 | ; Scan backwards to the first \n and count the length 1408 | ; 1409 | ; Args: 1410 | ; - bp : pointer to scan starting from 1411 | ; Returns: 1412 | ; - bp : pointer to the start of the current line 1413 | ; - cx : number of characters up until the the write pointer (di) 1414 | scan_backward: 1415 | xor cx, cx 1416 | ; Check if we started on a \n 1417 | cmp byte [es:bp], `\n` 1418 | je .done 1419 | inc cx 1420 | 1421 | .loop: 1422 | cmp bp, [cs:user_code_start] 1423 | je .done 1424 | cmp byte [es:bp-1], `\n` 1425 | je .done 1426 | dec bp 1427 | inc cx 1428 | jmp .loop 1429 | 1430 | .done: 1431 | ret 1432 | 1433 | ; Scan forward counting string length for printing the row 1434 | ; Stop at '\n' or the end of the buffer 1435 | ; 1436 | ; Args: 1437 | ; - bp : start pointer to scan from 1438 | ; Returns: 1439 | ; - cx : the length of the line from bp up to the 0 or \n 1440 | ; - bp : pointer to the end of the current line (a 0 or a \n) 1441 | scan_forward: 1442 | xor cx, cx 1443 | .find_string_length: 1444 | cmp byte [es:bp], 0 1445 | je .found_length 1446 | cmp byte [es:bp], `\n` 1447 | je .found_length 1448 | inc cx 1449 | inc bp 1450 | jmp .find_string_length 1451 | 1452 | .found_length: 1453 | ret 1454 | 1455 | ; Sets or clears left or right markers for horizontal line scrolling 1456 | ; Note: This should be called with call 1457 | ; 1458 | ; Args: 1459 | ; ah : 1 set the marker, 0 clear the marker 1460 | ; al : 0 for right side, 1 for left side 1461 | ; dx : cursor position (sets the marker on the cursor row) 1462 | set_line_scroll_marker: 1463 | push dx 1464 | push cx 1465 | 1466 | ; Set the column to print at and the character to print based on al 1467 | test al, al 1468 | jz .right_side 1469 | ; .left_side: 1470 | mov dl, START_COL-1 1471 | mov al, '>' 1472 | jmp .check_clear 1473 | .right_side: 1474 | mov dl, END_COL+1 1475 | mov al, '<' 1476 | .check_clear: 1477 | 1478 | ; Clear the marker depending on ah 1479 | test ah, ah 1480 | jnz .no_clear_marker 1481 | mov al, ' ' ; clear the marker by printing a space 1482 | .no_clear_marker: 1483 | 1484 | ; Move the cursor to the margin 1485 | mov ah, 0x02 1486 | xor bh, bh ; page number (for both calls) 1487 | int 0x10 1488 | 1489 | ; Print the character 1490 | ; Note: don't use a single Write String call because we'd have to change es 1491 | mov ah, 0x09 1492 | mov cx, 1 ; number of characters to print 1493 | mov bl, BORDER_COLOR 1494 | int 0x10 1495 | 1496 | pop cx 1497 | pop dx 1498 | 1499 | ; Move the cursor back 1500 | ; 1501 | ; Mostly this isn't needed because we just use dx and set the cursor at the 1502 | ; end, but sometimes it was causing bugs. So lets just do it for abstraction. 1503 | ; (also there's one case in save_new_line.reset_current_line where we need it) 1504 | mov ah, 0x02 1505 | xor bh, bh ; page number (for both calls) 1506 | int 0x10 1507 | 1508 | ret 1509 | 1510 | ; Reset the buffer gap only if needed. 1511 | ; 1512 | ; In the middle of the buffer it does a copy from es:si to the end, but at the 1513 | ; end we can do it without a copy. 1514 | ; 1515 | ; Clobbers bp,cx (this is called at the beginning of typing_loop helpers only) 1516 | maybe_reset_gap: 1517 | ; If we're at the end of the buffer we can reset the gap without a copy 1518 | cmp byte [es:si], 0 1519 | jne .reset_gap_with_move 1520 | 1521 | ; Reset the end of the gap to the gap_start+GAP_SIZE clipped to USER_CODE_MAX 1522 | lea si, [di+GAP_SIZE] ; reset to the default gap size (might overflow) 1523 | cmp di, USER_CODE_MAX-GAP_SIZE ; equivalent to cmp si, USER_CODE_MAX but still works if we overflowed 1524 | jbe .still_have_room 1525 | mov si, USER_CODE_MAX 1526 | .still_have_room: 1527 | 1528 | mov byte [es:si], 0 ; keep the string null-terminated 1529 | 1530 | lea bx, [si-1] 1531 | cmp di, bx 1532 | je .no_more_room 1533 | jmp .done 1534 | 1535 | .reset_gap_with_move: 1536 | ; if the gap is not closed don't reset it yet (this is the maybe part) 1537 | lea bx, [si-1] 1538 | cmp di, bx 1539 | jne .done 1540 | 1541 | ; Scan forward to find the \0 (we know we're not already on it) 1542 | mov bp, si ; Save the beginning 1543 | .scan_forward: 1544 | inc si 1545 | cmp byte [es:si], 0 1546 | jne .scan_forward 1547 | 1548 | ; set bx = the gap size we're using (which might be clipped) 1549 | mov bx, GAP_SIZE ; set the default gap size if we don't clip it 1550 | cmp si, USER_CODE_MAX-GAP_SIZE 1551 | jbe .shift_gap ; use GAP_SIZE 1552 | 1553 | ; Calculate the max amount we can expand the gap 1554 | mov bx, USER_CODE_MAX 1555 | sub bx, si 1556 | 1557 | ; Stop if we're out of space and can't expand the gap at all 1558 | cmp si, USER_CODE_MAX 1559 | jne .shift_gap 1560 | mov si, bp ; reset the end of the gap to where it was 1561 | jmp .no_more_room 1562 | 1563 | .shift_gap: 1564 | 1565 | ; Copy from si to si+GAP_SIZE in a loop in reverse order if there's space 1566 | ; stopping at bp 1567 | 1568 | ; We're copying 2 bytes at a time so we need to fix the alignment 1569 | ; (we know there's >= 2 chars because if it was only \0 or we'd be in the other branch) 1570 | dec si ; start on the char before the last so we can copy 2 1571 | mov ax, si 1572 | sub ax, bp ; ax == num chars - 2 (since the range is [bp, si+1] inclusive) 1573 | test ax, 1 1574 | jz .shift_buffer ; if the number of chars is even, skip the extra byte 1575 | ; Copy the first byte when there's an odd number of characters 1576 | mov byte al, [es:si+1] ; +1 since we started on the second-to-last char 1577 | mov byte [es:bx+si+1], al 1578 | dec si ; now on the third-to-last char (3 is the first odd length) 1579 | .shift_buffer: 1580 | mov word ax, [es:si] 1581 | mov word [es:bx+si], ax 1582 | cmp si, bp 1583 | je .end_shift 1584 | sub si, 2 1585 | jmp .shift_buffer 1586 | .end_shift: 1587 | 1588 | ; si == bp == original end of gap location 1589 | add si, bx ; set the final position (it was left on the original location) 1590 | jmp .done 1591 | 1592 | .no_more_room: 1593 | ; Print a warning message because we can't allow more typing now 1594 | mov bp, no_more_room_msg 1595 | mov cx, no_more_room_msg_len 1596 | call print_error 1597 | ; fallthrough 1598 | .done: 1599 | ret 1600 | 1601 | ; Print an error message at the top of the screen 1602 | ; 1603 | ; Args: 1604 | ; - [cs:bp] : pointer to the message (this call manages the segment registers) 1605 | ; - cx : number of chars to print 1606 | print_error: 1607 | push dx 1608 | 1609 | ; set es to cs since the interrupt reads from [es:bp] 1610 | mov ax, cs 1611 | mov es, ax 1612 | 1613 | mov dx, MAIN_TOP_LEFT-0x0100 ; one line above the top left corner 1614 | xor bh, bh ; page number 0 1615 | mov bl, ERROR_COLOR 1616 | mov ah, BIOS_PRINT_STRING 1617 | mov al, 0 ; no attr bytes, don't move the cursor 1618 | int 0x10 1619 | 1620 | ; reset the es segment 1621 | mov ax, USER_CODE_LOC 1622 | mov es, ax 1623 | 1624 | pop dx 1625 | ret 1626 | 1627 | ; Clears any error message (or anything else) right above the text. 1628 | ; No args. Does not reset the cursor to the dx location at the end. 1629 | clear_error: 1630 | ; Clear the buffer full warning if it was full because we're deleting one 1631 | push dx 1632 | 1633 | ; Move the screen cursor to the message start 1634 | mov dx, MAIN_TOP_LEFT-0x0100 ; one row above the top left 1635 | mov ah, 0x02 1636 | xor bh, bh 1637 | int 0x10 1638 | 1639 | ; Write spaces to clear the message 1640 | ; Note: this call uses the actual set cursor position not dx 1641 | mov ah, 0x09 1642 | mov al, ' ' 1643 | xor bh, bh 1644 | mov bl, BORDER_COLOR 1645 | mov cx, ROW_LENGTH 1646 | int 0x10 1647 | 1648 | pop dx 1649 | ret 1650 | --------------------------------------------------------------------------------