0

My embedded system has two regions of memory. I created macros to switch between these regions. I would like to be able to execute these macros at compile time but I'm getting error: initializer element is not constant for certain operations and not others. I've boiled down the example to this. These are global variables:

__attribute__((section(".vmem_constant.data"))) unsigned int buf0[1024];
unsigned int buf_word_ptr = ((unsigned int)buf0)>>2; // doesn't work
unsigned int buf_word_ptr2 = ((unsigned int)buf0)/4; // doesn't work
unsigned int buf_word_ptr3 = ((((unsigned int)x)-0x40000)>>2); // original problem doesn't work
unsigned int works_1 = ((unsigned int)buf0) + 2; // works
unsigned int works_2 = buf0 + 16; // works

It seems like I can't do a divide or bitshift, however add or subtract is ok.

I originally ran into this when I was trying to subtract a fixed offset, and then divide by 4. Maybe there is an easier way to do this? I'm using (GCC) 7.2.0

benathon
  • 7,455
  • 2
  • 41
  • 70
  • These declarations are not _macros_. They are as you say later _variables_. You would have no problem if you used macros because they would be evaluated at run-time if the expression were not a compile-time constant. – Clifford Jun 09 '18 at 07:37
  • You don't specify C or C++ compilation. Will compile if you use C++ compilation (where non-const initialisers are valid) . Will not compile as C code. Simple solution use C++, or use a macro as you said you were. Why it is not regarded as const however remains an interesting question. – Clifford Jun 09 '18 at 07:53
  • Probably a good idea to use `uintptr_t` (in stdint.h) in favour of `unsigned int` for portability to systems where a pointer may not be the same size as `unsigned int`. – Clifford Jun 09 '18 at 13:14
  • I am using C, not C++. sorry for not specifying – benathon Jun 10 '18 at 22:47
  • 1
    The best response is to edit or tag the question, rather than hide the information in a comment. Added tag. – Clifford Jun 11 '18 at 08:46

2 Answers2

2

As far as pure C language is concerned, you are not supposed to be able to use address-dependent values in arithmetic constant expressions. The fact that you can use (unsigned int) buf0 in an initializer for an integer is a compiler-specific extension.

Within this extension, the restrictions imposed on address constants usually still apply. These restrictions are rooted in capabilities of real-life loaders. In general case the specific address of your buf0 is not really a compile-time constant. Its actual value will be known at loading time only. The loader will have to perform last-minute updates of your "constant expressions" that depend on this address. And loader's arithmetic capabilities are quite limited. Loaders know how to add and subtract, but that's about it. For this reason, you are allowed to use addition and subtraction in address constant expressions (as well as other operators which eventually boil down to address addition or subtraction), but nothing else. Loaders can't do shifts.

Moreover, the fact that your compiler even accepts (unsigned int) buf0 in some of these initializers is a pure coincidence. Apparently, on your platform pointer size is the same as size of unsigned int. If this were not the case, a conversion from pointer to unsigned int would have to truncate or expand the value. Loaders cannot do that either, meaning that if not for that coincidence, all of your declarations would fail to compile. This is why when you want to convert pointers to integers a better idea would be to use uintptr_t instead of unsigned int.

AnT stands with Russia
  • 312,472
  • 42
  • 525
  • 765
  • I second most parts of your answer, but wouldn't say that accepting `(unsigned int) buf0` or the latter two forms is coincidental - all major C compilers (gcc, clang, icc) make that same design decision, at least when targeting x86_64. Also, in my understanding, it's not the loader that is in play here, but the linker, which has to resolve addresses at link time - though admitedly I'm not very experienced with loaders, so I may be wrong but happy to learn. BTW, I'm familiar with at least one DSP linker that supports address shifting using a dedicated assembler directive. – valiano Jun 10 '18 at 18:25
  • @valiano: The "coincidental" part is, again, the lucky match between pointer size and size of `unsigned` on the platform in question. It is not a matter of compiler's design decision. It is a matter of physical properties of the platform. Apparently, the OP is dealing with a 32-bit platform, which is why at least some of the OP's initializations compile. The very same compilers would refuse to compile anything in that code on a 64-bit platform: http://coliru.stacked-crooked.com/a/92b99ac92ff0dc4f. – AnT stands with Russia Jun 10 '18 at 22:56
  • And again, this is specifically the *loader* that acts as the limiting factor in this case, not linker. It is all about last-minute code and data relocation when loading shared libraries or performing address space randomizations. – AnT stands with Russia Jun 10 '18 at 23:00
1

Strictly speaking, all above forms of initialization are forbidden in standard C.

Objects with static storage are allowed to be initialized with the following kinds of expressions (described in more detail here, with standard references):

  1. An arithmetic constant expression;
  2. The NULL pointer constant;
  3. An address constant expression;
  4. An address constant expression of some complete object type, plus or minus an integer constant expression.

Specifically, an arithmetic constant expression may be made up of arithmetic operators, the sizeof operator, and literal operands of arithmetic types. buf0 is a variable, not a literal, and therefore none of the expressions in the example qualifies as an arithmetic constant expression. Expression kinds 2, 3 and 4 are not applicable as well, so the compiler is free to reject all initialization forms that use buf0.

This makes sense since, address of buf0 is only resolved at link time, not at compile time, so it cannot be used to make up compile time constant expressions.

However, gcc (and other C compilers, including clang and icc) will allow the two latter forms, when the target's address width and destination int width are the same, which is an extension. For example, on x86-64, we'd get:

uint64_t fails = ((uint32_t)buf0) + 2; // fails
uint64_t works_1 = ((uint64_t)buf0) + 2; // works
uint64_t works_2 = (uint64_t)buf0 + 16ul; // works

And, if we inspect the generated assembly (godbolt), we can see the gcc expanded code for works_1 and works_2:

works_1:
  .quad buf0+2
works_2:
  .quad buf0+16

The GNU assembler allows simple arithmetics in static address calculations, using + and -, but will not allow for more complex expressions. In theory, if the assembler (and linker) allow for more advanced address arithmetics, like shifting, the C compiler could allow for this extension as well (but not being strictly conforming).

valiano
  • 16,433
  • 7
  • 64
  • 79
  • Thanks for the in-depth breakdown. Like you said, in the end it boils down to the assembler. – benathon Jun 10 '18 at 22:48
  • 1
    (And @portforwardpodcast) The address of `buf0` is NOT resolved at link time. Your inspection of the assembly is incorrect. What you see there are definitely NOT values "calculated in advance", as you incorrectly believe. These are expressions dependent on an *not-yet-known* address of `buf0`. The exact value of that address will only become known at loading time. The space reserved for `works_1` and `works_2` will not contain the final value after linking. – AnT stands with Russia Jun 10 '18 at 23:07
  • Only the loader will be able write the proper value into that space. And this is exactly why compiler and linker *cannot* allow you to use more complex arithmetic there: the standard loader can only add and subtract. Until loaders expand their capabilities in that respect (if ever) there's nothing compiler or linker can do even in theory: their hands are tied. – AnT stands with Russia Jun 10 '18 at 23:18
  • @AnT "gcc calculates in advance" is indeed not the best choice of words. Once linker resolves the address of `buf0`, it could then resolve `buf0+2`, which is actually a request assembler makes to the linker. The assembler creates address placeholders (relocation entries), which are resolved during the (static) linking process to actual addresses, whether they are virtual, absolute or relative, depending on platform. I'm not arguing that, they are the final physical addresses, which may be determined at load time, but I argue it's not related to the described compiler behavior. – valiano Jun 11 '18 at 05:54
  • @valiano: In modern implementations for modern platforms the linker will NOT be able to calculate the final addresses, since the linker does not know the virtual address at which the code (or data) will be loaded at run time. And this address can (and will) change for more reasons than one. And many of those reasons exist since ancient times, which is why even the first C standard places such restrictions on address constants. – AnT stands with Russia Jun 11 '18 at 06:05
  • @AnT I understand, but on these modern platforms the linker will be able to calculate a *relative* address (where `0x0` is the base address). So to what final physical (or virtual) addresses these virtual / relative addresses are mapped to at load time, is irrelevant to the problem definition. That's at least, my understanding. – valiano Jun 11 '18 at 06:10