最近在研究WebAssembly,也写了几篇比较综合的文章:前端进阶:如何在浏览器上运行C/C++代码?Fast11KStar的WebAssembly,你应该这样学习各方面的方法和函数的使用,发现一些你可能不知道的知识点。所以这篇文章可以算是一篇学习使用ChromeDevtools调试工具的综合文章,或者说是介绍我们现阶段如何在浏览器中调试WebAssembly相关代码,助你成为一名合格的调试工程师:)。WebAssembly独创的调试方式ChromeDeveloperTools目前支持WebAssembly调试。虽然有一些限制,但它可以分析单个指令并查看WebAssembly文本格式文件的原始堆栈跟踪。具体见下图:上述方法对于一些没有其他依赖函数的WebAssembly模块效果很好,因为这些模块只涉及很小的调试范围。但是,对于复杂的应用,比如用C/C++编写的复杂应用,一个模块依赖很多其他模块,源代码和编译后的WebAssembly文本格式之间的映射差异较大,上述调试方法是行不通的。它非常直观,你只能通过猜测来理解代码是如何运行的,而对于大多数人来说,很难理解复杂的汇编代码。更直观的调试方式现代JavaScript项目在开发的时候通常会有一个编译过程,使用ES6开发,编译到ES5及以下版本运行,如果这时候需要调试代码,就涉及到SourceMap的概念,Thesourcemap用于映射编译后的相应代码在源代码中的位置。sourcemap使客户端代码更具可读性,更易于调试,但对性能影响不大。Emscripten,一个C/C++到WebAssembly代码的编译器,支持在编译时向代码中注入相关调试信息,生成相应的sourcemap,然后安装Chrome团队编写的C/C++DevtoolsSupport浏览器扩展,即可使用ChromeDeveloper工具调试C/C++代码。这里的原理其实就是Emscripten在编译的时候,会生成一个DWARF格式的调试文件,这是大多数编译器常用的调试文件格式,C/C++DevtoolsSupport会解析这个DWARF文件,ChromeDevtools提供源码图——调试时的相关信息,允许开发者在89+版本以上的ChromeDevtools上调试C/C++代码。调试一个简单的C应用程序因为DWARF格式的调试文件可以提供处理变量名、格式化类型、打印摘要、执行源代码中的表达式等,现在让我们实际编写一个简单的C程序并将其编译为WebAssembly并运行它在浏览器中查看实际调试效果。首先让我们进入之前创建的WebAssembly目录,激活emcc相关命令,然后查看激活效果:cdemsdk&&sourceemsdk_env.shemcc--version#emcc(Emscriptengcc/clang-likereplacement)1.39.18(a3beeb0d6c9825bd1757d03677e817d819949a77)然后在WebAssembly中创建一个temp文件夹,然后创建一个temp.c文件,填写以下内容并保存:#includevoidassert_less(intx,inty){if(x>=y){中止();}}intmain(){assert_less(10,20);assert_less(30,20);}上面代码执行asset_less时,如果x>=y,会抛出异常,终止程序执行。在终端中,将目录切换到temp目录下,执行emcc命令进行编译:emcc-gtemp.c-otemp.html上面的命令在正常的编译形式中加入了-g参数,告诉Emscripten注入DWARF调试在编译信息期间进入代码。现在可以启动一个HTTP服务器了,可以使用npxserve.,然后访问localhost:5000/temp.html看运行效果。你需要确保你已经安装了Chrome扩展:https://chrome.google.com/web…,并且ChromeDevtools已经升级到89+版本。为了看到调试效果,需要设置一些东西。在ChromeDevtools中打开WebAssembly调试选项后,工具栏上方会出现一个蓝色的Reload按钮,需要重新加载配置,点击即可。设置调试选项,在遇到异常的地方暂停刷新浏览器,然后你会发现断点停在temp.js,Emscripten编译的JS胶水代码,然后顺着调用栈找到temp.c并定位到抛出异常的位置:可以看到,我们在ChromeDevtools中成功查看了C代码,代码停在了abort()处,我们也可以在currentscope下查看currentscope类似当我们调试JS的时候Value:上面说到可以查看x和y的值,也可以将鼠标悬浮在x上显示此时的值。查看复杂类型值其实ChromeDevtools不仅可以查看原始C/C++代码中一些变量的普通类型值,比如数字、字符串,还可以查看更复杂的结构,比如结构体、数组,classes等。我们再举一个例子来演示这个效果。我们用一个用C++绘制Mandelbrot图形的例子来演示上面的效果。同样在WebAssembly目录下创建一个mandelbrot文件夹,然后添加`mandelbrot.cc文件,并填写以下内容:#include#includeintmain(){//初始化SDLintwidth=600,height=600;SDL_Init(SDL_INIT_VIDEO);SDL_Window*窗口;SDL_Renderer*渲染器;SDL_CreateWindowAndRenderer(宽度,高度,SDL_WINDOW_OPENGL,&window,&renderer);//用随机颜色填充画板enum{MAX_ITER_COUNT=256};SDL_Colorpalette[MAX_ITER_COUNT];srand(时间(0));对于(inti=0;icenter(0.5,0.5);双尺度=4.0;for(inty=0;ypoint((double)x/width,(double)y/height);std::complex<双>c=(点-中心)*比例尺;std::complexz(0,0);诠释我=0;对于(;i2.0)中断;}SDL_Colorcolor=palette[i];SDL_SetRenderDrawColor(renderer,color.r,color.g,color.b,color.a);SDL_RenderDrawPoint(渲染器,x,y);}}//渲染我们在画布上绘制的内容SDL_RenderPresent(renderer);//SDL_Quit();}上面的代码大约50行,但是引用了两个C++标准库:SDL和复数,这让我们的代码变得有点复杂。下面我们把上面的代码编译一下,看看ChromeDevtools的调试效果如何。通过在编译时加上-g标签,告诉Emscripten编译器在编译时带上调试信息和seekEmscripten。注入SDL2库并允许库在运行时使用任何内存大小:emcc-gmandelbrot.cc-omandelbrot.html\-sUSE_SDL=2\-sALLOW_MEMORY_GROWTH=1也使用npxserve。命令启动本地web服务器,然后访问http://localhost:5000/mandelb...可以看到如下效果:打开开发者工具,然后搜索mandelbrot.cc文件,我们可以看到内容如下:我们可以在第一个for循环里面的palette赋值语句的哪一行设置断点,然后刷新网页,我们发现执行逻辑会暂停到我们的断点处,通过查看Scope面板上的右边,我们可以看到一些有趣的内容。使用Scope面板,我们可以看到center、palette等复杂类型,展开可以查看复杂类型中的具体值:直接在程序中查看,将鼠标移到palette等变量上,也可以查看取值类型:在控制台中,也可以通过在控制台中输入变量名获取值,仍然可以查看复杂类型:还可以对复杂类型进行取值和计算相关操作:使用watch功能,我们也可以使用调试面板。watch函数,将for循环中的i加入watchlist,然后恢复程序执行,看i的变化:对于更复杂的单步调试,我们还可以使用其他几种调试工具:stepover,stepin,stepout,step等,如果我们使用stepover,则向后执行两步:可以查看当前step的变量值,也可以在Scope面板中看到对应的值。针对从非源代码编译的第三方库进行调试。我们之前只编译了mandelbrot.cc文件,在编译时要求Emscripten为我们提供内置的SDL相关库。由于SDL库不是从源码编译而来,因此不会带来调试相关的信息,所以我们只能通过查看mandelbrot.cc中的C++代码进行调试,而对于SDL相关的内容,只能查看WebAssembly相关的内容用于调试的代码。比如我们在第41行的SDL_SetRenderDrawColor调用处设置断点,使用stepin进入函数:会变成如下形式:我们回到了原来的WebAssembly调试形式,这也是不可避免的情况,因为我们开发过程中可能会遇到各种第三方库,但我们不能保证每个库都能从源码编译出来,并带来类似DWARF的调试信息。在大多数情况下,我们无法控制第三方库。图书馆的行为;还有一种情况就是有时候我们会在生产环境中遇到问题,而生产环境是没有调试信息的。暂时没有更好的办法来处理上面的情况,但是开发者工具改进了上面的调试体验,将所有的代码打包成一个单独的WebAssembly文件,对应我们这次的mandelbrot.wasm文件,这样我们可以不再需要担心某段代码来自哪个源文件。在新命名生成策略之前的调试面板中,只有一些WebAssembly的数字索引,没有函数名称。如果没有必要的类型信息,很难追踪到一个具体的值,因为指针将是一个以的形式显示的整数,但你不知道这些整数背后存储的是什么。新的命名策略参考了其他反汇编工具的命名策略,使用了WebAssembly命名策略的内容和导入/导出路径相关的内容。可以看到在我们现在的调试面板中,对于函数可以显示函数名相关的信息:即使遇到程序错误,根据语句的类型和索引也可以生成类似$func123这样的名字,大大提升了体验堆栈跟踪和反汇编。查看内存面板此时如果想调试程序占用的内存相关内容,可以在WebAssembly上下文的Scope面板中查看Module.memories.$env.memory,但是只能看到一些独立的字节。无法知道这些字节对应的是什么其他数据格式,比如ASCII格式。但是Chrome开发者工具也为我们提供了一些其他更强大的内存查看形式。当我们右键点击env.memory时,我们可以在MemoryInspector面板中选择Reveal:或者点击env.memory旁边的小图标:可以打开内存面板:从内存面板可以查看WebAssembly的十六进制内存或者ASCII,定位到特定的内存地址,将特定的数据解析成各种格式,比如十六进制65字符表示的ASCIIe。WebAssembly代码的性能分析因为我们在编译时向代码中注入了大量的调试信息,运行时的代码是未经优化且冗长的代码,所以运行时会很慢,所以如果要评估程序运行的性能,您不能使用performance.now或console.time等API,因为通过这些函数调用获得的与性能相关的数字通常不会反映真实世界的效果。所以如果需要对代码进行性能分析,就需要使用开发者工具提供的性能面板。性能面板会全速运行代码,并提供清晰的断点信息,说明执行不同函数所花费的时间:你可以看到上面典型的161ms的时间点,或者461ms的LCP和FCP,这些是性能能够反映现实世界的指标。或者可以在加载网页时关闭控制台,这样就不会涉及到调试信息等相关内容,这样可以保证更真实的效果,等到页面加载完毕,再打开控制台查看相关指标信息.在不同机器上调试在Docker、虚拟机或其他源站上构建时,可能会遇到构建时使用的源文件路径与本地文件系统上的文件路径不一致,这会导致开发者工具可以显示这个运行时在Sources面板中加载文件,但无法加载文件内容。为了解决这个问题,我们需要在之前安装的C/C++DevtoolsSupport配置中设置路径映射,点击扩展的“Options”:然后添加路径映射,在buildin中填写之前的源文件路径old/path,new/path填入本地文件系统当前存放的文件路径:上面映射的作用和一些C++调试器如GDB的setsubstitute-path和LLDB的target.source-map很相似。这样,开发者工具在查找源文件时,会检查配置的路径映射中是否有对应的映射。如果源路径无法加载文件,开发者工具会尝试从映射路径加载文件,否则加载失败。调试优化的构建代码如果你想调试一些在构建时优化的代码,你可能会得到不太理想的调试体验,因为函数被内联在一起,当你优化构建时代码可能会重新排序或者删除一部分无用的代码,这可能会使调试器感到困惑。目前开发者工具除了不支持函数内联外,可以支持绝大部分优化代码的调试体验。为了减少由于缺少函数内联支持而带来的调试影响,建议在编译代码的时候加入-fno-inline标志,在优化构建时禁用内联函数的功能(通常使用-O参数),开发者工具会在未来修复这个问题。所以前面提到的简单C程序的编译脚本如下:emcc-gtemp.c-otemp.html\-O3-fno-inline单独存放调试信息调试信息包括代码的详细信息,定义的类型,variables,function,functionscope,andfilelocation等任何对调试器有利的信息,所以通常调试信息比源代码大。为了加快WebAssembly模块的编译加载速度,可以在编译时将调试信息拆分成单独的WebAssembly文件,然后分别加载。为了拆分单独的文件,可以在编译时加入-gseparate-dwarf操作:emcc-gtemp.c-otemp.html\-gseparate-dwarf=temp.debug.wasm执行以上操作后,编译后的主程序code只会存储一个temp.debug.wasm文件名,然后在加载代码时,插件会定位调试文件并将其加载到开发者工具中。如果我们想同时优化构建,将debug信息单独拆分,后面需要调试的时候加载本地的debug文件进行调试,这种场景下,我们需要重载debug文件存放的地址来帮忙插件如果找到这个文件,可以运行如下命令进行处理:emcc-gtemp.c-otemp.html\-O3-fno-inline\-gseparate-dwarf=temp.debug.wasm\-sSEPARATE_DWARF_URL=file://[temp.debug.wasm在本地文件系统中的存储地址]在浏览器中调试ffmpeg代码通过本文,我们深入了解了如何调试C/C++代码由Emscripten在浏览器中构建。上面解释了一个常见的没有依赖的例子和一个依赖C++标准库SDL的例子,并且解释了现阶段调试工具可以做的事情和限制,接下来我们将运用所学来理解如何调试浏览器中与ffmpeg相关的代码。想要带调试信息的构建,我们只需要修改上一篇文章中提到的构建脚本build-with-emcc.sh,添加-g对应的flag即可:ROOT=$PWDBUILD_DIR=$ROOT/buildcdffmpeg-4.3.2-3ARGS=(-g#添加这里告诉编译器添加调试-I.-I./fftools-I$BUILD_DIR/include-Llibavcodec-Llibavdevice-Llibavfilter-Llibavformat-Llibavresample-Llibavutil-Llibpostproc-Llibswscale-Llibswresample-L$BUILD_DIR/lib-Qunused-arguments-owasm/dist/ffmpeg-core.jsfftools/ffmpeg_opt.cfftools/ffmpeg_filter.cfftools/ffmpeg_hw.cfftools/cmdutils.cfftools/ffmpeg.c-lavdevice-lavfilter-lavformat-lavcodec-lswresample-lswscale-lavutil-lpostproc-lm-lx264-pthread-O3#以性能优先优化代码-sUSE_SDL=2#使用SDL2-sUSE_PTHREADS=1#启用pthreads支持-sPROXY_TO_PTHREAD=1#detachmain()来自浏览器/UI主线程-sINVOKE_RUN=0#不运行main()i在开头-sEXPORTED_FUNCTIONS="[_main,_proxy_main]"#exportmain和proxy_mainfuncs-sEXTRA_EXPORTED_RUNTIME_METHODS="[FS,cwrap,setValue,writeAsciiToMemory]"#exportpreamblefuncs-sINITIAL_MEMORY=268435456#268435456bytes=2684354566)emcc"${ARGS[@]}"cd-然后用这个进行其他操作,最后通过nodeserver.js运行我们的脚本,然后打开http://localhost:8080/查看效果如下:可以看到,我们在Sources面板中,可以搜索到构建好的ffmpeg.c文件。我们可以在循环nb_output的时候在第4865行设置一个断点:然后在网页上传一个avi格式的视频,然后程序会暂停到断点位置:可以发现我们仍然可以在程序中移动鼠标到查看变量值和之前一样,在右侧的Scope面板中查看变量值,在控制台中查看变量值。类似的,我们还可以进行stepover,Stepin,stepout,step等复杂的调试操作,或者观察一个变量的值,或者查看此时的内存等等。通过本文介绍的知识可以看出,你可以在浏览器中调试任意大小的C/C++项目,你可以使用目前开发者工具提供的大部分功能。参考链接https://www.infoq.com/news/20...https://developer.chrome.com/...https://lucumr.pocoo.org/2020...https://v8.dev/docs/wasm-comp...使用ChromeDevTools调试WebAssembly|通过CharukaHerath|BitsandPieces(bitsrc.io)")让WebAssembly更快:使用AssemblyScript和GameboyEmulator调试WebAssembly性能|byAaronTurner|Medium??/感谢大家的支持/以上就是本次分享的全部内容,希望对你有所帮助^_^喜欢的话别忘了三连分享、点赞、收藏哦~欢迎关注公众号程序员巴士,字节跳动、虾皮、淘宝的三端兄弟招行,分享编程经验、技术干货和职业规划,助你少走弯路,进大厂。