최신 도구로 WebAssembly 디버깅

Ingvar Stepanyan
Ingvar Stepanyan

지금까지의 도로

1년 전 Chrome은 Chrome DevTools의 네이티브 WebAssembly 디버깅을 초기 지원한다고 발표했습니다.

기본적인 단계별 지원 방법을 시연하고 향후에 공개되는 소스 맵 대신 DWARF 정보를 사용할 수 있는 기회에 대해 이야기했습니다.

  • 변수 이름 확인
  • 예쁜 인쇄 유형
  • 출발어로 표현식 평가
  • 그 외 다양한 정보 제공

오늘 약속한 기능이 실제로 구현되고 Emscripten 및 Chrome DevTools팀이 올해 특히 C 및 C++ 앱에서 이룬 발전을 소개하게 되어 기쁩니다.

시작하기 전에 아직 새 환경의 베타 버전이라는 점을 염두에 두시기 바랍니다. 모든 도구의 최신 버전을 직접 사용해야 하며, 문제가 발생하면 https://bugs.chromium.org/p/chromium/issues/entry?template=DevTools+issue에 신고해 주세요.

지난번에 사용한 간단한 C 예시부터 시작해 보겠습니다.

#include <stdlib.h>

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

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

컴파일하려면 최신 Emscripten을 사용하고 원본 게시물과 마찬가지로 -g 플래그를 전달하여 디버그 정보를 포함합니다.

emcc -g temp.c -o temp.html

이제 생성된 페이지를 localhost HTTP 서버 (예: serve)에서 제공한 다음 최신 Chrome Canary에서 열 수 있습니다.

이번에는 Chrome DevTools와 통합되고 WebAssembly 파일에 인코딩된 모든 디버깅 정보를 이해하는 데 도움이 되는 도우미 확장 프로그램도 필요합니다. 이 링크(goo.gle/wasm-debugging-extension)로 이동하여 설치하세요.

또한 DevTools 실험에서 WebAssembly 디버깅을 사용 설정하는 것이 좋습니다. Chrome DevTools를 열고 DevTools 창 오른쪽 상단에 있는 톱니바퀴 () 아이콘을 클릭한 후 실험 패널로 이동하여 WebAssembly Debug: DWARF 지원 사용 설정을 선택합니다.

DevTools 설정의 실험 창

Settings를 닫으면 DevTools가 설정을 적용하기 위해 새로고침하도록 제안하므로 그렇게 해 보겠습니다. 이것으로 일회성 설정을 완료했습니다.

이제 소스 패널로 돌아가서 예외 시 일시중지 (⏸ 아이콘)를 사용 설정한 후 포착된 예외에서 일시중지를 선택하고 페이지를 새로고침하면 됩니다. 예외 상황에서 DevTools가 일시중지된 것을 확인할 수 있습니다.

&#39;포착된 예외에서 일시중지&#39;를 사용 설정하는 방법을 보여주는 소스 패널의 스크린샷

기본적으로 Emscripten 생성 글루 코드에서 중지되지만 오른쪽에서 오류의 스택 트레이스를 나타내는 Call Stack 뷰를 볼 수 있으며 abort를 호출한 원래 C 줄로 이동할 수 있습니다.

`assert_less` 함수에서 일시중지된 DevTools가 범위 뷰에 `x` 및 `y` 값을 표시함

이제 Scope 뷰를 보면 C/C++ 코드에서 변수의 원래 이름과 값을 확인할 수 있으므로 더 이상 $localN와 같이 손상된 이름의 의미와 작성한 소스 코드와의 관계를 파악할 필요가 없습니다.

이는 정수와 같은 기본 값뿐만 아니라 구조, 클래스, 배열 등의 복합 유형에도 적용됩니다.

리치 유형 지원

이를 보여주기 위해 더 복잡한 예를 살펴보겠습니다. 이번에는 다음 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();
}

이 애플리케이션은 여전히 상당히 작습니다. 50줄의 코드가 포함된 단일 파일입니다. 하지만 이번에는 C++ 표준 라이브러리의 복소수뿐만 아니라 그래픽용 SDL 라이브러리와 같은 일부 외부 API도 사용합니다.

디버그 정보를 포함하기 위해 위와 동일한 -g 플래그로 컴파일하고, 또한 Emscripten에 SDL2 라이브러리를 제공하고 임의 크기의 메모리를 허용하도록 요청합니다.

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

브라우저에서 생성된 페이지를 방문하면 임의의 색상으로 표시된 아름다운 프랙털 모양을 볼 수 있습니다.

데모 페이지

DevTools를 다시 열면 원본 C++ 파일이 표시됩니다. 하지만 이번에는 코드에 오류가 없으므로 대신 코드 시작 부분에 중단점을 설정해 보겠습니다.

페이지를 다시 새로고침하면 디버거가 C++ 소스 내에서 바로 일시중지됩니다.

`SDL_Init` 호출에서 DevTools가 일시중지됨

오른쪽에서 모든 변수를 볼 수 있지만 지금은 widthheight만 초기화되므로 검사할 것이 많지 않습니다.

기본 망델브로 루프 내부에 다른 중단점을 설정하고 실행을 재개하여 조금 앞으로 건너뛰겠습니다.

중첩된 루프 내에서 DevTools가 일시중지됨

이 시점에서 palette는 임의의 색상으로 채워졌으며 배열 자체와 개별 SDL_Color 구조를 모두 확장하고 구성요소를 검사하여 모든 것이 제대로 표시되는지 확인할 수 있습니다 (예: '알파' 채널은 항상 전체 불투명도로 설정됨). 마찬가지로 center 변수에 저장된 복소수의 실수부와 허수부를 확장하여 확인할 수 있습니다.

Scope 뷰를 통해 이동하기 어려운 복잡하게 중첩된 속성에 액세스하려는 경우에도 Console 평가를 사용할 수 있습니다. 그러나 더 복잡한 C++ 표현식은 아직 지원되지 않습니다.

`palette[10].r`의 결과를 보여주는 콘솔 패널

실행을 몇 번 다시 시작하면 Scope 뷰를 다시 확인하거나, 변수 이름을 감시 목록에 추가하거나, 콘솔에서 변수를 평가하거나, 소스 코드의 변수 위로 마우스를 가져가 내부 x가 어떻게 변경되는지 확인할 수 있습니다.

값 &#39;3&#39;을 표시하는 소스의 변수 &#39;x&#39;에 대한 도움말

여기에서 C++ 문을 Step Into 또는 Step Over하고 다른 변수도 어떻게 변하는지 관찰할 수 있습니다.

`color`, `point`, 기타 변수의 값을 보여주는 도움말 및 범위 뷰

디버그 정보를 사용할 수 있을 때는 이 모든 것이 잘 작동하지만 디버깅 옵션으로 빌드되지 않은 코드를 디버깅하려는 경우에는 어떻게 해야 할까요?

원시 WebAssembly 디버깅

예를 들어 Google에서는 Emscripten에 소스에서 직접 컴파일하는 대신 사전 빌드된 SDL 라이브러리를 제공해 달라고 요청했습니다. 따라서 현재 디버거는 연결된 소스를 찾을 방법이 없습니다. 다시 한번 SDL_RenderDrawColor에 들어가 보겠습니다.

`mandelbrot.worthm` 분해 뷰를 보여주는 DevTools

원시 WebAssembly 디버깅 환경으로 돌아왔습니다.

조금 무섭기 때문에 대부분의 웹 개발자는 처리할 필요가 없지만 경우에 따라 디버그 정보 없이 빌드된 라이브러리를 디버그해야 할 수도 있습니다. 개발자가 제어할 수 없는 3 라이브러리이거나 프로덕션에서만 발생하는 버그 중 하나가 발생했기 때문입니다.

이러한 경우에 도움이 되도록 기본 디버깅 환경도 몇 가지 개선했습니다.

우선, 이전에 원시 WebAssembly 디버깅을 사용했다면 이제 전체 디스어셈블리가 단일 파일에 표시되므로 Sources 항목 wasm-53834e3e/ wasm-53834e3e-7가 어떤 함수에 해당하는지 더 이상 추측할 필요가 없습니다.

새 이름 생성 스키마

디스어셈블리 뷰에서도 이름을 개선했습니다. 이전에는 숫자 색인만 표시되거나 함수의 경우 이름이 아예 표시되지 않았습니다.

이제 다른 분해 도구와 유사하게 이름을 생성합니다. WebAssembly 이름 섹션, 가져오기/내보내기 경로의 힌트를 사용하고 마지막으로 다른 모든 방법이 실패하는 경우 $func123와 같은 항목의 유형과 색인을 기반으로 이름을 생성합니다. 위 스크린샷에서 이렇게 하면 조금 더 읽기 쉬운 스택 트레이스와 디스어셈블리를 얻는 데 어떻게 도움이 되는지 알 수 있습니다.

사용 가능한 유형 정보가 없으면 프리미티브 외의 값을 검사하기 어려울 수 있습니다. 예를 들어 포인터는 일반 정수로 표시되며 메모리에 무엇이 저장되어 있는지 알 수 없습니다.

메모리 검사

이전에는 Scope 뷰에서 env.memory로 표시되는 WebAssembly 메모리 객체만 확장하여 개별 바이트를 조회할 수 있었습니다. 이 방법은 일부 사소한 시나리오에서 잘 작동했지만, 확장하기 특히 편리하지 않았고 바이트 값이 아닌 다른 형식으로 데이터를 해석할 수 없었습니다. 이 작업에 도움이 되는 새로운 기능인 선형 메모리 검사기도 추가되었습니다.

env.memory를 마우스 오른쪽 버튼으로 클릭하면 메모리 검사라는 새 옵션이 표시됩니다.

&#39;메모리 검사&#39; 항목을 보여주는 범위 창의 `env.memory` 컨텍스트 메뉴

클릭하면 Memory Inspector가 표시됩니다. 이 메모리에서 WebAssembly 메모리를 16진수 및 ASCII 뷰로 검사하고 특정 주소로 이동하며 데이터를 다양한 형식으로 해석할 수 있습니다.

메모리의 16진수 및 ASCII 뷰를 보여주는 DevTools의 Memory Inspector 창

고급 시나리오 및 주의사항

WebAssembly 코드 프로파일링

DevTools를 열면 WebAssembly 코드가 최적화되지 않은 버전으로 '계층화'되어 디버깅을 사용 설정합니다. 이 버전은 훨씬 느립니다. 즉, DevTools가 열려 있는 동안 console.time, performance.now, 코드 속도를 측정하는 다른 방법에 의존할 수 없습니다. 제공되는 숫자가 실제 성능을 전혀 나타내지 않기 때문입니다.

대신 DevTools Performance 패널을 사용해야 합니다. 이 패널은 코드를 최고 속도로 실행하고 다양한 기능에 소비된 시간에 관한 상세한 분류를 제공합니다.

다양한 Wasm 함수를 보여주는 프로파일링 패널

또는 DevTools를 닫은 상태에서 애플리케이션을 실행하고 완료되면 애플리케이션을 열어 콘솔을 검사할 수 있습니다.

향후 프로파일링 시나리오를 개선할 예정이지만 현재로서는 주의해야 합니다. WebAssembly 계층화 시나리오에 대한 자세한 내용은 WebAssembly 컴파일 파이프라인 문서를 확인하세요.

다른 머신 (Docker / 호스트 포함)에서 빌드 및 디버깅

Docker, 가상 머신 또는 원격 빌드 서버에서 빌드하는 경우 빌드 중에 사용된 소스 파일의 경로가 Chrome DevTools가 실행되는 자체 파일 시스템의 경로와 일치하지 않는 경우가 발생할 수 있습니다. 이 경우 파일이 Sources 패널에 표시되지만 로드할 수 없습니다.

이 문제를 해결하기 위해 C/C++ 확장 프로그램 옵션에 경로 매핑 기능을 구현했습니다. 이를 사용하여 임의의 경로를 다시 매핑하고 DevTools가 소스를 찾는 데 도움을 줄 수 있습니다.

예를 들어 호스트 머신의 프로젝트가 C:\src\my_project 경로 아래에 있지만 해당 경로가 /mnt/c/src/my_project로 표시된 Docker 컨테이너 내부에서 빌드된 경우 해당 경로를 프리픽스로 지정하여 디버깅 중에 다시 매핑할 수 있습니다.

C/C++ 디버깅 확장 프로그램의 옵션 페이지

첫 번째로 일치하는 접두사는 'wins'입니다. 다른 C++ 디버거에 익숙하다면 이 옵션은 GDB의 set substitute-path 명령어 또는 LLDB의 target.source-map 설정과 유사합니다.

최적화된 빌드 디버깅

다른 언어와 마찬가지로 디버깅은 최적화가 사용 중지된 경우에 가장 잘 작동합니다. 최적화는 함수를 다른 함수로 인라인 처리하거나 코드의 순서를 바꾸거나 코드의 일부를 완전히 삭제할 수 있습니다. 이로 인해 디버거와 사용자에게 혼동을 줄 수 있습니다.

더 제한적인 디버깅 환경에도 신경 쓰지 않고 최적화된 빌드를 계속 디버그하려는 경우 함수 인라인을 제외한 대부분의 최적화가 예상대로 작동합니다. 향후 나머지 문제를 해결할 계획이지만 현재로서는 -O 수준 최적화로 컴파일할 때 -fno-inline를 사용하여 사용 중지하세요.예를 들면 다음과 같습니다.

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

디버그 정보 분리

디버그 정보에는 코드, 정의된 유형, 변수, 함수, 범위, 위치 등 디버거에 유용할 수 있는 모든 세부정보가 포함됩니다. 따라서 코드 자체보다 클 수도 있습니다.

WebAssembly 모듈의 로드 및 컴파일 속도를 높이려면 이 디버그 정보를 별도의 WebAssembly 파일로 분할하는 것이 좋습니다. Emscripten에서 이를 수행하려면 원하는 파일 이름과 함께 -gseparate-dwarf=… 플래그를 전달합니다.

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

이 경우 기본 애플리케이션은 파일 이름 temp.debug.wasm만 저장하며 DevTools를 열면 도우미 확장 프로그램이 파일 이름을 찾아서 로드할 수 있습니다.

위에서 설명한 최적화와 결합하면 이 기능을 사용하여 애플리케이션의 거의 최적화된 프로덕션 빌드를 제공하고 나중에 로컬 측 파일로 디버그할 수 있습니다. 이 경우 확장 프로그램이 사이드 파일을 찾을 수 있도록 저장된 URL을 추가로 재정의해야 합니다. 예를 들면 다음과 같습니다.

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]

계속 진행됩니다.

정말 많은 새로운 기능이었습니다.

이러한 모든 새로운 통합을 통해 Chrome DevTools는 JavaScript는 물론 C 및 C++ 앱에서도 실행 가능하고 강력한 디버거가 되어 그 어느 때보다 쉽게 앱을 가져와 다양한 기술로 빌드된 공유 크로스 플랫폼 웹으로 가져올 수 있습니다.

하지만 우리의 여정은 아직 끝나지 않았습니다. 지금부터 작업할 사항은 다음과 같습니다.

  • 디버깅 환경의 대략적인 가장자리 정리
  • 커스텀 유형 형식 지정 도구 지원을 추가합니다.
  • WebAssembly 앱의 프로파일링을 개선하고 있습니다.
  • 사용되지 않는 코드를 더 쉽게 찾을 수 있도록 코드 적용 범위 지원이 추가되었습니다.
  • 콘솔 평가에서 표현식 지원 개선
  • 더 많은 언어를 지원합니다.
  • 그 밖의 다양한 신호

그동안 자체 코드에서 최신 베타를 사용해 보고 문제가 발견되면 https://bugs.chromium.org/p/chromium/issues/entry?template=DevTools+issue에 신고해 주세요.

미리보기 채널 다운로드

Chrome Canary, 개발자 또는 베타를 기본 개발 브라우저로 사용하는 것이 좋습니다. 이러한 Preview 채널을 통해 개발자는 최신 DevTools 기능에 액세스하고 최첨단 웹 플랫폼 API를 테스트하며 다른 사용자보다 먼저 사이트에서 문제를 찾을 수 있습니다.

Chrome DevTools팀에 문의하기

게시물에서 새로운 기능과 변경사항 또는 DevTools와 관련된 다른 항목에 대해 논의하려면 다음 옵션을 사용하세요.

  • crbug.com을 통해 제안 또는 의견을 제출하세요.
  • DevTools에서 옵션 더보기   더보기   > 도움말 > DevTools 문제 신고를 사용하여 DevTools 문제를 신고합니다.
  • @ChromeDevTools로 트윗을 보냅니다.
  • DevTools의 새로운 기능 YouTube 동영상 또는 DevTools 팁 YouTube 동영상에 댓글을 남겨주세요.