Some experiments with hacking the ЭЛЕКТРОНИКА МК-61

Our fine machine

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

Page from the MK-61's manual, showing a drawing of the MK-61.

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

Macro photo of segment display

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: INV), 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 #A
11 B #B
12 C #C
13 D #D
14 E #E
15 F #F

There are a few good reasons why the МК-61 uses the hexadecimal system and displays it in precisely this way:

  1. An eight-segment display cannot distinguish between a B and an 8, or a D and a 0, so the designers had to break something. (Update: it actually can if it uses the lowercase forms for b and d.)
  2. 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 of F means saving yourself the left-adjusting. You don't compromise arithmetic if you just stop at the first occurrence of F, 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 all F's, as we'll demonstrate below.
  3. 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 error.
  4. 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:

  1. The first number is the target number in hex notation, but with 8's substituted for all the occurrences of 8 and higher.
  2. 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 8.rror, 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 {x}, 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 [x] 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 {x}, setting the exponent to 2, and storing the number to RG0. Result on screen: 2E. As we all know, 2Eh is equivalent to 46, but if you normalize this number by pressing [x], 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 INV to get 8., which in reality is 8.FFFFFFF. Prune the 8 off the number (creating all F's), raise the exponent to 2 (this gives FF.FFFFFF) 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 come again?, 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 x=0 e 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 (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 square-x
01 14  
02 22 square-x
03 10  
04 21 root
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 NOP. The result will look like:

# Opcode Keys
00 54 NOP
01 22
02 54 NOP
03 14
04 54 NOP
05 22
06 54 NOP
07 10
08 54 NOP
09 21
10 54 NOP
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... 5. 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 root. Creates a non-saveable error. Stack and registers not affected.
28 This undocumented opcode can be inserted directly by square. Creates a non-saveable error. Stack and registers not affected.
29 This undocumented opcode can be inserted directly by 1/x. 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.