Debugowanie WebAssembly za pomocą nowoczesnych narzędzi

Ingvar Stepanyan
Ingvar Stepanyan

Do tej pory

Rok temu Chrome ogłosił wprowadzenie obsługi natywnego debugowania WebAssembly w Narzędziach deweloperskich w Chrome.

Przedstawiliśmy podstawowe wsparcie dotyczące stopniowego wdrażania i omówiliśmy możliwości, jakie daje nam w przyszłości korzystanie z informacji DWARF zamiast map źródłowych:

  • Rozwiązywanie nazw zmiennych
  • Typy formatowania stylistycznego
  • Ocenianie wyrażeń w językach źródłowych
  • ...i wiele więcej!

Dzisiaj z dużą przyjemnością prezentujemy obiecane funkcje, które są już dostępne, oraz postępy, jakie w tym roku poczyniły zespoły Emscripten i Chrome DevTools, zwłaszcza w przypadku aplikacji w językach C i C++.

Zanim zaczniemy, pamiętaj, że to wciąż wersja beta nowej funkcji. Musisz używać najnowszej wersji wszystkich narzędzi na własne ryzyko. Jeśli napotkasz jakiekolwiek problemy, zgłoś je na stronie https://issues.chromium.org/issues/new?noWizard=true&template=0&component=1456350.

Zacznijmy od tego samego prostego przykładu w języku C, co ostatnio:

#include <stdlib.h>

void assert_less(int x, int y) {
  if (x >= y) {
    abort();
  }
}

int main() {
  assert_less(10, 20);
  assert_less(30, 20);
}

Do skompilowania kodu używamy najnowszej wersji Emscripten i przekazujemy flagę -g, tak jak w pierwotnym poście, aby uwzględnić informacje debugowania:

emcc -g temp.c -o temp.html

Teraz możemy wyświetlić wygenerowaną stronę z serwera HTTP hosta lokalnego (na przykład za pomocą polecenia serve) i otworzyć ją w najnowszej wersji Chrome Canary.

Tym razem potrzebujemy też pomocnego rozszerzenia, które zintegruje się z Chrome DevTools i pomoże w zrozumieniu wszystkich informacji debugowania zakodowanych w pliku WebAssembly. Aby zainstalować rozszerzenie, kliknij ten link: goo.gle/wasm-debugging-extension

W DevTools możesz też włączyć debugowanie WebAssembly w sekcji Eksperymenty. Otwórz Narzędzia deweloperskie w Chrome, w prawym górnym rogu panelu Narzędzia deweloperskie kliknij ikonę koła zębatego (), przejdź do panelu Eksperymenty i zaznacz Debugowanie WebAssembly: włącz obsługę DWARF.

Okienko Eksperymenty w ustawieniach Narzędzi deweloperskich

Gdy zamkniesz Ustawienia, Narzędzia deweloperskie zaproponują ponowne załadowanie, aby zastosować ustawienia. Zrób to. To wszystko, jeśli chodzi o jednorazową konfigurację.

Teraz możesz wrócić do panelu Źródła, włączyć opcję Wstrzymaj w przypadku wyjątków (ikona ⏸), a potem zaznaczyć opcję Wstrzymaj w przypadku wykrytych wyjątków i ponownie załadować stronę. Narzędzia deweloperskie powinny zostać wstrzymane przy wyjątku:

Zrzut ekranu z panelem Źródła pokazujący, jak włączyć opcję „Wstrzymywanie przy wykrytych wyjątkach”

Domyślnie zatrzymuje się na kodzie łączącym wygenerowanym przez Emscripten, ale po prawej stronie możesz zobaczyć widok zbiór wywołań przedstawiający ścieżkę błędu. Możesz też przejść do oryginalnej linii kodu C, która wywołała tę funkcję:abort

Narzędzia deweloperskie zostały wstrzymane w funkcji `assert_less` i wyświetlają wartości „x” i „y” w widoku zakresu

Teraz w widoku Zakres widać oryginalne nazwy i wartości zmiennych w kodzie w C/C++, dzięki czemu nie trzeba już zgadywać, co oznaczają zniekształcone nazwy, takie jak $localN, i jaki mają związek z napisanym przez Ciebie kodem źródłowym.

Dotyczy to nie tylko wartości prymitywnych, takich jak liczby całkowite, ale też typów złożonych, takich jak struktury, klasy, tablice itp.

Obsługa typu rozszerzonego

Aby to zilustrować, przyjrzyjmy się bardziej skomplikowanemu przykładowi. Tym razem narysujemy fraktal Mandelbrota z tym kodem w C++:

#include <SDL2/SDL.h>
#include <complex>

int main() {
  // Init SDL.
  int width = 600, height = 600;
  SDL_Init(SDL_INIT_VIDEO);
  SDL_Window* window;
  SDL_Renderer* renderer;
  SDL_CreateWindowAndRenderer(width, height, SDL_WINDOW_OPENGL, &window,
                              &renderer);

  // Generate a palette with random colors.
  enum { MAX_ITER_COUNT = 256 };
  SDL_Color palette[MAX_ITER_COUNT];
  srand(time(0));
  for (int i = 0; i < MAX_ITER_COUNT; ++i) {
    palette[i] = {
        .r = (uint8_t)rand(),
        .g = (uint8_t)rand(),
        .b = (uint8_t)rand(),
        .a = 255,
    };
  }

  // Calculate and draw the Mandelbrot set.
  std::complex<double> center(0.5, 0.5);
  double scale = 4.0;
  for (int y = 0; y < height; y++) {
    for (int x = 0; x < width; x++) {
      std::complex<double> point((double)x / width, (double)y / height);
      std::complex<double> c = (point - center) * scale;
      std::complex<double> z(0, 0);
      int i = 0;
      for (; i < MAX_ITER_COUNT - 1; i++) {
        z = z * z + c;
        if (abs(z) > 2.0)
          break;
      }
      SDL_Color color = palette[i];
      SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a);
      SDL_RenderDrawPoint(renderer, x, y);
    }
  }

  // Render everything we've drawn to the canvas.
  SDL_RenderPresent(renderer);

  // SDL_Quit();
}

Jak widać, ta aplikacja jest nadal dość mała – to pojedynczy plik zawierający 50 wierszy kodu, ale tym razem używam też zewnętrznych interfejsów API, na przykład biblioteki SDL do obsługi grafiki, a także liczb złożonych ze standardowej biblioteki C++.

Skompiluję go z tą samą flagą -g, aby zawierał informacje debugowania, a także poproszę Emscripten o udostępnienie biblioteki SDL2 i zezwolenie na pamięć o dowolnym rozmiarze:

emcc -g mandelbrot.cc -o mandelbrot.html \
     -s USE_SDL=2 \
     -s ALLOW_MEMORY_GROWTH=1

Gdy wyświetlę wygenerowaną stronę w przeglądarce, widzę ten piękny ułamkowy kształt w losowych kolorach:

Strona demonstracyjna

Gdy otwieram ponownie Narzędzia deweloperskie, widzę pierwotny plik C++. Tym razem jednak nie ma błędu w kodzie (uff!), więc ustaw kilka punktów przerwania na początku kodu.

Gdy ponownie załadujemy stronę, debugger zatrzyma się w źródle kodu C++:

Narzędzia deweloperskie zostały wstrzymane na wywołaniu SDL_Init

Wszystkie zmienne są już widoczne po prawej stronie, ale obecnie inicjowane są tylko width i height, więc nie ma zbyt wiele do sprawdzenia.

Ustawmy kolejny punkt przerwania w głównej pętli Mandelbrota i wróćmy do wykonywania programu, aby przesunąć go trochę do przodu.

Narzędzia deweloperskie zostały wstrzymane wewnątrz zagnieżdżonych pętli

W tym momencie nasza tablica palette jest wypełniona losowymi kolorami. Możemy rozwinąć zarówno tablicę, jak i poszczególne struktury SDL_Color, i sprawdzać ich komponenty, aby upewnić się, że wszystko wygląda dobrze (np. czy kanał „alpha” jest zawsze ustawiony na pełną przezroczystość). W podobny sposób możemy rozwijać i sprawdzać rzeczywiste i urojone części liczby zespolonej zapisane w zmiennej center.

Jeśli chcesz uzyskać dostęp do głęboko zagnieżdżonej usługi, do której trudno się dostać w widoku Zakres, możesz też użyć oceny w Konsoli. Pamiętaj jednak, że bardziej złożone wyrażenia C++ nie są jeszcze obsługiwane.

Panel konsoli z wynikiem funkcji palette[10].r

Odłóżmy na chwilę wykonywanie programu i zobaczmy, jak zmienia się wewnętrzna zmienna x. Możemy to zrobić, ponownie otwierając widok Zakres, dodając nazwę zmiennej do listy obserwowanych zmiennych, oceniając ją w konsoli lub najeżdżając na nią kursorem w źródle kodu:

Etykietka nad zmienną „x” w źródle z jej wartością „3”

Możemy tu wejść do instrukcji C++ lub je pominąć i obserwować, jak zmieniają się inne zmienne:

Wskaźniki i widok zakresu pokazujące wartości „color”, „point” i innych zmiennych

Wszystko działa świetnie, gdy dostępne są informacje debugowania, ale co, jeśli chcemy debugować kod, który nie został skompilowany z opcjami debugowania?

Debugowanie nieprzetworzonego WebAssembly

Na przykład poprosiliśmy Emscripten o udostępnienie gotowej biblioteki SDL, zamiast samodzielnego kompilowania jej z źródła, ponieważ – przynajmniej na razie – nie ma możliwości znalezienia powiązanych źródeł przez debuger. Aby dowiedzieć się więcej o SDL_RenderDrawColor, zapoznaj się z tymi informacjami:

Narzędzia deweloperskie przedstawiające widok rozkładu pliku „mandelbrot.wasm”

Wracamy do debugowania w czystym środowisku WebAssembly.

Wydaje się to nieco straszne i większość programistów stron internetowych nie będzie musiała się z tym mierzyć, ale czasami może się przydać debugowanie biblioteki zbudowanej bez informacji na potrzeby debugowania – na przykład dlatego, że jest to 3biblioteka 3zespołu, nad którą nie masz kontroli, albo że występuje jeden z błędów występujących tylko w środowisku produkcyjnym.

W takich przypadkach ulepszyliśmy podstawowe funkcje debugowania.

Jeśli wcześniej korzystałeś(-aś) z debugowania w czystym formacie WebAssembly, możesz zauważyć, że cały deasembler jest teraz wyświetlany w jednym pliku – nie musisz już zgadywać, do której funkcji może pasować wpis wasm-53834e3e/ wasm-53834e3e-7 w sekcji Źródła.

Nowy schemat generowania nazw

Poprawiliśmy też nazwy w widoku rozłożenia. Wcześniej były widoczne tylko indeksy liczbowe, a w przypadku funkcji w ogóle nie było nazwy.

Teraz generujemy nazwy podobnie jak w innych narzędziach do demontażu, korzystając ze wskazówek w sekcji nazw WebAssembly, ścieżek importowania/eksportowania, a jeśli wszystko się nie udało, generując je na podstawie typu i indeksu elementu, takiego jak $func123. Na powyższym zrzucie ekranu widać, że dzięki temu informacje o wykonaniu kodu i rozbieżność są nieco czytelniejsze.

Gdy nie ma dostępnych informacji o typie, może być trudno sprawdzić wartości inne niż prymitywne. Na przykład wskaźniki będą wyświetlane jako zwykłe liczby całkowite, a nie będzie możliwości sprawdzenia, co jest przechowywane w pamięci.

Sprawdzanie pamięci

Wcześniej można było rozwinąć tylko obiekt pamięci WebAssembly reprezentowany przez env.memory w widoku Zakres, aby wyszukać poszczególne bajty. Takie rozwiązanie sprawdzało się w niektórych prostych scenariuszach, ale nie było szczególnie wygodne w rozwijaniu i nie umożliwiało reinterpretacji danych w formatach innych niż wartości bajtów. Dodaliśmy też nową funkcję, która pomoże Ci w tym zakresie: inspektor pamięci liniowej.

Jeśli klikniesz env.memory prawym przyciskiem myszy, zobaczysz nową opcję Sprawdź pamięć:

Menu kontekstowe „env.memory” w panelu Zakres, w którym widoczny jest element „Sprawdź pamięć”

Po kliknięciu tego przycisku otworzy się kontroler pamięci, w którym możesz sprawdzić pamięć WebAssembly w widoku szesnastkowym i ASCII, przejść do określonych adresów oraz interpretować dane w różnych formatach:

Panel narzędzia do inspekcji pamięci w Narzędziach deweloperskich z widokami pamięci w systemie heksadecymalnym i ASCII

Zaawansowane scenariusze i ostrzeżenia

Profilowanie kodu WebAssembly

Gdy otworzysz Narzędzia deweloperskie, kod WebAssembly zostanie „zmniejszony” do wersji nieoptymalizowanej, aby umożliwić debugowanie. Ta wersja jest znacznie wolniejsza, co oznacza, że nie możesz polegać na console.time, performance.nowi innych metodach pomiaru szybkości kodu, gdy DevTools są otwarte, ponieważ uzyskane liczby w żaden sposób nie odzwierciedlają rzeczywistej wydajności.

Zamiast tego użyj panelu wydajności w DevTools, który uruchomi kod z pełną prędkością i zapewni szczegółowy podział czasu spędzonego na różnych funkcjach:

Panel profilowania pokazujący różne funkcje Wasm

Możesz też uruchomić aplikację z zamkniętymi narzędziami deweloperskimi i otworzyć je po zakończeniu, aby sprawdzić konsolę.

W przyszłości będziemy ulepszać scenariusze profilowania, ale na razie jest to zastrzeżenie. Jeśli chcesz dowiedzieć się więcej o scenariuszach warstwowania WebAssembly, zapoznaj się z dokumentacją na temat przepływu kompilacji WebAssembly.

kompilowanie i debugowanie na różnych komputerach (w tym Dockerze / hostze).

Podczas kompilowania w Dockerze, maszynie wirtualnej lub na zdalnym serwerze kompilacji możesz napotkać sytuacje, w których ścieżki do plików źródłowych użytych podczas kompilacji nie będą pasować do ścieżek w Twoim własnym systemie plików, w którym działają narzędzia programistyczne Chrome. W takim przypadku pliki będą widoczne w panelu Źródła, ale nie uda się ich załadować.

Aby rozwiązać ten problem, wprowadziliśmy funkcję mapowania ścieżek w opcjach rozszerzenia C/C++. Możesz go użyć do ponownego mapowania dowolnych ścieżek i pomóc Narzędziom deweloperskim w lokalizowaniu źródeł.

Jeśli np. projekt na maszynie hosta znajduje się pod ścieżką C:\src\my_project, ale został skompilowany w kontenerze Dockera, w którym ta ścieżka jest reprezentowana jako /mnt/c/src/my_project, możesz ją ponownie przypisać podczas debugowania, podając te ścieżki jako prefiksy:

Strona opcji rozszerzenia do debugowania kodu C/C++

Wygrywa pierwszy pasujący prefiks. Jeśli znasz inne debugery C++, ta opcja jest podobna do polecenia set substitute-path w GDB lub ustawienia target.source-map w LLDB.

Debugowanie zoptymalizowanych kompilacji

Podobnie jak w przypadku innych języków, debugowanie działa najlepiej, gdy optymalizacje są wyłączone. Optymalizacje mogą wstawiać funkcje w inne, zmieniać kolejność kodu lub usuwać fragmenty kodu. Wszystko to może dezorientować debuger, a w konsekwencji także Ciebie jako użytkownika.

Jeśli nie przeszkadza Ci ograniczona funkcjonalność debugowania i nadal chcesz debugować zoptymalizowaną wersję, większość optymalizacji będzie działać zgodnie z oczekiwaniami (z wyjątkiem wstawiania funkcji). Pozostałe problemy planujemy rozwiązać w przyszłości, ale na razie zalecamy użycie opcji -fno-inline, aby wyłączyć kompilację z jakąkolwiek optymalizacją na poziomie -O, np.:

emcc -g temp.c -o temp.html \
     -O3 -fno-inline

Oddzielanie danych debugowania

Informacje debugowania zawierają wiele szczegółów dotyczących kodu, zdefiniowanych typów, zmiennych, funkcji, zakresów i miejsc – wszystkiego, co może być przydatne dla debugera. W rezultacie może ona być często większa niż sam kod.

Aby przyspieszyć wczytywanie i kompilowanie modułu WebAssembly, możesz rozdzielić te informacje debugowania na osobny plik WebAssembly. Aby to zrobić w Emscripten, przekaż parametr -gseparate-dwarf=… z wybraną nazwą pliku:

emcc -g temp.c -o temp.html \
     -gseparate-dwarf=temp.debug.wasm

W takim przypadku aplikacja główna będzie przechowywać tylko nazwę pliku temp.debug.wasm, a rozszerzenie pomocnicze będzie mogło ją zlokalizować i wczytać po otwarciu DevTools.

W połączeniu z optymalizacjami opisanymi powyżej można używać tej funkcji nawet do wysyłania niemal zoptymalizowanych kompilacji produkcyjnych aplikacji, a później debugować je za pomocą lokalnego pliku bocznego. W takim przypadku trzeba też zastąpić zapisany adres URL, aby rozszerzenie mogło znaleźć plik boczny, na przykład:

emcc -g temp.c -o temp.html \
     -O3 -fno-inline \
     -gseparate-dwarf=temp.debug.wasm \
     -s SEPARATE_DWARF_URL=file://[local path to temp.debug.wasm]

Aby kontynuować...

To było sporo nowych funkcji.

Dzięki tym wszystkim nowym integracjom Narzędzia deweloperskie w Chrome stają się praktycznym, potężnym debugerem nie tylko dla kodu JavaScript, ale też dla aplikacji w językach C i C++, co ułatwia tworzenie aplikacji w różnych technologiach i przenoszenie ich do wspólnej, wieloplatformowej sieci.

Nasza podróż jednak się jeszcze nie skończyła. Oto kilka kwestii, nad którymi będziemy pracować w przyszłości:

  • Pozbywanie się niedoskonałości interfejsu debugowania.
  • Dodano obsługę niestandardowych programów formatujących.
  • Pracujemy nad ulepszeniami profilowania aplikacji WebAssembly.
  • Dodawanie obsługi zasięgu kodu, aby ułatwić znajdowanie nieużywanego kodu.
  • Poprawiono obsługę wyrażeń podczas oceny konsoli.
  • Dodaliśmy obsługę kolejnych języków.
  • …i nie tylko

Tymczasem pomóż nam, testując najnowszą wersję beta na swoim kodzie i zgłaszając znalezione problemy na stronie https://issues.chromium.org/issues/new?noWizard=true&template=0&component=1456350.

Pobieranie kanałów podglądu

Rozważ użycie przeglądarki Chrome Canary, Dev lub Beta jako domyślnej przeglądarki deweloperskiej. Te kanały wersji wstępnej zapewniają dostęp do najnowszych funkcji DevTools, umożliwiają testowanie najnowocześniejszych interfejsów API platformy internetowej i pomagają znaleźć problemy w witrynie, zanim zrobią to użytkownicy.

Kontakt z zespołem ds. Narzędzi deweloperskich w Chrome

Aby omówić nowe funkcje, aktualizacje lub inne kwestie związane z Narzędziami deweloperskimi, skorzystaj z tych opcji.