2022年11月

JavaScript 操作符 Optional chaining (?.)

今天查一个问题的时候, 去reivew 别人最近提交的代码, 发现有下面这个改动, 感觉很奇怪, 怎么去掉了为空的短路判断并且中间加了一个问号? 很是疑惑. 难道提交之前碰到键盘了?
diff.png
再想想, 如果说这样, 本地测试应该就不会过吧, 于是开始怀疑自己. 果不其然, JavaScript 真的有这种操作符.

- 阅读剩余部分 -

通过ldd找到依赖的共享库

经常做docker image, 就会考虑到底用哪个base image 最好? 是用 scratch, 还是用 busybox? 还是用 alpine? 这不仅仅关乎image 的大小, 还关乎带了哪些软件? 是不是经常要打漏洞的patch 等. 在一次尝试中, 忘记这些最精简的 image 中可能连最基本的glibc 都没有, 直接复制可执行文件上去, 发现无法运行, 于是开始研究一下到底缺少了哪些运行时库, 于是开始了研究ldd.


一开始, 我们做了一个最简单打印 hello world 的 c 代码, hello.c 如下:

#include<stdio.h>

int main(int argc, char* argv[]) {
    printf("hello  %s\n", "world");
}

然后编译 并本地运行:

$ gcc -o hello hello.c
$ ./hello
hello  world

开始制作 docker image, Dockerfile 内容:

FROM scratch
COPY ./hello /
CMD ["/hello"]

制作 docker image 的命令:

$ docker build -t helloimage .
Sending build context to Docker daemon  25.09kB
Step 1/3 : FROM scratch
 --->
Step 2/3 : COPY ./hello /
 ---> d86014f25f94
Step 3/3 : CMD ["/hello"]
 ---> Running in ca6e160bbde4
Removing intermediate container ca6e160bbde4
 ---> 51874d5170b3
Successfully built 51874d5170b3
Successfully tagged helloimage:latest

然后运行这个新的image:

docker run --rm helloimage
exec /hello: no such file or directory

为啥我明明已经复制 hello 这个可执行文件到了跟目录, 为啥还说没这个文件? 于是用 dive 命令去查看image的文件结构:
dive.png
从这个图中, 也明显看到: 该文件就是明明在那里.

这时候突然想到, 也许是它的动态链接库文件不存在, 导致它无法加载, 最终报出这个错误. 那么看看它到底需要什么动态链接库吧:

$ docker run --rm helloimage ldd /hello
docker: Error response from daemon: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: exec: "ldd": executable file not found in $PATH: unknown.

奥, 原来这个scratch 啥都没有, ldd 当然也不存在了. 只好本地看看:

$ ldd ./hello
    linux-vdso.so.1 (0x00007ffec9f73000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5b7a06b000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f5b7a276000)

本地 ldd 给出了, hello 其实需要3个动态链接库. 第一个 vdso 其实是一个虚拟的动态链接库, 它不 map 到任何磁盘文件, 主要用来给 clib 使用并加速系统性能的. 第二个 libc.so.6 是 c 的lib, 它mapping 到磁盘文件 /lib/x86_64-linux-gnu/libc.so.6. 第三个 ld-linux-x86-64.so.2 其实是链接器本身.

$ ls -lah /lib/x86_64-linux-gnu/libc.so.6
lrwxrwxrwx 1 root root 12 Apr  6  2022 /lib/x86_64-linux-gnu/libc.so.6 -> libc-2.31.so
$ /lib64/ld-linux-x86-64.so.2
Usage: ld.so [OPTION]... EXECUTABLE-FILE [ARGS-FOR-PROGRAM...]

所以, 我们复制过去的 hello 文件缺少2个动态库文件, 导致它无法运行. 那么如何解决呢?
方案一: 静态编译:
先静态编译hello.c 到 staticHello

$ gcc --static -o staticHello hello.c
$ ls
Dockerfile  hello  hello.c  staticHello

然后修改 Dockerfile:

FROM scratch
COPY ./staticHello /
CMD ["/staticHello"]

然后build image, 并运行:

$ docker build -t helloimage .
$ docker run --rm helloimage
hello  world

完美运行.

方案二: 复制这些缺少的动态库文件过去:
首先复制需要的2个动态库到当前目录 (因为build 的context 是当前目录, 其它目录文件它看不到)

cp /lib/x86_64-linux-gnu/libc.so.6 ./
cp /lib64/ld-linux-x86-64.so.2 ./

然后修改 Dockerfile:

FROM scratch
COPY libc.so.6 /lib/x86_64-linux-gnu/
COPY ld-linux-x86-64.so.2 /lib64/
COPY ./hello /
CMD ["/hello"]

接着 build image 并且运行:

docker build -t helloimage .
Sending build context to Docker daemon  3.121MB
Step 1/5 : FROM scratch
 --->
Step 2/5 : COPY libc.so.6 /lib/x86_64-linux-gnu/
 ---> d72d728c639a
Step 3/5 : COPY ld-linux-x86-64.so.2 /lib64/
 ---> dafed808d94e
Step 4/5 : COPY ./hello /
 ---> d90b74b43ead
Step 5/5 : CMD ["/hello"]
 ---> Running in 27d2b82c8d52
Removing intermediate container 27d2b82c8d52
 ---> 1c419a965e87
Successfully built 1c419a965e87
Successfully tagged helloimage:latest
$ docker run --rm helloimage
hello  world

完美运行.

所以, 2种方案都可以解决这个问题.

关于ldd

回过头来, 我们关注一下 ldd. ldd(List Dynamic Dependencies) 显示依赖的共享对象. 比如 ls 命令依赖这些动态库:

$ ldd /bin/ls
    linux-vdso.so.1 (0x00007fff7834a000)
    libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007fb672c18000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fb672a26000)
    libpcre2-8.so.0 => /usr/lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x00007fb672995000)
    libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fb67298f000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fb672c7c000)
    libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fb67296c000)

ldd 是怎么工作的? ldd 调用标准的链接器 ld.so, 同时设置环境变量 LD_TRACE_LOADED_OBJECTS=1, 这样 ld 就会检查所依赖的所有动态库, 并且找到合适的库并且加载到内存. 这样 ldd 就能记录这些被加载到库以及它们在内存的地址. vdso 和 ld.so 是2个特殊的库.

其实 ldd 是一个shell 脚本, 我们能查看它的源文件:

$ which ldd
/usr/bin/ldd
$ file /usr/bin/ldd
/usr/bin/ldd: Bourne-Again shell script, ASCII text executable
$ less /usr/bin/ldd

另外, 可执行文件的依赖库可以通过 objdump 找到:

$ objdump -p hello | grep NEEDED

如何做一个最小的docker image?

当我们想做一个docker image 的时候, 经常想把它做的足够小, 不仅能够节省磁盘空间, 还能减少不必要的依赖, 加快启动, 减少可能出现的漏洞. 那么有哪些最精简的base image 呢?


  1. 如果你的app的所有依赖都是静态编译的, 那么你可以使用 scratch, 顾名思义, 它是一张白纸, 我们只能使用kernel 提供的服务. 它没有shell, 没有libc, 没有各种实用命令, 用户/组, 没有包管理器, 啥都没有.
  2. 如果你 busybox 提供的实用工具已经能满足, 那么你可以使用 busybox. busybox 是嵌入式linux上的瑞士军刀, 它把其它Linux 上常见的一些实用工具在一个很小的可执行文件内全部实现, 虽然选项更少, 但是基本能完成大部分常见功能.
  3. 如果你在 busybox的基础上还需要 libc库和包管理工具(package repo), 那么可以使用 alpine. alpine 在busybox的基础上, 增加了 musl libc 和 包管理器.
  4. 或者你可以尝试一下 https://github.com/GoogleContainerTools/distroless.