JavaScript 操作符 Optional chaining (?.)
今天查一个问题的时候, 去reivew 别人最近提交的代码, 发现有下面这个改动, 感觉很奇怪, 怎么去掉了为空的短路判断并且中间加了一个问号? 很是疑惑. 难道提交之前碰到键盘了?
再想想, 如果说这样, 本地测试应该就不会过吧, 于是开始怀疑自己. 果不其然, JavaScript 真的有这种操作符.
今天查一个问题的时候, 去reivew 别人最近提交的代码, 发现有下面这个改动, 感觉很奇怪, 怎么去掉了为空的短路判断并且中间加了一个问号? 很是疑惑. 难道提交之前碰到键盘了?
再想想, 如果说这样, 本地测试应该就不会过吧, 于是开始怀疑自己. 果不其然, JavaScript 真的有这种操作符.
经常做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的文件结构:
从这个图中, 也明显看到: 该文件就是明明在那里.
这时候突然想到, 也许是它的动态链接库文件不存在, 导致它无法加载, 最终报出这个错误. 那么看看它到底需要什么动态链接库吧:
$ 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(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 的时候, 经常想把它做的足够小, 不仅能够节省磁盘空间, 还能减少不必要的依赖, 加快启动, 减少可能出现的漏洞. 那么有哪些最精简的base image 呢?