Дизассемблирование blink.hex

разделы: AVR , ATtiny13A , АССЕМБЛЕР , дата: 18 марта 2014г.

Ассемблер считается сложным языком программирования. Хотя, припоминая, к слову, функциональное программирование, где нет ни переменных, ни циклов, а вместо них одни рекурсии и функции, я бы поспорил. Но, мне кажется, что я знаю причину такого мнения. Если взять рядовую книжку по предмету, то прежде чем написать свой blink, сперва придется прочитать добрую сотню страниц, если не больше, про архитектуру, регистры, шины, порты и т.д. Не самое легкое чтиво, надо сказать. Взяв для сравнения книжку Кернигана&Ритчи мы найдем "Hello World!" на первой же странице. Весомый аргумент в пользу Си, на мой взгляд.

В предыдущем посте я упоминал, что размер прошивки с программой Blink написанной на Си для ATtiny45 составляет 82 байта. В AVR каждая инструкция вместе с аргументом занимает два байта, т.е. одно слово. 82 делим на 2, получаем 41 ассемблерная инструкция. Наверно, должен сразу оговориться. Если вы не получаете за это деньги, то изучать Ассемблер нет никакого практического смысла, языка Си хватит "за глаза" для написания своих прошивок. НО... иногда желание заглянуть "под капот" бывает слишком сильным;)

Если уж экспериментировать с ассемблером, то для начала, разумно будет выбрать чип попроще. Мой выбор пал на ATtiny13. Его флеш-память составляет всего 1 Кбайт. Опять же 1024 делим пополам, получаем размер программы до 500 инструкций, что для ассемблера совсем немного. По правде говоря, многие устройства работают на этом чипе, на мой взгляд один из самых популярных микроконтроллеров. По счастливой случайности, распиновка ATtiny13 совпадает с распиновкой ATtiny45, следовательно и исходник blink.c не меняется:

// blink.c for AVR ATtiny 13/25/45/85

#include <avr/io.h>
#define F_CPU 1000000UL // частота резонатора 1МГц
#include <util/delay.h>
int main(void) {
 // макрос _BV(число) заменяет конструкцию (1 << число)
 DDRB |= _BV(PB0); // аналог pinMode(PB0,OUTPUT); в Wiring
 for (;;) {
  PORTB ^= _BV(PB0); // инвертируем состояние порта PB0
  _delay_ms(1000); // ждем 1 секунду
 }
 return 0;
}

Ах, чуть не забыл. Про книжки по ассемблеру AVR.

    Для меня очень полезны оказались материалы:
  1. AVR Assemble User Guid
  2. 8-bit AVR Instruction Set.
  3. Юрий Ревич "Практическое программирование микроконтроллеров Atmel AVR на языке ассемблера" 2-е издание. 2011г.

Продолжаем. Компиляция:

avr-gcc -mmcu=attiny13 -Wall -Os -o blink13.elf blink13.c
avr-objcopy -O ihex blink13.elf blink13.hex

Прошивка:

# avrdude -p attiny13 -c gromov -v -P /dev/ttyS0 -b9600 -U flash:w:blink13.hex

avrdude: Version 6.0.1, compiled on Nov 25 2013 at 14:33:17
         Copyright (c) 2000-2005 Brian Dean, http://www.bdmicro.com/
         Copyright (c) 2007-2009 Joerg Wunsch

         System wide configuration file is "/etc/avrdude.conf"
         User configuration file is "/root/.avrduderc"
         User configuration file does not exist or is not a regular file, skipping

         Using Port                    : /dev/ttyS0
         Using Programmer              : gromov
         Overriding Baud Rate          : 9600
         AVR Part                      : ATtiny13
         Chip Erase delay              : 4000 us
         PAGEL                         : P00
         BS2                           : P00
         RESET disposition             : dedicated
         RETRY pulse                   : SCK
         serial program mode           : yes
         parallel program mode         : yes
         Timeout                       : 200
         StabDelay                     : 100
         CmdexeDelay                   : 25
         SyncLoops                     : 32
         ByteDelay                     : 0
         PollIndex                     : 3
         PollValue                     : 0x53
         Memory Detail                 :

                                  Block Poll               Page                       Polled
           Memory Type Mode Delay Size  Indx Paged  Size   Size #Pages MinW  MaxW   ReadBack
           ----------- ---- ----- ----- ---- ------ ------ ---- ------ ----- ----- ---------
           eeprom        65     5     4    0 no         64    4      0  4000  4000 0xff 0xff
           flash         65     6    32    0 yes      1024   32     32  4500  4500 0xff 0xff
           signature      0     0     0    0 no          3    0      0     0     0 0x00 0x00
           lock           0     0     0    0 no          1    0      0  4500  4500 0x00 0x00
           calibration    0     0     0    0 no          2    0      0     0     0 0x00 0x00
           lfuse          0     0     0    0 no          1    0      0  4500  4500 0x00 0x00
           hfuse          0     0     0    0 no          1    0      0  4500  4500 0x00 0x00

         Programmer Type : SERBB
         Description     : serial port banging, reset=dtr sck=rts mosi=txd miso=cts

avrdude: AVR device initialized and ready to accept instructions

Reading | ################################################## | 100% 0.00s

avrdude: Device signature = 0x1e9007
avrdude: safemode: lfuse reads as 6A
avrdude: safemode: hfuse reads as FF
avrdude: NOTE: "flash" memory has been specified, an erase cycle will be performed
         To disable this feature, specify the -D option.
avrdude: erasing chip
avrdude: reading input file "blink13.hex"
avrdude: input file blink13.hex auto detected as Intel Hex

avrdude: writing flash (68 bytes):
Writing | ################################################## | 100% 0.04s avrdude: 68 bytes of flash written avrdude: verifying flash memory against blink13.hex: avrdude: load data flash data from input file blink13.hex: avrdude: input file blink13.hex auto detected as Intel Hex avrdude: input file blink13.hex contains 68 bytes avrdude: reading on-chip flash data: Reading | ################################################## | 100% 0.03s avrdude: verifying ... avrdude: 68 bytes of flash verified avrdude: safemode: lfuse reads as 6A avrdude: safemode: hfuse reads as FF avrdude: safemode: Fuses OK (H:FF, E:FF, L:6A) avrdude done. Thank you.

Как видно, для ATtiny13 blink весит уже всего 68 байт против 82 для ATyiny45. Заглядываем под капот:

$ avr-objdump -m avr -D ./blink13.hex

./blink13.hex:     file format ihex


Disassembly of section .sec1:

00000000 <.sec1>:
   0:   09 c0           rjmp    .+18            ;  0x14
   2:   0e c0           rjmp    .+28            ;  0x20
   4:   0d c0           rjmp    .+26            ;  0x20
   6:   0c c0           rjmp    .+24            ;  0x20
   8:   0b c0           rjmp    .+22            ;  0x20
   a:   0a c0           rjmp    .+20            ;  0x20
   c:   09 c0           rjmp    .+18            ;  0x20
   e:   08 c0           rjmp    .+16            ;  0x20
  10:   07 c0           rjmp    .+14            ;  0x20
  12:   06 c0           rjmp    .+12            ;  0x20
  14:   11 24           eor     r1, r1
  16:   1f be           out     0x3f, r1        ; 63
  18:   cf e9           ldi     r28, 0x9F       ; 159
  1a:   cd bf           out     0x3d, r28       ; 61
  1c:   02 d0           rcall   .+4             ;  0x22
  1e:   10 c0           rjmp    .+32            ;  0x40
  20:   ef cf           rjmp    .-34            ;  0x0
  22:   b8 9a           sbi     0x17, 0 ; 23
  24:   91 e0           ldi     r25, 0x01       ; 1
  26:   88 b3           in      r24, 0x18       ; 24
  28:   89 27           eor     r24, r25
  2a:   88 bb           out     0x18, r24       ; 24
  2c:   2f e3           ldi     r18, 0x3F       ; 63
  2e:   3d e0           ldi     r19, 0x0D       ; 13
  30:   83 e0           ldi     r24, 0x03       ; 3
  32:   21 50           subi    r18, 0x01       ; 1
  34:   30 40           sbci    r19, 0x00       ; 0
  36:   80 40           sbci    r24, 0x00       ; 0
  38:   e1 f7           brne    .-8             ;  0x32
  3a:   00 c0           rjmp    .+0             ;  0x3c
  3c:   00 00           nop
  3e:   f3 cf           rjmp    .-26            ;  0x26
  40:   f8 94           cli
  42:   ff cf           rjmp    .-2             ;  0x42

Итого 34 команды если пересчитаете нумерованые строки. т.е 68/2, все сходится. Сначала содержимое скорее всего не очень понятно, поэтому для внесения ясности, будет кстати вспомнить, что у нас имеется исходник программы и объектный файл. Запустив avr-objdump с опцией -S мы получим "смешаный код". Исходник и соответствующую трансляцию на ассемблере:

$ avr-objdump -S blink13.elf

./blink13.elf:     file format elf32-avr


Disassembly of section .text:

00000000 <__vectors>:
   0: 09 c0        rjmp .+18      ; 0x14 <__ctors_end>
   2: 0e c0        rjmp .+28      ; 0x20 <__bad_interrupt>
   4: 0d c0        rjmp .+26      ; 0x20 <__bad_interrupt>
   6: 0c c0        rjmp .+24      ; 0x20 <__bad_interrupt>
   8: 0b c0        rjmp .+22      ; 0x20 <__bad_interrupt>
   a: 0a c0        rjmp .+20      ; 0x20 <__bad_interrupt>
   c: 09 c0        rjmp .+18      ; 0x20 <__bad_interrupt>
   e: 08 c0        rjmp .+16      ; 0x20 <__bad_interrupt>
  10: 07 c0        rjmp .+14      ; 0x20 <__bad_interrupt>
  12: 06 c0        rjmp .+12      ; 0x20 <__bad_interrupt>

00000014 <__ctors_end>:
  14: 11 24        eor r1, r1
  16: 1f be        out 0x3f, r1 ; 63
  18: cf e9        ldi r28, 0x9F ; 159
  1a: cd bf        out 0x3d, r28 ; 61
  1c: 02 d0        rcall .+4       ; 0x22 <main>
  1e: 10 c0        rjmp .+32      ; 0x40 <_exit>

00000020 <__bad_interrupt>:
  20: ef cf        rjmp .-34      ; 0x0 <__vectors>

00000022 <main>:
22: b8 9a sbi 0x17, 0 ; 23 24: 91 e0 ldi r25, 0x01 ; 1 26: 88 b3 in r24, 0x18 ; 24 28: 89 27 eor r24, r25 2a: 88 bb out 0x18, r24 ; 24 2c: 2f e3 ldi r18, 0x3F ; 63 2e: 3d e0 ldi r19, 0x0D ; 13 30: 83 e0 ldi r24, 0x03 ; 3 32: 21 50 subi r18, 0x01 ; 1 34: 30 40 sbci r19, 0x00 ; 0 36: 80 40 sbci r24, 0x00 ; 0 38: e1 f7 brne .-8 ; 0x32 <main+0x10> 3a: 00 c0 rjmp .+0 ; 0x3c <main+0x1a> 3c: 00 00 nop 3e: f3 cf rjmp .-26 ; 0x26 <main+0x4>
00000040 <_exit>: 40: f8 94 cli 00000042 <__stop_program>: 42: ff cf rjmp .-2 ; 0x42 <__stop_program>

Прямо говоря не густо, ну чтож бывает. Обратите внимание на блок между main и exit, выделенный зеленым. Это и есть наша программа Blink на AVR Assembler. Все что выше зеленого блока, это таблица векторов прерываний, которая по спецификации должна идти перед программой. Это т.н. "шапка".

Теперь если вдуматься, то получим следующую структуру программы:


Как видете, все сводится к дюжине команд. В AVR есть две группы регистров: регистры общего назначения (РОН) и регистры ввода-вывода (РВВ).

Обозначения в коде:
r25,24,r18 и т.д. это регистры общего назначения.
0х17 и 0х18 это регистры ввода-вывода DDRB и PORTB соответственно.
sbi - устанавливает бит РВВ,
ldi - присваивает значение РОН,
in - копирует значение РВВ в РОН,
out - обратная операция,
eor - исключающее или,
subi - вычитание числа из РОН,
sbci - вычитание с флагом переноса,
brne - переход по условию: если результат предыдущей опереции не ноль,
rjmp - относительный переход. В качестве операнда смещение относительно текущего адреса в байтах

Наиболее интересный код здесь во второй фигурной скобке, который реализует задержку по времени. Этот алгоритм подробно разобран в книге Юрия Ревича, глава пятая "задержка". Только берите второе издание книги, в первом содержится опечатка: используется несуществующая команда suci вместо subi.

Вкратце. Нам нужно выполнить задержку на заданное время, мы знаем сколько времени выполняется каждая команда. Задержка выполняется последовательным вычитаним еденицы из числа до достижении нуля. комбинация команд: (subi + sbci + sbci) реализует декремент трехбайтного числа. Сначала вычитается младший байт, если он обнуляется, то вычитается еденица из среднего байта, если и он обнуляется от вычитается еденица из старшего байта. Если старший байт доходит до нуля, то условие перехода для оператора brne перестает выполнятся и программа выходит из цикла. В книге Юрия Ревича, вместо brne используется brcc. При этом операторе, цикл выполняет на одну итерацию больше, т.е. пока из нуля не вычитают еденицу.

Теперь считаем. Частота микроконтроллера 1 000 000 тактов в секунду. Цикл вычитания трехбайтного числа выполняется за: sbui, sbci - по одному такту, brne - два такта. Итого пять тактов. Т.е. надо вычитать число 1000000/5.

Смотрим на программу. Младший байт в r18 - 0x3F, средний байт в r19 - 0х0D, старший байт в r24 - 0x03. Итоговое число - 0x030D3F

$ echo $((0x030D3F*5))
999995

Думаю, c этим Blink'ом все понятно.