Debugowanie WebAssembly za pomocą nowoczesnych narzędzi

Ingvar Stepanyan
Ingvar Stepanyan

Droga do tej pory

Rok temu w Chrome ogłosiliśmy początkową obsługę natywnego debugowania WebAssembly w narzędziach deweloperskich Chrome.

Zademonstrowaliśmy podstawowe wsparcie i rozmawialiśmy o możliwościach użycie informacji DWARF zamiast mapy źródłowe otworzy się w przyszłości:

  • Rozpoznawanie nazw zmiennych
  • Rodzaje druku estetycznego
  • Ocenianie wyrażeń w językach źródłowych
  • …i wiele innych.

Z przyjemnością przedstawiamy, w jaki sposób weszły w życie obiecane funkcje oraz postęp, jaki zespoły Emscripten i Chrome Dev Tools osiągnęły w tym roku, zwłaszcza w przypadku aplikacji w języku 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 jakieś problemy, zgłaszaj 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 używamy najnowszej wersji Emscripten i przekazać flagę -g, tak jak w oryginalnym poście, aby uwzględnić debugowanie informacje:

emcc -g temp.c -o temp.html

Teraz możemy wyświetlić stronę wygenerowaną z serwera HTTP hosta lokalnego (dla np. obsługa), otwórz 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 ją zainstalować, kliknij ten link: goo.gle/wasm-debugging-extension

Warto też włączyć debugowanie WebAssembly w Narzędziach deweloperskich. 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.

Panel Eksperymenty w ustawieniach Narzędzi deweloperskich

Gdy zamkniesz Ustawienia, Narzędzia deweloperskie zasugerują ponowne załadowanie strony. aby zastosować ustawienia, więc zróbmy to tak. 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 odświeżyć stronę. Powinny się wyświetlić Narzędzia deweloperskie wstrzymane przy wyjątku:

Zrzut ekranu panelu Źródła pokazujące, jak włączyć opcję „Wstrzymaj 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 błąd:abort

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

Teraz, jeśli spojrzysz w widoku Zakres, zobaczysz oryginalne nazwy i wartości zmiennych w kodzie C/C++, dzięki czemu nie musisz już domyślać się, co oznaczają zmienione nazwy, takie jak $localN, i jak się one odnoszą do napisanego przez Ciebie kodu źródłowego.

Dotyczy to nie tylko wartości podstawowych, takich jak liczby całkowite, np. struktury, klasy, tablice itp.

Obsługa typu rozszerzonego

Aby to zilustrować, przyjrzyjmy się bardziej skomplikowanemu przykładowi. Ten narysujemy fraktal Mandelbrota z ten kod 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ć, aplikacja jest nadal dość mała – to pojedynczy plik zawierający 50 wierszy kodu. Tym razem używam też niektórych zewnętrznych interfejsów API, takich jak bibliotek SDL do grafiki czy liczbami zespolonymi z biblioteki standardowej 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 wchodzę na wygenerowaną stronę w przeglądarce, widzę fraktalny kształt o losowych kolorach:

Strona demonstracyjna

Gdy otwieram Narzędzie deweloperskie, widzę pierwotny plik C++. Ten ale w kodzie nie ma błędu (ale!), więc na początku kodu.

Po ponownym załadowaniu strony debuger zatrzyma się Kod źródłowy w języku C++:

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

Wszystkie zmienne są już widoczne po prawej stronie, ale w tej chwili zainicjowane są tylko zmienne widthheight, więc nie ma zbyt wiele do sprawdzenia.

Ustawmy kolejny punkt przerwania w głównej pętli Mandelbrota i wznowimy aby przewinąć film 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, aby sprawdzić ich komponenty i upewnić się, że wszystko wygląda dobrze (np. czy kanał „alpha” jest zawsze ustawiony na pełną przezroczystość). Podobnie możemy rozwinąć i sprawdzać części rzeczywiste i urojone liczby zespolonej przechowywanej w zmiennej center.

Jeśli chcesz uzyskać dostęp do głęboko zagnieżdżonej właściwości, która jest trudna do użycia w innym miejscu przejdź przez widok Zakres, możesz użyć Konsoli. także jego ocenę. Pamiętaj jednak, że bardziej złożone wyrażenia C++ nie są jeszcze obsługiwane.

Panel konsoli z wynikiem dla atrybutu „palette[10].r”

Przerwij kilkakrotnie wykonywanie programu, aby zobaczyć, jak zmienia się wewnętrzna zmienna x. Możesz 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”

Następnie możemy dodać stwierdzenia w języku C++ i zobaczyć, jak inne zmienne również się zmieniają:

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

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 do zamiast kompilować sami na podstawie źródła, obecnie debuger nie może znaleźć powiązanych źródeł. Wróćmy jeszcze raz, aby przejść do SDL_RenderDrawColor:

Narzędzia deweloperskie wyświetlające widok kodu po deasemblacji pliku „mandelbrot.wasm”

Wracamy do nieprzetworzonego interfejsu debugowania WebAssembly.

Teraz wygląda to trochę strasznie, a większość programistów stron internetowych nie ale od czasu do czasu warto biblioteka utworzona bez danych debugowania. Zewnętrzna biblioteka, nad którą nie masz kontroli lub dlatego, że jesteś na któryś z błędów, które pojawiają się tylko w wersji produkcyjnej.

W takich przypadkach ulepszyliśmy też 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. Poprzednio tylko indeksy liczbowe, a w przypadku funkcji – brak nazwy.

Teraz generujemy nazwy podobne do innych narzędzi do demontażu, korzystając ze wskazówek z sekcji nazwy WebAssembly, importowanie/eksportowanie i, jeśli wszystko inne zawiodło, generowanie na podstawie typu i indeksu elementu takiego jak $func123. Dostępne opcje Na zrzucie ekranu powyżej pozwala to uzyskać nieco czytelniejsze schematy i demontaż.

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

Sprawdzanie pamięci

Wcześniej w widoku Zakres można było rozwinąć tylko obiekt pamięci WebAssembly reprezentowany przez env.memory poszczególnych bajtów. Sprawdzało się to w kilku prostych sytuacjach, ale nie było co jest szczególnie wygodne i nie pozwala na ponowną interpretację danych, w formatach innych niż bajty. Dodaliśmy nową funkcję, liniowy inspektor pamięci.

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 inspektora pamięci w Narzędziach deweloperskich z widokiem pamięci w postaci szesnastkowej i ASCII.

Zaawansowane scenariusze i ostrzeżenia

Profilowanie kodu WebAssembly

Gdy otworzysz Narzędzia deweloperskie, kod WebAssembly zostanie „zredukowany” do wersji nieoptymalizowanej, aby umożliwić debugowanie. Ta wersja jest znacznie wolniejsza, co oznacza, że nie możesz polegać na tych usługach: console.time, performance.now i innych metod pomiaru szybkości kodu. ponieważ uzyskane wartości nie odzwierciedlają rzeczywistej skutecznoś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 przedstawiają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 należy mieć na uwadze ten warunek. 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 maszynach (w tym w Dockerze i na hoście)

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żywanych 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 pojawią się w panelu Źródła, ale nie można go wczytać.

Aby rozwiązać ten problem, wdrożyliśmy funkcję mapowania ścieżek w z opcjami rozszerzeń C/C++. Możesz go użyć do przemapowania 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ł utworzony 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 znacie inne wersje C++ debugerów, ta opcja jest podobna do polecenia set substitute-path w GDB lub w ustawieniu 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ą wbudować funkcje do siebie, zmienić ich kolejność lub całkowicie usunąć jego fragmenty. mogą zmylić debugera, a tym samym – 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 informacji debugowania

Dane debugowania zachowują wiele szczegółów dotyczących kodu, typów, zmiennych, funkcji, zakresów i lokalizacji. może być przydatna dla debugera. W rezultacie może ona być często większa niż sam kod.

Aby przyspieszyć ładowanie i kompilację modułu WebAssembly, możesz chcesz podzielić te informacje debugowania na osobny element 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żesz używać tej funkcji nawet do wysyłania prawie zoptymalizowanych wersji produkcyjnych aplikacji, a potem debugować je za pomocą pliku po stronie klienta. W takim przypadku musimy dodatkowo zastąpić zapisany adres URL, aby pomóc rozszerzeniu w znalezieniu pliku pobocznego, 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ć...

Ale sporo nowych funkcji!

Dzięki tym nowym integracjom Narzędzia deweloperskie w Chrome zaawansowany debuger nie tylko do obsługi JavaScriptu, ale też aplikacji w językach C i C++, który ułatwia pobieranie aplikacji. we wspólnej, wieloplatformowej sieci.

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

  • Poprawki w debugowaniu.
  • Dodaliśmy obsługę formaterów niestandardowych typów.
  • Pracujemy nad ulepszeniami profilowania w przypadku aplikacji WebAssembly.
  • Dodaliśmy obsługę zasięgu kodu, aby ułatwić wyszukiwanie nieużywanego kodu.
  • Ulepszona obsługa wyrażeń w ocenie konsoli.
  • Dodaliśmy obsługę kolejnych języków.
  • …i nie tylko

Tymczasem możesz pomóc nam, testując wersję beta własnego kodu we własnym kodzie i zgłaszając znalezione problemów na https://issues.chromium.org/issues/new?noWizard=true&amp;template=0&amp;component=1456350.

Pobierz kanały podglądu

Zastanów się, czy nie ustawić Chrome w wersji Canary, Dev lub beta jako domyślnej przeglądarki do programowania. Te kanały wersji testowej dają dostęp do najnowszych funkcji Narzędzi deweloperskich, umożliwiają testowanie najnowocześniejszych interfejsów API platformy internetowej i wykrywanie problemów w witrynie, zanim użytkownicy ją zobaczą.

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

Skorzystaj z poniższych opcji, aby porozmawiać o nowych funkcjach i zmianach w poście lub o innych kwestiach związanych z Narzędziami deweloperskimi.