Build Systems

Enforcing Binary Size Limits Using Make

When working with embedded systems, you have a limited amount of storage for your program's binary. Some tools provide ways to enforce limits on binary size, while others will happily let you exceed limits. It's no fun wasting hours debugging issues that arise from an incomplete binary being flashed.

If you're using gcc, you can rely on your linker scripts to enforce size limitations at compile time. If you're using clang for embedded development, you'll need to spin your own size enforcement.

Below is a quick make function which fails a build if the binary exceeds the size limit.

# Inputs:
#    $1: binary
#    $2: size limit
#  If max size is non-zero, use the specified size as a limit
ENFORCESIZE = @(FILESIZE=`stat -f '%z' $1` ; \
    if [ $2 -gt 0 ]; then \
        if [ $$FILESIZE -gt $2 ] ; then \
            echo "ERROR: File $1 exceeds size limit ($$FILESIZE > $2)" ; \
            exit 1 ; \
        fi ; \
    fi )

The function uses the stat tool to calculate the file size. We pass stat the '%z' format, which returns the size. This size is saved into the FILESIZE variable for later reference.

$ stat -f '%z' buildresults/src/test_applications/experimental
43440

We make sure that the size limit we received is non-zero. If the size is greater than zero, we compare our FILESIZE measurement against the max size. If the size is exceeded, the make will print an error message and exit with an error code.

You might be wondering why we check for a size limit of zero. Perhaps we use the same machinery to build and link multiple targets. We can keep the machinery in place for all targets while selectively enabling the size enforcement by controlling the max size input. Calling ENFORCESIZE with a size limit of 0 allows us to disable the call without having to modify our overall build process. All we have to do to control this behavior is utilize a MAX_SIZE variable which is populated for our given target.

Example Usage

I commonly use this function on the output file variable $@:

$(OUTPURDIR)/$(TARGET).bin : $(TARGET)
    # do some stuff
    $(call ENFORCESIZE,$@,$(MAX_SIZE))

You can also call the file on an explicit target:

.PHONY: experimental
experimental: groundwork
    $(Q)cd $(BUILDRESULTS); ninja
    $(call ENFORCESIZE, buildresults/src/test_applications/experimental, 65536)

Example Output

Let's check the function. For this test, I'll use an demo binary file:

$ ls -al buildresults/src/test_applications/experimental
-rwxr-xr-x  1 pjohnston  staff  43440 Jul 25 11:49 buildresults/src/test_applications/experimental

We'll call ENFORCESIZE after we finish the build. Since our file size is 43kB, setting a limit of 65kB will pass:

.PHONY: experimental
experimental: groundwork
    $(Q)cd $(BUILDRESULTS); ninja
    $(call ENFORCESIZE, buildresults/src/test_applications/experimental, 65536)

We can force the size down to 40kB and make sure an error is generated:

.PHONY: experimental
experimental: groundwork
    $(Q)cd $(BUILDRESULTS); ninja
    $(call ENFORCESIZE, buildresults/src/test_applications/experimental, 40000)

Then we will see:

$ make
ninja: no work to do.
*** ERROR:  buildresults/src/test_applications/experimental is larger than allowed size (43440 >  40000) ***
make: *** [experimental] Error 1

That's all there is to it!

Related Posts

Getting Started with CMake: Helpful Resources

Updated: 20190909

I've started familiarizing myself with CMake for a client's project. I was surprised at how few tutorials and good references exist on the internet. Here's a list of resources that I have found to be helpful.

Table of Contents:

  1. Learning CMake
  2. Videos
  3. Cheat Sheets
  4. Documentation
  5. Project Skeletons
  6. List of Global Variables

Learning CMake

Videos

Cheat Sheets

Documentation

Project Skeletons

List of Global Variables

  • CMAKE_BINARY_DIR
    • Same as CMAKE_SOURCE_DIR for in-source builds
    • Otherwise, top-level directory of your build tree
  • CMAKE_SOURCE_DIR
    • Top-level source directory
  • EXECUTABLE_OUTPUT_PATH
    • Set this variable to specify where executables should be placed (instead of CMAKE_CURRENT_BINARY_DIR)
    • SET(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
  • LIBRARY_OUTPUT_PATH
    • Set this variable to specify where libraries should be placed (instead of CMAKE_CURRENT_BINARY_DIR)
    • SET(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)
  • PROJECT_NAME
    • The name of the project set by the project() command
  • PROJECT_SOURCE_DIR
    • Full path to the root of your project source directory (nearest directory where CMakeLists.txt contains the project() command

Change Log

  • 20190909:
    • Added additional CMake resources recommended through the Embedded.fm Slack Group
    • Added a Videos section
    • Added a Project Skeletons section
    • Added a Table of Contents