Docker 使用 alpine 构建镜像时出现添加的可执行文件无法使用的问题

问题起因

笔者今天第一次使用 alpine 作为 docker 的基础镜像,只知道 alpine 是一个极其精简的 Linux 版本,没有更多地去了解。笔者像平时一样用 g++ 编译程序,在本地运行时正常,但在 Dockerfile 中使用 COPY 命令添加后,却出现了无法运行的问题,具体体现为:

$ docker run mazeprog:latest 
/bin/sh: /opt/maze: not found

对应的 Dockerfile 为:

From alpine:latest
COPY ./maze /opt/
COPY ./m2 /opt
RUN chmod 0777 /opt/maze
CMD /opt/maze /opt/m2

其中,maze 是一个 x86_64 的可执行 ELF 文件,m2 是一个主程序使用的文本文件。

解决思路

出现了 not found 的问题后,立刻想到两个可能的原因,可以试着先排查一下。
首先是,是否正确地放置到了预期的位置?这个通过基于构建的镜像运行一下 ls 命令就可以知道:

$ docker run mazeprog ls /opt -l 
total 28
-rw-r--r--    1 root     root           641 Apr 11 14:01 m2
-rwxrwxrwx    1 root     root         23448 Apr 11 14:01 maze

可以看到确实是正确放到了预期的位置 /opt 中。第二个原因是权限问题,上面的输出中可以看到我已经给可执行程序 maze 赋予了 0777 最高权限,但还是无法运行,是什么原因呢?
搜索了很多相同的问题后,问题并没有解决,于是突然想到第三种可能,会不会是平台或运行时环境的问题?关于平台无需多虑,既然是在一台机器上编译和拉取基础镜像制作的,应该不会有问题,但是当我用 file 命令查看 maze 的文件类型时,有一个信息引起了我的注意:dynamically linked
这表示我的程序是动态链接的(在大多数时候我们都默认用 gcc/g++ 编译动态链接的程序,具体不展开介绍),而动态链接就需要依赖运行时环境。在正常的系统中,我们肯定都是安装了 C 程序所需要的那些库的,然而 alpine 不一样。
作为最精简的 Linux 基础镜像,alpine 只有 5MB 多一点,只带了最基本的一些命令所对应的 GNU 程序,并没有携带 C 的运行时环境,然而在执行程序时系统或 docker 并不会告诉你因为缺少运行时环境所以跑不起来,取而代之的则是 not found
要解决这个问题,就要使用静态链接来编译这个程序,在 gcc/g++ 中则加入 -static 选项:

g++ -o ./maze -static main.cpp MazeStack.cpp

保持上面的 Dockerfile 不变,build 一下然后 run,果然程序成功在 docker 里运行起来了,问题解决。

原理剖析

这里涉及到一个程序的加载运行原理:动态链接的程序在编译阶段,只将调用的库函数名和相关重定位信息记录到 ELF 可执行文件中,当程序载入内存运行时,加载器和链接器会根据记录的动态链接信息调入 OS 中的相关运行时的库,和程序动态链接后运行,所以这里就有一个对运行时动态链接库的依赖,但是在 alpine 中并不带有。
反之,静态链接的程序则在编译阶段即将程序会用到的所有链接库函数全部取出来放到程序中作为程序的一部分。静态链接的好处就是不需要程序的运行环境有运行时环境的支持,即可用自身所带的最小需要的库函数等载入运行,这在嵌入式环境中是非常频繁用到的,此外在对运行时环境中某些库版本有需求时,为了保证程序在不同环境下的运行会用到。其坏处则在于当程序涉及的库函数较多时,会花费更多的时间来编译(其实时间就是花在了往程序里附带这些库函数的机器码上)且编译出来的程序和相同版本的动态链接出来的程序相比体积更大,这个很容易理解,毕竟带了那些库函数在自身的 ELF 文件中。

个人主页 2020.4.12