WebAssembly 中体系库是怎么编译的?

我在运用 WebAssembly 的时分经常会怀疑,他的 printf 是怎么完成的?是直接替换掉规范库的 printf 完成仍是只替换 syscall 的部分?如果只替换 syscall 那么相应的源代码在哪里?是 JS 仍是 WASM完成呢?本篇博客经过解读 Emscripten 的源代码来搞清楚这个问题

本篇适合有同样类似问题的同学进行观看

一个根本Demo

// 源代码
#include <stdio.h>
int main() {
    printf("Hello World\n");
    return 0;
}
// 编译脚本
emcc hello.c -o hello.html

编译以上程序咱们会得到一个HTML,HTML 中会调用 JS 打印出 Hello World 字样。这是就会发生疑问,stdio 这个库是怎么编译进去的呢?

一些补充常识

C 中的规范库与链接

  • 规范库文件参阅:en.cppreference.com/w/c/header
  • C 语言中的链接:www.0xffffff.org/2013/04/17/…

GCC 编译选项和库文件途径次序

  • -I(大写i) 之后增加的表明头文件会在这儿找,默许会去 /usr/include中去寻觅
  • -l (小写L)之后增加的表明要链接的库,比方说 gcc -Wall main.c -o main -lCPPfile。上面的代码会链接libCPPfile.so,发生可履行文件main ,-l 会依照必定次序寻觅这个 so 文件
  • -L 应该是说lib优先在哪个途径下进行寻觅,默许会在/usr/lib中去寻觅,而且默许链接libc.so
  • -c 只发生编译的代码,不进行链接
  • 参阅文档:www.noobyard.com/article/p-f…
  • sysroot:能够改变根目录途径的方位:Usediras the logical root directory for headers and libraries.For example,if the compiler normally searches for headers in/usr/includeand libraries in/usr/lib, it instead searchesdir/usr/includeanddir/usr/lib.

Emscripten 怎么编译 WASM 文件?

以下函数来自于 Emscripten源码中 emcc.py 中的 run 函数,emcc shell 会直接调用这个 python 脚本履行

  1. phase_compile_inputs 进行方针文件的编译,其间界说了 compile_source_file 函数,函数内依据参数组合了指令,指令内容如下。简略来说,em 运用 clang 将hello.c 编译成了可重定向文件 hello_0.o,保存在一个临时文件夹中,target 为 wasm 格局。这个格局的规范如果有兴趣能够在这儿检查到。

    /home/oem/wasm/emsdk/upstream/bin/clang -target wasm32-unknown-emscripten -DEMSCRIPTEN -D__EMSCRIPTEN_major__=3 -D__EMSCRIPTEN_minor__=1 -D__EMSCRIPTEN_tiny__=17 -fignore-exceptions -fvisibility=default -mllvm -combiner-global-alias-analysis=false -mllvm -enable-emscripten-sjlj -mllvm -disable-lsr -Werror=implicit-function-declaration -Xclang -iwithsysroot/include/SDL --sysroot=/home/oem/wasm/emscripten/cache/sysroot -Xclang -iwithsysroot/include/compat /home/oem/wasm/emscripten/zyz/hello.c -c -o /tmp/emscripten_temp_flysqgyd/hello_0.o
    // 提纯简化一下
    clang -target wasm32-unknown-emscripten hello.c -c -o /tmp/emscripten_temp_flysqgyd/hello_0.o
    
  2. 有关体系库的部分在 phase_calculate_linker_input进行调用,这个函数中调用 get_libs_to_link 依据编译选项和环境变量(比方EMCC_FORCE_STDLIBS, -nolibc 之类的),收集了所有要运用的体系库。

  3. phase_link函数真正履行衔接操作,其间的 link_lld函数真正运用 wasm-ld 工具进行衔接操作,也是构建了一个指令如下,这个指令经过 wasm-ld 链接刚刚生成的 hello_0.o 和其他有可能的方针文件。然后经过 -L 指定了体系库地点的文件夹,经过 -l 指定了需求链接的体系库

    /home/oem/wasm/emsdk/upstream/bin/wasm-ld -o /home/oem/wasm/emscripten/zyz/hello.wasm /tmp/emscripten_temp_e31bbpyk/hello_0.o -L/home/oem/wasm/emscripten/cache/sysroot/lib/wasm32-emscripten -lGL -lal -lhtml5 -lstubs-debug -lnoexit -lc-debug -ldlmalloc -lcompiler_rt -lc++-noexcept -lc++abi-noexcept -lsockets -mllvm -combiner-global-alias-analysis=false -mllvm -enable-emscripten-sjlj -mllvm -disable-lsr --import-undefined --strip-debug --export-if-defined=main --export-if-defined=__start_em_asm --export-if-defined=__stop_em_asm --export-if-defined=__main_argc_argv --export-if-defined=fflush --export=emscripten_stack_get_end --export=emscripten_stack_get_free --export=emscripten_stack_get_base --export=emscripten_stack_init --export=stackSave --export=stackRestore --export=stackAlloc --export=__wasm_call_ctors --export=__errno_location --export-table -z stack-size=5242880 --initial-memory=16777216 --no-entry --max-memory=16777216 --global-base=1024
    // 提纯简化一下
    wasm-ld -o hello.wasm /tmp/emscripten_temp_flysqgyd/hello_0.o -L/home/oem/wasm/emscripten/cache/sysroot/lib/wasm32-emscripten -lGL -lc-debug ......
    
  • 所以本质上来说,em 先编译为了 .o 文件,然后运用 wasm-ld 将 .o 文件与体系库进行链接
  • 咱们能够看到 -L 指定了一个目录 emscripten/cache/sysroot/lib/wasm32-emscripten,这表明咱们的体系库地点的目录,所以咱们下一步的方针是找到哪里发生了这个动态。咱们检查了这个目录,这个目录下实在的确实存在体系库,比方 libGL.a 和 libc-debug.a(能够看出这儿发生的是静态库文件,履行静态链接)。链接的时分优先查找 -L 指定的目录,优先于体系的默许目录,所以会链接到这儿

Emscripten 什么时分编译的体系库呢?

  • 首先咱们发现这些体系库都是在 cache 中的,并不在体系源码之内(究竟源码更新能够经过编译重新发生方针文件)。而咱们发现 emcc 也供给了这样的指令 emcc --clear-cache 进行 cache 的消除。

  • 咱们先消除 cache 内容,然后再编译 hello.c,就能够发现 log 里面有 build cache 的进程如下

    cache:INFO: generating system library: sysroot/lib/wasm32-emscripten/libc-debug.a... (this will be cached in "/home/oem/wasm/emscripten/cache/sysroot/lib/wasm32-emscripten/libc-debug.a" for subsequent builds)
    
  • 咱们持续对 emcc 做 debug 找到 build cache 的进程

    1. 经过检查 log 发生的时间能够发现,在编译进程的第二步 phase_calculate_system_librariesget_libs_to_link 函数中,每次调用 add_library 就会进行一次 cache build

    2. 咱们持续追踪就会发现如果 cache 中不存在这个体系库的话,就会进行编译,编译的进程在 system_libs.pyLibrary 类中进行完成

    3. 其间的 build_objects 函数会拼装指令履行,其间一个编译指令如下,这条指令运用 emcc (它本身)编译体系库中的 C 库源代码为一个可重定向文件(不进行链接)。一起,咱们能够看出,体系库文件是在 emscripten/system/lib 中进行存放的,其间的 readme 告知咱们许多体系库是 fork 一个叫做 musl 的项目的。

      /home/oem/wasm/emscripten/emcc -O2 -Werror -fno-unroll-loops -Oz -g -c /home/oem/wasm/emscripten/system/lib/gl/gl.c -o /home/oem/wasm/emscripten/cache/build/libGL/gl.o
      
    4. 当然,库文件可能是由许多文件链接的,所以 create_lib 中将这些进行链接,这儿拼装的指令如下,当然项目的 emar 是直接调用了 llvm-ar 进行拼装,然后生成静态库。

      /home/oem/wasm/emscripten/emar cr /home/oem/wasm/emscripten/cache/sysroot/lib/wasm32-emscripten/libGL.a /home/oem/wasm/emscripten/cache/build/libGL/gl.o /home/oem/wasm/emscripten/cache/build/libGL/libprocaddr.o /home/oem/wasm/emscripten/cache/build/libGL/webgl1.o
      
  • 综上,emsripten 在编译的时分怎么需求而且 cache 中不存在体系库,就会从 emscripten/system/lib 中运用 emcc 本身编译为可重定向文件并运用 llvm-ar 进行链接,然后构成体系库

  • 所以,如果要参阅 emscripten 的体系库完成,则直接检查 emscripten/system/lib 中的源代码即可。