77. Compilation and compilers
The build stages (preprocessor, compiler, assembler, linker) are covered in section 63. This section is about the compiler itself as a tool: which ones exist for STM32, how the code finds out which compiler builds it and for which core, which options are mandatory and which warnings are worth turning on. And at the end: what is inside the firmware file and what you can see in it.
Everything said here about options and warnings was taken from armclang in Keil,
the same one that was used to check the whole handbook. Where GCC behaves differently,
this is stated separately.
Terms used in the documentation
Term Meaning toolchain the set of build tools predefined macro a name the compiler defines by itself target, CPU, FPU options options for the target, core, FPU ABI, float ABI binary interface convention, convention for passing floating-point values warning, -Werrora warning, turning warnings into errors section, garbage collection a section, removal of unused code debug information (DWARF) data for the debugger ELF, Intel HEX, binary image formats symbol table the list of names and addresses strip removing symbols
77.1 Compilers for STM32
Three families are in use for Cortex-M:
- Arm Compiler 6:
armclangin Keil. It is built on clang and LLVM; this handbook uses version 6.24, based on clang 20; - GCC:
arm-none-eabi-gcc, used by CubeIDE; - IAR: commercial, with its own IDE and library.
They share the C language, but differ in extensions (__attribute__, built-in
functions), standard libraries (section 74), the memory layout description
for the linker and the startup files. That is why the device pack has its own variant
of the startup file for each compiler (section 65.9).
77.2 How the code knows the compiler and the core
The compiler itself defines several names from which the code can tell where it is being built:
| Name | M0 | M3 | M4 | Meaning |
|---|---|---|---|---|
__ARMCC_VERSION |
6240002 | 6240002 | 6240002 | Arm Compiler 6.24 |
__clang_major__ |
20 | 20 | 20 | the clang version inside |
__ARM_ARCH |
6 | 7 | 7 | architecture generation |
__ARM_ARCH_6M__ / _7M__ / _7EM__ |
6M | 7M | 7EM | profile |
__ARM_FEATURE_CLZ |
no | yes | yes | the clz instruction (section 76.8) |
__ARM_FEATURE_IDIV |
no | yes | yes | hardware division (section 75.4) |
__ARM_FEATURE_DSP |
no | no | yes | signal processing instructions |
__ARM_FP |
no | no | yes | FPU |
This is how you write code that picks the fast path by itself: with clz if it exists,
and with a fallback for M0.
One trap: armclang also defines __GNUC__, for compatibility with
code written for GCC. So the check "#ifdef __GNUC__ means
GCC" is wrong: check __ARMCC_VERSION and __clang__ first. This is exactly
what cmsis_compiler.h from CMSIS does: based on these names it includes
cmsis_armclang.h or cmsis_gcc.h, and functions such as __REV and __CLZ
look the same under any compiler.
77.3 Options for the core and the FPU
The main options are the core (-mcpu) and everything related to floating-point numbers.
The language edition, the -std= option, stands apart: the meaning of the code depends on it.
In C23 int f() means int f(void) (section 66.2), and bool, true
and false became keywords and clash with old typedefs. armclang
from MDK 5.43 and arm-none-eabi-gcc 12.2 build as C17 by default, while newer
GCC versions have moved to C23. That is why the edition is written explicitly in the project,
for example -std=c17.
The function float scale(float x) { return x * 2.5f; } on Cortex-M4 with
different options:
| Options | Result |
|---|---|
-mcpu=cortex-m4 |
FPU instructions, but x arrives in r0: move it to s0, multiply, move it back, 5 instructions |
-mfloat-abi=softfp |
the same |
-mfloat-abi=hard |
x arrives directly in s0, 3 instructions |
-mfpu=none |
a call to the library function __aeabi_fmul |
-mfloat-abi changes the calling convention: where floating-point
arguments are passed, in core registers or in FPU registers. This has a dangerous
consequence. A function is built with -mfloat-abi=hard, and the file that calls it
without this option. armlink linked such a pair silently, with no error and no
warning. And the machine code shows that the caller puts the
argument 1.0 in r0, while the function reads it from s0: it gets the wrong
number. All files of the project and all libraries must be built with
the same -mfloat-abi. In Keil you check this yourself. GNU ld, which
CubeIDE uses for linking, refuses to link such a pair: "uses VFP register arguments,
… does not", meaning one object passes floating-point values in FPU registers and the other
does not. That is why the GCC standard library comes in several variants, and
arm-none-eabi-gcc picks the matching one from the options by itself.
The code sees the chosen variant in names: __ARM_PCS_VFP appears only
with -mfloat-abi=hard, __SOFTFP__ only without an FPU. How the FPU is enabled at
startup is covered in section 69.8.
77.4 Warnings: summary table
Over part IX, a collection of mistakes that the compiler can find has built up. Here they are together, checked on one file, one line per mistake:
| Mistake | armclang with no options |
armclang -Wall -Wextra |
GCC: from which option | Section |
|---|---|---|---|---|
if (a = 5) |
yes | yes | -Wall |
65.4 |
x & 1 == 0 |
yes | yes | -Wall |
65.3 |
if (e); |
yes | yes | -Wextra |
65.4 |
sizeof of an array parameter |
yes | yes | with no options | 67.5 |
| returning the address of a local variable | yes | yes | with no options | 67.10 |
strcpy of a long string into a short buffer |
yes | yes | with no options | 74.5 |
| uninitialized pointer | — | yes | -Wall |
67.10 |
| signed compared with unsigned | — | yes | -Wextra |
64.6 |
| signed divided by unsigned | — | — | -Wsign-conversion |
64.6 |
fall-through in switch |
— | — | -Wextra |
65.6 |
| shadowing a global | — | — | -Wshadow |
66.4 |
| variable-length array | — | — | -Wvla |
68.5 |
implicit double |
— | — | -Wdouble-promotion |
69.7 |
With armclang, even -Wall -Wextra does not catch the bottom five. With GCC
(arm-none-eabi-gcc 12; CubeIDE builds with the same GCC) the picture is different: with no
options it catches only three mistakes, and the upper part of the table only with
-Wall and -Wextra, so in CubeIDE the options are needed all the more. That is why for
chip projects it is worth turning on the extended set right away:
-Wall -Wextra -Wshadow -Wdouble-promotion -Wimplicit-fallthrough -Wvla -Wsign-conversion
With it the same file gave a warning on every one of the thirteen lines,
both with armclang and with GCC.
The -Werror option turns warnings into errors: the build
stops. In a new project it is worth turning on from day one, so that
warnings do not pile up. In an old one, where there are hundreds of them, only after
they have been sorted out.
77.5 The linker throws out what is not needed
armclang puts each function into its own section (.text.used,
.text.unused), and by default the linker throws out sections
that nothing refers to. You can see this directly in the link map:
Removing g.o(.text.unused). That is why unused driver and
library functions do not take up Flash. The --no_remove option keeps everything.
GCC does not do this by itself: the unused function stayed in the program without special options,
and with -ffunction-sections at compile time and
-Wl,--gc-sections at link time it disappeared. In CubeIDE projects these options
are on by default; in your own Makefile they are easy to forget.
77.6 Debug information does not get into the firmware
The -g option adds a description of the source to the object file: lines, types,
variable names, everything the debugger needs (section 35). In the checked
example the debug data in the object file grew almost tenfold;
the exact figure depends even on the length of the path to the source.
But the firmware code and data did not change by a single byte: 340 bytes of code in
both variants. Debug information lives in the image file on
the computer and is not written to Flash. That is why -g is left on
always, including release builds: the firmware does not grow from it, and
analysing a failure is much easier with it.
On a computer, strip plays a similar role: it removes the symbol table from
an executable. The program shrank from 16016 to 14472 bytes and
kept working: symbols are needed by people and the debugger, not by the processor.
77.7 From the image to the firmware file
The linker creates an ELF image file (in Keil it is .axf). The programmer
is more often given one of two formats:
| Format | Contents |
|---|---|
| Intel HEX | text records with an address and a checksum (section 48) |
| bin | bare bytes in a row, with no addresses |
fromelf --i32 turned the checked image into HEX: the first record is
:020000040800F2, that is, the upper half of the address 0x0800 (Flash
starts at 0x0800 0000); then the data; the last one is :00000001FF, the end of
the file. The checksum of every record adds up.
fromelf --bin gave 356 bytes, exactly the code and constants in a row. A bin file has no
addresses: where these bytes must go, you tell the programmer separately.
77.8 Analysis tools
| Task | Keil | GNU |
|---|---|---|
| disassemble | fromelf -c |
objdump -d |
| symbol table | fromelf -s |
nm |
| section sizes | fromelf -z |
size |
| firmware file | fromelf --i32, --bin |
objcopy -O ihex, -O binary |
| remove symbols | — | strip |
nm shows where things are defined. In an object file with main
calling fact, fact is marked U, not defined here, and main is marked
T, defined. In the file with the body of fact it is marked T. The linker
connects them (section 63.7), and after linking the program prints 5! = 120.
Which object file provided each name can be seen in the link map (section
71.6).
77.9 What can be seen in the firmware
A string written in the program lies in the firmware file in plain
text. Take a function that compares a password with the string "hunter2-secret": in
the bin file this string is found by a simple search. Anyone who
reads the Flash will see the same.
Hence two rules:
- do not store secrets in plain text in the firmware: instead of a password, store its hash; instead of a key, keep the key in a protected area or even in a separate chip (section 43);
- lock Flash read-out with the option bytes (section 11.6): then an ordinary debugger cannot read the firmware. But level 1 protects against the curious, not against a lab: on F0 and F1 it is bypassed with known techniques through the debug interface. Level 2 disables debugging permanently and is stronger (F1 does not have it), but even it is not absolute against power glitches (section 2, question 8). A key whose leak is costly is kept in a separate chip.
And the reverse: a conditional branch in machine code is easy to replace with the opposite one, turning "wrong password" into "correct password". A check that can be bypassed by patching one byte is not protection as long as the image can be read and written.
77.10 Pitfalls
Code "for GCC" behaves strangely in Keil. It checks __GNUC__:
armclang defines it too.
A floating-point function from a library receives the wrong arguments.
The library was built with a different -mfloat-abi; armlink does not notice this,
GNU ld refuses at link time.
A floating-point function on M4 is slower than expected.
-mfloat-abi=hard is not enabled: arguments are moved between registers.
A colleague caught a bug, but your build is clean. The warning is turned off; turn on the extended set.
An old project does not build with -Werror. First sort out the accumulated
warnings.
Firmware built with GCC is bigger than with Keil. -ffunction-sections
and --gc-sections are not enabled.
You turned off -g to make the firmware smaller. It did not change:
debug information does not get into Flash.
The programmer wrote the bin to the wrong place. A bin file has no address; you give it to the programmer.
The password was found in the firmware in a minute. It was stored in plain text, and Flash read-out was not locked.
77.11 Summary
- For STM32 there are Arm Compiler 6 (
armclang), GCC and IAR; the language is shared, the extensions, libraries and startup files are their own. - The compiler reports the version, architecture and core features with
predefined names: M3 has
clzand division, M0 does not.armclangalso defines__GNUC__. -mfloat-abi=hardpassesfloatin FPU registers;armlinksilently links objects with different conventions, and they pass each other the wrong numbers; GNU ld refuses to link such a pair.- With no options
armclangcatches six common mistakes,-Wall -Wextratwo more, the other five only the extended set; GCC with no options catches only three, with-Wall -Wextranine.-Werrorturns them into errors. armlinkthrows out unused functions by itself; GCC does it with-ffunction-sectionsand--gc-sections.-gdoes not increase the firmware by a single byte.- HEX carries addresses and checksums, bin carries only bytes.
- Strings from the program can be read in the firmware in plain text: secrets are not stored this way, and Flash read-out is locked.
Next: section 78 "Sockets and a web server in C".