0

Tested and reproduced on Cortex-M 4 and Cortex-M 0.

I have discovered an issue with the GCC compiler. When a function is declared as type int (non-void), and contains a for loop, but does not have a return statement, the for loop will not break; after disassembling the compiled code, there is a difference between functions with a return, and without a return.

When this code is compiled, it does not throw an error message. On the first compile, a warning of missing return statements is thrown, but after that the warning will not reappear until you restart the IDE. An issue of this magnitude should probably fail to compile, or at least crash the Arduino, but it just never breaks out of the for loop.

I am mainly looking to find the proper channels to report this, since I am not sure if GNU ARM Embedded Toolchain launchpad or GNU Bugzilla are maintained anymore. If anyone knows which site (or both) are still maintained, or if there's a direct contact to someone in the project who I can share this with, please share.

Below is a more thorough description of the behavior.

Arduino Code
============
This is an attempt at a minimum reproducible example. I have run into this issue on two separate occasions in larger projects, which cause the program to behave in extremely unexpected and hard to debug ways (but always fixed by adding a return statement in the function definition).

/*
  gcc compiler error demonstration for Adafruit GrandCentral 
  gcc version: gcc version 9.2.1 20191025 (release) [ARM/arm-9-branch revision 277599] (GNU Tools for Arm Embedded Processors 9-2019-q4-major)
  Arduino IDE: all warinings on 
  Arduino IDE version 1.8.13
  Adafruit SAMD version 1.8.11
  based on Blink
  
  modified to call two functions which are identical except one does not have a return
  statement even though it is of return type int.
  In the list file, myList.GrandCentral.lst ,AFunctionWithReturn shows both the comparison of
  i with Count and the conditional comparison i>7 with break assembly instructions
  The AFunctionNoReturn does not show any assembly instructions for the end of 
  loop comparision or the conditional comparison i>7 with break 

  Found 4/8/21 Robert Calay and Tristan Calay



  Turns an LED on for one second, then off for one second, repeatedly.

  Most Arduinos have an on-board LED you can control. On the UNO, MEGA and ZERO
  it is attached to digital pin 13, on MKR1000 on pin 6. LED_BUILTIN is set to
  the correct LED pin independent of which board is used.
  If you want to know what pin the on-board LED is connected to on your Arduino
  model, check the Technical Specs of your board at:
  https://www.arduino.cc/en/Main/Products

  modified 8 May 2014
  by Scott Fitzgerald
  modified 2 Sep 2016
  by Arturo Guadalupi
  modified 8 Sep 2016
  by Colby Newman

  This example code is in the public domain.

  http://www.arduino.cc/en/Tutorial/Blink
*/
#define MAIN
//#include "Serial3.h" We are re-directing serial port output to SERCOM 5 on the Grand Central M4.

int AFunctionWithReturn(int count)
{
  Serial.print("CountWR");
  Serial.println(count);
  for(int i=0;i<count;i++) {
    Serial.println(i);
    if (i>7)
      break;
  }
 return(1); 
}

int AFunctionNoReturn(int count)
{
  Serial.print("CountNR");
  Serial.println(count);
  for(int i=0;i<count;i++) {
    Serial.println(i);
    if (i>7)
      break;
  }
 //Note: No return statement here.
}

// the setup function runs once when you press reset or power the board
void setup() {
  Serial.begin(115200);
  // initialize digital pin LED_BUILTIN as an output.
  pinMode(LED_BUILTIN, OUTPUT);
  AFunctionWithReturn(10); //This loops 8 times
  AFunctionNoReturn(10);   //This loops forever, never reaching loop()
}

// the loop function runs over and over again forever
void loop() {
  digitalWrite(LED_BUILTIN, HIGH);   // turn the LED on (HIGH is the voltage level)
  delay(1000);                       // wait for a second
  digitalWrite(LED_BUILTIN, LOW);    // turn the LED off by making the voltage LOW
  delay(1000);                       // wait for a second
}


/*
 OUTPUT ON ADAFRUIT GRANDCENTRAL SERIAL PORT
CountWR10
0
1
2
3
4
5
6
7
8
CountNR10
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
....
DOES NOT STOP CONTINUES 2000000+
* 
* 
 */

Disassembled Code
=================

There is a strange behavior in the brackets here. I'm no expert on the low level code, but it seems like AFunctionNoReturn calls itself recursively here. If not, it still has no break condition, and it does not have a compare call like AFunctionWithReturn in cmp r4, r5.

int AFunctionWithReturn(int count)
{
    42bc:   b570        push    {r4, r5, r6, lr}
  Serial.print("CountWR");
    42be:   490c        ldr r1, [pc, #48]   ; (42f0 <_Z19AFunctionWithReturni+0x34>)
  Serial.println(count);
  for(int i=0;i<count;i++) {
    Serial.println(i);
    42c0:   4e0c        ldr r6, [pc, #48]   ; (42f4 <_Z19AFunctionWithReturni+0x38>)
{
    42c2:   4605        mov r5, r0
  Serial.print("CountWR");
    42c4:   480b        ldr r0, [pc, #44]   ; (42f4 <_Z19AFunctionWithReturni+0x38>)
    42c6:   f000 fafa   bl  48be <_ZN5Print5printEPKc>
  Serial.println(count);
    42ca:   480a        ldr r0, [pc, #40]   ; (42f4 <_Z19AFunctionWithReturni+0x38>)
    42cc:   220a        movs    r2, #10
    42ce:   4629        mov r1, r5
    42d0:   f000 fb43   bl  495a <_ZN5Print7printlnEii>
  for(int i=0;i<count;i++) {
    42d4:   2400        movs    r4, #0
    42d6:   42ac        cmp r4, r5
    42d8:   da08        bge.n   42ec <_Z19AFunctionWithReturni+0x30>
    Serial.println(i);
    42da:   220a        movs    r2, #10
    42dc:   4621        mov r1, r4
    42de:   4630        mov r0, r6
    42e0:   f000 fb3b   bl  495a <_ZN5Print7printlnEii>
    if (i>7)
    42e4:   2c08        cmp r4, #8
    42e6:   d001        beq.n   42ec <_Z19AFunctionWithReturni+0x30>
  for(int i=0;i<count;i++) {
    42e8:   3401        adds    r4, #1
    42ea:   e7f4        b.n 42d6 <_Z19AFunctionWithReturni+0x1a>
      break;
  }
 return(1); 
}
int AFunctionNoReturn(int count)
{
    42f8:   b538        push    {r3, r4, r5, lr}
  Serial.print("CountNR");
    42fa:   4909        ldr r1, [pc, #36]   ; (4320 <_Z17AFunctionNoReturni+0x28>)
  Serial.println(count);
  for(int i=0;i<count;i++) {
    Serial.println(i);
    42fc:   4d09        ldr r5, [pc, #36]   ; (4324 <_Z17AFunctionNoReturni+0x2c>)
{
    42fe:   4604        mov r4, r0
  Serial.print("CountNR");
    4300:   4808        ldr r0, [pc, #32]   ; (4324 <_Z17AFunctionNoReturni+0x2c>)
    4302:   f000 fadc   bl  48be <_ZN5Print5printEPKc>
  Serial.println(count);
    4306:   4621        mov r1, r4
    4308:   4806        ldr r0, [pc, #24]   ; (4324 <_Z17AFunctionNoReturni+0x2c>)
    430a:   220a        movs    r2, #10
    430c:   f000 fb25   bl  495a <_ZN5Print7printlnEii>
  for(int i=0;i<count;i++) {
    4310:   2400        movs    r4, #0
    Serial.println(i);
    4312:   4621        mov r1, r4
    4314:   220a        movs    r2, #10
    4316:   4628        mov r0, r5
    4318:   f000 fb1f   bl  495a <_ZN5Print7printlnEii>
  for(int i=0;i<count;i++) {
    431c:   3401        adds    r4, #1
    431e:   e7f8        b.n 4312 <_Z17AFunctionNoReturni+0x1a>
    4320:   00006538    .word   0x00006538
    4324:   2000011c    .word   0x2000011c

00004328 <loop>:
  AFunctionWithReturn(10);
  AFunctionNoReturn(10);
}
Jens
  • 69,818
  • 15
  • 125
  • 179
  • 1
    What compiler options are you using? `missing return statements is thrown, but after that the warning will not reappear until you restart the IDE.` That is related to your IDE. Will you be able to re-create the behavior with simpler code, _without_ Arduino library? `reproduced on Cortex-M 4 and Cortex-M 0.` So we know cores - what controllers are you using? – KamilCuk Apr 09 '21 at 07:57
  • Please provide more information. a minimal reproduceable example, gcc version, command line options, get rid of the library calls and replace them with something else, etc. I am able to reproduce this yet. – old_timer Apr 09 '21 at 13:00
  • please use objdump -d not gdb – old_timer Apr 09 '21 at 13:02
  • it is quite obvious that that toolchain is still being maintained, the current version, builds with a return path. please provide a repeatable, minimal, example. – old_timer Apr 09 '21 at 13:06
  • Triple-check that the code actually compiles without errors. The `int AFunctionNoReturn()` should have been declared `void AFunctionNoReturn()` instead, since it does not return anything. GCC bugs are usually version dependend. – Turbo J Apr 10 '21 at 10:16

1 Answers1

0

Perhaps the most helpful thing that can be said is: "Why do you want to miss out the return statement? what are you hoping to achieve?"

The various language standards (Arduino is sort-of-C++ but with some funny pre-processing) tell you what will happen if you write valid code. They do not always tell you what happens if you write invalid code. In this case the compiler has very helpfully pointed out why your code is wrong, but then after that it is totally free to do anything. No matter what it does this is never a bug in the compiler, it is a bug in your code. This sometimes called "garbage in - garbage out".

To perhaps explain why you got the particular result you did, think about it like this: the compiler knows that in a valid program execution never runs to the end of the function without a return statement, so if there isn't a return statement after the loop, it is safe to assume that it never leaves the loop. Making this assumption helps to optimize valid code to run faster. If this assumption changes what an invalid program does, then the compiler authors usually don't care. They are usually only interested in what valid programs do.

(Regarding the launchpad page, if you click on the big link at the top of the page, you will see a message about where the site has moved to).

Tom V
  • 4,827
  • 2
  • 5
  • 22
  • We recently tested with the older 7-2017-q4 compiler, and it functions as expected (no function flow issues). When compiling the code for a standard Arduino Mega 2560, the code also functions this way, so it's limited to the Cortex M compiler. – Starwhip Apr 11 '21 at 01:43
  • We also tested these two functions: When AFunctionNoForNoReturn(5) is called, the output is as follows: CountNoForNR5 CountNoForWR0 What happens here is that the program executes AFunctionNoForNoReturn(), then bleeds into AFunctionNoForWithReturn(), and then crashes due to a messed-up stack. `int AFunctionNoForNoReturn(int count) { Serial.print("CountNoForNR"); Serial.println(count); delay(1000); } int AFunctionNoForWithReturn(int count) { Serial.print("CountNoForWR"); Serial.println(count); delay(1000); return(count); }` – Starwhip Apr 11 '21 at 01:46
  • It ALWAYS behaves as expected. Running into the next function IS the expected behaviour. crashing IS the expected behaviour. Your program demonstrates undefined behaviour. This means that you must expect that it could do absolutely anything. Even if it prints out "I am a fish", jumps up and down and catches fire, this is still the within the correct behaviour that you should expect. Try a google search for "undefined behaviour" to understand more. – Tom V Apr 11 '21 at 08:27
  • In C, a function was only required to return a value if its caller would make use of the value returned. Although modern style favors returning a value even when doing so is not required, on many implementations, falling off the end of a function would be slightly faster. Although C++ reclassified falling off the end of a non-void function as UB, compilers used with intermixed collection of functions written in C and C++ may usefully process functions whose behavior is defined in one language, and isn't defined in contrary fashion in the other, in the matter of the language which defines it. – supercat May 06 '21 at 17:01