Avocado uruchamia pierwszy program

Po kilkugodzinnej sesji czytania assemblera, analizy dumpów pamięci i żmudnego przeklikiwania się debuggerem po breakpointach udało mi się rozwiązać problem, który towarzyszył mi od początku projektu.

Problem

Wspominałem o nim w poprzednim poście - objawiał się tekstem "VSync: timeout", a efektem było wyłączanie przerwań systemowych, co z kolei blokowało wykonywanie kodu, gdyż BIOS jak i ładowane programy czekały na eventy (te generowane są przez przerwania).

Przez cały czas błędnie zakładałem, że to kod obsługujący przerwania znajdujący się w BIOSie wyłącza przerwania w systemie po kilku timeoutach. Przejrzałem dokładnie zdisassemblowany kod odpowiadający za wyświetlanie tego komunikatu, ale nie natrafiłem na nigdzie na modyfikację tej flagi. Znalazłem natomiast instrukcje SYSCALL. Okazuje się, że odpowiada ona za wchodzenie i wychodzenie z sekcji krytycznej - prościej mówiąc wyłącza i włącza przerwania na czas obsługi kodu związanego z przerwaniem. Zajrzałem do kodu emulującego tę instrukcję, a następnie do dokumentacji procesora i dowiedziałem się, że instrukcje SYSCALL i BREAK - w przeciwieństwie do skoków - wykonują się natychmiastowo i nie następuje branch delay slot, który spowodowałby załadowanie kolejnego opcodu po nich.

Rozwiązanie

Problemem był błędnie napisany kod obsługi wyjątków w module procesora, który nie czyścił delay slotu. Rozwiązaniem problemu było dodanie jednej linijki kodu:

Po tej zmianie emulator przestał zawieszać się na komunikacji ze sprzętem i wreszcie był w stanie załadować pierwszy program z płyty:

Gra bootowana z płyty

Timeout nadal się pojawia, ale przestało to wpływać na obsługę przerwań. Jego naprawa wymaga synchronizacji timerów, GPU i CPU.

Photo via VisualHunt.com
Opublikowano Avocado, Daj się poznać 2017 - Avocado | Skomentuj

Stan projektu – pierwszy miesiąc

Minął pierwszy miesiąc od startu Daj się poznać 2017 i uznałem to za dobry moment, aby podzielić się w jakim stanie znajduje się mój projekt oraz z jakimi problemami walczę na ten moment.

Muszę nadmienić, że w momencie startu tegorocznej edycji projekt był kontynuowany, a nie rozpoczynany od zera.

Co widać

Co się dzieje

Emulator ładuje BIOS konsoli i bez większych przeszkód wykonuje kod. Jak widać zaimplementowana jest obsługa GPU, a za ładny rendering tego co wyświetla GPU odpowiada OpenGL - jest obsługa trójkątów, prostokątów i sprite'ów, dzięki czemu wyświetla się animacja startowa oraz logo PlayStation. BIOS czyta pierwsze sektory z emulowanego napędu i przechodzi do procesu uruchamiania znajdującego się na płycie programu. W tym momencie emulator się zatrzymuje.

Czego brakuje

Niestety na ekranie nie widać modelu 3d, który powinien pokazać się po załadowaniu płyty. Jest to wina braku implementacji GTE - jednostki odpowiadającej za przekształcenia 3d.

W terminalu widać powtarzający się komunikat VSync: timeout(4:3) - jest to obecnie największy bloker. BIOS oczekuje przerwania VSync w miarę równych i dokładnych odstępach czasowych - 60 razy na sekundę (dla regionu NTSC). Niestety to w jaki sposób emuluję podzespoły wprowadza drobne różnice czasowe pomiędzy GPU, CPU i Timerami. BIOS czekając na następną klatkę zgłasza timeout i wyłącza przerwania sprzętowe. Objawia się to tym, że podzespoły takie jak CDROM czy Timery nie mogą zgłosić przerwania i BIOS zawiesza się w niektórych momentach czekając na zakończenie pewnych działań (np. odczyt sektoru).

Problem ten nie ogranicza się do BIOSu - obejmuje praktycznie wszystkie programy uruchamiane na emulatorze. Nawet proste gry homebrew w swoim kodzie oczekują na VSync (koniec klatki), aby synchronizować renderowanie do 60Hz.

Dla testów ręcznie wyzwalam przerwania co odblokowuje program, dzięki czemu emulator próbuje bootować płytę, jednak takie działanie często powoduje crashe i nie rozwiązuje problemu, a jedynie go tymczasowo omija.

Co robię

W ostatnich commitach refactorowałem kod: rozdzieliłem kod odpowiadający za renderowaniem pod Windowsem do osobnych plików, dzięki czemu wersja Linuksowa nie potrzebuje SDL i OpenGL do zbudowania. Poprawiłem synchronizację GPU z CPU z pozostałymi podzespołami, ale nie usunęło to błedu, o którym pisałem wyżej. Pozbyłem się obsługi GDB - debugger okazał się zbyt ograniczający, a jego interfejs jest bardzo nieintuicyjny dla osoby używającej Visual Studio.

Dodałem obsługę WebSockets - dzięki temu mogę stworzyć wygodny w obsłudze interfejs w HTML+JS i sterować wykonaniem emulatora z poziomu przeglądarki. W tym momencie komunikacja jest jednostronna i wyświetlane jest kilka rejestrów, ale efekty są zachęcające i bardzo chciałbym dopracować tą funkcjonalność. Używam Rivets.js do data bindingu.

WebSockets - JavaScript rozmawia z kodem natywnym

Photo via Visual hunt
Opublikowano Avocado, Daj się poznać 2017 - Avocado, Podsumowanie | Skomentuj

Procesor – skoki i delay slot

Operacje skoków nie są takie oczywiste jak mogłoby to się wydawać - jest tutaj kilka pułapek o chciałem wspomnieć.

Kodowanie instrukcji skoku

Przykładowa instrukcja skoku (j) w assemblerze będzie wyglądać tak:

   0xbfc00070  0bf00054:    j 0xfc00150
   0xbfc00074  00000000:    nop

 

Na zapisanie adresu skoku dostępne jest tylko 26 bitów. Jak wspominałem we wcześniejszym poście - każda instrukcja zajmuje 4 bajty i niemożliwy jest skok pod adres niebędący wielokrotnością 4 - taki skok zakończyłby się wyjątkiem procesora. Dlatego też adres docelowy zapisany w TARGET nie zawiera dwóch najmniej znaczących bitów. Aby odzyskać adres fizyczny z TARGET należy go przemnożyć przez 4 lub przesunąć o dwa bity w lewo.

Zostały 4 górne bity, które nie są nigdzie zakodowane. Skok jest wykonywany względem obecnego PC i właśnie z niego pobierane są 4 najwyższe bity:

uint32_t jumpPC = (PC & 0xf0000000) | (i.target << 2);

Delay slot

Po wykonaniu instrukcji nie następuje zmiana adresu. W architekturze MIPS występuje rzecz nazywana delay slot, która wynika z budowy pipeline. W skrócie - procesor wykonuje jedną instrukcję na przód. Kiedy program wykonywany jest liniowo - nie ma żadnego problemu. Gdy natomiast wykonywany jest skok, zostaje zaczytana kolejna instrukcja, jest wykonywana i dopiero wtedy procesor zmienia PC.

Sprawiało mi to dużo problemów, gdy próbowałem analizować kod generowany przez kompilator. Kod napisany ręcznie zazwyczaj po instrukcji skoku zawierał opcode nop, czyli nic-nie-rób - tak jak na przykładzie wyżej. Kompilatory natomiast lubią umieszczać tu prawidłowe instrukcje - dla wywołań funkcji jest to na przykład przekazanie argumentu do rejestru.

Warto wspomnieć, że dzisiejsze procesory są dużo bardziej zaawansowane i ilość wykonywanych naprzód instrukcji jest dużo większa - wiąże się to z ciekawym problemem branch prediction fail.

Implementacja

Rozwiązanie problemu jest dosyć proste - adres skoku zapisywany jest do zmiennej pomocniczej, a nie do PC - w moim przypadku pole to nazywa się jumpPC. Oprócz tego ustawiana jest flaga inDelaySlot, która będzie sprawdzona przy następnym wywołaniu funkcji.

Po modyfikacji kodu z poprzedniego postu funkcja wygląda tak:

void CPU::executeInstruction() {
    mipsInstructions::Opcode opcode;
    opcode.opcode = readMemory32(PC);

    bool isJumpCycle = inDelaySlot;

    if (opcode.op == 0x13) { // LUI, I-Type
        reg[opcode.rt] = opcode.imm << 16;
    } else if (opcode.op == 0x02) { // J, J-Type
        jumpPC = (PC & 0xf0000000) | (i.target << 2);
        inDelaySlot = true;
    } else {
        // PANIC
    }
    
    if (isJumpCycle) {
        inDelaySlot = false;
        PC = jumpPC;
    } else {
        PC += 4;
    }
}
Opublikowano Avocado, Daj się poznać 2017 - Avocado | Otagowano , | 1 komentarz

Procesor – emulacja pierwszej instrukcji

Procesor zastosowany w Playstation oparty jest na 32-bitowej architekturze MIPS. To co ją wyróżnia to fakt, że jest to architektura RISC (Reduced Instruction Set Computing), a nie CISC (Complex Instruction Set Computing) do której przyzwyczaiły nas PCty. Z czym to się wiążę? Zestaw instrukcji jest mocno zredukowany - kilkadziesiąt instrukcji zamiast kilkuset. Instrukcje te wykonują natomiast proste operacje. Przekłada się na to większą wydajność procesora, jednak niektóre operacje mogą wymagać większej ilości kodu. Kolejną cechą tych instrukcji jest to, że każda z nich kodowana jest na 4 bajtach. Ponownie - przyśpiesza to ich dekodowanie, ale wiele z instrukcji może marnować cenne miejsce w pamięci.

Rejestry

MIPSy posiadają 32 rejestry (oznaczone r0-r31) - to całkiem dużo, jednak nie wszystkie z nich są ogólnego przeznaczenia:

  • r0 (zero) - rejestr zawsze zwraca 0, nie można go modyfikować
  • r31 (ra) - przechowuje adres powrotu przy funkcjach skoku

Pozostałe 30 rejestrów jest dostępne dla programisty, ale chcąc pisać w C trzeba trzymać się konwencji mówiącej, które z rejestrów można dowolnie modyfikować, a które trzeba zapisać przy wywoływaniu funkcji. Z puntu widzenia emulatora nie ma to znaczenia, dlatego pominę ich opis.

Wewnątrz procesora i niedostępne dla programisty znajdują się 3 dodatkowe rejestry:

  • PC - program counter
  • hi lo - używane przy operacjach arytmetycznych, gdzie wynik jest większy niż 32 bity (mnożenie, dzielenie)

Mając te informacje można utworzyć strukturę opisującą procesor:

struct CPU {
    uint32_t PC;
    uint32_t reg[32];
    uint32_t hi, lo;
    CPU() {
        PC = 0xBFC00000; // First instruction executed after reset
        for (int i = 0; i < 32; i++) reg[i] = 0;
        hi = 0;
        lo = 0;
    }
    void executeInstruction();
    uint32_t readMemory32(uint32_t addr);
}

Używam typów wbudowanych w język z nagłówka stdint.h - mocno zalecam ich używanie w takich przypadkach, gdyż nie możemy pozwolić na dobieranie innych typów z zależności od kompilatora.

Wykonywanie kodu

Wykonanie instrukcji przez procesor składa się z 5 kroków:

  1. Pobranie instrukcji z pamięci (fetch)
  2. Zdekodowanie instrukcji (decode)
  3. Wykonanie (execute)
  4. Dostęp do pamięci (memory access)
  5. Zapis wyniku (writeback)

Taka rozwiązanie - pipeline - pozwala na równoczesne wykonywanie kilku kroków, co przyśpiesza pracę procesora.

Fetch

Pierwszym krokiem będzie pobranie instrukcji z adresu PC - w tym celu stworzyłem funkcję readMemory32. Jej implementacja w tym momencie może ograniczyć się do obsługi zakresu 0xBFC00000 - 0xBFC80000, gdzie powinien znajdować się załadowany BIOS.

uint32_t CPU::readMemory32(uint32_t addr) {
    if (addr <= 0xBFC00000 && addr < 0xBFC80000) {
        return bios[addr] | (bios[addr+1] << 8) | (bios[addr+2] << 16) | (bios[addr+3] << 24);
    }
    // PANIC
    return 0;
}

bios jest tablicą o rozmiarze elementu 8 bitów - ułatwia to adresowanie, ale pojawia się taki potworek przy implementacji odczytu 32 bitowego.

Po wczytaniu pierwszego opcode dostajemy wartość 0x3c080013.

Ze wzglądu na format little endian bajty w pliku są zapisane odwrotnie, czyli 31 00 08 3c

Decode

Instrukcje w MIPS dzielą się na 3 typy: Immediate, Jump i Register type. Sposób ich zapisu przedstawia poniższy rysunek:

MIPS instruction encoding

  • OP (Opcode) - część wspólna dla wszystkich 3 rodzajów, to właśnie ona określa jaka to instrukcja.
  • RS (Register Source), RT (Register Target), RD (Register Destination) - 5 bitowe pola, pozwalają na zaadresowanie każdego z 32 rejestrów głównych.
  • IMM (Immediate) - wartość zakodowana jako część instrukcji
  • TARGET - adres skoku
  • SH (SHIFT) - używane przy operacjach przesunięcia bitowego
  • FUN (FUNCTION) - koduje dodatkowe operacje, gdy OP == 0

Aby zdekodować taką strukturę można napisać funkcje, która z użyciem masek i przesunięć bitowych wyodrębni dane pola. Ja tym razem poszedłem inną ścieżką i wykorzystałem unię.

Unia nakłada na siebie pola, które się w niej znajdują:

union Example {
    uint32_t a;
    uint16_t b;
};

Zapisując coś do b nadpiszemy niższe 16 bitów a - na tej samej zasadzie jak w x86 działają rejestry eax oraz ax.

union Opcode {
    struct {
        uint32_t fun : 6;
        uint32_t sh : 5;
        uint32_t rd : 5;
        uint32_t rt : 5;
        uint32_t rs : 5;
        uint32_t op : 6;
    };
    uint32_t opcode;       // Whole 32bit opcode (raw value)
    uint32_t target : 26;  // JType instruction jump address
    uint16_t imm;          // IType immediate
    int16_t offset;        // IType signed immediate (relative address)
};

Notacja z ":" oznacza ile bitów zajmuje dane pole. Łącząc unię ze strukturą można opisywać skomplikowane struktury bitowe będące jednocześnie łatwo dostępne z poziomu kodu bez potrzeby operacji na bitach.

Podstawiając pod tę unię wcześniej odczytają wartość (0x3c080013) dostajemy .op = 0x13.

Execute

W tym momencie trzeba odwołać się do dokumentacji procesora, gdzie dowiemy się że jest to instrukcja LUI - Load Upper Immediate. Immediate, a to oznacza że jest to I-Type. Implementacja tej instrukcji jest banalnie prosta:

cpu->reg[opcode.rt] = opcode.imm << 16;

Instrukcja ta ładuje 16 bitów zakodowane w instrukcji (IMM - Immediate value, 0x0013) do górnych 16 bitów rejestru RT (r8 w tym przypadku).

Dlaczego taka dziwna operacja? Ze względu na ograniczenia w kodowaniu opcodów aby załadować pełną 32 bitową wartość do rejestru trzeba to zrobić w dwóch krokach - załadowanie górnych 16 bitów i ustawienie niższych 16 bitów za pomocą dodawania lub operacji OR (własnie ta jest kolejną instrukcją w BIOSie).

Krok Memory Access nie występuje w tej instrukcji, natomiast Writeback połączony jest z samym wykonaniem.

Ponieważ nie jest to instrukcja skoku, na koniec zostaje zwiększony licznik PC o 4

Podsumowanie

void CPU::executeInstruction() {
    mipsInstructions::Opcode opcode;
    opcode.opcode = readMemory32(PC);

    if (opcode.op == 0x13) { // LUI, I-Type
        reg[opcode.rt] = opcode.imm << 16;
    } else {
        // PANIC
    }
    
    PC += 4;
}

W ten sposób udało się zdekodować i wykonać pierwszą instrukcję w programie. Pominąłem tutaj wiele szczegółów jak działają MIPSy  - konkretne kruczki na przykładzie innych instrukcji przestawię w kolejnych postach.

Photo credit: GollyGforce - Living My Worst Nightmare via Visual Hunt / CC BY
Opublikowano Avocado, Daj się poznać 2017 - Avocado | Otagowano , | Skomentuj

Playstation – architektura systemu

Pisząc blog, w którym opisuje proces tworzenia emulatora dla konsoli Playstation wypada mi przybliżyć jak ten sprzęt jest zbudowany. Patrząc na specyfikację przypomina on komputery, które pojawiały się w tamtych czasach (1994), jednak niektóre rozwiązania wybrane przez twórców - szczególnie te związane z generowaniem grafiki 3D - nie przetrwały do dzisiejszych czasów.

SchematPSX Schematics

Powyższy schemat blokowy ukazuje architekturę konsoli widzianą z poziomu kodu. W rzeczywistości wiele bloków takich jak Interrupts DMA czy MDEC fizycznie znajdują się w procesorze. Dla programów oraz z punktu widzenia emulacji są to oddzielne urządzenia w przestrzeni adresowej (tzw. MMIO - Memory Mapped Input Output), dlatego takie rozdzielenie ma sens.

CPU

Procesor konsoli oparty jest o architekturę MIPS. Ze względu na rolę w całym systemie poświęcę mu osobny post. W CPU znajdują się dwie mniejsze jednostki:

  • COP0 - koprocesor systemowy, układ zajmujący się wyjątkami/przerwaniami, zarządzaniem user/kernel mode (nie używane w konsoli). Podstawowa implementacja jest wymagana do działania przerwań. Układ narysowałem wewnątrz CPU, ponieważ nie jest dostępny na magistrali, a komunikacja z nim odbywa się za pomocą specjalnych instrukcji MFC0 (Move From Co-processor 0) oraz MTC0 (Move To Co-processor 0)
  • COP2 (GTE) - Geometry Transformation Engine, jest to wydzielona część procesora do operacji na macierzach i wektorach - wszystkie transformacje związane z grafiką 3d wykonywane są właśnie tutaj. Operacje odbywają się na liczbach całkowitych, a nie zmiennoprzecinkowych jak dzieje się to dzisiaj - wiąże się to specyficznym wyglądem gier na tej konsoli. Jak w przypadku COP0 procesor ma specjalne instrukcje do komunikacji z nim.

Pamięć

  • Scratchpad - w rzeczywistości jest to cache procesora dla danych (1kB), jednak tutaj on wykorzystywany jako pamięć o szybkim dostępie.
  • RAM - pamięć główna, do dyspozycji 2MB
  • ROM (BIOS) - pamięć o wielkości 512kB - znajdują się tam kernel konsoli, część biblioteki standardowej C i program startowy wyświetlający ładne logo i ładujący program z płyty.
  • Expansion - port z tyłu konsoli, nie używany oficjalnie. Urządzenia typu Game Shark wykorzystywały go, aby umożliwić wprowadzanie kodów do gier. Zazwyczaj pod ten port podpinana była dodatkowa pamięć ROM.

    Ciekawostka - aby konsola uruchomiła kod zamiast tego znajdującego się w BIOSie wystarczyło na początku ROMu zamieścić entry point programu oraz tekst "Licensed by Sony Computer Entertainment Inc." - zabezpieczenia w tamtych czasach nie były skomplikowane 🙂

Urządzenia

  • GPU - najważniejsza część systemu, odpowiada za rysowanie kolorowych lub oteksturowanych linii, trójkątów i prostokątów. GPU operuje w całości w 2D i na liczbach całkowitych - wszystkie transformacje są wcześniej wykonywane przez procesor oraz GTE. Ma swój VRAM, do którego CPU nie ma bezpośredniego dostępu.
  • CDROM - zajmuje się odczytem z CD i dekodowaniem muzyki (jest połączony z SPU). Interakcja z nim odbywa się na niskopoziomowych instrukcjach typu "przeczytaj n-ty sektor" lub w przypadku Audio CD "odtwarzaj następną ścieżkę". Samą obsługą plików na płycie musi zająć się procesor. Zabezpieczenie przed kopiowaniem gier znajduje się właśnie w tym miejscu - procesor nie otrzyma danych, gdy płyta nie jest oryginalna.
  • Interrupts - służy maskowaniu i sprawdzaniu stanu danych przerwań (IRQ). Prawie każdy z wymienionych tutaj bloków mógł zgłosić przerwanie, które w zależności od ustawień było realizowane przez konsolę.
  • DMA (Direct Memory Access) - układ odciążający procesor przy transferze dużej ilości danych. Przykładami są kopiowanie grafiki z RAM do VRAM czy odczyt z CDROM do RAM.
  • Timers - 3 konfigurowalne liczniki zliczające różne wartości - ilość wyrysowanych linii na ekranie, pikseli lub cykli procesora. W tym momencie nie potrafię napisać jak są wykorzystywane przez gry, BIOS natomiast używa ich do synchronizacji z układem graficznym.
  • Controllers and Memory card - rejestry odpowiadające konfiguracji szyny SPI, która odpowiada za komunikację z urządzeniami peryferyjnymi. Wszystkie 4 porty z przodu dzielą te same linie komunikacyjne.
  • Serial - port szeregowy znajdujący się z tyłu konsoli. Garstka gier używała go do łączenia dwóch konsol ze sobą dostarczając specjalny tryb multiplayer. Nie będę zajmował się implementacją tej części.
  • MDEC (Macroblock Decoder)- układ dekodujący filmy używany przez konsolę, można go porównać do sprzętowego dekodera JPEGów. Nie jest konieczny do rozruchu konsoli.
  • SPU - jednostka przetwarzania dźwięku, chyba najbardziej skomplikowany z układów. Umożliwia zarówno dekodowanie skompresowanego (ADPCM) dźwięku jak i generowanie muzyki opartej na kanałach i próbkach (coś w rodzaju MIDI lub MOD). Posiada swoją pamięć do której CPU nie ma bezpośredniego dostępu. Nie jest konieczny do uruchomienia gier, w przyszłości chciałbym zaimplementować chociaż podstawową obsługę dźwięku.

Implementacja

W celu uruchomienia kodu na emulowanym systemie wiele z wyżej wymienionych układów nie wymaga całkowitej emulacji - w przypadku SPU wystarczy zasymulować poprawne zwracanie statusu, aby program nie zawieszał się w momencie komunikacji. W ten sposób potraktowałem początkowo wiele części systemu tworząc mocki, aby skupić się na implementacji CPU lub GPU.

 

Opublikowano Avocado, Daj się poznać 2017 - Avocado | Skomentuj

Integracja CI ze Slackiem

Przy procesie ciągłej integracji ważną częścią jest monitorowanie czy ostatnie zmiany nie sprawiają problemów z kompilacją i uruchamianiu na różnych platformach. Podczas developmentu na bieżąco buduję i uruchamiam program na lokalnej maszynie. Niestety to nie wyklucza możliwości, kiedy na czystym środowisku projekt nie będzie w stanie się zbudować. Tutaj z pomocą przychodzą powiadomienia - kilka minut po wprowadzaniu zmian do repozytorium programista może dostawać informację o tym, że dany commit popsuł buildy dla danej platformy.

Obydwa używane przeze mnie rozwiązania - Travis CI oraz AppVeyor - wspierają domyślnie powiadomienia przez email, ale przy okazji pisania poprzedniego wpisu chciałem wypróbować coś innego. Wiele projektów posiada własny kanał IRC - jest to dobrze miejsce, aby podpiąć powiadomienia o popsutych buildach czy oczekujących Pull Requestach. Niestety ja nie należę do pokolenia, które korzystało z IRCa i jeżeli mogę to unikam tej technologii.

Ostatnio bardzo popularnym rozwiązaniem jest Slack - dziwna hybryda czatu, IRCa i forum. Pierwszy raz zetknąłem się z nim rok temu przy okazji Daj się poznać 2016. Wtedy ktoś wpadł na pomysł żeby zebrać programistów z Polski w jednym miejscu i tak powstał Slack Devs PL (dołączyć można tutaj).

Każdy może stworzyć własnego Slacka - tak też i ja uczyniłem. Na stronie https://slack.com/create założyłem konto powiązane z projektem. Następnie po zalogowaniu dodałem kanał #build, gdzie będą lądować informacje o stanie integracji.

Konfiguracja Travis CI

Proces łączenia Travisa ze Slackiem jest bardzo prosty - aplikacji Travis CI integration pozwalamy na dostęp do naszego Slacka i wybieramy na jakim kanale ma umieszczać informacje. Automatycznie zostanie wygenerowany token, który musimy zamieścić w pliku .travis,yml. Zanim jednak to uczynimy powinniśmy go zaszyfrować.

W tym celu potrzebujemy Rubiego (na Windowsa - RubyInstaller), aby zainstalować narzędzia Travisa:

λ gem install travis -v 1.8.8 --no-rdoc --no-ri

Następnie szyfrujemy token (gotowa komenda do skopiowania znajduje się w Setup Instructions):

λ travis encrypt "avocado-emu:TOKEN"

Ostatni krok to dodanie zmian do .travis.yml - można użyć parametru --add notifications.slack, ale w moim przypadku narzędzie to przeformatowało plik .yml i paradoksalnie zepsuło buildy. Na końcu wystarczy YAMLa dopisać:

notifications:
  email: false
  slack:
    secure: ENCRYPTED_TOKEN

Wraz z wprowadzeniem zmian do repozytorium na Slacku po kilku minutach powinien pojawić się komunikat:

Travis CI successful build

Przy okazji wyłączyłem powiadomienia mailowe, które nie będą już potrzebne.

Konfiguracja AppVeyor

W tym przypadku jest trochę trudniej - nie ma gotowej aplikacji na Slacku i takową musimy utworzyć sami.

Na stronie https://api.slack.com/apps zakładamy nową aplikację - ja nazwałem ją "AppVeyor build bot" i podpiąłem ją pod Slacka projektu. Przed możliwością instalacji aplikacji musiałem dodać uprawnienia:

Slack permissions

Nie jestem pewien czy to konieczny krok, ale bez niego nie byłem w stanie wygenerować tokenu.

Teraz można wygenerować token OAuth:

OAuth token

Identycznie jak w przypadku Travisa token należy zaszyfrować. Tutaj sprawa jest prosta - token wklejamy na tej stronie: https://ci.appveyor.com/tools/encrypt.

Token encryption

Integracje można przeprowadzić w dwóch miejscach - panel AppVeyor lub plik appveyor.yml. Wybrałem drugi sposób:

notifications:
  - provider: Slack
    auth_token:
      secure: ENCRYPTED_TOKEN
    on_build_success: true
    on_build_failure: true
    on_build_status_changed: true
    channel: '#build'

Po commicie powinny pojawić się obydwie notyfikacje:

CI notifications

 

Na koniec

Korzystając z aplikacji na telefon można skonfigurować push notyfikacje. Może to być jednak irytujące kiedy wpadniemy w szał commitowania i do każdego buildu będzie pojawiać się notyfikacja. W takim przypadku można ustawić powiadomienia jedynie dla uszkodzonych buildów - jak widać na przykładzie AppVeyor włączone są wszystkie powiadomienia. Można również zdefiniować format wiadomości wysyłanej na chacie jeżeli ta domyślna nam nie odpowiada.

Dokumentacja Travis CI

Dokumentacja AppVeyor

Photo credit: Nick J Webb via Visualhunt / CC BY
Opublikowano Avocado, Daj się poznać 2017 - Avocado | Otagowano , , | 3 komentarze

Ciągła integracja projektu z użyciem Travis CI i AppVeyor

Continuous Integration jest mocno powiązany ze współczesnym podejściem do tworzenia oprogramowania. Usprawnia proces automatycznego budowania i testowania oprogramowania.

Oto jak ja rozumiem zasadę działania CI:

  • programista wprowadza zmiany w kodzie i wysyła je do głównego repozytorium (git push)
  • serwer wykrywa zmiany, pobiera kopię brancha na którym je wprowadzono
  • serwer buduje aplikacje
  • serwer uruchamia testy
  • informacja o sukcesie lub raport z błędami przesyłane są do programisty/repozytorium

Mocno uprościłem ideę działania - bardziej skomplikowane konfiguracje uruchamiają statyczną analizę kodu, testy integracyjne. Są w stanie przygotować artefakt czy nawet go wdrożyć - wszystko bez ingerencji człowieka. Te etapy wychodzą już poza granice CI i poruszają tematy związane z Continuous Delivery i Continuous Deployment - nie stosowałem ich jeszcze w tym projekcie, ale chciałbym te tematy poruszyć w przyszłości.

Jak zrealizować taki schemat? Jedną z opcji jest przygotowanie repozytorium Gita na innej maszynie i umieszczeniu w nim git hooków, które będą reagować na zmiany przesłane do tego repozytorium, a następnie wywoływać skrypty budujące projekt. To bardzo dużo pracy - wynajdujemy koło od nowa i nie każdy przepada za pisaniem skryptów bashowych. Zasadę działania git hook na przykładzie Avocado zaprezentuję w innym razem.

Zamiast tego można wykorzystać gotowe darmowe rozwiązania takie jak Jenkins, Bamboo czy TeamCity. W nich takie scenariusze można skonfigurować z poziomu wygodnego interfejsu webowego z masą opcji. Niestety nie każdy ma możliwość postawienia maszyny do obsługi takiego oprogramowania.

Istnieją na szczęście jeszcze inne rozwiązania pozbawione tej wady. Dla Avocado zastosowałem dwa - Travis CI oraz AppVeyor. Ogromnymi zaletami są:

  • darmowa usługa online dla projektów Open Source
  • integracja z GitHubem
  • prosta konfiguracja z użyciem plików .yml

Dlaczego wybrałem dwa serwisy, których możliwości pokrywają się? Travis dostarcza wirtualne środowisko oparte o Ubuntu i umożliwia budowanie projektu pod Linuksa z użyciem gcc lub clang (obsługuje też OS X, ale na ten moment nie testowałem jego obsługi). Moim głównym systemem jest jednak Windows i to właśnie jego AppVeyor (z pomocą Visual Studio) używa do kompilacji projektu. Dzięki temu rozwiązaniu testowane są automatycznie dwie platformy. Wiąże się to jednak z potrzebą utrzymania dwóch skryptów budujących projekt.

Oprócz budowania na różne platformy oba rozwiązania dostarczają build matrix, co pozwala na testowanie konfiguracji z różnymi wersjami kompilatorów (gcc, clang, VS2015, VS2017)

Travis CI

Integracja jest prosta - na stronie travis-ci.org logujemy się z użyciem GitHuba, co połączy oba serwisy. Travis odczyta listę repozytoriów i pozwoli wybrać, w których z nich ma śledzić zmiany. Następnie do repozytorium należy dodać plik .travis.yml, gdzie umieszczamy schemat według którego ma być budowany projekt (dokumentacja).

Oczywiście u mnie nie obeszło się bez problemów. Travis używa dosyć starej wersji systemu - Ubuntu 12.04 lub 14.04. Przekłada się to na stare wersje kompilatorów dostępnych w środowisku, a to rzutuje na brak możliwości korzystania z nowych standardów języka.

Poniżej krótkie wyjaśnienie konfiguracji w pliku .travis.yml:

sudo: false
language: generic

sudo: false oznacza, że korzystamy z infrastruktury opartej o kontenery. Ta opcja jest zalecana dla nowych konfiguracji, gdyż przyśpiesza czasy budowania. Ograniczenia: brak dostępu do komendy sudo, co wyklucza użycie apt-get. Dodatkowe pakiety można dostarczać w inny sposób.

language określa dla jakiego języka ma zostać przygotowane środowisko. Ja używam generic, ponieważ ręcznie dodaję kompilatory i chcę uniknąć konfliktu z tymi dostarczanymi przez Travisa.

matrix:
  include:
    - os: linux
      env: COMPILER_NAME=clang CXX=clang++-3.8 CC=clang-3.8
      addons:
        apt:
          packages:
            - clang-3.8
          sources:
            - ubuntu-toolchain-r-test
            - llvm-toolchain-precise-3.8
    - os: linux
      env: COMPILER_NAME=gcc CXX=g++-5 CC=gcc-5
      addons:
        apt:
          packages:
            - g++-5
          sources:
            - ubuntu-toolchain-r-test

Dla powyższej konfiguracji będą przygotowane dwa środowiska do buildów - Linux z kompilatorem clang 3.8 oraz Linux z kompilatorem gcc 5. Aby mieć możliwość pobrania tych pakietów wymagane jest dodanie odpowiedniego źródła apt. Te z kolei muszą zostać dodane na whitelistę przez twórców Travisa.

install:
  - export BASE=$PWD

  # Build SDL2 from externals
  - mkdir buildsdl
  - export PREFIX=$PWD/buildsdl
  - export PKG_CONFIG_PATH=$PREFIX/lib/pkgconfig
  - export PATH=$PATH:$PWD/buildsdl/bin
  - cd externals/SDL2
  - ./configure --prefix=$PREFIX
  - make -j
  - make install

  - cd $BASE
  - export PREMAKE_VERSION=5.0.0-alpha5
  # Download and build premake5 from source
  # new versions have problems with build on travis-ci enviroment
  - wget https://github.com/premake/premake-core/releases/download/v`echo $PREMAKE_VERSION`/premake-`echo $PREMAKE_VERSION`-src.zip -O premake.zip
  - unzip premake.zip
  - cd premake-`echo $PREMAKE_VERSION`/build/gmake.unix
  - make config=release
  - cd ../../..
  - mv premake-`echo $PREMAKE_VERSION`/bin/release/premake5 premake5

Ten skrypt pobiera i buduje brakujące zależności (premake5) oraz kompiluje SDL2 ze źródeł.

before_script:
  - cd $BASE
  - ./premake5 gmake --headless

Przed właściwą kompilacją generowany jest Makefile.

script:
  - make config=release -j

I ostatecznie uruchamiany jest make, który buduje projekt.

Cały plik .travis.yml można podejrzeć tutaj: https://github.com/JaCzekanski/Avocado/blob/develop/.travis.yml. Na ten moment
zakomentowana została obsługa gcc - niektóre z flag używanych do kompilacji nie są kompatybilne pomiędzy kompilatorami i build kończy się porażką.

Buildy są wyzwalane dla każdego commitu w repozytorium. Można to ograniczyć w konfiguracji projektu na stronie Travisa, aby budować przy pushach lub przy okazji pull requestów. W pliku .yml można odfiltrować które branche mają być obserwowane - domyślne obserwowane będą wszystkie.

Od teraz commity na Githubie będą oznaczane statusem buildu:

AppVeyor

Konfiguracja jest w zasadzie identyczna do tej z Travisa - integrujemy serwis appveyor.com z kontem Github, oznaczamy repozytorium i dodajemy plik appveyor.yml. Konfiguracja w tym przypadku była dużo prostsza, ponieważ aktualne wersje Visual Studio są dostępne out-of-the-box.

os:
  - Visual Studio 2015

environment:
  matrix:
  - TOOLSET: vs2015

before_build:
  - git submodule update --init --recursive

  # premake
  - ps: Start-FileDownload 'https://github.com/premake/premake-core/releases/download/v5.0.0-alpha5/premake-5.0.0-alpha5-windows.zip' 'premake.zip'
  - 7z x premake.zip

  # SDL
  - cd externals
  - rmdir /Q /S SDL2
  - ps: Start-FileDownload 'https://www.libsdl.org/release/SDL2-devel-2.0.4-VC.zip' 'SDL.zip'
  - 7z x SDL.zip 
  - ren SDL2-2.0.4 SDL2
  - cd ..
  
  # generate solution
  - premake5.exe %TOOLSET% --headless

configuration:
  - Release

build:
  project: Avocado.sln

Plik appveyor.yml dostępny tutaj: https://github.com/JaCzekanski/Avocado/blob/develop/appveyor.yml.

Przed kompilacją aktualizowane są submoduły, pobierany jest Premake5 i SDL2 (tutaj nie jest kompilowany ze źródeł) i generowana jest solucja kompatybilna z Visualem. AppVeyor już wie jak zbudować taki plik - wystarczy podać jego nazwę.

W celu przyśpieszenia buildów uruchamiana jest tylko konfiguracja Release dla VS2015.

Badges

Obydwa serwisy pozwalają na wygenerowanie obrazka mówiącego o obecnym statusie (build passing/failed). Można je dodać do pliku README.md i w łatwy sposób pokazać status CI. Dla Travisa opis znajduje się tutaj. AppVeyor w ustawieniach projektu posiada zakładkę Badges, gdzie można skopiować gotowy kod markdown.

Na koniec

Pliki .yml to świetny sposób, żeby dowiedzieć się w jaki sposób zbudować dany projekt - są one aktualizowane na bieżąco przez programistów (aby CI działało poprawnie), natomiast sekcje How to build w dokumentacji nie zawsze poruszają wszystkie problemy lub posiadają braki. Ja sam posiłkowałem się plikami z innych projektów przy konfiguracji Travisa i trafiałem na działające rozwiązania w przeciwieństwie do sprzecznych lub nieaktualnych informacji zawartych na forach i blogach.

Opublikowano Avocado, Daj się poznać 2017 - Avocado | Otagowano , | Skomentuj

Git, aliasy i AutoHotkey

Git jest obecnie najczęściej używanym przeze mnie narzędziem w pracy. Poza wbudowanym klientem w IDE oraz SourceTree staram się jak najwięcej rzeczy robić z poziomu terminala - takie operacje są dużo szybsze (SourceTree na Windowsie z dużym projektem nie działa najlepiej), a do tego ma się pełną kontrolę nad tym co się dzieje.

Konfiguracja

Konfiguracja parametrów gita odbywa się poprzez użycie git config:

λ git config --global param value

Zamiast --global można użyć flagi --local - w takim przypadku edytowane będą opcje dla obecnego repozytorium, a nie dla całego systemu.

Możliwa jest również bezpośrednia edycja pliku .gitconfig - pod Windowsem znajduje się on w katalogu %UserProfile%. Najprościej użyć flagi --edit:

λ git config --global --edit

Aliasy

Aliasy pozwalają na zastąpienie długich czy często powtarzanych komend krótszymi - zasada działania jest identyczna do tego co oferuje bash. Aby stworzyć alias (przykładowo git s, który będzie tym samym co git status) ogranicza się do wpisania:

λ git config --global alias.s status

Aliasy, których używam najczęściej to:

  • git lg - podgląd ostatnich commitów w formie drzewa z jednolinowymi wpisami
λ git config --global alias.lg log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)&amp;amp;amp;lt;%an&amp;amp;amp;gt;%Creset' --abbrev-commit

  • git serve - uruchomienie serwera umożliwiającego na dostęp do lokalnej kopii repozytorium z innej maszyny
λ git config --global alias.serve daemon --verbose --export-all --base-path=.git --reuseaddr --strict-paths --export-all .git/
λ git serve
[7328] Ready to rumble
λ git remote add pc git://192.168.1.2/
λ git checkout -t pc/feature/test-branch

AutoHotkey

If used with software that could keep up, a scroll wheel mapped to send a stream of 'undo' and 'redo' events could be kind of cool.

Do często powtarzanych fraz używam AutoHotkey, który umożliwia mi rozwinięcie danego tekstu na całą komendę. Rozwiązania tego używałem od dawna, przykładowo do wpisywania numeru konta bankowego (po wpisaniu frazy <mill>). Zastosowanie AHK do Gita podpatrzyłem u Macieja. Zaletą nad aliasami jest to, że przed wykonaniem komendy mogę ją zweryfikować lub zedytować. Dodatkowo działa niezależnie od .gitconfigu, co przydaje się przy pracy przez ssh.

::gts::git status
::gtl::git lg
::gta::git commit --amend --no-edit -a
::gtp::git push
::gtpf::git push --force-with-lease
::gtr::git checkout --
::gtrhu::git fetch && git reset --hard @{{}upstream{}}
::gtpr::git pull --rebase
::gtmp::git merge --no-ff -

Komendy git status czy git push ograniczają się teraz do wpisania gts i gtp.
Oprócz tego dodałem też takie komendy jak gta (doklejenie obecnych zmian do ostatniego commita) czy gtrhu (reset brancha do wersji remote), które łącznie z git serve pozwalają mi na szybkie robienie zmian na jednym komputerze, a testowanie ich na innym.
gtpf czy gtpr wytłumaczę innym razem przy okazji omawiania git rebase.

Opublikowano Daj się poznać 2017 - Avocado | Otagowano , | 1 komentarz

Daj się poznać 2017

Daj Się Poznać 2017 logo

Minął okrągły rok, Daj się poznać 2016 zakończyło się ogromnym sukcesem - 70 osób z 296 zarejestrowanych dotrwało do końca. To ogromna liczba biorąc pod uwagę pracę jaka jest potrzebna. Dwa posty tygodniowo mogą wydawać się niczym trudnym, ale mnie to zadanie przerosło po zaledwie 3 tygodniach.

Kolejna próba

W tym roku próbuję ponownie - ten sam projekt (Avocado, emulator PlayStation 1), ten sam cel (możliwość uruchomienia komercyjnej gry), ale motywacja dużo większa - teraz ten projekt jest tematem mojej pracy inżynierskiej. Nie liczę na wygraną - na ten moment 787 osób ukończyło rejestrację. Tygodniowo będziemy dostawać ponad 1500 postów. Śledzenie takiej ilości projektów jest niemożliwe. Mam nadzieję, że lekko poluzowany regulamin konkursu (2 posty tygodniowo, ale 1 ściśle powiązany z projektem) pozwoli na poświęcenie większej ilości czasu na programowanie, aby pokazać wyniki swojej pracy.

Na koniec chciałem podlinkować jeden projekt:

Artemis Entity Tracker

https://www.namekdev.net/ Twitter: @NamekDev

Kamila poznałem na Programistoku 2016. Też brał udział rok temu, tworzył nietypowy projekt (skryptowalna w JavaScripcie konsola, podobna do tej z Quake 3), był na gali finałowej. Swoim doświadczeniem podzielił się na blogu oraz spotkaniu lokalnej grupie programistów w Rzeszowie (rgdev#2, slajdy). Zainspirował mnie, żeby spróbować ponownie. W tym roku kontynuuje pracę nad jeszcze bardziej nietypowym projektem - będzie Java, low level JVM, pewnie jakieś ciekawe optymalizacje.

Opublikowano Blog, Daj się poznać 2017 - Avocado | Otagowano | 2 komentarze

Integracja z GDB

header-regs

Debugger jest nieocenioną pomocą przy takich programach jak emulator, ponieważ zachodzi potrzeba analizy działania zarówno naszej aplikacji jak i programu będącego uruchomionego w wirtualnym środowisku. W tym poście poruszę jedynie drugi przypadek.

Gdy zaczynałem implementować procesor jedyną opcją było logowanie każdej wykonywanej instrukcji na standardowe wyjście:

Trace

Trace

Takie rozwiązanie umożliwiało szybkie sprawdzenie czy instrukcje są poprawnie wykonywane i jak wygląda ścieżka wykonywania programu (porównując ją z wersją rozkompilowaną). Implementacja jest bardzo prosta - do każdej instrukcji jest doklejone makro, które używając sprintf-a formatuje daną instrukcję, a przy jej wykonaniu jest ona wyświetlana.

Rozwiązanie niestety nie sprawdza się gdy zachodzi potrzeba zatrzymania programu w danym miejscu. Można to rozwiązać wstawiając breakpoint z odpowiednim warunkiem w kodzie emulatora przed wykonaniem instrukcji, ale to spowalnia wykonywanie. Inny problem występuje gdy wykonywana jest pętla. Do terminala wyrzucane są tysiące powtarzających się instrukcji, które niewiele mówią o przebiegu i stanie programu.

Sensowną alternatywą wydaje się napisanie dekompilatora z możliwością debugowania. Ta funkcjonalność na pewno będzie zaimplementowana prędzej czy później, ale póki co wolę wybrać gotowe rozwiązanie.

GDB

GDB w pełnej okazałości

GDB w pełnej okazałości

GDB to debugger dostępny razem z pakietem GCC. Oznacza to że jest on dostępny na większość systemów operacyjnych i obsługuje wiele architektur (w tym MIPS). Jedną z interesujących opcji GDB jest działanie w trybie remote. Pozwala on na debugowanie programu w sytuacji, gdzie urządzenie nie jest w stanie zapewnić środowiska do działania GDB - przykłady to debugowanie jądra systemu (kgdb), debugowanie kodu na platformach embedded czy innych gdzie nie ma systemu operacyjnego. Zasadza działania tego trybu jest prosta - komputer łączymy z urządzeniem docelowym z użyciem portu szeregowego, a do wykonywanego programu dopisujemy funkcję obsługującą komunikację tzw. gdb stub.

Stub reaguje na kilka podstawowych komend - zrzuć i zapisz wartości rejestrów, odczytaj lub zapisz pamięć, kontynuuj wykonywanie. Komend są dziesiątki, ale wystarczy implementacja tylko 6 podstawowych, aby gdb zaczęło współpracować. Dzięki temu rozwiązaniu cała praca związana z dekompilacją kodu, podglądem pamięci i sterowania kodem wykonywana jest na komputerze.

Takie rozwiązanie implementują inne emulatory - QEMU umożliwia podpięcie do gdb i debugowanie programu działającego w maszynie.

Komunikacja

Otworzyłem dokumentację GDB, zobaczyłem przykładową implementację w kernelu i wziąłem się do roboty - zacząłem pisać przykładową implementację stub-a, aby poznać zasadę działania. Żeby nie zajmować się obsługą gniazd wykorzystałem pewną sztuczkę - mój program komunikuje się z użyciem standardowego wejścia i wyjścia, a ono przekazywane jest do GDB z użyciem komenty netcat (dodam, że pracowałem na Linuksie).

Wcześniej wspomniałem o użyciu portu szeregowego do komunikacji. Oprócz niego obsługiwane są zwykłe sockety, co pozwala na debugowanie kodu przez sieć.

netcat jest prostym narzędziem (na Windowsa dostępny jest ncat) umożliwiającym uruchomienie serwera lub klienta TCP, który będzie przekazywał dane z użyciem gniazd sieciowych.  W moim przypadku nasłuchiwał on na porcie 1234, a dane były przekazywane do mojego programu:

$ nc -l -p 1234 -e gdb_stub

#  (l - listen, p - port, e - exec)

Ważna uwaga - nc będzie wysyłał dane gdy zostanie naciśnięty enter. Można wymusić wysyłanie co znak używają fflush przy każdym putchar.

Aby debugować kod z innej architektury niż ta na której uruchamiany jest gdb potrzebujemy odpowiedniej wersji. Na Debianie można to wykonując:

$ sudo apt-get install gdb-multiarch

Następnie uruchamiany gdb-multiarch, wydajemy polecenie set arch mips:3000 oraz target remote :1234, które ustawią architekturę na MIPS i spróbują nawiązać połączenie na porcie 1234. Jeżeli uruchomimy nc bez parametru -e w konsoli będą widoczne ciągi znaków podobne do tych:

$g#67

Jest to pakiet od GDB proszący o przesłanie stanu rejestrów. Każdy pakiet trzyma się tej samej struktury

  • $ - początek pakietu
  • pojedyńczy znak komendy
  • ciąg znaków będącymi dodatkowymi parametrami (tutaj brak)
  • # - koniec pakietu
  • 2 bajty będące sumą kontrolną

GDB oczekuje odpowiedzi:

  • '+' lub '-' oznaczające kolejno poprawna ramka, nie poprawna ramka (prośba o retransmisje)
  • odpowiedź o strukturze jak powyżej

Implementacja

Budowa pakietu bardzo upraszcza napisanie funkcji obsługującej jej. Ja zastosowałem prostą maszynę stanów:

enum PacketState {
	waitingForStart = 0,
	reading,
	waitingForChecksum1,
	waitingForChecksum2
};

Na początku program czeka na rozpoczęcie pakietu

while (1) {
	c = getDebugChar();

	switch (state) {
	case waitingForStart:
		if (c == '$') state = reading;
		break;

Gdy natrafi na znak $ czyta komendę do bufora aż do napotkania znaku #. Dodatkowo liczona jest suma kontrolna, która polega na sumowaniu wszystkich bajtów pomiędzy $ i #

	case reading:
		if (c == '#') {
			state = waitingForChecksum1;
			break;
		}
		buffer[pos++] = c;
		calculatedChecksum += c;
		break;

Dwa kolejne stany to pobranie pierwszej i drugiej liczby sumy kontrolnej, weryfikacja i wysłanie '-' lub '+' w zależności od stanu.

	case waitingForChecksum1:
		checksum = toInt(c) >> 4;
		state = waitingForChecksum2;
		break;

	case waitingForChecksum2:
		checksum |= toInt(c);

		if (calculatedChecksum != checksum) {
			appendToBuffer('-');
			return false;
		}
		appendToBuffer('+');
		return true;

Kolejnym krokiem jest rozróżnienie komendy. Do prawidłowego działania GDB oczekuje implementacji 6 pakietów:  ‘g’ i ‘G’ - odczyt i modyfikacja rejestrów, ‘m’ and ‘M’ - odczyt i zapis pamięci, ‘c’ - kontynuuj wykonywanie i ‘s’ - wykonywanie krokowe (dokumentacja).

Omówię tylko jedną z nich - 'g'. Debugger oczekuje zrzutu wszystkich rejestrów procesora - 32 rejestrów ogólnych, 6 rejestrów kontrolnych (sr, lo, hi, bad, cause, pc) oraz 35 rejestrów FPU (niedostępne na PS1). Wartości wysyłane są w little endian w formacie hex (wartość 0xBFC00000 będzie wysłana jako 00 00 c0 bf). Rejestry niedostępne oznaczane są jako (xx xx xx xx).

Po kilku godzinnej walce (głupie błędy typu brak fflush, zła ilość wysyłanych rejestrów, zły format pakietu) i implementacji reszty komend GDB myślał, że udało mu podłączyć się z procesorem działającym na architekturze MIPS.

GDB rozmawia z "procesorem"

GDB rozmawia z "procesorem"

Komunikacja po sieci

Rozwiązanie z nc bardzo przypadło mi do gustu, ale nie zadziała ono gdy dodam stub do emulatora. Aby zaprogramować obsługę gniazd miałem kilka opcji:

  • standardowe Berkeley sockets
  • ASIO z Boost
  • SDL

Nie wybrałem czystych socketów - wymagają dużej ilości kodu i nie są do końca wieloplatformowe. ASIO ma wielkie możliwości, ale to inny kaliber i nie chciałem dodawać Boosta tylko dla tej funkcjonalności. Wybrałem SDL_net, który jest tak naprawdę nakładką na sockety. Nasłuchiwanie i obsługa połączenia to kilka linii kodu + obsługa błędów.

Dzięki temu mogę uruchomić emulator ze stubem na komputerze lokalnym, a debugger na zdalnej maszynie (nie znalazłem gdb-multiarch na Windowsa) i tunelować ruch po ssh.

Poza GDB są jeszcze inne rozwiązania - IDA wspiera debugowanie z użyciem tego samego protokołu, w dodatku oferując bardzo przyjazny interfejs. Radare2 (darmowa alternatywa) podobno też wspiera ten protokół, ale nie byłem w stanie zmusić go do współpracy.

Opublikowano Avocado | Skomentuj