Security and the Cortex-M MPU, part 3: Defining MPU regions
As noted in my previous articles, Security and the Cortex-M MPU and MPU Multitasking, embedded systems are being drawn more into the IoT. Consequently, security in the form of protection of critical system resources, is becoming increasingly important. Furthermore, effective protection can only be achieved via hardware means.
The Cortex-M memory protection unit (MPU) is difficult to use, but it is the main means of hardware memory protection available for Cortex-M processors. These processors are in widespread use in small- to medium-size embedded systems. Hence, it behooves us to learn to use the Cortex-M MPU effectively to achieve the reliability, security, and safety that modern embedded systems require.
This is the third in a series of articles presenting a software approach to satisfy the need for security using an MPU. The previous article discussed the assignment of a memory protection array, MPA, to each task. The MPA is designed to be quickly loaded into the MPU when switching to its task. We further covered creating an MPA template, which is loaded into a task’s MPA after the task is created using the task set service. For example:
An MPA template may be unique to a task or shared between a group of tasks, depending on security requirements and system structure. Templates are composed of regions. In this article, I’ll present the method for creating regions. Please go back to the first article for general orientation and definitions of terms used herein.
The process starts with defining sections in the code. In the following discussion, .
ucom_code and .
ucom_data, are common code and data sections shared among
.ucom_code contains the shell functions necessary for
utask system service calls; it also may contain shared subroutines.
.ucom_data contains variables shared among
utasks, if any.
Start by defining sections in the C source code modules. For example, in each C module containing code for the
.ucom_code region, start the code with:
.ucom_code is a name to identify the section. Several functions can be enclosed above. Also, the above structure can be repeated in other modules and all
ucom functions will be combined into one
As with code, many variables can be enclosed above and the above structure can be repeated in other modules to create one .ucom_data section containing all of the variables.
Creating and locating blocks
The linker plays a prominent role in assigning the sections defined in the C code to actual memory locations. In general, splitting control between code and a command file is not advisable because the two can get out of sync. This is particularly bad in this situation where the goal is to increase reliability, not to sabotage it. The approach presented here is intended to make the process of creating MPU regions easier, thus reducing synchronization errors.
For example, in the linker command file (.icf for ILINK) for the .ucom_code section:
ro in the place directive represents all other read-only objects.) In this example, only the
.ucom_code section is placed in the
ucom_code block. Note that the section and block names differ by only a “
.”. This is not a requirement—another naming convention can be used—but this one is convenient. Note that the name “
ucom_code” defined by the linker
block directive is available to the C compiler.
In the C file, where the template is being defined or in an included header file:
ucom_code can be used in the MPA template definition, as shown in the previous article, for example:
The macros used above are defined as follows:
An idiosyncrasy of the IAR tools is that the section pragmas accept block names as well as section names. In the above macros, the section name would work in
__section_begin(), if it is the first or only section in the block, but it would be wrong in
__section_size() because the block is likely to be larger than the section and the section size is unlikely to be a power of two.
Although multiple sections can be put into blocks, and blocks can be put into other blocks, the simplest solution is one section per block. Then blocks in the linker command file are equivalent to regions in MPA templates and ultimately in the MPU, itself. Using blocks mitigates against assigning wrong sizes and/or alignments. If a block is too small for the section in it, the linker will complain.
Tips for avoiding errors
It’s particularly annoying to make errors that degrade a system when the intent is to improve it. The following tips may help you avoid this:
- Make each block just large enough to contain its section. As code grows, if a block overflows, the linker will let you know and you can increase the size and alignment of the block to the next power of two.
- If you are a little weak on your powers of two, use hexadecimal numbers (e.g. 0x800 instead of 2048). Because the size must be a power of two, only one digit is permitted and that digit must be 1, 2, 4, or 8.
- In the block definition, be sure to immediately paste the size into the alignment, whenever the size is changed.
- Templates should initially be broadly defined for groups of tasks. This avoids wasted time tracking down MMFs due to unreachable variables or subroutines. When code has stabilized, it’s then practical to start isolating tasks more, if that is the security plan.
- Placing blocks into large regions that correspond to physical memories (e.g. DRAM, SRAM, flash, etc.) allows the linker to order the blocks efficiently to avoid gaps that waste memory.
- Use ample memory during development. Techniques do exist to significantly improve MPU memory efficiency later in a project. (See using subregion and overlay techniques in the smxMPU Manual.)
For more information on the MPU software architecture, see previous blogs:
As always, you can find lots more information on Micro Digital’s page dedicated to ARM Cortex-M.