Reference to local variable in main

Go To Last Post
13 posts / 0 new
Author
Message
#1
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Hi all

I have now had the same issue twice and I am wondering if I am breaking some C rule. Let's say we have the following code:

typedef struct {
  // members
  // ...
}  mytype1_t;  // Custom type 1

// Custom type 2
typedef struct {
    mytype1_t *p_a;   // A pointer to instance of type 1
    // other members
    // ...
} mytype2_t;

int main(void)
{
    mytype1_t var1;
    mytype2_t var2;
    
    mytype1_init(&var1);    // Initialize var1
    
    var2.p_a = &var1; // Initialize pointer to var1
    
    while(1)
    {
        // Application code
        // var1 is never used directly again, only through var2
    }
}

Note that the variable var1 is never referenced directly in the while-loop. However var2 keeps a reference (pointer) to it.

 

I have had issues with this several times, and when debugging I have seen the dereferenced pointer to var1 being all garbage at some point after running the main loop. Changing the var1 to static storage has solved the problem both times.

 

Does my code break some coding rule regarding references to local variables?

 

 

This topic has a solution.

/Jakob Selbing

Last Edited: Wed. May 8, 2019 - 07:03 AM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

More likely its the optimizer that sees var 1 is not used, so removes it, setting it static prevents the optimizer from touching it.

Jim

 

Click Link: Get Free Stock: Retire early! PM for strategy

share.robinhood.com/jamesc3274

 

 

 

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

If you are low on memory, the stack can overwrite

RAM variables resulting in garbage.

 

--Mike

 

This reply has been marked as the solution. 
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

This did cross my mind but at the same time it seems like a very unsafe behaviour if the optimizer would do this. I did a simple test where I inserted some use of the local variable in the main loop and indeed this did NOT fix the problem.

 

After a night's sleep I had some ideas on how to catch the error. One problem was that the erroneous behaviour was only present once after power-on and not after reset, but I inserted some infinite loops in the code to try and catch the execution at one point and then used the "attach to target" feature of Atmel Studio to look at the current variable contents. I discovered that the contents was not really garbage but actually only ONE of the elements of the variable was not initialized.

 

An uninitialized auto variable has undefined contents and in my code any non-zero value would trigger an unexpected behaviour (blinking a LED). Changing the variable to static would implicitly initialize it to 0 so no unexpected blinking would occur. That's why this "fixed" the problem.

 

So I am glad I did not just assume the problem was in the optimizer. One should always be careful assuming problem lies in the tools and not in the code.

/Jakob Selbing

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

jaksel wrote:
it seems like a very unsafe behaviour if the optimizer would do this.
How do you figure that? If I write:

int main(void) {
    int foo, bar;
    foo = 3;
    bar = 7;
    while (1) {
        PORTB = foo++;
    }
}

then what possible reason could there be for the compiler to generate anything to do with "bar"? It is initialized but never used so why waste time/space to even create the thing? The optimiser knows this and will remove the associated code.

 

If there was some reason I wanted to say "bar may look entirely pointless but I want you to create it and access it anyway" then that is what "volatile" it for so:

int main(void) {
    int foo;
    volatile int bar;
    foo = 3;
    bar = 7;
    while (1) {
        PORTB = foo++;
    }
}

would create code to generate and assign to "bar". In which case a debugger should be able to "see" it.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

But your example is different. In my example var2 contains a reference to var1, but in your example you don't keep any reference to "bar" so in that case - yes the optimizer should be allowed to remove it.

/Jakob Selbing

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Is that reference used ?

 

But, yeah, I missed that cross-ref. If var2 is then passed on to some other function then var1 has to exist to resolve the link so the optimiser could not ditch the creating/accessing code

Last Edited: Wed. May 8, 2019 - 01:10 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I re-enacted your scenario using two .c files here:

typedef struct {
	int n;
	// members
	// ...
}  mytype1_t;  // Custom type 1

// Custom type 2
typedef struct {
	mytype1_t *p_a;   // A pointer to instance of type 1
	// other members
	// ...
} mytype2_t;

void mytype1_init(mytype1_t * pt1) {
	pt1->n = 12345;
}

extern void some_fn(mytype2_t * pt2);

int main(void)
{
	mytype1_t var1;
	mytype2_t var2;
	
	mytype1_init(&var1);    // Initialize var1
	
	var2.p_a = &var1; // Initialize pointer to var1
	
	while(1)
	{
		// Application code
		// var1 is never used directly again, only through var2
		some_fn(&var2);
	}
}

then I deliberately put some_fn() in a sepearate file so the compilation of the first could not "see" it or make optimizing assumptions about it:

#include <avr/io.h>
/*
 * fn.c
 *
 * Created: 08/05/2019 14:16:34
 *  Author: uid23021
 */ 
typedef struct {
	int n;
	// members
	// ...
}  mytype1_t;  // Custom type 1

// Custom type 2
typedef struct {
	mytype1_t *p_a;   // A pointer to instance of type 1
	// other members
	// ...
} mytype2_t;


extern void some_fn(mytype2_t * pt2) {
	PORTB = pt2->p_a->n;
}

When I build this I get:

0000006c <some_fn>:
	// ...
} mytype2_t;


extern void some_fn(mytype2_t * pt2) {
	PORTB = pt2->p_a->n;
  6c:	dc 01       	movw	r26, r24
  6e:	ed 91       	ld	r30, X+
  70:	fc 91       	ld	r31, X
  72:	80 81       	ld	r24, Z
  74:	88 bb       	out	0x18, r24	; 24
  76:	08 95       	ret

00000078 <mytype1_init>:
	// other members
	// ...
} mytype2_t;

void mytype1_init(mytype1_t * pt1) {
	pt1->n = 12345;
  78:	29 e3       	ldi	r18, 0x39	; 57
  7a:	30 e3       	ldi	r19, 0x30	; 48
  7c:	fc 01       	movw	r30, r24
  7e:	31 83       	std	Z+1, r19	; 0x01
  80:	20 83       	st	Z, r18
  82:	08 95       	ret

00000084 <main>:
}

extern void some_fn(mytype2_t * pt2);

int main(void)
{
  84:	cf 93       	push	r28
  86:	df 93       	push	r29
  88:	00 d0       	rcall	.+0      	; 0x8a <main+0x6>
  8a:	00 d0       	rcall	.+0      	; 0x8c <main+0x8>
  8c:	cd b7       	in	r28, 0x3d	; 61
  8e:	de b7       	in	r29, 0x3e	; 62
	mytype1_t var1;
	mytype2_t var2;
	
	mytype1_init(&var1);    // Initialize var1
  90:	8e 01       	movw	r16, r28
  92:	0f 5f       	subi	r16, 0xFF	; 255
  94:	1f 4f       	sbci	r17, 0xFF	; 255
  96:	c8 01       	movw	r24, r16
  98:	0e 94 3c 00 	call	0x78	; 0x78 <mytype1_init>
	
	var2.p_a = &var1; // Initialize pointer to var1
  9c:	1c 83       	std	Y+4, r17	; 0x04
  9e:	0b 83       	std	Y+3, r16	; 0x03
	
	while(1)
	{
		// Application code
		// var1 is never used directly again, only through var2
		some_fn(&var2);
  a0:	ce 01       	movw	r24, r28
  a2:	03 96       	adiw	r24, 0x03	; 3
  a4:	0e 94 36 00 	call	0x6c	; 0x6c <some_fn>
  a8:	fb cf       	rjmp	.-10     	; 0xa0 <main+0x1c>

so you can see that the pointer to var1 IS being filled into the member in var2 and at the point of usage there is a double dereference as expected so this code is bound to work. The only way it might fail would be if:

void mytype1_init(mytype1_t * pt1) {
	pt1->n = 12345;
}

failed to "init" all the fields in var1. Now THAT would be a big mistake. As you say "var1" is created on the stack so it inherits junk and nothing within it could be trusted so EVERY field in it (that is later used) must be run-time initialised or it'll just hold any old stack noise.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

clawson wrote:

...so you can see that the pointer to var1 IS being filled into the member in var2 and at the point of usage there is a double dereference as expected so this code is bound to work.

Sure, the reference must be valid when assigned to var2. My though was that maybe the optimizer deems the lifetime of var1 to be limited to that reference only and that later on the memory location used by var1 is re-used for some other local variable (since var1 is not used anyway). You could compare this to a function that assigns an input object a reference to a local variable - that reference is also valid at the point of assignment but it is not valid after the control leaves the function.

 

Again - I think I was being too paranoid about this. A reference to a local variable must (IMHO) always be valid within the scope of the local variable - isn't this right?

 

/Jakob Selbing

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

jaksel wrote:
later on the memory location used by var1 is re-used for some other local variable (since var1 is not used anyway)
Nope, stack frame autos are created on entry to a function and destroyed when it leaves - they are never "re-used". main() is a bit of a special case in fact because in the embedded world it's unlikely that you will ever leave main() so, once created (if deemed necessary) nothing will then destroy autos in main().

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

jaksel wrote:

Does my code break some coding rule regarding references to local variables?

 

No, it doesn't. Your code is fine. If you had problems with pointers to local variables, they apparently took place in some other code.

 

jaksel wrote:
Again - I think I was being too paranoid about this. A reference to a local variable must (IMHO) always be valid within the scope of the local variable - isn't this right?

 

Almost.

 

In C parlance it is about storage duration or lifetime of an object, not scope. Scope has nothing to do with it. As long as the object stays within the limits of its lifetime, you can safely keep pointers to such object. And storage duration of an automatic object is defined by its surrounding block. In your case, it is the entire body of `main`.

 

In C++ it gets trickier, since in C++ lifetime might be detached from storage duration. But the idea remains the same.

 

jaksel wrote:
My though was that maybe the optimizer deems the lifetime of var1 to be limited to that reference only and that later on the memory location used by var1 is re-used for some other local variable (since var1 is not used anyway).

 

Memory used by one automatic variable might (and definitely will) be reused for another automatic variable when these variables have non-overlapping storage durations. An obvious example would be variables declared in sibling blocks

 

{
  uint8_t a[1024];
  ...
}
{
  uint16_t b[512];
  ...
}  

In a typical implementation the arrays in the above example will occupy the same location in memory, i.e. the memory occupied by `a` will effectively be reused for `b` after the storage duration of `a` ends.

 

Also, a smart compiler might be able to reuse memory in much trickier cases, like

 

{
  uint8_t a[1024];
  ...

  // The code after that point does not use `a` and the compiler is certain about it

  uint16_t b[512];
  ...
}

If the compiler can prove that nobody else needs `a` anymore after the marked point, it will be able to reuse `a`s memory for `b`, even though from the abstract language point of view these two arrays are supposed to co-exist alongside each other.

 

However, taking a long-lived pointer that points to `a` in this example will immediately prevent the compiler from trying to end `a`s storage duration prematurely. That applies to your code as well.

 

 

Last Edited: Mon. May 13, 2019 - 12:49 AM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

AndreyT wrote:

in C++ lifetime might be detached from storage duration.

 

Can you please explain when this might occur?
Garbage collection?

--Mike

 

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

avr-mike wrote:

AndreyT wrote:

in C++ lifetime might be detached from storage duration.

 

Can you please explain when this might occur?
Garbage collection?

 

No, no, nothing like that. I'm just referring to some pedantic points of C++ terminology.

 

In C++ the object's storage duration begins once the raw memory for that object has been allocated. But lifetime is trickier:

 

1. If the object has no constructor (e.g. fundamental types `int`, `double` etc.) then obtaining storage is enough to also begin the object's lifetime (just like in C).

2. If the object has trivial constructor, then obtaining storage is enough to also begin the object's lifetime. For example `struct Point { int x, y; };` has a constructor, but it is trivial (basically, it does nothing).

3. But if the object has non-trivial constructor, then beginning of its lifetime is separated from beginning of its storage duration. Such object's lifetime begins when its constructor successfully completes its execution. Not before.

 

So, in simple words, in C++ in order to start object's lifetime it is not enough to allocate the memory. You also need to execute the constructor (if it exists).

 

The situation with destructors and ending of the lifetime is perfectly symmetrical to the above.

 


 

To observe the separation consider this piece of code

 

#include <string>

int main()
{
  goto skip;

  static std::string str;

skip:

  std::string *ptr = &str; // <- OK
  str = "abc";             // <- Undefined behavior!
}

When the control reaches the label `skip:`, identifier `str` is in scope, storage duration of object `str` has already begun, but its lifetime has not. Class `std::string` has non-trivial constructor and `str`'s constructor has never been executed. It is OK to form pointers to such an object, but any attempts to use it as a fully functional `std::string` object will lead to undefined behavior.

Last Edited: Mon. May 13, 2019 - 12:46 AM