一个仅有1KB大小的Docker容器

更新时间:2017-10-16 09:35:14点击次数:347次

摘要:本文介绍了如何使用汇编程序编写一个极小的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 

handlerrestorer这两个标签我们稍后会提到。现在我们可以调用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 

是的!一个非常小的容器!

  • 项目经理 点击这里给我发消息
  • 项目经理 点击这里给我发消息
  • 项目经理 点击这里给我发消息
  • 项目经理 点击这里给我发消息