Have you ever wanted to open the main menu, and let the user return to the exact state they were in just before opening it?

Well, now you can!

Here's the function:

Code:
void SaveAndOpenMainMenu(void) {
  int addr;

  // get the address of the syscall table in it
  addr = *(unsigned int *)0x8002007C;

  if (addr < (int)0x80020070)
    return;
  if (addr >= (int)0x81000000)
    return;

  // get the pointer to syscall 1E58 - SwitchToMainMenu
  addr += 0x1E58 * 4;
  if (addr < (int)0x80020070)
    return;
  if (addr >= (int)0x81000000)
    return;

  addr = *(unsigned int *)addr;
  if (addr < (int)0x80020070)
    return;
  if (addr >= (int)0x81000000)
    return;

  // Now addr has the address of the first operation in %1e58

  // Run up to 150 times (300/2). OS 3.60's is 59 instructions, so this should
  // be plenty, but will let it stop if nothing is found
  for (unsigned short *currentAddr = (unsigned short *)addr;
       (unsigned int)currentAddr < ((unsigned int)addr + 300); currentAddr++) {
    // MOV.L GetkeyToMainFunctionReturn Flag, r14
    if (*(unsigned char *)currentAddr != 0xDE)
      continue;

    // MOV #3, 2
    if (*(currentAddr + 1) != 0xE203)
      continue;

    // BSR <SaveAndOpenMainMenu>
    if ((*(unsigned char *)(currentAddr + 2) & 0xF0) != 0xB0)
      continue;

    // MOV.B r2, @r14
    if (*(currentAddr + 3) != 0x2E20)
      continue;

    // BRA <some addr>
    if ((*(unsigned char *)(currentAddr + 4) & 0xF0) != 0xA0)
      continue;

    // NOP
    if (*(currentAddr + 5) != 0x0009)
      continue;

    unsigned short branchInstruction = *(currentAddr + 2);

    // Clear first 4 bits (BSR identifier)
    branchInstruction <<= 4;
    branchInstruction >>= 4;

    // branchInstruction is now the displacement of BSR

    // Create typedef so we can cast the pointer
    typedef void (*voidFunc)(void);

    // JMP to disp*2 + PC + 4
    ((voidFunc)((unsigned int)branchInstruction * 2 +
                (unsigned int)currentAddr + 4 + 4))();

    return;
  }
}


Running this function should reliably open the main menu and allow you to access other apps, whether or not GetKey to main function return is enabled. If the user does not open a different app, but chooses to re-open the one it was previously in, the function will exit and execution continues.

How I found it:
By reverse-engineering (disassembling) the GetKey, GetKeyWait, and SpecialMatrixcodeProcessing syscalls, I have found the OS' internal method of opening the main menu.
A function which I call 'SaveAndOpenMainMenu' is called, which saves state information such as the VRAM, input mode, and frame colour, among other things, then runs another function (which I dub 'OpenMainMenu') and reloads the saved information. Obviously, OpenMainMenu opens the main menu.

Unfortunately, there is no way to access SaveAndOpenMainMenu or OpenMainMenu directly through syscalls. The closest to this is syscall 0x1e58, which does some currently unknown stuff, and then starts an infinite loop, calling SaveAndOpenMainMenu.

This is not very useful as there are many situations where you want to continue execution after opening the menu (not having it infinitely open). It also seems to crash when run on the calculator - this is probably due to the unknown stuff beforehand.

With all of this in mind, I created a function which looks at the assembly of syscall 0x1e58, finds the pointer to SaveAndOpenMainMenu, and runs the function! This works on both the fx-CG 20 and 50 emulators, as well as my OS 3.60 fx-CG 50 AU physical calculator. The function should work on all OS versions, but I don't have access to OS 1.00/2.00 emulators or physical calculators to test it on.

If you want more details, check out my Planete Casio bible folder. In the Syscalls/ folder, you can see the C equivalent of over 110 syscalls/subroutines, including the ones mentioned above. Documenting these and many others are Disassembly.ods/Disassembly.pdf, a spreadsheet and PDF providing the signature of, and information about, many syscalls and other stuff. For example, if you want to know how the called function works, check out SaveAndOpenMainMenu.c and related files.

If you have any questions, feel free to ask.

Thanks for reading,
dr-carlos.
Excellent! When I mentioned dynamic disassembly I didn't realize you'd already done it!


Code:
  // get the pointer to the syscall table
  addr = *(unsigned char *)0x80020071; // get displacement

  addr++;
  addr *= 4;
  addr += 0x80020070;
  addr = *(unsigned int *)addr;

This part isn't strictly needed, as the syscall table address is always located at 0x8002007c. That short section at 0x80020070 is written in assembler (and hasn't changed in 20 years).

I should both play with it and document the addresses for every OS version. This is really useful for me, thanks a ton!
Lephe wrote:
Excellent! When I mentioned dynamic disassembly I didn't realize you'd already done it!


Code:
  // get the pointer to the syscall table
  addr = *(unsigned char *)0x80020071; // get displacement

  addr++;
  addr *= 4;
  addr += 0x80020070;
  addr = *(unsigned int *)addr;

This part isn't strictly needed, as the syscall table address is always located at 0x8002007c. That short section at 0x80020070 is written in assembler (and hasn't changed in 20 years).

I should both play with it and document the addresses for every OS version. This is really useful for me, thanks a ton!


Thanks! You may notice that SaveAndOpenMainMenu is just a rewritten version of simLo's SetGetkeyToMainFunctionReturnFlag. I have tested using 0x8002007C and changed it in my folder and main post.
This broke on OS 3.80, but I have a fix.

Change

Code:

    // BSR <SaveAndOpenMainMenu>
    if (*(unsigned char *)(currentAddr + 2) != 0xB5)
      continue;

to

Code:

    // BSR <SaveAndOpenMainMenu>
    if ((*(unsigned char *)(currentAddr + 2) & 0xF0) != 0xB0)
      continue;


This isn't needed for 3.80, but if you want to be future proof you can also change

Code:

    // BRA <some addr>
    if (*(currentAddr + 4) != 0xAFFB)
      continue;

to

Code:

    // BRA <some addr>
    if ((*(unsigned char *)(currentAddr + 4) & 0xF0) != 0xA0)
      continue;


Final code:

Code:
void SaveAndOpenMainMenu(void) {
  int addr;

  // get the address of the syscall table in it
  addr = *(unsigned int *)0x8002007C;

  if (addr < (int)0x80020070)
    return;
  if (addr >= (int)0x81000000)
    return;

  // get the pointer to syscall 1E58 - SwitchToMainMenu
  addr += 0x1E58 * 4;
  if (addr < (int)0x80020070)
    return;
  if (addr >= (int)0x81000000)
    return;

  addr = *(unsigned int *)addr;
  if (addr < (int)0x80020070)
    return;
  if (addr >= (int)0x81000000)
    return;

  // Now addr has the address of the first operation in %1e58

  // Run up to 150 times (300/2). OS 3.60's is 59 instructions, so this should
  // be plenty, but will let it stop if nothing is found
  for (unsigned short *currentAddr = (unsigned short *)addr;
       (unsigned int)currentAddr < ((unsigned int)addr + 300); currentAddr++) {
    // MOV.L GetkeyToMainFunctionReturn Flag, r14
    if (*(unsigned char *)currentAddr != 0xDE)
      continue;

    // MOV #3, 2
    if (*(currentAddr + 1) != 0xE203)
      continue;

    // BSR <SaveAndOpenMainMenu>
    if ((*(unsigned char *)(currentAddr + 2) & 0xF0) != 0xB0)
      continue;

    // MOV.B r2, @r14
    if (*(currentAddr + 3) != 0x2E20)
      continue;

    // BRA <some addr>
    if ((*(unsigned char *)(currentAddr + 4) & 0xF0) != 0xA0)
      continue;

    // NOP
    if (*(currentAddr + 5) != 0x0009)
      continue;

    unsigned short branchInstruction = *(currentAddr + 2);

    // Clear first 4 bits (BSR identifier)
    branchInstruction <<= 4;
    branchInstruction >>= 4;

    // branchInstruction is now the displacement of BSR

    // Create typedef so we can cast the pointer
    typedef void (*voidFunc)(void);

    // JMP to disp*2 + PC + 4
    ((voidFunc)((unsigned int)branchInstruction * 2 +
                (unsigned int)currentAddr + 4 + 4))();

    return;
  }
}
Looks great, thanks!
I've updated the OP.
I've noticed that syscall 1E5A calls that syscall when the first parameter is 4:


Code:

undefined4 syscall_1E5A(undefined4 param_1)

{
  undefined uVar1;
  undefined4 uVar2;
  int iVar3;
  undefined auStack_18 [16];
  undefined4 local_8;
 
  local_8 = param_1;
  switch(param_1) {
  case 2:
    syscall_08E2();
    FUN_80375906();
    break;
  case 3:
    syscall_08E2();
    FUN_803759aa();
    break;
  case 4:
    SaveAndOpenMainMenu();
    uVar1 = DAT_8c0a3042;
    DAT_8c0a3041 = uVar1;
    memset(auStack_18,0,0x10);
    GetAppName(auStack_18);
    uVar2 = MB_ByteCount("@E-CON4");
    iVar3 = memcmp3("@E-CON4",auStack_18,uVar2);
    if (iVar3 == 0) {
      Keyboard_PutKeycode(5,9,0);
    }
    uVar2 = MB_ByteCount("@E-CON5");
    iVar3 = memcmp3("@E-CON5",auStack_18,uVar2);
    if (iVar3 == 0) {
      Keyboard_PutKeycode(5,9,0);
    }
    LoadRegisters((register_info *)&DAT_8c0a2ff0,5);
    break;
  case 6:
    DAT_8c0a3040 = 0;
    FUN_8037895c();
    break;
  case 8:
    DAT_8c0a3040 = 0;
    FUN_80376254();
    LoadRegisters((register_info *)&DAT_8c0a2ff0,9);
    break;
  case 9:
    iVar3 = GetKeyboardInputMode();
    if (iVar3 == 1) {
      Keyboard_PutGetKey(0x7536,0);
    }
  case 5:
  case 7:
    DAT_8c0a3040 = 1;
    break;
  case 10:
    DAT_8c0a3040 = 1;
    FUN_803772de();
    LoadRegisters((register_info *)&DAT_8c0a2fa0,0xb);
    break;
  case 0xb:
    DAT_8c0a3040 = 0;
  }
  return local_8;
}


I'll look into calling it via that, hopefully the part after doesn't cause problems
Heath wrote:
I've noticed that syscall 1E5A calls that syscall when the first parameter is 4:


Code:

undefined4 syscall_1E5A(undefined4 param_1)

{
  undefined uVar1;
  undefined4 uVar2;
  int iVar3;
  undefined auStack_18 [16];
  undefined4 local_8;
 
  local_8 = param_1;
  switch(param_1) {
  case 2:
    syscall_08E2();
    FUN_80375906();
    break;
  case 3:
    syscall_08E2();
    FUN_803759aa();
    break;
  case 4:
    SaveAndOpenMainMenu();
    uVar1 = DAT_8c0a3042;
    DAT_8c0a3041 = uVar1;
    memset(auStack_18,0,0x10);
    GetAppName(auStack_18);
    uVar2 = MB_ByteCount("@E-CON4");
    iVar3 = memcmp3("@E-CON4",auStack_18,uVar2);
    if (iVar3 == 0) {
      Keyboard_PutKeycode(5,9,0);
    }
    uVar2 = MB_ByteCount("@E-CON5");
    iVar3 = memcmp3("@E-CON5",auStack_18,uVar2);
    if (iVar3 == 0) {
      Keyboard_PutKeycode(5,9,0);
    }
    LoadRegisters((register_info *)&DAT_8c0a2ff0,5);
    break;
  case 6:
    DAT_8c0a3040 = 0;
    FUN_8037895c();
    break;
  case 8:
    DAT_8c0a3040 = 0;
    FUN_80376254();
    LoadRegisters((register_info *)&DAT_8c0a2ff0,9);
    break;
  case 9:
    iVar3 = GetKeyboardInputMode();
    if (iVar3 == 1) {
      Keyboard_PutGetKey(0x7536,0);
    }
  case 5:
  case 7:
    DAT_8c0a3040 = 1;
    break;
  case 10:
    DAT_8c0a3040 = 1;
    FUN_803772de();
    LoadRegisters((register_info *)&DAT_8c0a2fa0,0xb);
    break;
  case 0xb:
    DAT_8c0a3040 = 0;
  }
  return local_8;
}


I'll look into calling it via that, hopefully the part after doesn't cause problems


Interesting - this must be new in OS 3.80, as I've never came across it and the disassembly is totally different in OS 3.60 and OS 3.70.
dr-carlos wrote:
Heath wrote:
I've noticed that syscall 1E5A calls that syscall when the first parameter is 4:


Code:

undefined4 syscall_1E5A(undefined4 param_1)

{
  undefined uVar1;
  undefined4 uVar2;
  int iVar3;
  undefined auStack_18 [16];
  undefined4 local_8;
 
  local_8 = param_1;
  switch(param_1) {
  case 2:
    syscall_08E2();
    FUN_80375906();
    break;
  case 3:
    syscall_08E2();
    FUN_803759aa();
    break;
  case 4:
    SaveAndOpenMainMenu();
    uVar1 = DAT_8c0a3042;
    DAT_8c0a3041 = uVar1;
    memset(auStack_18,0,0x10);
    GetAppName(auStack_18);
    uVar2 = MB_ByteCount("@E-CON4");
    iVar3 = memcmp3("@E-CON4",auStack_18,uVar2);
    if (iVar3 == 0) {
      Keyboard_PutKeycode(5,9,0);
    }
    uVar2 = MB_ByteCount("@E-CON5");
    iVar3 = memcmp3("@E-CON5",auStack_18,uVar2);
    if (iVar3 == 0) {
      Keyboard_PutKeycode(5,9,0);
    }
    LoadRegisters((register_info *)&DAT_8c0a2ff0,5);
    break;
  case 6:
    DAT_8c0a3040 = 0;
    FUN_8037895c();
    break;
  case 8:
    DAT_8c0a3040 = 0;
    FUN_80376254();
    LoadRegisters((register_info *)&DAT_8c0a2ff0,9);
    break;
  case 9:
    iVar3 = GetKeyboardInputMode();
    if (iVar3 == 1) {
      Keyboard_PutGetKey(0x7536,0);
    }
  case 5:
  case 7:
    DAT_8c0a3040 = 1;
    break;
  case 10:
    DAT_8c0a3040 = 1;
    FUN_803772de();
    LoadRegisters((register_info *)&DAT_8c0a2fa0,0xb);
    break;
  case 0xb:
    DAT_8c0a3040 = 0;
  }
  return local_8;
}


I'll look into calling it via that, hopefully the part after doesn't cause problems


Interesting - this must be new in OS 3.80, as I've never came across it and the disassembly is totally different in OS 3.60 and OS 3.70.


I think I'm actually decompilimg version 3.60?
Heath wrote:
dr-carlos wrote:
Interesting - this must be new in OS 3.80, as I've never came across it and the disassembly is totally different in OS 3.60 and OS 3.70.


I think I'm actually decompilimg version 3.60?


Ah, sorry, you're right. Just looked at the disassembly again and it's using BRAF to branch to a different address based on the first parameter, so the individual switch cases don't show up.
Heath wrote:
I've noticed that syscall 1E5A calls that syscall when the first parameter is 4:


Code:

undefined4 syscall_1E5A(undefined4 param_1)

{
  undefined uVar1;
  undefined4 uVar2;
  int iVar3;
  undefined auStack_18 [16];
  undefined4 local_8;
 
  local_8 = param_1;
  switch(param_1) {
  case 2:
    syscall_08E2();
    FUN_80375906();
    break;
  case 3:
    syscall_08E2();
    FUN_803759aa();
    break;
  case 4:
    SaveAndOpenMainMenu();
    uVar1 = DAT_8c0a3042;
    DAT_8c0a3041 = uVar1;
    memset(auStack_18,0,0x10);
    GetAppName(auStack_18);
    uVar2 = MB_ByteCount("@E-CON4");
    iVar3 = memcmp3("@E-CON4",auStack_18,uVar2);
    if (iVar3 == 0) {
      Keyboard_PutKeycode(5,9,0);
    }
    uVar2 = MB_ByteCount("@E-CON5");
    iVar3 = memcmp3("@E-CON5",auStack_18,uVar2);
    if (iVar3 == 0) {
      Keyboard_PutKeycode(5,9,0);
    }
    LoadRegisters((register_info *)&DAT_8c0a2ff0,5);
    break;
  case 6:
    DAT_8c0a3040 = 0;
    FUN_8037895c();
    break;
  case 8:
    DAT_8c0a3040 = 0;
    FUN_80376254();
    LoadRegisters((register_info *)&DAT_8c0a2ff0,9);
    break;
  case 9:
    iVar3 = GetKeyboardInputMode();
    if (iVar3 == 1) {
      Keyboard_PutGetKey(0x7536,0);
    }
  case 5:
  case 7:
    DAT_8c0a3040 = 1;
    break;
  case 10:
    DAT_8c0a3040 = 1;
    FUN_803772de();
    LoadRegisters((register_info *)&DAT_8c0a2fa0,0xb);
    break;
  case 0xb:
    DAT_8c0a3040 = 0;
  }
  return local_8;
}


I'll look into calling it via that, hopefully the part after doesn't cause problems


So I've tried calling it and it works, but when called a second time seems to crash. Let me know if you find anything otherwise.

I've also tried syscall 0x1E59 (which calls 0x1E5A) and this just infinitely opens the menu (i.e. it opens, and if the app is re-entered it just re-opens the menu), just like exiting the app.
I finally came back to this and implemented it in gint. It's working fine for me and since it's on gint's development branch, further testing from add-in developers will automatically occur.

In the gint version of the function I ended up matching registers r14/r2 instead of hardcoding them, because I feel that's a pretty volatile aspect of compiled code (ie. realistically likely to change).

A slight caveat is that the we still get a frame of the OS banner and saved add-in VRAM before we get control back. This flickers a bit. But this is a really minor detail in a huge improvement in usability. Overall I'm very happy with this Smile
Lephe wrote:
I finally came back to this and implemented it in gint. It's working fine for me and since it's on gint's development branch, further testing from add-in developers will automatically occur.

In the gint version of the function I ended up matching registers r14/r2 instead of hardcoding them, because I feel that's a pretty volatile aspect of compiled code (ie. realistically likely to change).

A slight caveat is that the we still get a frame of the OS banner and saved add-in VRAM before we get control back. This flickers a bit. But this is a really minor detail in a huge improvement in usability. Overall I'm very happy with this Smile


Sounds great!
Not hard-coding the registers is probably a good call, I might have to re-write it at some point.

If you haven't already, I'd recommend checking out SaveAndOpenMainMenu.c from my Planete Casio bible folder to see how the syscall works - you might be able to use OpenMainMenu (at 0x80364266 in OS 3.60) to open the menu without the frame and saved VRAM.
Oh wow I tend to forget how detailed these decompilations are. Thanks! Since we have to redraw anyway (the main menu being displayed before we come back) I think the simplicity of the current method is likely preferable to the ever so slight polish of avoiding the flicker, but I'll keep that in mind!

(Something else I should keep in mind is to program it into fxos so that when a new OS version is out I can just run a command and check that this and other similar widgets still work)
Lephe wrote:
Oh wow I tend to forget how detailed these decompilations are. Thanks! Since we have to redraw anyway (the main menu being displayed before we come back) I think the simplicity of the current method is likely preferable to the ever so slight polish of avoiding the flicker, but I'll keep that in mind!

(Something else I should keep in mind is to program it into fxos so that when a new OS version is out I can just run a command and check that this and other similar widgets still work)


It would be great to stick a whole bunch of addresses into fxos (and where they are referenced within syscalls) and be able to find them across versions. Might be a bit complex, but would save a whole bunch of work.
Oh, you mean like "here's a 3.60 address: 0x80363184, please find the address of the same instruction in OS 3.80"?

I think this could be possible with the following algorithm. First, find the function, then find the instruction. Finding the function is pretty easy if it's like a syscall, otherwise fxos could build the call graph and use a graph matching algorithm to match the old and new versions of each function. Then, within the function, run a diff algorithm (basically a longest common subsequence but without being too sensitive to the function's address) which will return a mapping of old and new instructions.

That being said, there obviously going to be new/removed functions and instructions so it wouldn't always spit something out. And I'm not sure how to to that for data.
Quote:
(Something else I should keep in mind is to program it into fxos so that when a new OS version is out I can just run a command and check that this and other similar widgets still work)

Well, I ended up doing just that: https://gitea.planet-casio.com/Lephenixnoir/fxos/commit/12e6cd45a47e3a90d96619a435948255bfb4d489

I'm really feeling the lack of function exploration by now. I'll see if I can add some basic stuff in there...
Lephe wrote:
Quote:
(Something else I should keep in mind is to program it into fxos so that when a new OS version is out I can just run a command and check that this and other similar widgets still work)

Well, I ended up doing just that: https://gitea.planet-casio.com/Lephenixnoir/fxos/commit/12e6cd45a47e3a90d96619a435948255bfb4d489

I'm really feeling the lack of function exploration by now. I'll see if I can add some basic stuff in there...

Looks good!
I'm pretty busy atm, but I'll have a better look at `am` and `if` and see what I can do with them.
Thanks. There's not much for now, am is just an official spot to put random analysis functions, and if has almost no info yet. I'm slowly building up the data structures and in particular the storage with data for functions, so that we can do more stuff later (each. cross-reference functions, search stuff more aggressively, etc).

One difficulty here is that to build the function call graph we need to identify the targets of function calls, which is easy only for bsr, and not for jsr because the value of registers are context-dependent. The registers in question can be found to be a fixed address 90% of the time just be reading the code, however to do that automatically a static analysis pass is needed. I've laid some groundwork before; right now I mostly need to build actual function CFGs and then I'll be able to write a simple abstract interpreter.

Not sure when this is all gonna happen, but at least there's a clear plan.
  
Register to Join the Conversation
Have your own thoughts to add to this or any other topic? Want to ask a question, offer a suggestion, share your own programs and projects, upload a file to the file archives, get help with calculator and computer programming, or simply chat with like-minded coders and tech and calculator enthusiasts via the site-wide AJAX SAX widget? Registration for a free Cemetech account only takes a minute.

» Go to Registration page
Page 1 of 1
» All times are UTC - 5 Hours
 
You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot vote in polls in this forum

 

Advertisement