Embedded HandbookApps
All sections › IX. The C language: from basics to advanced
STM32F103, STM32F407, STM32G431 · level basic · 11 min read
Open in the interactive handbook

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, -Werror a 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:

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.

What the core can do, by the compiler's names: Cortex-M0 with architecture 6 and profile 6M has no clz, no division, no DSP and no FPU; M3 with profile 7M has clz and division; M4 with profile 7EM has all four; the trap is that armclang also defines GNUC

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.

Firmware file: HEX records, bin without addresses and the password string in the dump

Hence two rules:

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

Next: section 78 "Sockets and a web server in C".

Open in the interactive handbook