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:可以改变根目录路径的位置:Use dir as the logical root directory for headers and libraries. For example, if the compiler normally searches for headers in /usr/include and libraries in /usr/lib, it instead searches dir/usr/include and dir/usr/lib.
Emscripten 如何编译 WASM 文件?
以下函数来自于 Emscripten源码中 emcc.py 中的 run 函数,emcc shell 会直接调用这个 python 脚本执行
-
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
-
有关系统库的部分在
phase_calculate_linker_input
进行调用,这个函数中调用get_libs_to_link
根据编译选项和环境变量(比如EMCC_FORCE_STDLIBS, -nolibc 之类的),收集了所有要使用的系统库。 -
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 的过程
-
通过查看 log 产生的时间可以发现,在编译过程的第二步
phase_calculate_system_libraries
的get_libs_to_link
函数中,每次调用 add_library 就会进行一次 cache build -
我们继续追踪就会发现如果 cache 中不存在这个系统库的话,就会进行编译,编译的过程在
system_libs.py
的Library
类中进行实现 -
其中的 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
-
当然,库文件可能是由很多文件链接的,所以 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
中的源代码即可。