GRUBBY
GRUBBY is a name given to an underutilized graphical "programming language" by User:BoundedBeans.
Description
To run a GRUBBY program, start up some sort of x86 virtual machine booting off of some sort of medium or disk containing only the GRUB2 bootloader and a single grub.cfg
file containing the text of the program. Note that since the initial GRUB must be on the EFI partition, which uses a filesystem similar to FAT32, there is a limit of around 4 gigabytes of program text, since that is the maximum size of a file in FAT32. This limit does not necessarily prevent GRUBBY from being Turing-complete, since a Turing-complete system may be constructable within 4 gigabytes using disk or RAM storage (disk space and RAM size is not usually considered to affect computational class as it is a technical limitation that does not affect the language).
The full language specification is spread across the Official GNU GRUB2 Manual.
Tricks for computation
The first major limitation is that there is no numerical arithmetic. This is tricky and slow to implement, but is just barely possible with the features GRUB2 configuration files provide.
Data storage
You can store in environment variables using the set
commands.
Conditionals and looping
The first thing we need are conditionals and looping.
Luckily we have for, if, elif, else, while
, similar to the Bourne shell commands of the same name. We also have until
, which is redundant with while with an inverted condition but makes things slightly easier, and function
, which is really good since we can organize the code.
$?
is the exit status of the previous command, which is useful if we want to store it in a variable to check later, since otherwise commands would have to be directly in a conditional.
The loops, however, are constant length (using for with n words) until we can make a given length of number (such as strings of 16 or 32 binary digits).
Lookup table
We can create a binary lookup table for logic gates operating on strings containing one binary digit, using conditionals.
Strings of digits
We can create a full number with a list of bits, but to to make an adder or subtractor using the logic gate lookup tables, we need ways to concatenate strings and separate them.
Luckily, concatenation is easy. Just store both segments in variables s1
and s2
, then store ${s1}${s2}
in a variable. (You can change s1 and s2 to any valid identifier).
Separating strings is a lot harder. The only way the author could find is the regexp
command. This command has an argument --set [name]
to assign the match to a variable, but you can prefix the name with a number n and a colon to store the nth match of the regex in the string.
So, to get the first character of the variable string
, use:
regexp --set result '.' ${string}
Every other character except the first can be obtained with:
regexp --set 2:result '^.|.*$' "${string}"
The result will be stored in the variable result
.
This means numbers are optimally stored with the least significant bit first, in order to be added and subtracted.
Indefinite loops
Once we make numbers, we can now use them for looping. We can reverse a least-significant first binary string by taking the head over and over, replacing the string with its tail, and prepending the head to a new growing string. The reversed string will lexicographically order itself in the same way than numbers would order themselves, though negative numbers would complicate this and maybe require an additional conditional.
Using the test
command allows us to check string equality and lexicographical comparison with the ==, !=, <, >, <=, and >=
operators. By using a fixed length number, we can have arbitrary counted for loops, as well as richer while and until loops.
I/O
Printing to the screen can be done with the echo
command.
Input can be done with the read
command, which has the option of storing into a variable.
Drivers
Drivers can be implemented with the undocumented in*
and out*
commands for reading and writing I/O ports. From here you can connect to the disk, keyboard, mouse, USB devices, network, etc. This is just enough to implement a full operating system in GRUBBY, along with the also undocumented read_*
and write_*
for RAM and memory-mapped devices.
Seemingly, through reading the source code (grub-core/commands/iorw.c
and grub-core/commands/memrw.c
), the formats are:
in(TYPE) [-v VARNAME] PORT out(TYPE) PORT VALUE [MASK] read_(TYPE) [-v VARNAME] ADDR write_(TYPE) ADDR VALUE [MASK]
Arbitrary assembly execution
There appears to be no way to do this in RAM, since the program counter is a register, not a RAM value.
The only way to do this is:
- Write a disk driver for every disk that you plan on being compatible with, using the information in the #Drivers section.
- Use the appropriate driver to write the code onto the disk.
- Use the
chainloader
command to boot into another bootloader and maybe kernel on disk. Potentially an entire operating system, though the process of writing it and everything else would have to fit into 4 gigabytes, putting a harsh limit on the size of the operating system.
Unfortunately, the chainloader command can only be used from within a menuentry
block. The good thing is that menuentry supports any commands inside of its block, so we can just write all the code inside of a single menuentry, then execute it automatically by setting the timeout
variable outside of the menuentry block (setting it to 0 runs it instantly).
Self-modification
It's possible using drivers for a GRUB configuration file to modify its own file on disk. I'm not really sure how that would play out since it depends on whether GRUB reads it all at once (in which case it would require a restart), or sequentially (in which case self-modification could happen at runtime but would have weird effects).
GRUB configurations files are also capable of loading other configuration files of the same format using source FILE
, and that configuration file could be rewritten and executed again for more nuanced eval. Potentially it could also load itself???, in which case self-modification may be somewhat simpler.
Examples
If the command line doesn't show up for these, try pressing c.
Hello, world!
echo 'Hello, world!'
Truth-machine
read truth if test truth == 0; then echo 0 elif test truth == 1; while true; do echo 1 done fi
Logic gate calculator
Input as (1|0) (AND|OR|XOR) (1|0).
function AND { if test $1 == 0; then set result=0 else set result=$2 fi } function OR { if test $1 == 1; then set result=1 else set result=$2 fi } function XOR { if test $1 == $2; then set result=0 else set result=1 fi } read logic regexp --set gate 'AND|X?OR' regexp --set 1:left '[10]' regexp --set 2:right '[10]' eval ${gate} ${left} ${right} echo ${result}
menuentry "0/False" { echo 0 } menuentry "1/True" { while true; do echo 1 done }