学习Docker时一般刚开始接触的第一个docker image就是hello-world,这个image运行起来的效果也很简单直接,仅仅是在屏幕上输出一段Docker的使用说明就结束了。这个镜像虽然简单,然而仔细分析下还是涉及不少底层机制的。
我之所以会对这个镜像感兴趣,是发现它的大小仅仅只有1.84kB,这实在是太小了,写一个printf("Hello Wolrd\n");的程序编译出来大小就远超1.84kB了,所以很好奇这个镜像是如何构建出来的。
Dockerfile
Docker的镜像构建过程是由其镜像描述文件Dockerfile决定的,所以就先找到其Dockerfile来看看。hello-world用于AMD64架构的Dockerfile可以在Github上找到,只有简单的3行:
1 | FROM scratch |
第1行导入了一个名为scratch的东西,这并不是一个真正的image,可以把它视为是所有image的最底层虚拟镜像,类似于一个基本抽象类,Docker官方对其的说明如下:
This image is most useful in the context of building base images (such as
debianandbusybox) or super minimal images (that contain only a single binary and whatever it requires, such ashello-world).As of Docker 1.5.0 (specifically,
docker/docker#8827),FROM scratchis a no-op in theDockerfile, and will not create an extra layer in your image (so a previously 2-layer image will be a 1-layer image instead).……
You can use Docker’s reserved, minimal image,
scratch, as a starting point for building containers. Using thescratch“image” signals to the build process that you want the next command in theDockerfileto be the first filesystem layer in your image.
后面两行的含义也很直接,把一个名为hello的程序copy到根目录下,在运行image的时候运行此程序。下面就来看下这个如此小的hello world程序是如何实现的。
主程序
hello.c文件的源码也在同一个Github仓库中,省略掉过长的字符串常量后很简单:
1 |
|
这个最简版本的Hello World和C语言教科书中第一个Hello World是有不小差别的。首先是程序入口点上,众所周知正常C/C++程序的入口点是main(),然而这里使用的是_start()。
我们的程序是运行在Linux系统上的,程序的加载与运行必然是由OS发起的,对于Linux来说,OS层面的程序入口点就是_start()而不是main() 函数,一个程序要能正常运行在main()之前是有一些准备工作要做的,比如建立程序运行环境(初始化.bss全局变量等);在main()返回之后也有些收尾工作要处理,比如调用exit()通知系统等。这些工作正常情况下是由语言标准库来完成的,也就是所谓的Runtime运行环境,对于C语言来说就是crt0.o。大部分程序的_start()就位于其中,在建立好运行环境后_start()会调用main()跳转到用户定义的入口点处。当main()返回后程序又将回到ctr0.o中,最终调用exit()通知OS回收进程资源。
这里为了缩小程序体积和简单起见,没有使用标准的ctr0.o Runtime,事实上这一个简单的程序也不需要什么Runtime。程序最后直接通过syscall函数调用了SYS_exit系统调用结束了自身的运行。
将字符串输出到屏幕上也没有使用标准库中的printf(),同样是直接调用了SYS_write这个系统调用,其第一个参数显式的写为了1,其实就是STDOUT_FILENO,Linux系统在unistd.h中定义了stdin, stdout, stderr这几个标准文件描述符。
可以看到,这样一个程序是可以不依赖于任何其他的库在Linux上独立运行的,为了实现不链接C标准库的目的,需要使用一些特殊的编译选项。从编译这个hello-world程序使用的Makefile中可以找到使用的编译选项为:
1 | CFLAGS := -static -Os -nostartfiles -fno-asynchronous-unwind-tables |
-static表示静态链接,虽然对这个程序来说无所谓动态链接还是静态链接……-Os表示为空间进行-O2级别的优化,专门用于减少目标文件大小;-nostartfiles是关键编译选项,此选项表示不使用标准C语言运行库(即crt0.o),也不链接C标准库;-fno-asynchronous-unwind-tables选项也是用于减少代码空间的,其大概含义是不产生C++异常处理机制中使用的.eh_frame段,关于什么是unwind-tables和.eh_frame是个比这篇文章复杂多了的问题,文末有几篇参考资料,之后有空可以深入学习下C++的底层机制……
进行了以上诸多特殊优化处理后,终于可以得到一个只有1k多的可以正常运行于Linux上的Hello World程序了。
参考资料:
What is the use of _start() in C?
When is the gcc flag -nostartfiles used?