avr-gcc destructive optimizations

2.5k views Asked by At

I'm programming an Atmel ATtiny13a microcontroller using avr-gcc 4.8.2.

This is my c code:

#include <avr/io.h> 
#include <util/delay.h> 

int main(void) {
    DDRB = 1; // PB0 is output
    for (uint8_t i = 0; i < 10; i++) {
        PORTB = 1;
        _delay_ms(500);
        PORTB = 0;
        _delay_ms(500);
    }
    while(1);
}

void test(void) {
    DDRB = 1; // PB0 is output
    for (uint8_t i = 0; i < 10; i++) {
        PORTB = 1;
        _delay_ms(100);
        PORTB = 0;
        _delay_ms(100);
    }
}

The test function (fast blinking of an LED) is never called from the main function, so the controller should only enter the main function (slow blinking).

When I compile the code with -O1, everything works fine:

avr-gcc -std=gnu99 -funsigned-char -funsigned-bitfields -fpack-struct -fshort-enums -mmcu=attiny13 -DF_CPU=1200000   -Wall -Wstrict-prototypes -Os -c test.c -o test.o
avr-gcc  test.o -o test.elf
avr-objcopy -O ihex -R .eeprom -R .fuse -R .lock -R .signature test.elf test.hex

But if I use -Os (optimization for size) or -O2, the microcontroller runs the test function instead of the main function: The LED blinks quickly and never stops.

Is the -Os flag simply too dangerous to use, should it be avoided? Or is there something I can change in my code to avoid this kind of bug? The ATtiny13a only has 1K of flash, so size reduction is something important.


Edit: As suggested in the comments, here's the assembler diff with -O1 and -O2: http://www.diffchecker.com/3l9cdln6

In there you can see that -O2 changes the first section from .text to .text.startup.

--- test.o1.txt 2013-12-03 19:10:43.874598682 +0100
+++ test.o2.txt 2013-12-03 19:10:50.574674155 +0100
@@ -3,7 +3,7 @@
 __SREG__ = 0x3f
 __tmp_reg__ = 0
 __zero_reg__ = 1
-       .text
+       .section        .text.startup,"ax",@progbits
 .global        main
        .type   main, @function
 main:

That's probably the main issue here. After some further testing I found that the culprit is the -freorder-functions optimization. Is there a way to prevent this behavior?

2

There are 2 answers

1
Danilo Bargen On BEST ANSWER

I did some further debugging, and found that the "culprit" was the -freorder-functions optimization. It is documented in the manpage as follows:

-freorder-functions
    Reorder functions in the object file in order to improve code locality.
    This is implemented by using special subsections ".text.hot" for most
    frequently executed functions and ".text.unlikely" for unlikely executed
    functions. Reordering is done by the linker so object file format must
    support named sections and linker must place them in a reasonable way.

The last line in the documentation explains the problem I was having/causing. If we look at the compile commands from the original question again:

$ avr-gcc -std=gnu99 -funsigned-char -funsigned-bitfields -fpack-struct \
   -fshort-enums -mmcu=attiny13 -DF_CPU=1200000   -Wall -Wstrict-prototypes \
   -Os -c test.c -o test.o
$ avr-gcc  test.o -o test.elf

...we see that I was passing the optimization flags to the compiler, but not to the linker. I assumed that the CFLAGS only affect compilation and not linking, so I did not pass them to the linker, but in that case I was wrong.

The result: The assembly code was reordered by the compiler (including appropriate labels), but the linker did not consider these labels. And because the test function was placed before the main function by the compiler and not re-arranged by the linker, that was the code that was actually executed on the microcontroller.

So the solution turned out to be: Compiler flags should also be passed to the linker!

0
Aleksander Z. On

I am aware that my response comes after ~2 years since this question has been asked, but I belive that there's still no correct, in-depth answer.


Let us begin with a little bit of theory:

When you invoke GCC, it normally does preprocessing, compilation, assembly and linking. The “overall options” allow you to stop this process at an intermediate stage. For example, the -c option says not to run the linker. Then the output consists of object files output by the assembler.

Other options are passed on to one stage of processing. Some options control the preprocessor and others the compiler itself. Yet other options control the assembler and linker; most of these are not documented here, since you rarely need to use any of them.

source: GCC Online Docs

LDFLAGS

Extra flags to give to compilers when they are supposed to invoke the linker, ‘ld’, such as -L. Libraries (-lfoo) should be added to the LDLIBS variable instead.

source: GNU make Manual

As you can see, it is up to GCC (I will call it this way to distinguish from actual compiler; you can find it beeing called C compiler frontend or simply compiler though) which options will be passed to which tools and it appears that -On option isn't passed to the linker (you can check it by giving -v option to GCC). So invoking GCC without this option when it is supposed to do only the linking is OK.

The real problem is that you don't provide -mmcu=dev option to GCC when linking. It is therefore unable to find proper crt*.o file (C RunTime) and tell the linker to link it in; your application ends up without any initialization code.

So please note that you must include -mmcu=dev in LDFLAGS or pass it to GCC regardless of what it is meant to do (preprocessing/compilation/assemblation/linking). I've already seen a couple of makefiles without this option in LDFLAGS on the Internet, so beware.


Now it's time for some practice -- assuming that your source is in test.c file, issue the following commands (on linux):

avr-gcc -mmcu=attiny13a -DF_CPU=1200000 -Wall -O1 -c -o testO1.o test.c
avr-gcc -mmcu=attiny13a -DF_CPU=1200000 -Wall -Os -c -o testOs.o test.c
avr-gcc -o testO1_nodev.elf testO1.o
avr-gcc -v -o testOs_nodev.elf testOs.o > testOs_nodev.log 2>&1
avr-gcc -v -mmcu=attiny13a -o testOs_correct.elf testOs.o > testOs_correct.log 2>&1

I left only necessary options + -Wall, for ATtiny13a you need -mmcu=attiny13a instead of -mmcu=attiny13.

Let's take a look at testOs_nodev.log and testOs_correct.log. Issue the following command:

diff testOs_nodev.log testOs_correct.log

and you will see something like:

2c2
< Reading specs from /usr/lib/gcc/avr/5.2.0/device-specs/specs-avr2
---
> Reading specs from /usr/lib/gcc/avr/5.2.0/device-specs/specs-attiny13a
10,12c10,12
< LIBRARY_PATH=/usr/lib/gcc/avr/5.2.0/:/usr/lib/gcc/avr/5.2.0/../../../../avr/lib/
< COLLECT_GCC_OPTIONS='-v' '-o' 'testOs_nodev.elf' '-specs=device-specs/specs-avr2'
<  /usr/lib/gcc/avr/5.2.0/collect2 -plugin /usr/lib/gcc/avr/5.2.0/liblto_plugin.so \
-plugin-opt=/usr/lib/gcc/avr/5.2.0/lto-wrapper -plugin-opt=-fresolution=/tmp/ccqBjM6T.res \
-plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lm -plugin-opt=-pass-through=-lc \ 
-o testOs_nodev.elf -L/usr/lib/gcc/avr/5.2.0 -L/usr/lib/gcc/avr/5.2.0/../../../../avr/lib \
testOs.o --start-group -lgcc -lm -lc --end-group
---
> LIBRARY_PATH=/usr/lib/gcc/avr/5.2.0/avr25/tiny-stack/:\
/usr/lib/gcc/avr/5.2.0/../../../../avr/lib/avr25/tiny-stack/:\
/usr/lib/gcc/avr/5.2.0/:/usr/lib/gcc/avr/5.2.0/../../../../avr/lib/
> COLLECT_GCC_OPTIONS='-v'  '-o' 'testOs_correct.elf' '-specs=device-specs/specs-attiny13a' \
'-mmcu=avr25' '-msp8'
>  /usr/lib/gcc/avr/5.2.0/collect2 -plugin /usr/lib/gcc/avr/5.2.0/liblto_plugin.so \
-plugin-opt=/usr/lib/gcc/avr/5.2.0/lto-wrapper -plugin-opt=-fresolution=/tmp/ccV919rY.res \
-plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lm -plugin-opt=-pass-through=-lc \
-plugin-opt=-pass-through=-lattiny13a -mavr25 -o testOs_correct.elf \
/usr/lib/gcc/avr/5.2.0/../../../../avr/lib/avr25/tiny-stack/crtattiny13a.o \
-L/usr/lib/gcc/avr/5.2.0/avr25/tiny-stack -L/usr/lib/gcc/avr/5.2.0/../../../../avr/lib/avr25/tiny-stack \
-L/usr/lib/gcc/avr/5.2.0 -L/usr/lib/gcc/avr/5.2.0/../../../../avr/lib testOs.o \
--start-group -lgcc -lm -lc -lattiny13a --end-group

(I broke a few lines to make it readable)

The difference is that without -mmcu=dev option GCC defaults to use avr2 specs file and doesn't link any CRT file.

Examine object files (*.o) and output files (*.elf) using avr-objdump:

avr-objdump -xd testOs_nodev.elf

You will notice that *_nodev.elf files don't contain correct information about architecture (avr instead of avr:25) nor any startup code (compare testOs_correct.elf with testOs_nodev.elf). The code section seems to a verbatim copy of what was supplied in object file.


If any part of my elaboration seems unclear or needs extra explanation, feel free to ask (comment).