avr-gcc деструктивные оптимизации


Я программирую микроконтроллер Atmel ATtiny13a с помощью avr-gcc 4.8.2.

Вот мой код на языке Си:

#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);
    }
}
Тестовая функция (быстрое мигание светодиода) никогда не вызывается из основной функции, поэтому контроллер должен вводить только основную функцию (медленное мигание).

Когда я компилирую код с помощью -O1, все работает нормально:

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

Но если я использую -Os (оптимизация по размеру) или -O2, микроконтроллер запускает функцию test вместо функции main функция: светодиод мигает быстро и никогда не останавливается.

Является ли флаг -Os просто слишком опасным для использования, следует ли его избегать? Или есть что-то, что я могу изменить в своем коде, чтобы избежать такого рода ошибок? ATtiny13a имеет только 1K вспышки, поэтому уменьшение размера является чем-то важным.


Edit: как и предлагалось в комментариях, вот ассемблерная разница с -O1 и -O2: http://www.diffchecker.com/3l9cdln6

Там вы можете увидеть, что -O2 изменяет первый раздел с .text на .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:
Это, вероятно, главная проблема здесь. После некоторого дальнейшего тестирования я обнаружил, что виновником является оптимизация -freorder-functions. Есть ли способ предотвратить такое поведение?
2 2

2 ответа:

Я провел дополнительную отладку и обнаружил, что "виновником" была оптимизация -freorder-functions. Это задокументировано в manpage следующим образом:

-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.

Последняя строка в документации объясняет проблему, которую я имел / вызывал. Если мы снова посмотрим на команды компиляции из исходного вопроса:

$ 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

...мы видим, что я передавал флаги оптимизации компилятору, но не компоновщику. Я предположил, что CFLAGS влияет только на компиляцию, а не на связывание, поэтому я не прошел их к линкеру, но в этом случае я ошибся.

Результат: ассемблерный код был переупорядочен компилятором (включая соответствующие метки), но компоновщик не учитывал эти метки. И поскольку функция test была помещена перед функцией main компилятором и не была переупорядочена компоновщиком, это был код, который фактически выполнялся на микроконтроллере.

Таким образом, решение оказалось следующим: флаги компилятора также должны быть переданы в линкер!

Я знаю, что мой ответ приходит через ~2 года после того, как был задан этот вопрос, но я считаю, что до сих пор нет правильного, глубокого ответа.


Давайте начнем с небольшой теории:

Когда вы вызываете GCC, он обычно выполняет предварительную обработку, компиляцию, сборку и связывание. "Общие параметры" позволяют остановить этот процесс на промежуточной стадии. Например, параметр-c говорит не запускать компоновщик. Тогда выход состоит из объекта файлы, выводимые ассемблером.

Другие варианты передаются на один этап обработки. Некоторые параметры управляют препроцессором, а другие-самим компилятором. Однако другие параметры управляют ассемблером и компоновщиком; большинство из них здесь не задокументированы, поскольку вам редко нужно использовать какой-либо из них.

Источник: GCC Online Docs

LDFLAGS

Дополнительные флаги для компиляторов, когда они должны вызывать компоновщик, ‘ld', такие вместо этого в переменную LDLIBS следует добавить библиотеки as-L. (-lfoo).

Источник: GNU make Manual

Как вы можете видеть, это зависит от GCC (я буду называть его так, чтобы отличить от фактического компилятора; вы можете найти его называемым C compiler frontend или просто compiler), какие параметры будут переданы в какие инструменты, и оказывается, что опция -On не передается компоновщику (вы можете проверить это, предоставив опцию -v GCC). Так вызов GCC без этой опции, когда предполагается, что он будет выполнять только связывание, в порядке.

Реальная проблема заключается в том, что вы не предоставляете -mmcu=dev опцию GCC при связывании. Поэтому он не может найти правильный crt*.o файл (C RunTime) и сообщить компоновщику, чтобы связать его; ваше приложение заканчивается без какого-либо кода инициализации. Поэтому обратите внимание, что вы должны включить -mmcu=dev в LDFLAGS или передать его в GCC независимо от того, что он должен делать (предварительная обработка/компиляция/сборка/связывание). Я уже видел пару файлов Makefile без этой опции в LDFLAGS в интернете, так что будьте осторожны.

Теперь пришло время для некоторой практики-предположив, что ваш источник находится в файле test.c, выполните следующие команды (в 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

Я оставил только необходимые опции + -Wall, для ATtiny13a вам нужно -mmcu=attiny13a вместо -mmcu=attiny13.

Давайте посмотрим на testOs_nodev.log и testOs_correct.log. Выполните следующую команду:
diff testOs_nodev.log testOs_correct.log

И ты кое-что увидишь. например:

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

(я сломал несколько строк, чтобы сделать его читаемым)

Разница в том, что без опции -mmcu=dev GCC по умолчанию использует файл спецификаций avr2 и не связывает ни один файл CRT.

Исследуйте объектные файлы (*.o) и выходные файлы (*.elf) с помощью avr-objdump:

avr-objdump -xd testOs_nodev.elf

Вы заметите, что файлы *_nodev.elf не содержат корректной информации об архитектуре (avr вместо avr:25) и никакого кода запуска (сравните testOs_correct.elf С testOs_nodev.elf). В разделе кода, кажется, дословно копия того, что было предоставлено в объектном файле.


Если какая-либо часть моей разработки кажется неясной или нуждается в дополнительном объяснении, не стесняйтесь спрашивать (комментировать).