Odświeżenie YOR-beaconów, czyli inżynieria wsteczna AVR, lekcja 1.

“Klubowe” beacony mikrofalowe (23, 13 oraz 3 cm) wybierają się do nowego, trochę wyższego niż dach bloku QTH. Żeby wszystko było legalne, poprawne i zgodne ze sztuką, należało wymienionym urządzeniom zaktualizować oprogramowanie keyerów, aby przedstawiały się odpowiednim znakiem (SR3YOR) oraz nowym lokatorem.

Beacony na 3 i 23 cm to konstrukcje “klasyczne”, tzn. schemat blokowy urządzenia żywcem przypomina “nadajnik telegraficzny na jedno pasmo” z egzaminu na uprawnienia amatorskie.

Źródłem sygnału jest generator kwarcowy, którego sygnał jest wielokrotnie powielany, filtrowany i wzmacniany, aż do uzyskania odpowiedniej częstotliwości i mocy wyjściowej. Kluczowanie odbywa się poprzez włączanie i wyłączanie stopnia mocy (dla 23 cm) lub delikatną zmianę częstotliwości generatora za pomocą napięcia (3 cm – nadaje sygnał CW-FSK).

W paśmie 3 cm zastosowano kwarc o częstotliwości 108,009 MHz co pomnożone 96 razy produkuje sygnał w okolicy 10 368 MHz – dostępny jest trymer do precyzyjnego dostrajania, a o stabilność temperatury (i częstotliwości) dba OCXO wg. G8ACE, utrzymujące stałe 60°C.

Układy kluczujące w obydwu urządzeniach są względnie proste, bo ich zadaniem jest tylko wytworzenie przebiegu on/off odpowiadającego telegraficznej reprezentacji znaku i lokatora.

3 cm

Na pierwszy ogień weźmy beacon na 3 cm, z keyerem wykonanym przez SP3FYK i zaprogramowanym przez SP5MX.

Serce urządzenia to leciwy (a jeszcze niedawno całkiem nowy, w porównaniu do 2051…) mikrokontroler attiny2313. Wygodnie wyprowadzone złącze ISP kusi żeby podłączyć doń programator i zobaczyć co w beaconie piszczy.

Za pomocą programu avrdude odczytałem pamięc flash urządzenia:
avrdude -c usbasp -p t2313 -U flash:w:3cm.hex:i
i zabrałem się za jej analizę. Mógłbym oczywiście bardzo małym nakładem pracy napisać swój program, w końcu to tylko ustawianie stanu HIGH oraz LOW na danym pinie, ale w ramach zabawy stwierdziłem, że przerobię to co już siedzi w mikrokontrolerze.

Tak prezentuje się surowy program. Zanim sięgniemy po deasembler, warto wizualnie ocenić zawartość pamięci – moją uwagę natychmiast przykuły ciągi “2D” i “2E” w pierwszej połowie zrzutu – odpowiadają one znakom ‘-‘ i ‘.’:

>>> chr(0x2d), chr(0x2e)
('-', '.')

To nie może być nic innego jak telegrafia. Zamieńmy więc wszystkie wystąpienia 2D na myślniki, 2E na kropki a 00 na spacje (global search-and-replace):

Cały alfabet telegraficzny w formie tablicy. Super, czyli gdzieś w kodzie musi znajdować się funkcja które iteruje po ciągu znaków i dla każdego wywołuje funkcję nadającą. To pozwala domniemywać, że gdzieś w pamięci musi znajdować się ciąg tekstowy, LUB, ewentualnie, szereg liczb określający pozycję w tablicy które tworzą znak i lokator. Tekst w pliku binarnym najłatwiej znaleźć za pomocą polecenia strings, ale żeby móc go użyć, trzeba przekonertować nasz plik .hex na format binarny:

objcopy -I ihex -O binary 3cm.hex 3cm.bin

➜  strings 3cm.bin 
..--.-
.-.-.
--..---....-.-.-.--..-.
(....)
-.--
--..
sr3xhy sr3xhy sr3xhy jo82lj jo82lj jo82lj 

Voila – mamy co chcieliśmy. Pozostało tylko znaleźć interesujący nas tekst w pliku .hex i podmienić go na nowy.

>>> ''.join([hex(ord(x))[2:] for x in 'sr3xhy'])
'737233786879'

Zaznaczone na niebiesko bajty w pliku .hex (6A i 23) to sumy kontrolne – należy przeliczyć je, aby program avrdude nie zgłosił błędu przy programowaniu. Użyłem pluginu do edycji plików intel hex do VS Code i zrobiłem to jednym kliknięciem. Pozostało wgrać zmieniony program:

avrdude -c usbasp -p t2313 -U flash:w:3cm.hex:i

I zgodnie z oczekiwaniami – dioda led na keyerze zaczęła mrugać w takt nowego znaku i lokatora.

23 cm

Tu jest gorzej. Narzędzie strings nie zwraca żadnych “oczywistych” rezultatów, tak samo rzut oka na plik hex. Czas spojrzeć na program w postaci zdeasemblowanej:

avr-objdump -D -m avr 23cm.hex | less

Podstawowym punktem zaczepienia może być oczywiście fakt, że kluczowanie CW odbywa się za pomocą jakiegoś pinu procesora. Sterowanie pinami odbywa się za pomocą instrukcji sbi i cbi, które muszą występować listingu asemblera:

 3b6:   08 95           ret
 3b8:   95 9a           sbi     0x12, 5 ; 18
 3ba:   a0 e6           ldi     r26, 0x60       ; 96
 3bc:   8d 91           ld      r24, X+
 3be:   9c 91           ld      r25, X
 3c0:   5b d0           rcall   .+182           ;  0x478
 3c2:   95 98           cbi     0x12, 5 ; 18
 3c4:   a0 e6           ldi     r26, 0x60       ; 96
 3c6:   8d 91           ld      r24, X+
 3c8:   9c 91           ld      r25, X
 3ca:   56 d0           rcall   .+172           ;  0x478
 3cc:   08 95           ret
 3ce:   95 9a           sbi     0x12, 5 ; 18
 3d0:   a2 e6           ldi     r26, 0x62       ; 98
 3d2:   8d 91           ld      r24, X+
 3d4:   9c 91           ld      r25, X
 3d6:   50 d0           rcall   .+160           ;  0x478
 3d8:   95 98           cbi     0x12, 5 ; 18
 3da:   a0 e6           ldi     r26, 0x60       ; 96
 3dc:   8d 91           ld      r24, X+
 3de:   9c 91           ld      r25, X
 3e0:   4b d0           rcall   .+150           ;  0x478
 3e2:   08 95           ret

W powyższym fragmencie widać dwie funkcje (pomiędzy ret i ret), które ustawiają stan wysoki a następnie niski na pinie 5 portu “D” – czyli 0x12, zgodnie z dokumentacją attiny2313.

Poszukajmy w kodzie odwołań do tych funkcji, czyli skoków pod adresy 0x3b8 oraz 0x3ce:

 29e:   08 95           ret
 2a0:   8b d0           rcall   .+278           ;  0x3b8
 2a2:   95 d0           rcall   .+298           ;  0x3ce
 2a4:   94 d0           rcall   .+296           ;  0x3ce
 2a6:   93 d0           rcall   .+294           ;  0x3ce
 2a8:   9d d0           rcall   .+314           ;  0x3e4
 2aa:   08 95           ret
 2ac:   90 d0           rcall   .+288           ;  0x3ce
 2ae:   84 d0           rcall   .+264           ;  0x3b8
 2b0:   8e d0           rcall   .+284           ;  0x3ce
 2b2:   98 d0           rcall   .+304           ;  0x3e4
 2b4:   08 95           ret
 2b6:   80 d0           rcall   .+256           ;  0x3b8
 2b8:   8a d0           rcall   .+276           ;  0x3ce
 2ba:   7e d0           rcall   .+252           ;  0x3b8
 2bc:   7d d0           rcall   .+250           ;  0x3b8
 2be:   92 d0           rcall   .+292           ;  0x3e4
 2c0:   08 95           ret
 2c2:   85 d0           rcall   .+266           ;  0x3ce
 2c4:   84 d0           rcall   .+264           ;  0x3ce
 2c6:   8e d0           rcall   .+284           ;  0x3e4
 2c8:   08 95           ret

Mamy tutaj kilka funkcji. Jeśli przyjęlibyśmy, że funkcja spod 3b8 nadaje kropkę, a 3ce kreskę, to składa się to w telegraficzne literki “J”, “K”, “L”, “M”, a z dużym prawdopodobieństwem 3e4 oznacza po prostu czekanie. Czas spojrzeć więc skąd wołane są w/w funkcje…

 15a:   cc d0           rcall   .+408           ;  0x2f4
 15c:   c1 d0           rcall   .+386           ;  0x2e0
 15e:   ec d0           rcall   .+472           ;  0x338
 160:   aa d0           rcall   .+340           ;  0x2b6
 162:   98 d0           rcall   .+304           ;  0x294
 164:   c2 d0           rcall   .+388           ;  0x2ea
 166:   3e d1           rcall   .+636           ;  0x3e4
 168:   9b d0           rcall   .+310           ;  0x2a0
 16a:   af d0           rcall   .+350           ;  0x2ca
 16c:   08 d1           rcall   .+528           ;  0x37e
 16e:   dd d0           rcall   .+442           ;  0x32a
 170:   a2 d0           rcall   .+324           ;  0x2b6
 172:   96 d0           rcall   .+300           ;  0x2a0
 174:   37 d1           rcall   .+622           ;  0x3e4

Pomijając funkcję pauzy (3e4), mamy więc odwołania do adresów 2f4 2e0 338 2b6 294 2ea 2a0 2ca 37e 32a 2b6 2a0. Użyjmy krótkiego skryptu w pythonie do zobaczenia, które funkcje są wywoływanie – nie będziemy tego sprawdzać ręcznie, w końcu to aż 12 znaków…

#!/usr/bin/python3

f = open('23cm.asm').read().splitlines()

for addr in ['2f4', '2e0', '338', '2b6', '294', '2ea', '2a0', '2ca', '37e', '32a', '2b6', '2a0']:
    prin = False
    out = ""
    for l in f:
        if l.startswith(f" {addr}") or prin:
            prin = True
            out += "-" if "0x3ce" in l else '.' if "0x3b8" in l else ""
        if 'ret' in l:
            prin = False

    print(out)

Odpalmy:

➜  python3 parse.py 
...
.-.
...--
.-..
....
-.--
.---
---
---..
..---
.-..
.---

I wszystko jasne – znaki układają się w ciąg SR3LHY JO82LJ. Analogicznie do poprzedniego przykładu z pasma 3 cm, należy teraz “tylko” podmienić adresy wywoływanych funkcji.

15a:   cc d0           rcall   .+408           ;  0x2f4

Skok pod adres 0x2f4 kodowany jest przez op-code “cc d0“. Tak wygląda op-kod dla instrukcji rcall: (strona 137)

Cały trick polega na tym, że adres pod który skaczemy jest w instrukcji rcall podawany relatywnie do naszej aktualnej pozycji w pamięci, czyli jeśli z 15a chcemy skoczyć na 2f4, to musimy skoczyć o 0x0cc, czyli 204, razy dwa – 408 komórek pamięci.

Sprawia to, że podmiana wywołań nie jest trywialna, ale z wiedzą jak działa instrukcja rcall, nie jest też absolutnie trudna – wystarczy kalkulator i chwila cierpliwości. Po naniesieniu poprawek w pliku hex (i korekcie checksum na końcu każdej modyfikowanej linii) można taki plik wrzucić jeszcze raz do deasemblera (avr-objdump) i upewnić się, że wołamy do odpowiednich miejsc. Finalnym potwierdzeniem było użycie avrdude i uważna obserwacja diody na pudełku keyera 🙂

Dwa beacony ćwierkają już nowymi znakami, pozostał jeszcze model na 13 cm – już wkrótce!

73 / SQ3SWF

This entry was posted in Bez kategorii. Bookmark the permalink.