Friday, Debtember 01, 2023
Unit testing from inside an assembler, part II
I started working on unit tests from inside the assembler. I'm not sure how MOS does it (as I don't read Rust) so I'm making this up as I go along. I'm using the following file as a test case for the work:
lfsr equ $F6 org $4000 start bsr random rts the.byte fcb $55 the.word fdb $AAAA ;*********************************************** ; RANDOM Generate a random number ;Entry: none ;Exit: B - random number (1 - 255) ;*********************************************** random ldb lfsr andb #1 negb andb #$B4 stb ,-s ldb lfsr lsrb eorb ,s+ stb lfsr rts .test "random" ldx #.result_array clra clrb .setmem sta ,x+ decb bne .setmem ldx #.result_array + 128 lda #1 sta lfsr lda #255 .tron .loop bsr random .assert /B <> 0 , "degenerate LFSR" tst b,x .assert /CC.z <> 1 , "non-repeating" .troff inc b,x deca bne .loop .assert @the.byte == $55 && @@the.word == $AAAA , "tis a silly test" rts .result_array rmb 256 .endtst nop ;*********************************************** end start
I've made the “unit test” … thing, a backend (like I have for binary and Color Computer-specific output as backends) because it's less intrusive on the code and I wasn't sure where to assemble the test code (within the memory space of the 6809). By making this a specific backend, it should be apparent that this is not for the final version of the code.
So far, I have it such that all the non-test backends don't see the code at all:
| FILE test.asm 1 | 2 | lfsr equ $F6 3 | 4 | org $4000 4000: 8D 04 5 | start bsr random 4002: 39 6 | rts 7 | 4003: 55 8 | the.byte fcb $55 4004: AAAA 9 | the.word fdb $AAAA 10 | 11 | ;*********************************************** 12 | ; RANDOM Generate a random number 13 | ;Entry: none 14 | ;Exit: B - random number (1 - 255) 15 | ;*********************************************** 16 | 4006: D6 F6 17 | random ldb lfsr 4008: C4 01 18 | andb #1 400A: 50 19 | negb 400B: C4 B4 20 | andb #$B4 400D: E7 E2 21 | stb ,-s 400F: D6 F6 22 | ldb lfsr 4011: 54 23 | lsrb 4012: E8 E0 24 | eorb ,s+ 4014: D7 F6 25 | stb lfsr 4016: 39 26 | rts 27 | 28 | .test "random" 29 | ldx #.result_array 30 | clra 31 | clrb 32 | .setmem sta ,x+ 33 | decb 34 | bne .setmem 35 | ldx #.result_array + 128 36 | lda #1 37 | sta lfsr 38 | lda #255 39 | .tron 40 | .loop bsr random 41 | .assert /B <> 0 , "degenerate LFSR" 42 | tst b,x 43 | .assert /CC.z <> 1 , "non-repeating" 44 | .troff 45 | inc b,x 46 | deca 47 | bne .loop 48 | .assert @the.byte == $55 && @@the.word == $AAAA , "tis a silly test" 49 | rts 50 | .result_array rmb 256 51 | 52 | .endtst 52 | .endtst 53 | 4017: 12 54 | nop 55 | 56 | ;*********************************************** 57 | 58 | end start 2 | equate 00F6 3 lfsr 17 | address 4006 1 random 5 | address 4000 1 start
Ignore that line 52 shows up twice here—that's a bug that I'll work on
(my initial fix removed the duplicate line,
but line 51 didn't show up—it's not a show-stopping bug which I why it's going on the “fix it later” list).
Also, the labels the.byte
and the.word
don't show up on the symbol list at the end due to a “feature” where labels that aren't referenced aren't printed
(that was to remove unused equates from the symbol list).
So for the non-test backends,
the actual testcase isn't part of the build.
The other added directives,
like .tron
, .troff
and .assert
are also ignored by the other backends if the directives appear outside a “unit test.”
With the .test
backend though,
all the directives are recognized and most of them work,
although I'm still working on .assert
(see below).
One issue—when to run the actual tests.
Right now,
the code is run when then .endtst
directive is hit,
as running the code as it's assembled won't work well I think,
especially with branches and calls to other routines,
and it would be a nightmare to get correct.
It's easier if all the code exists in “memory,”
but one issue I've noticed is that any code further down in the file can't be used.
I'll have to move the execution of tests to after the assembly pass is done.
The .tron
and .troff
directives work,
dumping out the instructions between them as the code is run:
... lots of lines cut PC=402A X=40B4 Y=0000 U=0000 S=7FFE DP=00 A=09 B=D2 CC=-f-i---c | 402A 8D DA - BSR 4006 ; ----- backwards PC=402C X=40B4 Y=0000 U=0000 S=7FFE DP=00 A=09 B=69 CC=-f-i---- | 402C 6D 85 - TST B,X ; -aa0- 411D = 00 PC=402A X=40B4 Y=0000 U=0000 S=7FFE DP=00 A=08 B=69 CC=-f-i---- | 402A 8D DA - BSR 4006 ; ----- backwards PC=402C X=40B4 Y=0000 U=0000 S=7FFE DP=00 A=08 B=80 CC=-f-in--c | 402C 6D 85 - TST B,X ; -aa0- 4034 = 00 ... more lines cut
Another issue is dealing with the .assert
directive.
I have to save the test somehow since the assembler can't do the check when it parses the .assert
because not all the code for the test has been assembled yet.
I could store the text to the test expression and then evaluate it at run time,
but as this code shows,
that would mean re-interpreting the text many, many times.
No,
the solution I came up with is a mini-Forth-like language for evaluating the test expression.
Yup, I'm embedding a mini-Forth interpreter in a 6809 assembler written in C.
A classic blunder I'm sure,
like getting involved in a land war in Asia,
or going against a Sicilian when death is on the line,
but I'm not sure of any other way.
The mini-Forth is very small though,
only 41 words are defined,
but it's enough for my needs.
The first .assert
expression translates to:
VM_CPUB ( push contents of the B register onto the stack ) VM_LIT 0 ( push a literal 0 onto the stack ) VM_NE ( compare the two, leaving a flag on the stack ) VM_EXIT ( exit the VM )
The second one to:
VM_CPUCCz ( push the CC zero flag ) VM_LIT 0 ( push a literal 0 ) VM_NE ( compare the two, leave flag on stack ) VM_EXIT ( exit the VM )
And the last one to:
VM_LIT 0x4003 ( push the literal 0x4003 ) VM_AT8 ( fetch the byte from the 6809 memory buffer ) VM_LIT 0x55 ( push the literal 0x55 ) VM_EQ ( compare the two, leave flag on stack ) VM_LIT 0x4004 ( push the literal 0x4004 ) VM_AT16 ( fetch two bytes from the 6809 memory buffer ) VM_LIT 0xAAAA ( push the literal 0xAAAA ) VM_EQ ( compare the two, leave flag on stack ) VM_LAND ( AND the two results, leaving flag on stack ) VM_EXIT ( exit the VM )
This works, and it was easy to implement the VM. Now all I have to do is parse the expression to assemble the VM code (right now the addresses and VM functions are hard coded into the assembler just to prove it works).
This feature is proving to be an interesting problem.
Monday, Debtember 04, 2023
I've been blogging for 757,382,400 seconds
Wow! It's been a full 365 days since my blog went under the lock! And it's been a full 8,766 days since I started blogging. Here's to another 8,766 days!
The Gopher Situation
Over the past few days,
I've been battling a pernicious bug in my gopher server wherein it becomes CPU bound and cause other issues on the server.
I then have to go in and kill the gopher process
(and the one time I couldn't even do that—I had to have the virtual server restarted).
I initially attributed this to an over-aggressive bot crawling my site and blocked it with iptables
but even that didn't solve the issue.
The problem is—nothing to my knowledge has changed on my virtual server, nor the server that it is running under, nor the network it's on. My Gemini server gets way more traffic than my gopher site and it's fine, and the only difference between the two is—the Gemini server uses TLS but otherwise, is nearly identical to the gopher server.
It's very odd.
The other day I added some code (in a branch, not in the main line version) to log memory usage, number of threads (technically, Lua coroutines), number of running threads, number of waiting threads, and number of active sockets. And since adding that, the gopher server has been running fine, but just now I do see a potential problem—the number of threads is two higher than the number of actual connections, which “shouldn't” happen.
Woot! I now have a lead on the problem!
But I do wonder what recently caused the issue? The code hasn't changed since April, and now I'm wondering if my Gemini server has a similar issue, since the code bases are similar in nature.
Tuesday, Debtember 05, 2023
Notes on an overheard conversation at 4:15 am
SLAM!
“Hey! I heard the screen door slam! Could you check the front porch?”
“Who would be here at … 4:00 am‽”
“Probably an Amazon delivery. I ordered a white noise machine last night.”
“Seriously?”
“Just go look!”
“Hmm … whoa! Lovely! They left the package right on the door step. Sorry about stepping on it. And yes, it's from Amazon.”
“It arrived! My white noise machine!”
“When did you say you ordered it?”
“9:00 pm. They said it would arrive shortly.”
“That's insane!”
“That's Amazon.”
Wednesday, Debtember 06, 2023
Unit testing from inside an assembler, part III
I'm done with the “unit testing” backend for my 6809 assembler. The mini-Forth engine is working out fine, although the number of words increased from 41 to 47 to support some conveniences (like indexing and string comparison). It took some work to support, but the number of assertions one can make in the code is extensive. For example, a test case for this bit of code (which I do need to discuss, but that's a post for another time) looks like this:
test sts [$3333,x] .next pshs pc,u,y,x,dp,b,a,cc .test "STS" ldx #.results ldy #test jsr init .assert /x = .results , "X=results" .assert /y = .next , "Y=next" .assert @@/0,x = .address .assert @@/2,x = .opcode .assert @@/4,x = .operand .assert @@/6,x = .topcode .assert @@/8,x = .toperand .assert @.nowrite = $12 , "overwrite" .assert @/-47,s = $01 , "stack mod?" .assert .address = "0800"z , "hex address" .assert .opcode = "10EF"z , "hex opcode" .assert .operand = "993333"z , "hex operand" .assert .topcode = "STS"z , "decoded opcode" .assert .toperand = "[3333,X]"z , "decoded operand" rts .results fdb .address fdb .opcode fdb .operand fdb .topcode fdb .toperand .address rmb 5 .opcode rmb 5 .operand rmb 7 .topcode rmb 9 .toperand rmb 19 .nowrite nop .endtst
The code being tested is a 6809 disassembler written in 6809 assembly code
(I wrote that a few years back—any testing now is academic at this point).
The .TEST
directive takes an optional string as the name of the test.
If one isn't given,
it will use the last non-local label seen in the source code as the name of the test.
The first two lines:
.assert /x = .results , "X=results" .assert /y = .next , "Y=next"
assert that the X register points to .results
and the Y register points to .next
.
I use the leading slash to denote a register instead of a label.
One can use register names for labels and it's mostly unambiguous as the register is typically part of the mnemonic itself.
The only exception is for the A, B and D registers,
and then,
only in the index addressing mode,
as you can use the A, B or D register for an offset.
But in the context of the .ASSERT
directive it makes it easier to parse the intent if I use '/' to designate a register.
Each register,
and each bit in the condition code register
(like /cc.z
for the zero-flag)
can be used.
The bit after the comma,
“X=results”,
will be printed if the check fails:
test-disasm.asm:7: warning: W0015: STS:13 X=results: test failed:
(there can be text after the “test failed” bit, thus the colon).
The next few lines:
.assert @@/0,x = .address .assert @@/2,x = .opcode .assert @@/4,x = .operand .assert @@/6,x = .topcode .assert @@/8,x = .toperand
assert the contents of memory pointed to by X. The double “@” fetches 16 bits from the address following, and in the first line, this is the address in the X register. The second line retrieves the 16 bits from the address two bytes past where the X register points to. You could write these lines as:
.assert @@(/x + 2) = .opcode
but a little syntactic sugar never hurts, and it mimics the native method of using the index registers. This was possibly the hardest bit of code to write, as the index addressing mode of the 6809, while great from an assembly programmer's perspective, is a nightmare from an assembler-implementer's perspective. Even here, where it's simplified, was a pain to get right, but I think it was worth it.
The next two lines:
.assert @.nowrite = $12 , "overwrite" .assert @/-47,s = $01 , "stack mod?"
check that the given addresses,
nowrite
and a byte down in the system stack,
contain certain 8-bit values.
Each byte of the memory in the virtual 6809 system is filled with the value 1
(it can be changed on the command line),
so here,
each untouched byte will contain a 1.
I picked that value since it's an illegal opcode,
which the emulator will trap.
The final few lines:
.assert .address = "0800"z , "hex address" .assert .opcode = "10EF"z , "hex opcode" .assert .operand = "993333"z , "hex operand" .assert .topcode = "STS"z , "decoded opcode" .assert .toperand = "[3333,X]"z , "decoded operand"
does indeed, do a string compare. And therein lies a tale. Again, this is a form of syntactic sugar:
.assert @.address=$30 && @(.address+1)=$38 && @(.address+2)=$30 && @(.address+3)=$30 && @(.address+4)=0
This was the second hardest bit to to support,
is a bit fragile,
and,
if I'm honest,
a hack.
The string literal has to be on the right hand side of the conditional,
and worse,
there's no easy way to enforce this in the assembler
(so I currently don't).
Third,
the second string has to be a literal string—you can't compare two different memory regions from the 6809 VM.
There's also a limit of only one string literal per .ASSERT
directive,
again,
because supporting more than one would vastly complicate the already somewhat complicated code
(this “unit test“ backend is already 30% of the entire assembler).
To keep from having to add a ton of code for the conditional checks to support two different primitive types, or to keep from having to create a duplicate set of string conditionals, I cheated (or came up with a brilliant hack—take your pick). The code generated is:
VM_LIT .address VM_SCMP VM_EQ VM_EXIT
That VM_SCMP
is hiding things—it knows which string literal to use
(as it's part of the VM program and there's only space for one string literal per .ASSERT
directive)
but it also leaves two values on the stack: -1,0 if the result is less than,
1,0 if the result is greater than, and 0,0 if the result is equal.
This way, the conditional operators can work as is.
Oh,
those “z”s on the end of each string literal?
Well,
the assembler supports several methods of storing string data in memory.
There's the standard C NUL terminated strings;
the OS-9 method of setting bit 7 of the last character of the string,
and the sometimes used method where the first character of the string is actually the length.
I originally had separate non-standard directives to support these methods,
so when I wanted to support string-comparisons,
I needed a way to support these methods.
Then it hit me—the use of a suffix on the string—“Z” for the NUL terminated one (“Z” stands for “zero”),
“H” for the bit 7 set (“H” for “high-bit”) and “C” for counted strings.
And if I'm using the suffixes for the “unit test” backend,
why not in general?
So I replaced the .ASCIIZ
and .ASCIIH
directives
(I was contemplating adding counted strings but I never got around to adding .ASCIIC
)
with just .ASCII
and the use of a suffix
(no suffix, string is left as-is).
So, back on track. The expressions can get quite involved. Some examples:
.assert /b = -(@lfsr & 1) & $B4 .assert @tvalue = $10*3+(1<<3)+2*2+(7-5)+1 .assert @@(tvalue + 1) = $10+3+1<<3+2*2+7-5+1
You are also not limited to using the .ASSERT
,
.TRON
and .TROFF
directives inside a .TEST
directive.
You can put them anywhere in the codebase,
and if that code is executed as part of a “unit test”,
they'll trigger
(and if you aren't using the “unit test” backend,
they're ignored outright).
There are other changes too—each backend will parse its own command line options, I added some new warnings (such as a waring for self-modifying code), and the memory of the virtual 6809 can have various protections (read-only, write-only, execute-only, trace) set from the command line for further testing.
Now I just need to update the README.txt
file and release the code.
Friday, Debtember 08, 2023
The Gopher Situation, part II Unicode Booglaloo
The lead I thought I had was a red herring. I thought it may have had something to do with reporting errors back to the client as seen from the logs:
Dec 04 21:44:38 daemon info 71.19.142.20 gopher maxco=1 runco=0 toq=0 sc=1 mem=3012577 Dec 04 21:46:23 daemon err 71.19.142.20 gopher stat("/home/spc/gopher/share/MGLNDD_71.19.142.20_70") = No such file or directory Dec 04 21:46:23 daemon info 71.19.142.20 gopher remote=XXXXXXXXXXXXXXX status=false request="MGLNDD_71.19.142.20_70" bytes=68 Dec 04 21:49:38 daemon info 71.19.142.20 gopher maxco=2 runco=0 toq=0 sc=1 mem=2903218 Dec 04 21:50:52 daemon info 71.19.142.20 gopher remote=XXXXXXXXXXXXXXX status=true request="CONNECT api64.ipify.org:443 HTTP/1.1" bytes=562 Dec 04 21:54:38 daemon info 71.19.142.20 gopher maxco=2 runco=0 toq=0 sc=1 mem=3035838
Notice how maxco
(total number of coroutines)
increments and stays that way after a failed request.
And the evidence was pretty convincing too:
Dec 04 22:19:38 daemon info 71.19.142.20 gopher maxco=2 runco=0 toq=0 sc=1 mem=3411531 Dec 04 22:23:44 daemon info 71.19.142.20 gopher remote=XXXXXXXXXXXXXXX status=true request="Phlog:2010/03/08" bytes=189 Dec 04 22:24:38 daemon info 71.19.142.20 gopher maxco=2 runco=0 toq=0 sc=1 mem=3185119 Dec 04 22:24:39 daemon info 71.19.142.20 gopher remote=XXXXXXXXXXXXXXX status=false request="\3\0\0/*?\0\0\0\0\0Cookie: mstshash=Administr" bytes=82 Dec 04 22:25:57 daemon info 71.19.142.20 gopher remote=XXXXXXXXXXXXXXX status=true request="Phlog:2006/11/19.1" bytes=1028 Dec 04 22:29:38 daemon info 71.19.142.20 gopher maxco=3 runco=0 toq=0 sc=1 mem=3242133 Dec 04 22:33:31 daemon info 71.19.142.20 gopher remote=XXXXXXXXXXXXXXX status=true request="Phlog:" bytes=978 Dec 04 22:34:38 daemon info 71.19.142.20 gopher maxco=3 runco=0 toq=0 sc=1 mem=3207881
Error reporting with the gopher protocol is clearly an afterthought. The official RFC has two occurances of the word “error” in it—and one of them is redundant. I did read somewhere (that's difficult to find now) that perhaps gopher should simple close the connection upon an error instead of sending an “error” to the client, so I thought I would try that. Instead of sending:
3Selector not foundHTfooHTgopher.conman.orgHT70CRLF
I would just close the connection.
That didn't work.
The gopher server was still getting stuck.
Attaching gdb
to the stuck process didn't show anything,
as the Lua executable I was using didn't have debugging symbols.
So then I recompiled Lua and the modules used that were written in C to include debugging information and restarted the server,
yet again.
So now I think I think I found the root issue.
Attaching gdb
this time showed the server was stuck in LPEG.
Even better,
I could see the text it was trying to parse and well … previously I said,
“[t]he code hasn't changed since April.”
That's not quite true.
The server code hadn't changed since April,
but an extension had!
Back in late October I modified the code that renders my blog on gopher to use Unicode combining characters to do some typographical tricks,
and it seems that the code used to wrap the text just … wasn't up to par
(Unicode is hard! Let's go to Mars!).
I also noticed that my Gemini server had finally crashed—hard. And I changed that too, to use Unicode typographical tricks. So out it comes!
Let's see if this was the problem.
Update on Tuesday, December 19th, 2023
Monday, Debtember 11, 2023
Some thoughts on unit testing from inside an assembler
I've been writing some new 6809 assembly code as well as going back to some existing projects, trying out the “unit test” feature from my 6809 assembler. I will admit that running “unit tests” from the assembler is wonderful! It cuts debugging time since the feedback loop goes from “edit code, assemble, load into emulator, run, edit code” to “edit code, assemble, edit code,” which makes it more likely I'll use the feature. Also nice is that when I'm done with the testing, I change the backend and the testing code is no longer part of the program. Yes, the tests still reside in the source code, but they're ignored if not required.
The issue I've always had with “testing über alles” is that it doesn't take the language or tooling into account, and it's the tooling and language support that can make or break “unit tests” (whatever a “unit test” is). Personally, I like it that I can write tests near the code to be tested and have the assembler run them for me (and it seems like the only modern language to get this right is Rust). Having “unit tests” in a separate file, or having to go through several hoops to run the tests is, for me, just too much friction to use unless forced.
As an aside, I'm amazed that IDEs haven't made writing “unit tests” easier, or just write them entirely as they already have information about each function—what they take and what they return. I mean, they already support refactoring, how hard can it be to support automatic “unit tests?” Or is this a thing I'm missing out on because I don't use IDEs?
Tuesday, Debtember 19, 2023
The Gopher Situation, part III, The Search For Uptime
It's been over two weeks and the gopher server has been up and running for all that time. Yup, it was Unicode. Or rather, my inability to wrap Unicode properly.
A bit of background on compilers exploiting signed overflow
Why do compilers even bother with exploiting undefinedness signed overflow? And what are those mysterious cases where it helps?
A lot of people (myself included) are against transforms that aggressively exploit undefined behavior, but I think it's useful to know what compiler writers are accomplishing by this.
TL;DR: C doesn't work very well if int!=register width, but (for backwards compat) int is 32-bit on all major 64-bit targets, and this causes quite hairy problems for code generation and optimization in some fairly common cases. The signed overflow UB exploitation is an attempt to work around this.
Via Comment on ”Bug in my code from compiler optimization [video] | Hacker News”, A bit of background on compilers exploiting signed overflow
A cautionary tale about compiler writers exploiting undefined behavior. I don't have much to add here, other than to spread a bit of awareness of why this happens.
Timing code from inside an assembler
Back in March, I wrote about some 6809 optimizations where I counted CPU cycles by hand. I came across that code the other day and thought to myself, my 6809 emulator counts cycles, and I've embedded it into my 6809 assembler—how hard could it be to time code in addition to testing it?
Turns out—not terribly hard.
I added an option to the .TRON
directive to count cycles instead of printing code execution and have the .TROFF
directive print the cycle count
(indirectly,
since the code isn't run until the end of the second pass of the assembler).
Then I wrote up a few tests:
.test "ROM-RAMx1-byte" ldx #$8000 .tron timing r2r1 sta $FFDE lda ,x sta $FFDF sta ,x+ cmpx #$FF00 bne r2r1 .troff rts .endtst ;***************************************************************** .test "ROM-RAMx2-byte" ldx #$8000 .tron timing r2r2 sta $FFDE ldd ,x sta $FFDF std ,x++ cmpx #$FF00 bne r2r2 .troff rts .endtst ;***************************************************************** .test "ROM-RAMx4-byte" ldx #$8000 .tron timing r2r4 sta $FFDE ldd ,x ldu 2,x sta $FFDF std ,x++ stu ,x++ cmpx #$FF00 bne r2r4 .troff rts .endtst ;***************************************************************** .test "ROM-RAMx8-byte" savesp equ $0100 orcc #$50 sts savesp lds #$FF00 - 8 .tron timing r2r8 sta $FFDE puls u,x,y,d sta $FFDF pshs u,x,y,d leas -8,s cmps #$8000 - 8 bne r2r8 .troff lds savesp andcc #$AF rts .endtst
And upon running it:
GenericUnixPrompt% a09 -ftest r2r.asm ROM-RAMx1-byte:13: cycles=877824 ROM-RAMx2-byte:28: cycles=487680 ROM-RAMx4-byte:45: cycles=357632 ROM-RAMx8-byte:64: cycles=199136
The results match what I calculated by hand, so that's good. It also found a bug in the emulator—I had the wrong cycle count for one of the instructions. It's a bit scary how easy it has become to test 6809 assembly code now that I can do much of it when assembling the code.