Updating a Release System with ELF Programs – No Symbol Tables
You can easily extend the firmware in your released, embedded system using ELF program provide via a file system. For example, an SD card or, perhaps, downloaded into on-board SPI FLASH. In order to support such post-release update, your released firmware would have to support execution of fully linked, relocatable ELF programs loaded into RAM (see, for example, apps/examples/elf
).
The files shown in this Wiki page can be downloaded here.
Alan Carvalho de Assis has also made a video based on this example in the YouTube NuttX Channel.
Creating the Export Package
At the time that you release the firmware, you should create and save an export package. The export packet is all that you need to create post-release, add-on modules for your embedded system. Let's illustrate this using the STM32F4-Discovery networking NSH configuration with the STM32F4DIS-BB baseboard (This demonstration assumes that you also have support for some externally modifiable media in the board configuration. The could be removable media such as SD card or a USB FLASH stick, an internal file system that is remotely accessible via USB MSC, FTP, or whatever, or a remote file system (NFS). The networking NSH configuration uses the SD card on the STM32 baseboard for this demonstration. Other NSH configurations could be used provided that you supply the necessary file system support in some fashion.)(No baseboard? You can add support file system support to the basic STM32F4-Discovery board following these instructions: USB FLASH drive or SD card)
...
Code Block |
---|
nuttx-export-x.x |- arch/ |- build/ |- include/ |- libs/ |- startup/ |- System.map `- .config |
The Add-On Build Directory
In order to create the add-on ELF program, you will need (1) the export package, (2) the program build Makefile
, (3) a linker script used by the Makefile
, and (3) a Bash script to create a linker script. That Makefile
and Bash Script are discussed in the following paragraphs(NOTE that these example files implicitly assume a GNU tool chain is used and, in at least one place, and the target is a ARMv7-M platform. A non-GNU tool chain would probably require a significantly different Makefile
and linker script. There as at least one ARMv7-M specific change that would have to be made for other platforms in the script that creates the linker script, mkdefines.sh
.)
Hello Example
To keep things manageable, let's use a concrete example Suppose the ELF program that we wish to add to the release code is the since source file hello.c
:
Code Block |
---|
#include <stdio.h>
|
Code Block |
int main(int argc, char **argv) { printf("Hello from Add-On Program!\n"); return 0; } |
Let's say that we have a a directory called addon
and contains the hello.c
source file, a Makefile
that will create the the ELF program, and a Bash script called mkdefines.sh
that will create the a linker script.
Building the ELF Program =
The first step in creating the ELF program is the unzip the Export Package. We start with out addon
directory containing the following:
...
Then we have a new directory called nuttx-export-7.25
that contains all of the content from the released NuttX code that we need to build the ELF program.
The Makefile
The ELF program is created simply as:
...
This is the Makefile that I used to create ELF program:
Code Block |
---|
include nuttx-export-7.25/build/Make.defs
|
Code Block |
# Long calls are need to call from RAM into FLASH
|
Code Block |
ARCHCFLAGS += -mlong-calls ARCHWARNINGS = -Wall -Wstrict-prototypes -Wshadow -Wundef ARCHOPTIMIZATION = -Os -fno-strict-aliasing -fno-strength-reduce -fomit-frame-pointer ARCHINCLUDES = -I. -isystem nuttx-export-7.25/include |
Code Block |
CFLAGS = $(ARCHCFLAGS) $(ARCHWARNINGS) $(ARCHOPTIMIZATION) $(ARCHINCLUDES) -pipe
|
Code Block |
CROSSDEV = arm-none-eabi- CC = $(CROSSDEV)gcc LD = $(CROSSDEV)ld STRIP = $(CROSSDEV)strip --strip-unneeded |
Code Block |
# Setup up linker command line options
|
Code Block |
LDRELFLAGS = -r
|
Code Block |
LDELFFLAGS = -r -e main LDELFFLAGS += -T defines.ld -T gnu-elf.ld |
Code Block |
# This might change in a different environment |
Code Block |
OBJEXT ?= .o
|
Code Block |
# This is the generated ELF program
|
Code Block |
BIN = hello REL = hello.r |
Code Block |
# These are the sources files that we use
|
Code Block |
SRCS = hello.c OBJS = $(SRCS:.c=$(OBJEXT)) |
Code Block |
# Build targets
|
Code Block |
all: $(BIN) .PHONY: clean |
Code Block |
$(OBJS): %$(OBJEXT): %.c $(CC) -c $(CFLAGS) -o $@ $< |
Code Block |
System.map: nuttx-export-7.25/System.map cat nuttx-export-7.25/System.map | sed -e "s/\r//g" >System.map |
Code Block |
$(REL): $(OBJS) $(LD) $(LDRELFLAGS) -o $@ $< |
Code Block |
defines.ld: System.map $(REL) ./mkdefines.sh System.map "$(REL)" >defines.ld |
Code Block |
$(BIN): defines.ld $(REL) $(LD) $(LDELFFLAGS) -o $@ $(REL) $(STRIP) $(REL) |
Code Block |
clean: rm -f $(BIN) rm -f $(REL) rm -f defines.ld rm -f System.map rm -f *.o |
The Linker Script
Two linker scripts are used. One, I'll call it the main linker script, is just a normal file. The other, defined.ld
is a created on-the-fly as described in the following paragraph.
The main linker script that I am using in this example, gnu-elf.ld
, contains the following:
Code Block |
---|
SECTIONS { .text 0x00000000 : { _stext = . ; *(.text) |
...
*(.text.*) |
...
*(.gnu.warning) |
...
*(.stub) |
...
*(.glue_7) |
...
*(.glue_7t) |
...
*(.jcr) |
...
Code Block |
---|
_etext = . ; } |
Code Block |
.rodata : { _srodata = . ; *(.rodata) |
...
*(.rodata1) *(.rodata.*) |
...
*(.gnu.linkonce.r*) |
...
Code Block |
---|
_erodata = . ; } |
Code Block |
.data : { _sdata = . ; *(.data) |
...
*(.data1) |
...
*(.data.*) |
...
*(.gnu.linkonce.d*) |
...
Code Block |
---|
_edata = . ; } |
Code Block |
.bss : { _sbss = . ; *(.bss) |
...
*(.bss.*) |
...
*(.sbss) |
...
*(.sbss.*) |
...
*(.gnu.linkonce.b*) |
...
*(COMMON) |
...
Code Block |
---|
_ebss = . ; } |
Code Block |
/* Stabs debugging sections. */
|
Code Block |
.stab 0 : { *(.stab) } .stabstr 0 : { *(.stabstr) } .stab.excl 0 : { *(.stab.excl) } .stab.exclstr 0 : { *(.stab.exclstr) } .stab.index 0 : { *(.stab.index) } .stab.indexstr 0 : { *(.stab.indexstr) } .comment 0 : { *(.comment) } .debug_abbrev 0 : { *(.debug_abbrev) } .debug_info 0 : { *(.debug_info) } .debug_line 0 : { *(.debug_line) } .debug_pubnames 0 : { *(.debug_pubnames) } .debug_aranges 0 : { *(.debug_aranges) } } |
Creating the defined.ld Linker Script
The additional linker script defines.ld
was created through a three step process: First, the Makefile
generates a partial linked ELF object, hello.r
. The partially linked object is the hello
ELF program but with undefined symbols. The Makefile
then invokes the mkdefines.sh
script which generates the defined.ld
linker script that provides values for all of the undefined symbols. Finally, the Makefile
then produces the fully linked, relocatable hello
ELF object using the defines.ld
linker script..
Here is the version of mkdefines.sh
that I used in this demo:
Code Block |
---|
#!/bin/bash
|
Code Block |
usage="Usage: $0 <system-map> <relprog>"
|
Code Block |
# Check for the required path to the System.map file
|
Code Block |
sysmap=$1 if if [ -z "$sysmap" ]; then echo "ERROR: Missing <system-map>" echo "" echo $usage exit 1 fi |
Code Block |
# Check for the required partially linked file
|
Code Block |
relprog=$2 if [ -z "$relprog" ]; then echo "ERROR: Missing <program-list>" echo "" echo $usage exit 1 fi |
Code Block |
# Verify the System.map and the partially linked file
|
Code Block |
if [ ! -r "$sysmap" ]; then echo "ERROR: $sysmap does not exist" echo "" echo $usage exit 1 fi |
Code Block |
if [ ! -r "$relprog" ]; then echo "ERROR: $relprog does not exist" echo "" echo $usage exit 1 fi |
Code Block |
# Extract all of the undefined symbols from the partially linked file and create a # list of sorted, unique undefined variable names. |
Code Block |
varlist=`nm $relprog | fgrep ' U ' | sed -e "s/^[ ]*//g" | cut -d' ' -f2 | sort - | uniq`
|
Code Block |
# Now output the linker script that provides a value for all of the undefined symbols
|
Code Block |
for var in $varlist; do map=`grep " ${var}$" ${sysmap}` if [ -z "$map" ]; then echo "ERROR: Variable $var not found in $sysmap" echo "" echo $usage exit 1 fi |
Code Block |
varaddr=`echo ${map} | cut -d' ' -f1` echo "${var} = 0x${varaddr} | 0x00000001;" done |
This script basically just uses the nm
utility to find all of the undefined symbols in the ELF object. Then it searches for the address of each undefined symbol in the System.map
that was created when the released firmware was created. Finally, it uses the symbol name and the symbol address to create each symbol table entry.
...
Code Block |
---|
printf = 0x0800aefc | 0x00000001 ; |
Replacing an NSH Built-In Function
Files can be executed by NSH from the command line by simply typing the name of the ELF program. This requires (1) that the feature be enabled with CONFIG_NSH_FILE_APP=y
and (2) that support for the PATH variable is enabled (CONFIG_BINFMT_EXEPATH=y
and CONFIG_PATH_INITIAL
set to the mount point of the file system that may contain ELF programs.
...
Code Block |
---|
nsh> mount -t vfat /dev/mmcsd0 /bin nsh> hello Hello from Add-On Program! nsh> |
Version Dependency
NOTE that this technique generates ELF programs using fixed addresses from the System.map
map file of a versioned release. The generated ELF programs can only be used with that specific firmware version. A crash will most likely result if used with the different firmware version because the addresses from the System.map will not match the addresses in a different version of the firmware.
The alternative approach using Symbol Tables is more or less version independent.
Tightly Coupled Memories
Most MCUs based on ARMv7-M family processors support some kind of Tightly Coupled Memory (TCM). These TCMs have somewhat different properties for specialized operations. The STM32 F4 supports similar Core Coupled Memory (CCM). It is important to not that you cannot execute programs from CCM!Depending on the bus matrix of the processor, you may not be able to execute programs from the TCM. For instance, the STM32 F4 supports Core Coupled Memory (CCM), but since it is tied directly to the D-bus, cannot be used to execute programs! On the other hand, the STM32F3 has a CCM that is accessible to both the D-Bus and the I-Bus, in which case it should be possible to execute programs from this TCM.
When ELF programs are loaded into memory, the memory is allocated from the heap via a standard memory allocator. By default with the STM32 F4, the CCM in included in HEAP and will typically be allocated first. If CCM memory is allocate to hold the ELF program in memory, then a hard-fault will occur immediately when you try to execute the ELF program in memory.
...