Issue
I tried to set an external variable and get its value afterwards, but the value I got was not correct. Do external variables always need to be volatile
when compiled with gcc?
The code is as follows (updated the complete code, the previous access to the memory address 0x00100000 is changed to the another
variable):
main.c
extern unsigned int ex_var;
extern unsigned int another;
int main ()
{
ex_var = 56326;
unsigned int *pos=&ex_var+16;
for (int i = 0; i < 6; i++ )
{
*pos++ = 1;
}
another = ex_var;
}
another.c
unsigned int ex_var; // the address of this variable is set to right before valid_address
unsigned int valid_address[1024]; // to indicate valid memory address
unsigned int another;
And the value set to another
is not 56326.
Note: another.c seems to be strange to indicate that the memory region after ex_var
is valid. For the actual running example on bear metal, please refer to this post.
Here is the disassembly of main.c. It is compiled with x86-64 gcc 12.2 with -O1 option:
main:
mov eax, OFFSET FLAT:ex_var+64
.L2:
add rax, 4
mov DWORD PTR [rax-4], 1
cmp rax, OFFSET FLAT:ex_var+88
jne .L2
mov eax, DWORD PTR ex_var[rip]
mov DWORD PTR another[rip], eax
mov eax, 0
ret
It can be found that the code for setting the external variable ex_var
is optimized out.
I tried several versions of gcc, including x86-64 gcc, x86 gcc, arm64 gcc, and arm gcc, and it seems that all tested gcc versions above 8.x have such issue. Note that optimization option -O1 or above is needed to reproduce this issue.
The code can be found at this link at Compiler Explorer.
Update:
This bug in the above code is not related to external references.
Here is another example code that has the same bug. Note that it should be compiled with -O1 or above. You can try it at Compiler Explorer.
#include <stdio.h>
unsigned int var;
// In embedded environment, LD files can be used to make valid_address stores right after var
volatile unsigned int valid_address[1024];
int main ()
{
var = 56326;
unsigned int *ttb=&var;
ttb += 16;
for (int i = 0; i < 8; i++ )
{
*ttb++ = 1;
}
valid_address[0] = var;
printf("Value is: %d", valid_address[0]);
}
If you compile this code with gcc like
gcc -O1 main1.c
and execute this code, you might get the following output:
Value is: 0
Which is not correct.
Solution
The calculation &ex_var+16
is not defined by the C standard (because it only defines pointer arithmetic within an object, including to the address just beyond its end) and the assignment *pos++ = 1
is not defined by the C standard (because, for the purposes of the standard, pos
does not point to an object). When there is behavior not defined by the C standard on a code path, the standard does not define any behavior on the code path.
You can make the behavior defined, to the extent the compiler can see, by declaring ex_var
as an array of unknown size, so that the address calculation and the assignments would be defined if this translation unit were linked with another that defined ex_var
to be an array of sufficient size:
extern unsigned int ex_var[];
int main ()
{
ex_var[0] = 56326;
unsigned int *pos = ex_var+16;
for (int i = 0; i < 6; i++ )
{
*pos++ = 1;
}
*(volatile unsigned int*)(0x00100000) = ex_var[0];
}
(Note that *(volatile unsigned int*)(0x00100000) =
remains not defined by the C standard, but GCC is intended for some use in bare-metal environments and appears to work with this. Additional compilation switches might be necessary to ensure it is defined for GCC’s purposes.)
This yields assembly that sets ex_var[0]
and uses it in the assignment to 0x00100000
:
main:
mov DWORD PTR ex_var[rip], 56326
…
mov eax, DWORD PTR ex_var[rip]
mov DWORD PTR ds:1048576, eax
mov eax, 0
ret
Answered By - Eric Postpischil Answer Checked By - Pedro (WPSolving Volunteer)