Главная > AVR > Тайна AVR GCC

Тайна AVR GCC

Всем известно, что, даже если проект содержит только while (1), сгенерированный компилятором код имеет довольно существенный размер (около пары сотен байт). Собственно, меня давно интересовало назначение этих инструкций. И сегодня я наконец-то от нечего делать взял дизассемблер, проанализировал содержимое стандартного кода, генерируемого AVR-GCC для каждого проекта, и на случай, если упомянутый вопрос мучал не только меня, решил написать статью, посвященную его полному разбору.

Итак, исходник:

#include <avr/io.h>

volatile unsigned char i=0x15;

void main(void)
{
  while (1)
  {
    i++;
  }
}

Сначала переменной не было. Но после анализа выяснилось, что код инициализации переменных компилятор добавляет всегда, даже если инициализировать нечего. Потому, чтобы было интереснее, была добавлена переменная с начальным значением.

Компилируем (под ATmega128 для разнообразия) и, дизассемблировав, получаем следующее:

+00000000:   940C0046    JMP     0x00000046       Jump
+00000002:   940C005D    JMP     0x0000005D       Jump
+00000004:   940C005D    JMP     0x0000005D       Jump
+00000006:   940C005D    JMP     0x0000005D       Jump
+00000008:   940C005D    JMP     0x0000005D       Jump
+0000000A:   940C005D    JMP     0x0000005D       Jump
+0000000C:   940C005D    JMP     0x0000005D       Jump
+0000000E:   940C005D    JMP     0x0000005D       Jump
+00000010:   940C005D    JMP     0x0000005D       Jump
+00000012:   940C005D    JMP     0x0000005D       Jump
+00000014:   940C005D    JMP     0x0000005D       Jump
+00000016:   940C005D    JMP     0x0000005D       Jump
+00000018:   940C005D    JMP     0x0000005D       Jump
+0000001A:   940C005D    JMP     0x0000005D       Jump
+0000001C:   940C005D    JMP     0x0000005D       Jump
+0000001E:   940C0000    JMP     0x00000000       Jump
+00000020:   940C005D    JMP     0x0000005D       Jump
+00000022:   940C005D    JMP     0x0000005D       Jump
+00000024:   940C005D    JMP     0x0000005D       Jump
+00000026:   940C005D    JMP     0x0000005D       Jump
+00000028:   940C005D    JMP     0x0000005D       Jump
+0000002A:   940C005D    JMP     0x0000005D       Jump
+0000002C:   940C005D    JMP     0x0000005D       Jump
+0000002E:   940C005D    JMP     0x0000005D       Jump
+00000030:   940C005D    JMP     0x0000005D       Jump
+00000032:   940C005D    JMP     0x0000005D       Jump
+00000034:   940C005D    JMP     0x0000005D       Jump
+00000036:   940C005D    JMP     0x0000005D       Jump
+00000038:   940C005D    JMP     0x0000005D       Jump
+0000003A:   940C005D    JMP     0x0000005D       Jump
+0000003C:   940C005D    JMP     0x0000005D       Jump
+0000003E:   940C005D    JMP     0x0000005D       Jump
+00000040:   940C005D    JMP     0x0000005D       Jump
+00000042:   940C005D    JMP     0x0000005D       Jump
+00000044:   940C005D    JMP     0x0000005D       Jump
+00000046:   2411        CLR     R1               Clear Register
+00000047:   BE1F        OUT     0x3F,R1          Out to I/O location
+00000048:   EFCF        SER     R28              Set Register
+00000049:   E1D0        LDI     R29,0x10         Load immediate
+0000004A:   BFDE        OUT     0x3E,R29         Out to I/O location
+0000004B:   BFCD        OUT     0x3D,R28         Out to I/O location
+0000004C:   E011        LDI     R17,0x01         Load immediate
+0000004D:   E0A0        LDI     R26,0x00         Load immediate
+0000004E:   E0B1        LDI     R27,0x01         Load immediate
+0000004F:   ECEE        LDI     R30,0xCE         Load immediate
+00000050:   E0F0        LDI     R31,0x00         Load immediate
+00000051:   E000        LDI     R16,0x00         Load immediate
+00000052:   BF0B        OUT     0x3B,R16         Out to I/O location
+00000053:   C002        RJMP    PC+0x0003        Relative jump
+00000054:   9007        ELPM    R0,Z+            Extended load program memory and postincrement
+00000055:   920D        ST      X+,R0            Store indirect and postincrement
+00000056:   30A2        CPI     R26,0x02         Compare with immediate
+00000057:   07B1        CPC     R27,R17          Compare with carry
+00000058:   F7D9        BRNE    PC-0x04          Branch if not equal
+00000059:   940E005F    CALL    0x0000005F       Call subroutine
+0000005B:   940C0065    JMP     0x00000065       Jump
+0000005D:   940C0000    JMP     0x00000000       Jump
@0000005F: main
---- demo.c ---------------------------------------------------------------------------------------
6:        {
+0000005F:   91800100    LDS     R24,0x0100       Load direct from data space
+00000061:   5F8F        SUBI    R24,0xFF         Subtract immediate
+00000062:   93800100    STS     0x0100,R24       Store direct to data space
+00000064:   CFFA        RJMP    PC-0x0005        Relative jump
+00000065:   94F8        CLI                      Global Interrupt Disable
+00000066:   CFFF        RJMP    PC-0x0000        Relative jump
+00000067:   0015        ???                      Data or unknown opcode
+00000068:   FFFF        ???                      Data or unknown opcode
+00000069:   FFFF        ???                      Data or unknown opcode
+0000006A:   FFFF        ???                      Data or unknown opcode

... (пустая память)

Ну что, можно анализировать.

Конечно, первое, что бросается в глаза — таблица прерываний:

+00000000:   940C0046    JMP     0x00000046       Jump
+00000002:   940C005D    JMP     0x0000005D       Jump
+00000004:   940C005D    JMP     0x0000005D       Jump
+00000006:   940C005D    JMP     0x0000005D       Jump
+00000008:   940C005D    JMP     0x0000005D       Jump
+0000000A:   940C005D    JMP     0x0000005D       Jump
+0000000C:   940C005D    JMP     0x0000005D       Jump
+0000000E:   940C005D    JMP     0x0000005D       Jump
+00000010:   940C005D    JMP     0x0000005D       Jump
+00000012:   940C005D    JMP     0x0000005D       Jump
+00000014:   940C005D    JMP     0x0000005D       Jump
+00000016:   940C005D    JMP     0x0000005D       Jump
+00000018:   940C005D    JMP     0x0000005D       Jump
+0000001A:   940C005D    JMP     0x0000005D       Jump
+0000001C:   940C005D    JMP     0x0000005D       Jump
+0000001E:   940C0000    JMP     0x00000000       Jump
+00000020:   940C005D    JMP     0x0000005D       Jump
+00000022:   940C005D    JMP     0x0000005D       Jump
+00000024:   940C005D    JMP     0x0000005D       Jump
+00000026:   940C005D    JMP     0x0000005D       Jump
+00000028:   940C005D    JMP     0x0000005D       Jump
+0000002A:   940C005D    JMP     0x0000005D       Jump
+0000002C:   940C005D    JMP     0x0000005D       Jump
+0000002E:   940C005D    JMP     0x0000005D       Jump
+00000030:   940C005D    JMP     0x0000005D       Jump
+00000032:   940C005D    JMP     0x0000005D       Jump
+00000034:   940C005D    JMP     0x0000005D       Jump
+00000036:   940C005D    JMP     0x0000005D       Jump
+00000038:   940C005D    JMP     0x0000005D       Jump
+0000003A:   940C005D    JMP     0x0000005D       Jump
+0000003C:   940C005D    JMP     0x0000005D       Jump
+0000003E:   940C005D    JMP     0x0000005D       Jump
+00000040:   940C005D    JMP     0x0000005D       Jump
+00000042:   940C005D    JMP     0x0000005D       Jump
+00000044:   940C005D    JMP     0x0000005D       Jump

Но ведь я не использовал прерываний! Разве компилятор не мог отследить отсутствие макросов sei() и сэкономить на ТВП? Ох. Ну да ладно.

Все прерывания, кроме первого, содержат переход к адресу 0x5D. По нему, очевидно, расположен обработчик неописанных прерываний:

+0000005D:   940C0000    JMP     0x00000000       Jump

Видно, что это переход в самое начало, т.е., soft-reset. Мораль: если включить прерывание и не описать обработчик, контроллер будет перезагружаться при его возникновении. В принципе, это написано в документации, но кто ж ее читает, правда?

Теперь займемся вектором сброса:

00000000:   940C0046    JMP     0x00000046       Jump

Он указывает как раз на конец ТВП/начало основного исполняемого кода. А начинается код так:

+00000046:   2411        CLR     R1               Clear Register
+00000047:   BE1F        OUT     0x3F,R1          Out to I/O location

0x3F — это SREG, регистр статуса. Ну, собственно, он обнуляется, хотя при включении он и так ноль. Однако, если учесть возможность софт-ресета, обнулить его в целом не вредно.

Читаем дальше:

+00000048:   EFCF        SER     R28              Set Register
+00000049:   E1D0        LDI     R29,0x10         Load immediate
+0000004A:   BFDE        OUT     0x3E,R29         Out to I/O location
+0000004B:   BFCD        OUT     0x3D,R28         Out to I/O location

Ну, это понятно. 0x3E — SPH, 0x3D — SPL, инициализация стека. Т.е., SP=0x10FF.

+0000004C:   E011        LDI     R17,0x01         Load immediate
+0000004D:   E0A0        LDI     R26,0x00         Load immediate
+0000004E:   E0B1        LDI     R27,0x01         Load immediate
+0000004F:   ECEE        LDI     R30,0xCE         Load immediate
+00000050:   E0F0        LDI     R31,0x00         Load immediate

Тут стоит вспомнить про регистровые пары: X=R27:R26, Y=R29:R28, Z=R31:R30.

Видно, что X=0x0100, Z=0x00CE. Хм, а что там у нас во флеше по адресу 0x00CE? Память программы организована ячейками по два байта, так что поделим 0xCE пополам (получим 0x67) и поглядим:

+00000067:   0015        ???                      Data or unknown opcode

Ух ты, да это же начальное значение объявленной переменной! Расположено прямо за концом исполняемого кода.

+00000051:   E000        LDI     R16,0x00         Load immediate
+00000052:   BF0B        OUT     0x3B,R16         Out to I/O location

0x3B — регистр RAMPZ. Тоже обнуление. В RAMPZ полезную нагрузку несет всего один бит, отвечающий за работу инструкций чтения/записи, использующих указатель Z. Если в бите ноль, работа происходит с нижними 64КБ памяти, если 1 — с верхнми 64КБ. В принципе, при включении сей регистр тоже ноль. Но, поскольку возможен софт-ресет…

Дальше компилятор приступает к инициализации переменных:

+00000053:   C002        RJMP    PC+0x0003        Relative jump
+00000054:   9007        ELPM    R0,Z+            Extended load program memory and postincrement
+00000055:   920D        ST      X+,R0            Store indirect and postincrement
+00000056:   30A2        CPI     R26,0x02         Compare with immediate
+00000057:   07B1        CPC     R27,R17          Compare with carry
+00000058:   F7D9        BRNE    PC-0x04          Branch if not equal

Это цикл. Читаем из памяти по адресу Z (а там как раз стартовый адрес начальных значений) с сопутствующим увеличением Z, Пишем в память по адресу X, тоже с увеличением. В R17 ранее положено 0x01, так что сравнение X (R27:R26) идет с числом 0x0102, т.е., обрабатываем две ячейки. Кстати, интересный факт. Переменная-то всего одна и восьмибитная… Но AVR Studio честно отрапортовала, что занято два байта RAM.

Мораль: AVR-GCC размещает переменные в памяти, начиная с адреса 0x0100. С этого адреса как раз и начинается SRAM, стр. 19 даташита.

После этого идет следующий код:

+00000059:   940E005F    CALL    0x0000005F       Call subroutine
+0000005B:   940C0065    JMP     0x00000065       Jump
+0000005D:   940C0000    JMP     0x00000000       Jump

В конце уже знакомый нам вектор пустого прерывания. А вот в начале вызов функции. И эта функция — main(). Второй безусловный переход поставлен на случай, если main() вдруг завершится — ну, пишут же некоторые ревнители стандарта return в main(), даже если это прошивка МК, на котором операционной системы отродясь не было. Чтобы избежать неожиданностей, по смещению 0x65 расположена заглушка:

+00000065:   94F8        CLI                      Global Interrupt Disable
+00000066:   CFFF        RJMP    PC-0x0000        Relative jump

while (1), в общем.

Ну а с 0x5F начинается основной пользовательский код:

+0000005F:   91800100    LDS     R24,0x0100       Load direct from data space
+00000061:   5F8F        SUBI    R24,0xFF         Subtract immediate
+00000062:   93800100    STS     0x0100,R24       Store direct to data space
+00000064:   CFFA        RJMP    PC-0x0005        Relative jump

Собственно, ровно то, что и написано в исходнике: вынули из памяти, прибавили единицу, положили обратно, и так пока не выключат питание.

Итак, теперь все предельно ясно. В самом начале AVR-GCC укладывает таблицу прерываний, дальше инициализирует SREG, SP, RAMPZ, после чего задает начальные значения переменных и передает управление main(). В конце на всякий случай стоит заглушка.

Из не совсем понятного — зачем компилятор упорно создает ТВП, даже если прерывания не используются, и зачем вставляет код инициализации переменных, даже если их нет (проверял, это так). Но в целом все логично.

Ну вот, день прошел не зря.

P.S.

По написании сей статьи мне был задан вопрос: а что будет, если объявить кучу глобальных переменных без начального значения, которые по стандарту должны инициализироваться нулями? А и правда, что? Сказано — сделано:

#include <avr/io.h>

volatile unsigned char a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w;

void main(void)
{
  while (1)
  {
  }
}

Не буду приводить весь листинг, только самое интересное (после ТВП и до вызова main()):

+00000046:   2411        CLR     R1               Clear Register
+00000047:   BE1F        OUT     0x3F,R1          Out to I/O location
+00000048:   EFCF        SER     R28              Set Register
+00000049:   E1D0        LDI     R29,0x10         Load immediate
+0000004A:   BFDE        OUT     0x3E,R29         Out to I/O location
+0000004B:   BFCD        OUT     0x3D,R28         Out to I/O location
+0000004C:   E011        LDI     R17,0x01         Load immediate
+0000004D:   E0A0        LDI     R26,0x00         Load immediate
+0000004E:   E0B1        LDI     R27,0x01         Load immediate
+0000004F:   EDE4        LDI     R30,0xD4         Load immediate
+00000050:   E0F0        LDI     R31,0x00         Load immediate
+00000051:   E000        LDI     R16,0x00         Load immediate
+00000052:   BF0B        OUT     0x3B,R16         Out to I/O location
+00000053:   C002        RJMP    PC+0x0003        Relative jump
+00000054:   9007        ELPM    R0,Z+            Extended load program memory and postincrement
+00000055:   920D        ST      X+,R0            Store indirect and postincrement
+00000056:   30A0        CPI     R26,0x00         Compare with immediate
+00000057:   07B1        CPC     R27,R17          Compare with carry
+00000058:   F7D9        BRNE    PC-0x04          Branch if not equal
+00000059:   E011        LDI     R17,0x01         Load immediate
+0000005A:   E0A0        LDI     R26,0x00         Load immediate
+0000005B:   E0B1        LDI     R27,0x01         Load immediate
+0000005C:   C001        RJMP    PC+0x0002        Relative jump
+0000005D:   921D        ST      X+,R1            Store indirect and postincrement
+0000005E:   31A7        CPI     R26,0x17         Compare with immediate
+0000005F:   07B1        CPC     R27,R17          Compare with carry
+00000060:   F7E1        BRNE    PC-0x03          Branch if not equal

Сюрприз! Кусок кода инициализации предопределенных начальных значений остался:

+00000053:   C002        RJMP    PC+0x0003        Relative jump
+00000054:   9007        ELPM    R0,Z+            Extended load program memory and postincrement
+00000055:   920D        ST      X+,R0            Store indirect and postincrement
+00000056:   30A0        CPI     R26,0x00         Compare with immediate
+00000057:   07B1        CPC     R27,R17          Compare with carry
+00000058:   F7D9        BRNE    PC-0x04          Branch if not equal

Только видно, что он не работает, т.к. X изначально имеет значение 0x0100 и сравнивается с ним же (в R17 лежит 0x01). Но к нему добавился здравый код инициализации:

+00000059:   E011        LDI     R17,0x01         Load immediate
+0000005A:   E0A0        LDI     R26,0x00         Load immediate
+0000005B:   E0B1        LDI     R27,0x01         Load immediate
+0000005C:   C001        RJMP    PC+0x0002        Relative jump
+0000005D:   921D        ST      X+,R1            Store indirect and postincrement
+0000005E:   31A7        CPI     R26,0x17         Compare with immediate
+0000005F:   07B1        CPC     R27,R17          Compare with carry
+00000060:   F7E1        BRNE    PC-0x03          Branch if not equal

Таки да, во все переменные честно кладем ноль, положенный в R1 в самом начале.

Вот так интересно.

Рубрики:AVR

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s