Some experiments with hacking the ЭЛЕКТРОНИКА МК-61
Just so Google knows, the МК-61 is a Soviet-Russian programmable scientific calculator. In Latin characters, it's written Elektronika MK-61. МК is short for Микрокалькулятор ("microcalculator").
I acquired mine by swapping it with Sander from Estonia for a batch of old lenses and super-8 cameras, but if you don't have friends in the Baltic states, you can probably find them at various online auctions, such as the German eBay. I especially recommend travelling to the former Iron Curtain and trying to find one on the flea markets yourself.
When you get your hands on a Soviet-Russian programmable calculator like I did, and you're even the slightest bit curious, you look it up on the Internet and find that they can be very quirky machines. Your required reading is the Museum of Soviet Calculators On the Web (abbreviated M.O.S.C.O.W.), and Greg Escov's absolutely wonderful (but unfortunately completely dead) site about how to hack the МК-61. Once you've read those sites, applied Greg's recipes to your machine and ventured into its strange innards, you start to play around a little bit and come across some interesting properties that are apparently undocumented on the web. From one thing comes another, and before you know it you've started on a site explaining some advanced calculator wizardry.
Before this page comes up to steam, I would like to apologize to all the Russian calculator hackers of the 1980's for stealing their turf, because I'm certain that they pioneered everything I discovered back then and probably considered it child's play. It's just because these people don't publish (or maybe only in Russian) that I haven't seen any of their articles and had to figure out everything by myself. I don't consider myself a hacker, and credit to whom it's due and all that, and please, mythical Russian МК-61 hacking scene, don't beat me up.
And let's give some extra credit to Greg Escov, who I mentioned before and on whose sholders I stand. His work was the starting point for mine. If you, the reader, have any additional tips or information, send them my way!
The МК-61, a hacker's system?
Compared to western programmable calculators, the МК-61 is far less refined, certainly when it comes to locking the machine down against abuse. It seems like the people who designed it were counting on the fact that anyone competent to use a programmable calculator would have the common sense to not push it past its limits and expect it to stay orthogonal. I like this attitude because it doesn't patronize me. If you use the МК-61 as a normal programmable machine you will never experience any odd behaviour, but if you decide not to play fair and willingly inflict some good old torture, you will have it squealing in no time. That's the point of this page: let's pull some stuff and and smirkingly observe how it reacts!
The BCD system
Don't you hate it when you're reading a site on an advanced technical subject and the author takes you on a ride to some really basic concepts? Let's say you're reading about Win32 assembly language and they start explaining the hexadecimal system. What were they thinking? I like to take the position that if you're smart enough to land on a page like that, you most likely know what hex is, or else you're smart enough to find that out (or decide that you shouldn't be doing assembly). The absolutely last place where you want to have a big discussion about the fundamentals is on a specialized page.
In case you hadn't noticed, this page is about hacking Soviet-Russian programmable calculators. Shall we agree that that's a fairly advanced topic? I would like that to imply that I don't have to explain all the basics to you like you're some random luser, but unfortunately I can't shed my engineer's conscience and avoid from doing a chapter about the BCD system, because BCD is quite fundamental to the way the МК-61 operates. Don't worry though, if you know what BCD is you can skip this whole chapter. Knowledge saves time once again!
Say you're a calculator. You work on numbers. So. In order to work on numbers, you need some sort of way to represent them in your cold electronic internal brain thing. The standard approach seems to be storing the numbers as a series of on/off transistor states that when combined, represent a certain number in binary notation. Simple enough. But ever considered that there's more than one kind of binary notation? There's the very flat representation of the natural numbers that most everyone is familiar with from high school, there's two's complement where the most significant bit is a sign bit, and there are various floating point formats. Plus probably a few more that are not really relevant here.
A calculator that only operates on natural numbers (0, 1, 2, 3, ...) wouldn't be very useful, and neither one be only works on whole positive or negative numbers. You'd at least want some sort of floating-point representation to do meaningful computations. So it would seem that one of the many floating point notations would be the way to go. However, that's not how most older calculators do it, and for good reason. Since calculators have very limited hardware and firmware, their designers sought for ingenious ways to lower the resource requirement. One of those ways was to use Binary Coded Decimal notation instead of floating point, for reasons that I'll give below. But first I'll explain BCD.
In the Binary Coded Decimal system, every chunk of four bits represents one digit
from 0−9. Think of it as a "fixed-field" binary encoding where every block of four
bits is one digit. For instance, the number 36 would look like 00110110, which can
be chopped up into 0011 0110, or 0011 (= 3) and 0110 (= 6), giving us
36h
(h is for hexadecimal) and confirming our suspicion that
BCD numbers, when written out in hex
notation, resemble their decimal value.
For calculators, there are two seeming disadvantages and one definite advantage
to this system. The first disadvantage is that BCD is less economical than pure binary, because each group of
four bits is only allowed to hold the values 0−9
, while four bits are
physically capable of storing numbers 0−15
. This means you only use
62.5%
of the number space. However, this only becomes a problem if
you're planning to use BCD numbers
for mass storage, which obviously isn't the case in a calculator.
The second disadvantage is that arithmetic is easier on pure binary numbers than
it is on BCD numbers, due to the
somewhat artificial notion of a BCD
number. BCD arithmetic involves a
couple of well understood postfix operators to ensure that the result of a
calculation involving BCD numbers is
itself a BCD number. For instance,
there's the Adjust After Addition instruction that ensures that when you add
7
and 4
, the result is not 0Bh
, but
11h
. It does this by carrying any digits larger than nine. There are a
few more of these postfix operators that are well understood and easy to implement
directly in hardware, making this only a minor disadvantage.
The important advantage for calculators is that with BCD you always know where to find the individual decimal digits
of a number, since every decimal digit always occupies exactly one block of four
bits. The first digit is always in bits 0−3
, the second always in bits
4−7
, the third always in bits 8−11
, and so on. This means
you can easily output numbers to the screen without having to implement a costly
"hex to decimal" routine, because you just hard-wire four-bit chunks from the
display register to the corresponding digits on the display and be done. For
calculators with no programmable processor to even run any kind of hex-to-decimal
routine, this is a major advantage.
Hexadecimal digits on the МК-61
Since this site is about programming a Soviet-Russian calculator, and since I
used hexadecimal numbers in the paragraphs above and you're obviously still reading,
I think I can afford to assume that you know what the hexadecimal number system is.
Briefly, it's counting from 0 to 15 instead of from 0 to 9, using the letters
A
through F
as single-digit abbreviations for the decimal
numbers 10 to 15. This means you can now express values from 0 to 15 in a single
digit, which in turn means you can express all 16 states of a group of four bits in
a single digit. And this is useful because it allows us to make concise block
statements about given binary values. For instance, DAAh
is equivalent
to 3498, or 110110101010b
, which is also simply 1101−1010−1010 (D−A−A).
Three-letter acronyms like this allow us to concisely specify all possible
states of a group of twelve bits in just three characters. For any kind of computer
related use, hexadecimal notation is often the most natural way of doing things.
As was noted above, if you express BCD numbers in hexadecimal, they look like their normal decimal equivalent. For instance, 6753, if converted to BCD, would be 6753h. This is because each digit in a BCD number is four bits large, just like in a hex number. You could say that BCD numbers are just a special case of hexadecimal numbers, one where you don't use A to F. (But you'd be wrong, because hexadecimal numbers are base-16 instead of base-10 like BCD, so don't be fooled by the apparent similarity.)
Any valid BCD number only
contains the characters 0−9
, and not any of the additional six hex
characters A
, B
, C
, D
,
E
and F
that could also be represented in four
bits. If BCD was implemented cleanly
on this calculator, the extra digits would forever be unreachable and we would have
to resort to hardware hacks to find out how they would display. Fortunately for us,
the МК-61 does not implement BCD
cleanly. For certain good reasons, the designers of the МК-61 incorporated full
hexadecimal numbers that display in certain strange ways, and for reasons more
unclear, they allowed the simple user to manipulate hex numbers by means of the
fairly ingermane hex logic functions AND
, NOT
(which I
will call INV
from now on, because so does the calculator: ), OR
and XOR
. It's hard
for me to see why they included this, given the presumed lack of any large Russian
computing industry back in 1983, but it's at least laudable that they made the extra
effort to help computer scientists federation-wide.
Believe it or not, the normal МК-61 user (assuming the normal user wasn't by
definition a hacker) was quite accustomed to the МК-61's use of full hexadecimal
digits. The МК-61 uses them for various good reasons, as we will see below, and this
is also why the calculator has a nonstandard way of displaying them. Instead of
showing the characters A
through F
, it displays them as
follows:
Decimal | Hex | МК-61 |
---|---|---|
10 | A | |
11 | B | |
12 | C | |
13 | D | |
14 | E | |
15 | F |
There are a few good reasons why the МК-61 uses the hexadecimal system and displays it in precisely this way:
An eight-segment display cannot distinguish between a(Update: it actually can if it uses the lowercase forms for b and d.)B
and an8
, or aD
and a0
, so the designers had to break something.- The
F
character is probably purposely non-printing because it's used to right-pad numbers with. Though it's possible that the calculator suppresses leading zeroes and left-adjusts the number for output to the display, trailing numbers with non-printing values ofF
means saving yourself the left-adjusting. You don't compromise arithmetic if you just stop at the first occurrence ofF
, since it's not a valid BCD number anyway. This is corroborated by the fact that the МК-61 acts very strangely when presented with a number consisting of allF
's, as we'll demonstrate below. - The designers needed an R character to display "ERROR", but since
implementing full alphanumeric support was out of the question, they
tailored some unused character to their needs and hard-coded the error
message as a constant,
EDD0DFFFh
, displayed as the familiar . - There are 105 program steps and over 200 opcodes; the only way all of these can consistently be represented in two digits is by harnessing the extra number space of hexadecimal digits.
And once you're using hexadecimal for all kinds of things anyway, you might as well add the logic functions as an easy bonus.
Creating any hex number
The МК-61's reluctance about hexadecimal numbers exposes itself in the strange
fact that you can't actually enter the hexadecimal digits A
through
F
in auto mode through the keyboard for use in the hex logic functions.
Sure, you can enter A
through E
(not F
)
directly in programming mode as names of registers, but that doesn't work in auto
mode. The obvious reason is that this protects the МК-61 from the inadvertent
appearance of hex digits in the wild, where they could potentially cause strange
behaviour (and do, as we'll see later when we forge some hex digits anyway). This is
because the МК-61 doesn't (officially) know how to perform hex arithmetic, so with
the carries and all. The question remains why the hell we got the logic functions,
because they don't really make a whole lot of sense. Luckily, they still come in
handy...
Okay, you think at first, banning the outright entry of hex numbers works fine to safeguard the calculator's integrity, but then you think: why on earth did they give us those hex logic functions, if they allow us to craft any hex number with only minor inconvenience? (Gigglish hacker laugh.) Gone is the chastity belt! The method is as follows, but you can use all sorts of combinations:
Pick a hexadecimal number. Then split the number into two parts that produce our
hex number when OR
'ed. You can use this algorithm:
- The first number is the target number in hex notation, but with
8
's substituted for all the occurrences of8
and higher. - The second number is all zeroes, except for the values of
8
and higher, they're subtracted by eight.
The idea is that the first number will set the MSB's where required and the
second number will set the remaining three bits as necessary. Let's take
EDD0DFFFh
as an example; the error message constant. Applying the
algorithm, the first number becomes 88808888
and the second
65505777
. Now OR
these two. Hmm, strange, not what we
expected: you get , or 8.DD0DFFFh
. How could this be? Well, to not keep you in
suspense, the МК-61 knows that it would be in trouble if it just let you forge any
hex number in memory, because that would be practically the same as simply entering
it from the keyboard, and we can't have that. So numbers generated by the
hex arithmetic functions are normalized, or quarantined, or tamed, or
neutered, as you will, by only allowing them to live as the fractional part of an
8
. Danger averted, plan thwarted.
This obviously isn't any fun, as we can't use our newly-forged hexadecimal
numbers for evil. Simple demonstration: if you save a hex number to RG0
and try to jump to that register with
, the 8 in front of
the comma takes precedence and you find yourself at location 7
. It
makes them completely harmless.
Is there any way to free a hexadecimal number from its curved captor? At first
the answer seems to be no, since all arithmetic functions, such as subtracting eight
from the number, normalize the hex 'tail' by converting it to a decimal number, so
the fractional part stays harmless. Yet there is a way, and it comes in the form of
the function ,
which returns the fractional part of a number. It returns the literal
contents behind the decimal point, for some reason it doesn't normalize. This
means we're in! Using this operator on a quarantined hex number frees it of its
8
and releases it into the wild, ready to roam free and reap havoc. Now
just add any powers of ten to give you the number of before-the-comma digits you
want, save the number to a register to make the power-raising take effect, and you
have a full blown hex number! If you want only a certain number of digits, you can
chop off the unwanted ones by using the function, which returns the non-fractional part of the
argument.
So now, all of a sudden, we can create full hexadecimal numbers on this machine. Is there anything interesting we can do with them?
Inner space
Because wild hex numbers weren't meant to exist on this calculator, they can lead
to some strange behavior. Take, for instance, the normalisation function. Say, we
craft the hex number 2Eh
by entering 8.28
, then
8.06
, OR
'ing them, doing , setting the exponent to 2
, and
storing the number to RG0
. Result on screen: . As we all know, 2Eh
is equivalent
to 46, but if you normalize this number by pressing , you get 34
− incorrect! This
happens because the МК-61 doesn't really convert the hex number to decimal,
but expands the individual digits one by one, carrying any excess to the
next digit. So E
expands to 14, the ten carries over to the next digit,
increasing it from 2 to 3, and you get 34
. This is what I meant when I
said that the МК-61 doesn't officially know how to perform hex arithmetic.
It only knows how to adjust BCD
numbers after certain mathematical operations, and that's what's going on here. You
can do arithmetic with hex numbers, but the result will be incorrect.
Next trick. With 2Eh
still in RG0
, type
to go to the non-existent program location 2Eh
. Will it go to
2E
or to 34
? Go to program mode... and you've just urged
your calculator to apply the sloppy noose and hang itself. Reboot and try again. Now
note that if you press
(step through) once before entering program mode, the location is shown to be
34
, and the program commences normally. This probably means that the
program counter holds 2Eh
until it's increased by , at which point the number is
expanded and no longer poses a threat. But if you try to view the memory location
2Eh
directly, the calculator somehow says 'does not compute' and again
hangs itself.
What happens when we do this in a program? Will it crash the calculator or will
it step over it unharmed like it sometimes does with illegal commands? Let's try. At
program location 00
, enter and then 2E
. Go back to auto mode, press and step through the program twice with . Then go back to the program and note that
the program counter is at 35
, like we saw before when we did this
manually. Now return to the start of the program with and press once; this should leave us at location 34
, but when you
check the program, you kill the calculator again. (You might think that the
calculator dies because you're in the middle of a jump instruction, but there is no
such thing as middle; the thing is just a two-opcode instruction, and is
executed in its entirety after one .)
So I have to conclude, just as before, that it's perfectly okay for the program
counter to have an illegal value, as long as you don't try to view
the program contents at that location, because it kills the calculator. As they say,
God abhors naked singularities.
I explained previously that the number F
is probably used as some
sort of stop sign for the calculator to quit operating on a number and "return to
prompt". As a result of this, if you have a number which is all F
's,
executing and jumping F
's results in panicky behavior, because the
calculator doesn't know what to do. You get weird behavior that no other hex numbers
provoke, since they all resolve to some sort of number while FF
is pure
void. Let's examine this by trying to jump to address FFh
and see what
happens. At the blank prompt, press to get , which in reality is
8.FFFFFFF
. Prune the 8
off the number (creating ), raise the exponent
to 2 (this gives ) and save the number to RG0
. Now, don't even take the trouble to do
an indirect addressing round − go immediately to the program and try shifting to
higher addresses with .
Hah! Program counter screwed! You get garbage like , showing that
we're really in inner space here. It's like a garbled version of programming mode
where nothing works, you can't enter anything, and running the program just shows
that the calculator is as confused as you are. Sometimes you can shoehorn some
commands into this strange environment, but you can't see where they go, let alone
do anything useful. If you halt the program, you'll find that some arbitrary
characters have been written to program memory, but nothing meaningful. We're really
trespassing into illegal programming memory here!
Now, the strange thing is, if these addresses are illegal, then why are we allowed to enter them? Why not decrease the number of legal program steps from 105 to 99 and avoid hexadecimal addresses at all? Well, maybe full hex addresses were interesting for another reason...
Synthetic programming on the МК-61
Normally when you write programs, your keypresses are translated to 8-bit numbers called opcodes. These numbers are the calculator's instruction set, or what the processor eats for input. Normally you don't give these numbers a lot of thought, unless you're keying in other people's programs and checking your work by cross-referencing the opcodes, or if you're some guru and know them by heart. But by going down into the opcode level, we can program special instructions that are not available through any of the keyboard functions. We do this by a method very similar to what HP-41 programmers have been doing since the early 1980's, called synthetic programming. What that means is that you synthesize certain instructions in memory. You take some regular instructions, you delete some bytes from them, and what you're left with is a residue that means something totally different to the processor.
The first thing we need to know before even trying to synthetically program our МК-61's is if its instruction set has holes in it that we can explore. If every opcode is documented then there's no fun, is there? A quick glance in the manual shows that of the 256 possible instructions, 213 are in use. (Actually it's 214, because my manual accidentally omitted the instruction.) This gives us 42 undefined opcodes that we could theoretically enter and explore. Only how?
I came up with the idea (although I'm certain that people in the Soviet
Union found this possibility over twenty years ago) that the МК-61/52 has two
instructions that are just right for synthetic programming. They're
GoTo
and GoSub
. Every time they appear in a program, they
are followed by a two-byte label that you are almost completely free to choose,
regardless of whether it's inside or out of the program space. This means you can
'infect' the program space with any number you like, except for any number
containing F
. This is because there is no F
on the
keypad. Because there are 31 numbers with an F
in them, of which one is
used in the regular instruction set (0F
) and already counted, there are
30 opcodes which we cannot reach, leaving 12 easily reachable unused opcodes. (But
as we'll see, we can make some F? opcodes by other means later on...)
Having now entered an arbitrary address annex opcode, all you have to do is step
backwards twice and overwrite the GoTo
code (51) or the
GoSub
code (53) with a (NOP
, No Operation, do-nothing), and what's left is an
opcode of your own choosing! Is it really this simple? Let's run a small test.
Here's a simple program to calculate Pythagoras from the x and y registers:
# | Opcode | Keys | |
---|---|---|---|
00 | 22 | ||
01 | 14 | ||
02 | 22 | ||
03 | 10 | ||
04 | 21 | ||
05 | 50 |
Now, let's test our assumption by entering this program through synthetic programming. Enter the following program, rewritten so that the jump addresses form the opcodes we want:
# | Opcode | Keys | |
---|---|---|---|
00 | 51 | ||
01 | 22 | ||
02 | 51 | ||
03 | 14 | ||
04 | 51 | ||
05 | 22 | ||
06 | 51 | ||
07 | 10 | ||
08 | 51 | ||
09 | 21 | ||
10 | 51 | ||
11 | 50 |
Don't run this program just yet, it will only jump to location 22 and trail off into the void. Instead, trace back and replace all instances of 51 with a . The result will look like:
# | Opcode | Keys | |
---|---|---|---|
00 | 54 | ||
01 | 22 | ||
02 | 54 | ||
03 | 14 | ||
04 | 54 | ||
05 | 22 | ||
06 | 54 | ||
07 | 10 | ||
08 | 54 | ||
09 | 21 | ||
10 | 54 | ||
11 | 50 |
So this is just our little pythagoras program, only it was arrived at in a different way, and half of the commands are NOPs. Not the world's most efficient program maybe, but let's see if it works. Go back to auto mode and type . What's the square root of 32 + 42? It's the most famous Pythagorean triplet, and the answer, after a lot of flickering is... dadadaaa... . So this works, the МК-61 can be synthetically programmed! We can feed it any opcode by entering its value as a jump location, then eliminating the prefix and executing the residue as code. It's so easy that I wonder if it was deliberate.
Undocumented opcodes
Next step. In order to know where to find our 12 easily reached opcodes, we need
a numerically ordered opcode overview. The manual has one, but it it's ordered by
function, not number. At least it's reasonably complete. From it, I was able to
create a numerically ordered version, which I release to the world here. Next is to explore all opcodes one by one. I did it by
entering some easy to remember numbers on the stack (1−2−3−4) and filling the
registers with their associated number (0 for RG0
, 1 for
RG1
, 10 for RGA
, and so on). Then running a short program
to see what's affected. These are my findings:
Opcode | Result |
---|---|
27 | This undocumented opcode can be inserted directly by . Creates a non-saveable error. Stack and registers not affected. |
28 | This undocumented opcode can be inserted directly by . Creates a non-saveable error. Stack and registers not affected. |
29 | This undocumented opcode can be inserted directly by . Creates a non-saveable error. Stack and registers not affected. |
2B | Creates a non-saveable error. Stack and registers not affected. |
2C | Creates a non-saveable error. Stack and registers not affected. |
2D | Creates a non-saveable error. Stack and registers not affected. |
2E | Creates a non-saveable error. Stack and registers not affected. |
3C | Creates a non-saveable error. Stack and registers not affected. |
3D | No error, does not affect stack and registers. |
3E | Copies X to Vx, Y to X, leaves Y, Z, T and the registers unaffected. This is an undocumented instruction. |
55 | Turns out this opcode can be inserted directly by . Not very exciting, and does nothing. |
56 | Turns out this opcode can be inserted directly by . Not very exciting, and does nothing. |
So what did we win in this opcode hunt? Well, one new instruction! We can now
duplicate Y! Okay, so it's neither very cool or very useful, but don't you just feel
better knowing that you found a completely new command on this machine? I thought
so. Also, it isn't completely clear what the other commands do. Most of them seem to
just NOP
, but maybe if you use them in special cases, or in
conjunction, they perform some special tricks. Anyone with more time than me care to
find out?
Then there is one class of undefined opcodes that takes some special care to
produce. These are the opcodes of the form F?
, and they're
normally unreachable since they cannot be input from the keyboard directly.
I'm grateful to Greg Escov for
putting me on the right trail with this one. In his article about the pseudo-writing
mode of the МК-61, he mentions how he could insert an FF
code into a
program, which prompted me to experiment further with his recipe to see if I could
create different F
opcodes. By following his instructions but varying
the numbers, I indeed found a way to create the opcodes F0
through
F9
, and FF
.
To make any opcode F0
through F9
, choose two exponents
whose sum is larger than 99. The size of the first exponent, specifically its first
digit, determines the opcode you will create. Then follow the simple recipe. Say we
want to create F3
. We choose our first exponent as
30 (this gives us the '3') and our second as 70
(so the sum is over 99). Then we type:
.
This throws us into programming mode and inserts an F3
at position 53. If we did this with 40 and 60, we
would get an F4
at position 54, and
so on. As far as I'm aware, it isn't possible to create these opcodes anywhere else
in memory, the locations are fixed by the procedure.
The only exceptions are F0
, which can only be made by choosing the
first exponent 07, 08 or 09, and FF
, which is created by first
exponents 01 through 06.
The next question is whether these opcodes do anything interesting. To find out,
we fill the stack with 1, 2, 3, 4, and we fill all the registers with their
respective values, so 0 for RG0
, 1 for RG1
, and so on. We
fill locations 50–59 with NOPs and put a return at position 60. Then we write a
small program that jumps to 50, returns and terminates.
Only I don't have the zest to do all this right now. If you have an МК-61, I leave this as an excercise for you.
Update 2016-10-04: this article was featured on Hacker News. One of the commenters submitted a link to the "Eggogologia" page on the Russian Wikipedia, which continues where this page ends. It also confirms my suspicion that all of this stuff had been figured out long before.