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