push {}

General discussions about working with the Astrobe IDE and programming ARM Cortex-M3, M4 and M7 microcontrollers.

push {}

Postby gray » Tue May 14, 2019 10:19 am

I am dabbling with getting a stack trace upon run-time errors. For this, I obviously need to understand the structure of the stack when entering the error handler (SVC call).

A related questions about interrupt handlers in general.
Code: Select all
PROCEDURE runtimeErrorHandler[0];
  BEGIN
.   632  E92D0000H  push     { }
.   636  EA4F0C0DH  mov      fp,sp
    (* ... *)
.   796  EA4F0D0CH  mov      sp,fp
.   800  E8BD9FF0H  pop      { r4 r5 r6 r7 r8 r9 r10 r11 fp pc }

I scoured the ARM docs, but didn't find any info on the 'push {}' instruction there in the prologue, ie. with an empty register list.

From the 'pop' instruction in the epilogue, I infer that this must do 'push { r4 r5 r6 r7 r8 r9 r10 r11 fp lr }', ie. the registers not pushed by the processor itself during the stacking phase of the exeception entry. (The processor will itself pop the other registers it has pused upon exit from the exception handler).

My stack trace works (but only indicating the module names and addresses, as I don't have the information about the procedures corresponding to the addresses), so I think my view of the stack structure, and how it is being built upon exception entry, is not far off. But I'd like to be sure.

Can you please enlighten me what 'push {}' means and does?
gray
 
Posts: 51
Joined: Tue Feb 12, 2019 2:59 am

Re: push {}

Postby cfbsoftware » Tue May 14, 2019 11:34 am

The disassembler generates the listing 'on the fly' as each instruction is generated. There are a few situations (this is one of them) where the instruction that is created initially is just a placeholder which is patched retrospectively. Push only needs to save the registers that are used but that is not known until the procedure is ended. At that time the Push {} instruction is modified to include all of the registers that you see in the subsequent Pop.
cfbsoftware
Site Admin
 
Posts: 395
Joined: Fri Dec 31, 2010 12:30 pm

Re: push {}

Postby gray » Thu May 16, 2019 3:13 am

I see. Thanks for the clarification. May I place a feature request here? If yes, I'd like an extended ResData (".ref") that not only gives the name for a module's address range, but also the procedure names (with address range) within the module.
gray
 
Posts: 51
Joined: Tue Feb 12, 2019 2:59 am

Re: push {}

Postby cfbsoftware » Thu May 16, 2019 11:20 am

Thank you for your request. It is already one of the features on our list of prospective enhancements.

In the meantime, if you require this information to assist with diagnosing runtime errors, refer to Section 4.5 Diagnosing System Exceptions in the Oberon for Cortex-M documents. Given the following:

  • The module name and exception address that is reported in a runtime error message
  • The map file which is created when you linked the application
  • The disassembler listing of the module
a formula is supplied for you to use with this information to locate the offending line of source code.
cfbsoftware
Site Admin
 
Posts: 395
Joined: Fri Dec 31, 2010 12:30 pm

Re: push {}

Postby gray » Fri May 17, 2019 7:01 am

Good hint, thanks. For the module with the run-time error (I only consider run-time error exceptions here, not hardware fault exceptions), the compiler/linker embeds the source line number right in the code at the return address, so that case is covered. The modules in the stack trace need another approach -- here the formula that works for hardware fault exceptions should work.

Here's the relevant code for the stack trace:
Code: Select all
  TYPE
    ModuleName* = ARRAY 16 OF CHAR;
    TracePoint* = RECORD
      moduleName*: ModuleName;
      address*: INTEGER
    END;
    Trace* = RECORD
      tp*: ARRAY TraceDepth + 1 OF TracePoint;
      count*: INTEGER
    END;

  PROCEDURE stackTrace(fpAddr: INTEGER; VAR trace: Trace);
    (* called from the run-time error handler *)
    VAR
      lr: INTEGER;
  BEGIN
    trace.count := 0;
    SYSTEM.GET(fpAddr, fpAddr);
    REPEAT
      SYSTEM.GET(fpAddr + 4, lr);
      GetModuleName(lr - 4, trace.tp[trace.count].moduleName);
      trace.tp[trace.count].address := lr - 4;
      INC(trace.count);
      SYSTEM.GET(fpAddr, fpAddr);
    UNTIL (fpAddr >= LinkOptions.StackStart - 8) OR (trace.count = TraceDepth);
    (* handling of the case when the stack trace would be deeper than TraceDepth *)
  END stackTrace;

  PROCEDURE runtimeErrorHandler[0];
    (* called via SVC *)
  BEGIN
    (* getting info on the offending module, similar to Traps.mod *)
    stackTrace(SYSTEM.FP + 56, errorDesc.trace);
    errorHandler(errorDesc)
  END runtimeErrorHandler;

I retrieve the link register values on the stack and subtract 4 to find the calling procedure. For a test case, I get the following, using 'ASSERT(FALSE, Error.Trace)', with Error.Trace defined as 100 and the message below message (the annotations as comments are not part of the real output):
Code: Select all
code:    100
message: stack trace
module:  DisplayTask
line:    43
address: 08005298H
trace: (* format: 'module name': 'address' from link register minus 4 *)
  Task1: 08005AE5H     (* test task *)
  Tasks: 080046B1H     (* task scheduler *)
  M: 08005C35H         (* main module *)

Applying the formula to find the calling procedure, without the '-8' correction, I get these offsets:
Code: Select all
  Task1:  37
  Tasks:  1873
  M:      197

Now, let's check.
Code: Select all
  (* case 1: from Task1.mod *)
    DisplayTask.SetNumItem(0, r);
.    28  F2400B00H  mov      r11,0
.    32  F85CAC04H  ldr      r10,[fp,-4]
.    36  04010000H  bl       Proc #1

37 looks fine, even though I don't fully understand yet why it's not 36.

Code: Select all
  (* case 2: from Tasks.mod *)
    ct.taskState;
.  1856  F8DFB000H  ldr      r11,[pc+offset]  <Global:4>
.  1860  F8DBB000H  ldr      r11,[r11]
.  1864  F8DBB000H  ldr      r11,[r11]
.  1868  EA4F000BH  mov      r0,r11
.  1872  3001H      add      r0,1
.  1874  4780H      blx      r0

1873 looks off in this case. 'ct.taskState' refers to a procedure variable. I realise that subtracting 4 from the link register value causes this.

Code: Select all
  (* case 3: from M.mod *)
  Tasks.Go
.   196  040F0027H  bl       Proc #15

Same as with case 1, Task1.mod.

Apologies for going on that long, but I thought I'd present the details to finally ask my questions.

1) Do you see any flaws or hidden issues in creating the stack trace as above?
2) Why exactly is the offset off by 1 for cases 1 and 3?
3) Can I detect case 2, and subtract only 2 from the link register value?

Depending on the structure and contents of an extended ".ref" ResData, I probably do not even need to do the subtractions from the link registers if the goal is to identify the procedure name in the source code.

Thanks!
gray
 
Posts: 51
Joined: Tue Feb 12, 2019 2:59 am

Re: push {}

Postby cfbsoftware » Sun May 19, 2019 5:03 am

Nice work!

Read up on how the Frame Pointer is used by the Oberon compiler to check if there are any flaws or hidden issues. This is detailed in An Oberon Compiler for the ARM Processor which you can download from the Oberon page on Niklaus Wirth's website.

The odd addresses are explained in Section 3.1.4 Link Register R14 of Joseph Yiu's book The Definitive Guide to the ARM Cortex-M3.

Despite the fact that bit 0 of the PC is always 0 (because instructions are word aligned or half word aligned), the LR bit 0 is readable and writable. This is because in the Thumb instruction set, bit 0 is often used to indicate ARM/Thumb states. To allow the Thumb-2 program for the Cortex-M3 to work with other ARM processors that support the Thumb-2 technology, this least significant bit (LSB) is writable and readable.

If you are planning more work at this level of detail I recommend that you get a copy of the book.

When bit 0 is set to 1 the instruction is interpreted as a Thumb-2 instruction. Hence the requirement to call add r0,1 before blx r0 in your second example. These are both 16-bit instructions but you can interpret the combined pair as a single 32-bit instruction for this exercise.
cfbsoftware
Site Admin
 
Posts: 395
Joined: Fri Dec 31, 2010 12:30 pm

Re: push {}

Postby gray » Tue May 21, 2019 11:13 am

FWIW, here's an improved version of the stack trace procedure.
Code: Select all
  PROCEDURE stackTrace(fpAddr: INTEGER; VAR trace: Trace);
    VAR
      lr, stopAt: INTEGER;
  BEGIN
    trace.count := 0;
    stopAt := LinkOptions.StackStart - 8;
    SYSTEM.GET(fpAddr, fpAddr);
    WHILE (fpAddr < stopAt) & (trace.count # TraceDepth) DO
      SYSTEM.GET(fpAddr + 4, lr);
      GetModuleName(lr, trace.tp[trace.count].moduleName);
      trace.tp[trace.count].address := lr;
      INC(trace.count);
      SYSTEM.GET(fpAddr, fpAddr);
    END;
    (* handle stack depth of more then TraceDepth levels *)
  END stackTrace;

This version also works correctly for the "base" code execution path in the main program, ie. without calling procedures. The former version collects garbage in this case. Sorry.
gray
 
Posts: 51
Joined: Tue Feb 12, 2019 2:59 am

Re: push {}

Postby gray » Tue May 28, 2019 1:13 pm

Another version of the stack trace procedure. The above one stops at the address of the last FP-entry on the stack, this one stops with the contents of this FP-entry.

I ran into the problem of where to stop the stack trace when I implemented simple coroutines (as basis for a cooperative task scheduler), as each coroutine has its own stack. Hence, 'LinkOptions.StackStart - 8' could not be used for these stacks, obviously. A first attempt simply changed a corresponding variable upon transfer from one coroutine to the next, which works, but is clumsy, as it encumbers each context switch with an operation only needed for error reporting.

Now I mark the end of the FP-call-chain up the stack by setting the first FP-entry to point to itself. It's the FP-entry that gets created by 'push {fp lr}' right at the start of the main program. Practically, I set this FP stack entry to point to itself in my Main.mod module. If coroutines are used, their stacks are prepared accordingly by the 'Init' procedure for the coroutines.
Code: Select all
PROCEDURE stackTrace(fp1: INTEGER; VAR trace: Trace);
    VAR
      lr, fp2: INTEGER;
  BEGIN
    trace.count := 0;
    SYSTEM.GET(fp1, fp1);
    SYSTEM.GET(fp1, fp2);
    WHILE (fp1 # fp2) & (trace.count # TraceDepth) DO
      SYSTEM.GET(fp1 + 4, lr);
      GetModuleName(lr, trace.tp[trace.count].moduleName);
      trace.tp[trace.count].address := lr;
      INC(trace.count);
      fp1 := fp2;
      SYSTEM.GET(fp1, fp2);
    END;
    (* handle stack depth of more then TraceDepth levels *)
  END stackTrace;

For more context, this is the definition of the coroutine module. As said, simple concepts. The stack is passed as array upon initialisation of the coroutine. As this is the basis for a scheduler for embedded programs, it is assumed that the coroutines never terminate. The scheduler on top of the coroutines works, but needs more testing, and probably some API clean-up.
Code: Select all
  TYPE
    PROC* = PROCEDURE;
    Coroutine* = POINTER TO CoroutineDesc;
    CoroutineDesc* = RECORD END;

  PROCEDURE Init*(cor: Coroutine; proc: PROC; stack: ARRAY OF BYTE);
  PROCEDURE Transfer*(from, to: Coroutine);

This loosely follows the definitions (not implementations) of Modula-2's NEWPROCESS and TRANSFER procedures provided by SYSTEM. No IOTRANSFER-equivalent though. In fact, my coroutines stay out of the handling of interrupts. There be dragons. Interrupts are handled in modules on a lower architectural level.
gray
 
Posts: 51
Joined: Tue Feb 12, 2019 2:59 am


Return to Astrobe for ARM Cortex-M3, M4 and M7

cron