1
头图

Recently I am studying WebAssembly, and I have also written several comprehensive articles:

This article is the third article in the series of learning WebAssembly. I also want to explore how Chrome Developer Tools supports WebAssembly debugging. Through this process of exploration, we will learn about the usage and functions of various aspects of Chrome debugging tools. Some knowledge points you may not know.

Therefore, this article can be used as a comprehensive article for learning to use Chrome Devtools debugging tools, or as an introduction to how we can debug WebAssembly-related code in the browser at this stage, and help you become a qualified debugging engineer:) .

WebAssembly's original debugging method

Chrome Developer Tools currently supports the debugging of WebAssembly. Although there are some limitations, the text format file for WebAssembly can be analyzed for a single instruction and view the original stack trace, as shown in the following figure:

The above method can run well for some WebAssembly modules without other dependent functions, because these modules only involve a small debugging scope. But for complex applications, such as complex applications written in C/C++, a module depends on many other modules, and the source code is different from the text format of the compiled WebAssembly, the above debugging method is not It's so intuitive that you can only understand how the code runs by guessing, and it is difficult for most people to understand the complex assembly code.

More intuitive debugging method

Modern JavaScript projects usually have a compilation process during development. Use ES6 for development and compile to ES5 and below to run. At this time, if you need to debug the code, it involves the concept of Source Map. Source map is used for mapping. The position of the compiled code in the source code. The source map makes the client code more readable and easier to debug, but it will not have a great impact on performance.

Emscripten, the compiler for C/C++ to WebAssembly code, supports injecting relevant debugging information into the code at compile time, generating the corresponding source map, and then installing the C/C++ Devtools Support browser extension written by the Chrome team. Use Chrome Developer Tools to debug C/C++ code.

The principle here is actually that when Emscripten compiles, it will generate a debugging file in DWARF format, which is a general debugging file format used by most compilers, and C/C++ Devtools Support will parse the DWARF file , Provides source map related information for Chrome Devtools during debugging, so that developers can debug C/C++ code on Chrome Devtools version 89+ and above.

Debug simple C applications

Because the debug file in DWARF format can provide processing variable names, formatting type printing digestion, executing expressions in the source code, etc., let us actually write a simple C program, and then compile it to WebAssembly and in the browser Run and check the actual debugging effect.

First, let us enter the WebAssembly directory created earlier, activate emcc-related commands, and then view the activation effect:

cd emsdk && source emsdk_env.sh

emcc --version # emcc (Emscripten gcc/clang-like replacement) 1.39.18 (a3beeb0d6c9825bd1757d03677e817d819949a77)

Then create a temp folder in WebAssembly, and then create a temp.c file, fill in the following content and save:

#include <stdlib.h>

void assert_less(int x, int y) {

  if (x >= y) {

    abort();

  }

}

int main() {

  assert_less(10, 20);

  assert_less(30, 20);

}

When the above code executes asset_less , if it encounters x >= y , it will throw an exception and terminate the program execution.

In the terminal, switch the directory to the temp directory and execute the emcc command to compile:

emcc -g temp.c -o temp.html

-g parameter to the normal compilation form to tell Emscripten to inject DWARF debugging information into the code during compilation.

Now you can start an HTTP server, you can use npx serve . , and then visit localhost:5000/temp.html view the running effect.

You need to make sure you have installed the Chrome extension: https://chrome.google.com/webstore/detail/cc++-devtools-support-dwa/pdcpmagijalfljmkmjngeonclgbbannb, and Chrome Devtools upgraded to version 89+.

In order to view the debugging effect, some content needs to be set.

  1. Open the WebAssembly debugging option in Chrome Devtools

After setting, a blue Reload button will appear at the top of the toolbar, and the configuration needs to be reloaded, just click it.

  1. Set debugging options, pause at abnormal places

  1. Refresh the browser, and then you will find that the breakpoint stops at temp.js , the JS glue code compiled by Emscripten, and then follow the call stack to find it, you can view temp.c and locate the location where the exception was thrown:

As you can see, we successfully viewed the C code in Chrome Devtools, and the code stopped at abort() . At the same time, we can also view the value under the current scope as we did when debugging JS:

As you can see above x , y value, floating mouse x the value at that time may also be displayed.

View complex type values

In fact, Chrome Devtools can not only view the ordinary type values of some variables in the original C/C++ code, such as numbers and strings, but also view more complex structures, such as structures, arrays, classes, etc., let’s take another example. Show this effect.

We use an example of drawing Mandelbrot graphics in C++ to demonstrate the above effect. Also create a mandelbrot folder in the WebAssembly directory, then add the `mandelbrot.cc file, and fill in the following content:

#include <SDL2/SDL.h>

#include <complex>

int main() {

  // 初始化 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);

  // 为画板填充随机的颜色

  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,

    };

  }


  // 计算 曼德博 集合并绘制 曼德博 图形

  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);

    }

  }


  // 将我们在 canvas 绘制的内容渲染出来

  SDL_RenderPresent(renderer);


  // SDL_Quit();

}

The above code is about 50 lines, but two C++ standard libraries are referenced: SDL and complex numbers , which makes our code a bit more complicated. Let’s compile the above code and take a look at Chrome Devtools. How effective is the debugging?

-g tag at compile time, tell the Emscripten compiler to bring debugging information, and seek Emscripten to inject the SDL2 library at compile time and allow the library to use any memory size at runtime:

emcc -g mandelbrot.cc -o mandelbrot.html \

     -s USE_SDL=2 \

     -s ALLOW_MEMORY_GROWTH=1

Also use the npx serve . command to start a local Web server, and then visit http://localhost:5000/mandelbrot.html to see the following effects:

Open the developer tools, and then you can search for the mandelbrot.cc file, we can see the following:

palette assignment statement in the first for loop, and then refresh the web page, we find that the execution logic will pause to our breakpoint, by looking at the Scope panel on the right, we can see some Interesting content.

Use the Scope panel

We can see complex types such as center , palette , and we can expand them to view the specific values in the complex type:

View directly in the program

At the same time, move the mouse to palette and other variables, you can also view the type of value:

Use in the console

At the same time in the console, you can also get the value by entering the variable name, and you can still view the complex type:

You can also perform value and calculation-related operations on complex types:

Use watch function

We can also use the watch function in the debug panel to add the i in the for loop to the watch list, and then resume the program execution to see the change of i:

More complex step-by-step debugging

We can also use several other debugging tools: step over, step in, step out, step, etc. For example, we use step over to perform two steps backwards:

You can view the variable value of the current step, and you can also see the corresponding value in the Scope panel.

Debug third-party libraries that are not compiled from source code

Before, we only compiled the mandelbrot.cc file, and asked Emscripten to provide us with the built-in SDL-related library when compiling. Since the SDL library is not compiled from the source code, we will not bring debugging-related information, so we Only in mandelbrot.cc can be debugged by viewing C++ code, while for SDL related content, you can only view WebAssembly related code for debugging.

For example, we set a breakpoint at the call of SDL_SetRenderDrawColor on line 41, and use step in to enter the function:

Will become the following form:

We have returned to the original form of WebAssembly debugging. This is also an unavoidable situation, because we may encounter various third-party libraries during the development process, but we cannot guarantee that every library can be compiled from source code. Come and bring debugging information similar to DWARF. In most cases, we cannot control the behavior of third-party libraries. In another case, we sometimes encounter problems in production, and there is no debugging information in the production environment. of.

There is no better way to deal with the above situation, but the developer tools have improved the above debugging experience. All the code is packaged into a single WebAssembly file, which corresponds to our mandelbrot.wasm file this time, so we don’t need to worry anymore. Which source file did a certain piece of code come from?

New name generation strategy

In the previous debug panel, there were only some numerical indexes for WebAssembly, but for functions, there was no name. If there is no necessary type information, it is difficult to trace a specific value, because the pointer will be displayed as an integer. But you don't know what is stored behind these integers.

The new naming strategy refers to the naming strategy of other disassembly tools, using WebAssembly naming strategy part of the content, import/export path-related content, you can see that we can display the function name related to the function in the debug panel. Information:

$func123 can be generated based on the type and index of the statement, which greatly improves the experience of stack tracing and disassembly.

View the memory panel

If you want to debug the content related to the memory occupied by the program at this time, you can view Module.memories.$env.memory in the Scope panel under the context of WebAssembly, but this can only see some independent bytes, and you cannot understand the other bytes that these bytes correspond to. Data format, such as ASCII format. But Chrome Developer Tools also provides us with some other more powerful forms of memory viewing. When we right-click env.memory , we can select Reveal in Memory Inspector panel:

Or click on the small icon next to env.memory

You can open the memory panel:

From the memory panel, you can view the memory of WebAssembly in hexadecimal or ASCII form, navigate to a specific memory address, and parse specific data into various formats, such as the ASCII character e represented by hexadecimal 65 .

Performance analysis of WebAssembly code

Because we injected a lot of debugging information into the code when compiling, the running code is unoptimized and verbose code, so the runtime will be very slow, so if you want to evaluate the performance of the program, you cannot use APIs such as performance.now or console.time Because the performance-related numbers obtained by these function calls usually do not reflect real-world effects.

So if you need to perform performance analysis of the code, you need to use the performance panel provided by the developer tools. The performance panel will run the code at full speed and provide clear breakpoint information about the time spent in the execution of different functions:

It can be seen that the above-mentioned typical time points such as 161ms, or 461ms LCP and FCP, these are all performance indicators that can reflect the real world.

Or you can close the console when loading the webpage, so that it will not involve the call of related content such as debugging information, and you can ensure a more realistic effect. Wait until the page is loaded, and then open the console to view the relevant indicator information.

Debug on different machines

When building on Docker, virtual machines, or other original servers, you may encounter that the source file path used during the build is inconsistent with the file path on the local file system, which will cause the developer tool to run This file is shown in the Sources panel, but the content of the file cannot be loaded.

In order to solve this problem, we need to C/C++ Devtools Support configuration that was installed before, and click the extended "option":

Then add path mapping, fill in old/path with the path of the previous source file when it was built, and fill in new/path with the file path that currently exists on the local file system:

The above mapping function is very similar to some C++ debuggers such as GDB's set substitute-path and LLDB's target.source-map In this way, when the developer tool searches for the source file, it will check whether there is a corresponding mapping in the configured path mapping. If the source path cannot load the file, the developer tool will try to load the file from the mapped path, otherwise the loading will fail.

Debug optimized build code

If you want to debug some code that is optimized during the build, you may get a less than ideal debugging experience, because during the optimized build, the functions are inlined, and the code may be reordered or some useless code removed. , These may confuse debuggers.

The current developer tools can support most of the optimized code debugging experience, in addition to not providing good support for function inlining. In order to reduce the debugging impact caused by the lack of function inlining support ability, it is recommended to compile the code. Add the -fno-inline flag to cancel the function of inline processing the function during the optimization build (usually with the -O parameter). The developer tools will fix this problem in the future. So the compilation script for the simple C program mentioned earlier is as follows:

emcc -g temp.c -o temp.html \

     -O3 -fno-inline

Store debugging information separately

Debugging information includes detailed information about the code, defined types, variables, functions, function scopes, and file locations, and any other information that is beneficial to the debugger. Therefore, the debugging information is usually larger than the source code.

In order to speed up the compilation and loading speed of WebAssembly modules, you can split the debugging information into independent WebAssembly files during compilation, and then load them separately. In order to split separate files, you can add the -gseparate-dwarf operation during compilation:

emcc -g temp.c -o temp.html \

     -gseparate-dwarf=temp.debug.wasm

After performing the above operations, the compiled main application code will only store a temp.debug.wasm , and then when the code is loaded, the plug-in will locate the debug file and load it into the developer tool.

If we want to optimize the construction at the same time and split the debugging information separately, and when we need to debug later, load the local debugging file for debugging. In this scenario, we need to reload the address of the debugging file to help the plug-in If you find this file, you can run the following command to deal with it:

emcc -g temp.c -o temp.html \

     -O3 -fno-inline \

     -gseparate-dwarf=temp.debug.wasm \

     -s SEPARATE_DWARF_URL=file://[temp.debug.wasm 在本地文件系统的存储地址]

Debug ffmpeg code in the browser

Through this article, we have an in-depth understanding of how to debug the C/C++ code built through Emscripten in the browser. The above explained an ordinary non-dependency example and an example that depends on the C++ standard library SDL, and explained the current stage The things and limitations that debugging tools can do, then we will learn how to debug ffmpeg-related code in the browser through the knowledge we have learned.

Build with debug information

We only need to modify the build script build-with-emcc.sh mentioned in the previous article and add the flag corresponding -g

ROOT=$PWD

BUILD_DIR=$ROOT/build

cd ffmpeg-4.3.2-3

ARGS=(

  -g # 在这里添加,告诉编译器需要添加调试

  -I. -I./fftools -I$BUILD_DIR/include

  -Llibavcodec -Llibavdevice -Llibavfilter -Llibavformat -Llibavresample -Llibavutil -Llibpostproc -Llibswscale -Llibswresample -L$BUILD_DIR/lib

  -Qunused-arguments

  -o wasm/dist/ffmpeg-core.js fftools/ffmpeg_opt.c fftools/ffmpeg_filter.c fftools/ffmpeg_hw.c fftools/cmdutils.c fftools/ffmpeg.c

  -lavdevice -lavfilter -lavformat -lavcodec -lswresample -lswscale -lavutil -lpostproc -lm -lx264 -pthread

  -O3                                           # Optimize code with performance first

  -s USE_SDL=2                                  # use SDL2

  -s USE_PTHREADS=1                             # enable pthreads support

  -s PROXY_TO_PTHREAD=1                         # detach main() from browser/UI main thread

  -s INVOKE_RUN=0                               # not to run the main() in the beginning

  -s EXPORTED_FUNCTIONS="[_main, _proxy_main]"  # export main and proxy_main funcs

  -s EXTRA_EXPORTED_RUNTIME_METHODS="[FS, cwrap, setValue, writeAsciiToMemory]"   # export preamble funcs

  -s INITIAL_MEMORY=268435456                    # 268435456 bytes = 268435456 MB

)

emcc "${ARGS[@]}"

cd -

Then perform other operations, and finally node server.js , and then open http://localhost:8080/ see the effect as follows:

As you can see, we can search for the built ffmpeg.c file in the Sources panel, and we can hit a breakpoint at line 4865 when we loop nb_output

avi format video on the web page, and then the program will pause to the breakpoint position:

It can be found that we can still view the variable value by moving the mouse in the program as before, and view the variable value in the Scope panel on the right, as well as the variable value in the console.

Similarly, we can also perform complex debugging operations such as step over, step in, step out, step, or watch a variable value, or view the memory at this time, etc.

You can see that through the knowledge introduced in this article, you can debug C/C++ projects of any size in the browser, and you can use most of the functions provided by the current developer tools.

Reference link

❤️/ Thanks for your support/

The above is all the content shared this time, I hope it will be helpful to you^_^

If you like it, don’t forget to share, like, and favorite.

Welcome to pay attention to the public number programmer bus , the three-terminal brothers from Byte, Xiaopi, and China Merchants Bank, share programming experience, technical dry goods and career planning, and help you avoid detours into the big factory.


程序员巴士
52 声望8 粉丝

一辆有趣、有范儿、有温度的程序员巴士,涉猎大厂面经、程序员生活、实战教程、技术前沿等内容,关注我,交个朋友。