原文:A 1 KB Docker Container
作者:Nathan Osman
翻译:雁惊寒
摘要:本文介绍了如何使用汇编程序编写一个极小的docker容器。以下是译文。
不,这不是打错字,也不是玩笑。我创建了一个Docker容器,该容器包含一个Unix可执行文件,没有其他依赖关系,磁盘空间占用不足1KB。容器中没有其他文件,甚至没有libc。
为什么?
在解释如何实现这个容器之前,应该先解释下为什么要这么做。 caddy-docker(这是我写的另外一个工具,在这里有详细的说明)将传入的请求根据其标签路由到其他运行的容器中去。
我需要用caddy-docker作为特定主机的反向代理,要实现这个最简单的方法就是启动一个容器,这个容器唯一的目的就是包含两个特殊的标签,并且容器在停止之前不应该做任何事情。
就在那个时候我想到了这个绝妙透顶的主意。
什么?
我立即开始研究这个应用程序,并因为其不寻常的目的而命名为“hang”。 Go可以轻松生成没有依赖关系的可执行文件,允许Docker容器从scratch
继承。唯一的缺点是,Go可执行文件相当的大,大小通常会超过8MB。
这绝对不行。
我认为,编写一个这样的C程序很容易:为SIGTERM注册一个信号处理程序,并在收到这个信号的时候退出。不幸的是,这意味着我需要使用libc,这样,容器很快会变得与Go可执行文件差不多大小。这根本就没有任何优势。
汇编?
是的,生成一个没有依赖关系的小型可执行文件的最快方法是用汇编编写。我更喜欢Intel风格的语法,所以,NASM是不二选择。
系统调用
曾几何时,在x86架构出现的早些年间,系统调用看起来是这样的:
mov eax, 0x01
mov ebx, 0x00
int 0x80
第一行指定要执行哪个系统调用 - sys_exit
。第二行指定返回值(0)。第三行产生一个内核后续会处理的中断。
x86操作系统后来转为使用sysenter
/sysret
,而x86_64则引入了一个新的操作码:syscall
。与上面的例子类似,rax
寄存器用于指定要调用的特定系统调用。上面的示例可以在x86_64程序集中进行重写,如下所示:
mov rax, 0x3c
mov rdi, 0x00
syscall
请注意,sys_exit
的系统调用号在x86_64上是不同的。
信号处理程序
在C中注册信号处理程序很普通:
#include <signal.h>
void handler(int param) {}
int main() {
struct sigaction sa;
sa.sa_handler = handler;
sigaction(SIGTERM, &sa, 0);
return 0;
}
不幸的是,C标准库隐瞒了以下这几件事情:
- 标志
SA_RESTORER
添加到了sa.sa_flags
上 sa.sa_restorer
成员上设置了一个特殊的函数
我们不能直接将C代码转换成汇编,因为sigaction
的结构与sys_rt_sigaction
所期望的不一致。以下是NASM中内核结构的样子:
struc sigaction
.sa_handler resq 1
.sa_flags resq 1
.sa_restorer resq 1
.sa_mask resq 1
endstruc
每个成员的大小为8字节。
设置信号处理程序
首先,我们必须在.bss
段中为该结构体分配空间:
section .bss
act resb sigaction_size
请注意,sigaction_size
是汇编程序为我们创建的特殊值 - 它等于sigaction
的大小(以字节为单位)。然后可以在.text
段中初始化该结构体,如下所示:
section .text
global _start
lea rax, [handler]
mov [act + sigaction.sa_handler], rax
mov [act + sigaction.sa_flags], dword 0x04000000 ; SA_RESTORER
lea rax, [restorer]
mov [act + sigaction.sa_restorer], rax
handler
和restorer
这两个标签我们稍后会提到。现在我们可以调用sys_rt_sigaction
这个系统调用了:
mov rax, 0x0d ; sys_rt_sigaction
mov rdi, 0x0f ; SIGTERM
lea rsi, [act]
mov rdx, 0x00
mov r10, 0x08
syscall
处理信号
下一步是等待SIGTERM
信号的到来。 sys_pause
这个系统调用可以用下面这种方式轻松地实现:
mov rax, 0x22 ; sys_pause
syscall
处理程序本身很普通,它没有做任何事情:
handler:
ret
恢复器(restorer)也很简单,虽然它需要调用sys_rt_sigreturn
系统调用:
restorer:
mov rax, 0x0f ; sys_rt_sigreturn
syscall
构建
需要两个命令来构建应用程序。假定源文件名为hang.asm
,则命令是:
nasm -f elf64 hang.asm
ld -s -o hang hang.o
这将产生一个名为hang
的可执行文件,它很小:
$ stat hang
File: hang
Size: 736
是的,它只有736字节。
Dockerfile
相当简单,只需要两个命令:
FROM scratch
ADD hang /usr/bin/hang
ENTRYPOINT ["/usr/bin/hang"]
测试
我们来看看容器是否能工作:
$ docker build -t nathanosman/hang .
$ docker run -d --name hang nathanosman/hang
此时,容器应该保持运行状态:
$ docker ps -a
CONTAINER ID IMAGE COMMAND STATUS
f1861f628ea8 nathanosman/hang "/usr/bin/hang" Up 3 seconds
当执行docker stop
时,应该立即停止:
$ docker stop hang
hang
有用!我们来确认以下容器的大小是否和可执行文件的大小一致:
$ docker images
REPOSITORY TAG CREATED SIZE
nathanosman/hang latest 2 minutes ago 736B
是的!一个非常小的容器!
链接
你可以在这里找到源代码:
https://github.com/nathan-osman/hang
2017年10月14日,SDCC 2017之大数据技术实战线上峰会即将召开,邀请圈内顶尖的布道师、技术专家和技术引领者,共话大数据平台构建、优化提升大数据平台的各项性能、Spark部署实践、企业流平台实践、以及实现应用大数据支持业务创新发展等核心话题,七位大牛与你相聚狂欢,详情查看所有嘉宾和议题,以及注册参会,分享还可优惠30元。