Baby Steps
Other kings said I was daft to build a castle on a swamp…
Slow poking
Okay, so we’ve got to a point where we have a way to write code, encode it, enter it into memory and actually run the code. I’ve been playing around with the Enbugger and some small routines to get a handle on just how difficult it is to program like this.
The answer is that it is hellishly difficult. The encoding part, now that I’ve got the hang of it, may be slow but isn’t that bad. What is bad is that we’re poking in 1 byte at a time which takes forever. As well as being tedious, if even one byte of the routine is wrong it has the potential to take the processor off into a maze of random gibberish from which it cannot recover. Then we have to type it all in again, and again, and again. There are a couple of beneficial side effects: I’m starting to learn some simple encodings by heart and I’m definitely paying a lot more attention to what I’m typing. But it is by far the most annoying and error prone method of programming I have ever employed and I’ve learned what I can from it, it’s time for this Dwarf to build a better hammer.
The most immediate problem was that the original Enbugger has no way to save or load our progress over resets and there have been many, many resets. It’s something I considered including in the original Enbugger because I knew it was going to be a problem, but didn’t because it broke my concept of “minimal”. The first long range goal I’m setting is to come up with a better Enbugger but the first issue I needed to address was to get this persistence problem alleviated.
At this point it’s easy enough to rush in and write some code on the fly and I did just that as a learning exercise when getting used to the encoding/poking mechanism of entering a program.
Crutches
I’ve been a bit torn on the use of BIOS interrupts. On the one hand it feels like I’m cheating myself because it’s code I didn’t write. On the other hand I’ll be replacing these functions anyway so it’s not like I’m avoiding writing the code.
In the end I took a pragmatic view and I’ll be leaning on the BIOS for hardware related functions until I get around to rewriting them. This is to avoid drowning in new learning while I’m getting used to coding at this low level. For example, for floppy disk operations I would need to figure out IRQ handling, interrupts, the DMA controller, the floppy controller, etc. It would just be too much, so for now we’ll lean on the BIOS to get going and over time I’ll replace those interrupts with my own code.
Components
You may have come across the concept of library code. Essentially this is a collated set of functions which can be called from within your program. There’s something like a table in the library which matches function names to addresses and then at compile or load time a utility comes along and patches your program so that it calls the right address in memory to use the library function. There’s obviously more detail but hopefully you get the idea.
Well we’re going to use something similar. For now generic code is going to exist as a set of independent components with specific entry addresses. Each component will get instantiated with a far call and return using a far return. Unless the component needs to do a lot of stack operations it will not setup its own stack. For this journal entry we’ll be talking about constructing two components: “Load” and “Save” and some organisation to make things work nicely
Take 1: Pop quiz, hotshot
Remember I said I originally sat down and started writing some code on the fly? Well here’s the sorry tale of how that escapade wound up.
I wrote myself a “Save” component (details are coming) and called that from the Enbugger. Then I wrote a “Load” component and again called that from the Enbugger. It worked and I could make changes, load in an old save and see that the change had been reverted. This took a couple of days of intense frustration and system restarts even though the code itself is tiny. But persistence had been achieved! Champagne time!
However, there’s a problem. It’s a pretty nasty problem too. The BIOS loads in the first sector from the disk and the original bootloader then loads the Enbugger from the next couple of sectors, it doesn’t know anything about components. This means that every system reset I’d have to type in the load component so that it was available to load in whatever I’d saved. Given the sheer number of resets I was causing this was costing me hours in re-entering the load component. I’d also given future me more work to do because I’d ignored saving the bootloader and Enbugger entirely.
I’m going to dub this reaction programming and it won’t be the last time I get suckered into this kind of thinking.
Take 2: Engage brain
So I sat down and asked myself what I really wanted to achieve here. World peace? World domination? How about being able to store and restore everything, both by manually loading and automatically at system restart.
In order to do this what I came up with is to use a 1:1 mapping of memory to sectors on my floppy disk, 8KiB worth of sectors to be precise. Every time there’s a save operation all 8KiB gets written out and every time there’s a load operation (including boot) 8KiB is read in. That gives an environment where we can poke around and if things fall over then we get back to the last good saved situation, that’s a lot better than we originally had.
I decided to make my 8KiB start at address 0x1000 and after some back of envelope diagramming it is kind of arranged like this:
1000 - 1200 : Boot sector
1200 - 1400 : Stack
1400 - 2000 : Enbugger
2000 - 3000 : Components
That’s 8KiB of memory corresponding directly to the first 16 sectors, or 8KiB, of the floppy disk.
Implementation
Odd as it may seem the first thing to do wasn’t to enter the “Load” and “Save” components, I already had those. What I needed was to write a copy component to move the Enbugger from where it had originally been loaded to where I need it to be (0x1400). As it turns out, thanks to a typo the Enbugger had originally been loaded to somewhere entirely different to where I thought it was. A whole day got wasted copying it from 0x7f00 where it should have been instead of 0x07f0 and wondering why everything kept crashing when I called into the copied code. Fun times.
Copy component
# Bulk copy bytes between addresses
2100 : pusha 60
2101 : push es 06
2102 : push ds 1e
# Load up our segment:offset string operation pointers.
# ds:si - source
# es:di - destination
2103 : mov ax, 007f b8 7f 00
2106 : mov ds, ax 8e d8
2108 : mov ax, 0140 b8 40 01
210b : mov es, ax 8e c0
210d : xor si, si 31 f6
210f : xor di, di 31 ff
2111 : mov cx, 0100 b9 00 01
2114 : rep movsw f3 a5
2116 : pop ds 1f
2117 : pop es 07
2118 : popa 61
2119 : retf cb
Code listings are likely to look like this for a while and the line format is:
<physical address>: <assembly instruction> <encoded instruction>
This got written at 0x2100 because it’s temporary and I’d already given save and load their addresses.
The code should be pretty simple to understand. It just loads up a couple of pointers then uses the “rep movsw” instruction to copy 512 bytes (0x100 words) of data.
Once that had been poked in a quick “CALL 2100” moved the Enbugger to 0x1400 where I wanted it.
Encoding
I did say in a previous post that I’d work through some encodings.
| Instruction | Encoding |
|---|---|
| pusha | Read straight from the manual. |
| push es | For segment registers read straight from the manual. |
| mov ax, 007f | The manual says “B8+ rw iw” for instruction “MOV r16,imm16”. So we take b8 and add the value for the register. That value can found be in the “reg” row of the “ModR/M” table mentioned when discussing addressing. For AX “reg” equals 0 so our opcode is B8. The “iw” part just means an immediate little-endian encoded word value. Little-endian is just how Intel encodes things and means that the bytes of a value are encoded from least to greatest significance, i.e, the reverse of how you’d naturally write them. |
| mov ds, ax | The manual says “8E /r” for instruction “MOV Sreg,r/m16”. So the first byte is 8e and we need a “ModR/M” byte to indicate registers. Reading off the operand encoding table we can see the first operand (Sreg) is encoded in the “Reg” part of the byte and the second operand (r/m16) is in the “ModR/M” parts. Reading off the “ModR/M” table we cross reference the value d8. |
| xor si, si | The manual says “31 /r” for instruction “XOR r/m16,r16”. So the first byte is 31 and we need a “ModR/M” byte. First operand is in the “ModR/M” parts and second operand in the “Reg” part. Reading off the table gives us f6. |
| rep movsw | Read straight from the manual |
| pop ds | For segment registers read straight from the manual. Note how it’s the push operation +1. |
| popa | Read straight from the manual. Again it is the pusha operation +1. |
| retf | Read straight from the manual. |
Save component
2000: pusha 60
2001: push es 06
# Nothing fancy for starters. Write 8KiB starting at address 1000 out to disk.
2002: mov ax, 0100 b8 00 01
2005: mov es, ax 8e c0
2007: xor bx, bx 31 db
2009: mov ax, 0310 b8 10 03
200c: mov cx, 0001 b9 01 00
200f: mov dx, 0000 ba 00 00
2012: int 13 cd 13
2014: jnc 73 06
2016: xor ax, ax 33 c0
2018: int 13 cd 13
201a: jmp eb ed
201c: pop es 07
201d: popa 61
201e: retf cb
Nothing much going on here. We use the BIOS to save the 8KiB at 0x1000 to the first 16 sectors of the floppy.
Load component
Same as the save component but using “int 13, 2” instead of “int 13, 3” and starting at 0x2050. It’s literally a 1-byte change.
Bootloader
The bootloader code needed to be rewritten to take into account our new 1:1 mapping system.
# Super simple boot sector
# Set up segment registers and stack
1000: cli fa
1001: mov ax, 0100 b8 00 01
1004: mov es, ax 8e c0
1006: mov ss, ax 8e d0
1008: mov sp, 0400 bc 00 04
100b: sti fb
100c: xor bx, bx 33 db
# Load first 16 sectors to address 1000
100e: mov ax, 0210 b8 10 02
1011: mov cx, 0001 b9 01 00
1014: mov dx, 0000 ba 00 00
1017: int 13 cd 13
1019: jnc 73 06
101b: xor ax, ax 33 c0
101d: int 13 cd 13
101f: jmp eb ed
# Setup 80x25 16-colour text mode
1021: mov ax, 0003 b8 03 00
1024: int 10 cd 10
# Jump to our Enbugger shell
1026: jmp far 0140:0000 ea 00 00 40 01
# BIOS Boot signature
11fe: 55
11ff: aa
Again this is very simple stuff. The code is loaded by the BIOS at 0x7c00. In turn it loads our 8KiB from the floppy into address 0x1000 and jumps to the Enbugger.
Hang on
Now some of you might have figured out something very fun.
When you use the load component (CALL 2050) and it reaches the instruction that calls the BIOS the BIOS will overwrite the existing load component with whatever was saved and return to execute the instruction that now exists at 0x2064. Think how much fun there is to be had by loading in different code at that address.
Where next?
It will be more components I think. The new Enbugger is going to need things like getting lines of text, outputting strings, etc. I’d like to build up a little library of components to make that job easier before starting on rewriting the Enbugger itself. At least now I can trip over the virtual power cable and not have to type everything back in from scratch.
I’m in two minds about setting up a repository for the “code” and maybe disk images. Perhaps I’ll do that when there’s a bit more to see.
One last thing
Nobody cares if you would have written it differently. Literally nobody.
— Curufir