通过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的文件结构:
从这个图中, 也明显看到: 该文件就是明明在那里.
这时候突然想到, 也许是它的动态链接库文件不存在, 导致它无法加载, 最终报出这个错误. 那么看看它到底需要什么动态链接库吧:
$ 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