Pull to refresh
Флант
DevOps-as-a-Service, Kubernetes, обслуживание 24×7

Самый маленький Docker-образ — меньше 1000 байт

Reading time5 min
Views26K
Original author: Ian Miell
Прим. перев.: Автор этого материала — архитектор в Barclays и Open Source-энтузиаст из Великобритании Ian Miell. Он задаётся целью сделать удобный образ Docker (со «спящим» бинарником), который не нужно скачивать, а достаточно просто копировать через copy & paste. Методом проб, ошибок и экспериментов с Assembler-кодом он достигает цели, подготовив образ размером менее килобайта.



Вот он (закодирован в base64)
H4sICIa2A1sCA2IA7Vrrbts2FFYL7M9+7QUGGNyfDYhtkuJFFLAhWZOhBYJmaLMOWBAEFC+xVlkyJLpYEBjdY+0l+k6jfGvqtkEWp2qD8TMg8vAqnsNzDg9lQhhmEjHDhY4zgWJBBUQJ5ZnCGAubMUQMyhJqoRRMJxYbo7Q2CedYxlQO/myqMroeEEHICIngApspxohEKI4h5DHmGEUQQw7jqAejDjBtnKz9q2w7zubi7gkugazVKHdGuWltQArkWDMCdoCqSpufg/QSPK4aV8pxW+nL96uxzMu39G+NqRe5PeekGj13Oi9BamXRmCtl1dS9X2jqel147C7W+aOJKd8dZ04dlcqsSw7KVyA9Ab/uHT/+cTht6mFRKVkMmywv0yv0mnxbMc8sSP8Apzvg0ViDtJwWxQ54Mpbny5W9qIrp2DSrmt+r+mVenu/ny+UelK6+mFR56VYtjsqfp3mxHupQZqZYdp/NGeo850x99r9j7QloyWEz8kvpK//47vuymvzQ29vf79m8MKnIaIa8bUmwRdByw6TKREIoIzE3xBrjrY7MGDUilomQ3GrNrFaIKqSZ4lkvL3tD12sn/IQCrI10xtcC7C1kH9I+xseQpYilRAwoZ5AI9IcfWFfqpRfzK1M3eeUZDRAfQDGAfc/jHTDKG1fVXiInlzcfctnwLPP9Vszs9VXvUzFy5jlZV5WzTbtN3cWkZWkhL/yS2gXm1p7lumkl24wkpv51FbYcU0EZy7SV0ucEZowkiCjvLbAVikCaGUqhyjT0c0Lj/YrElmmSWANOZ7MooHPwRCiLRaJEzBXKFGTCy49lUHNKjEigVdD6H4uTzPj9wzDCSawU0TQT2ujhjVwjgZzSj/n/eX7D/xPm/T8N/v/Ll/+Lg2fPnxw93eL85xFvyB9Rn4TzXwdAAxiMYLD/t9f/7eM/xDja1P+YBf3vKP7L2+PnttsA/IfjcQiE7nkgdH18Ey4O7pjdH7ygmX0p9n8eFA5aG3pb+0/eP/9jzFmw/13AdTBHK3/OPx7/Ic4X8qecQ9K244QG/98JXh8c/vLwwYM1/TD6KWqpv6LdOb37gT67URKterTpVxu1V9PXq3lW1d8skn++9Y83f4cDeEBAQMBnwliWuTWNu8l33G38/3X3fzGk79wFQ4S4Lwr+vwOcXIJHy4ANkLv4L4APcJ6ZSXUsz+efh1xaSOf3VxstHS6+H/nSu4s6wOns9OugxrdG7WXV5K6qc9NEn0n/ESab+s9o0P+O7v9ce1WzVNI7uAiczYI6BgQEBNwD/AvqV/+XACoAAA==

Как я к этому пришёл?


Однажды коллега показал Docker-образ, который он использовал для тестирования кластеров Kubernetes. Он ничего не делал: просто запускал под и ждал, пока вы его убьёте.

«Смотри, он занимает всего 700 килобайт! Его по-настоящему быстро скачивать!»

И тут мне стало любопытно, какой же минимальный образ Docker я смогу создать. Хотелось получить такой, что можно было бы закодировать в base64 и отправлять буквально куда угодно простым copy & paste. Поскольку Docker-образ — это просто tar-файл, а tar-файл — это всего лишь файл, всё должно получиться.

Крохотный бинарник


В первую очередь мне был нужен очень маленький Linux-бинарник, которые ничего не делает. Потребуется немного волшебства — и вот две замечательные, содержательные и достойные прочтения статьи о создании маленьких исполняемых файлов:


Мне нужен был не «Hello World», а программа, которая просто спит и работает на x86_64. Я начал с примера из первой статьи:

	SECTION .data
msg:	db "Hi World",10
len:	equ $-msg

	SECTION .text

        global _start
_start:
	mov	edx,len
	mov	ecx,msg
	mov	ebx,1
	mov	eax,4
	int	0x80

	mov	ebx,0
	mov	eax,1
	int	0x80

Запустим:

nasm -f elf64 hw.asm -o hw.o
ld hw.o -o hw
strip -s hw

Получается бинарник в 504 байта.

Но всё-таки нужен не «Hello World»… Во-первых, я выяснил, что излишни секции .data или .text и не требуется загрузка данных. Вдобавок, верхняя половина секции _start занимается выводом текста. В итоге, я попробовал следующий код:

global _start
_start:
 mov ebx,0
 mov eax,1
 int 0x80

И он скомпилировался уже в 352 байта.

Но это ещё не искомый результат, потому что программа просто завершает свою работу, а нам нужно, чтобы она спала. В результате дополнительных исследований выяснилось, что команда mov eax заполняет регистр процессора соответствующим номером системного вызова Linux, а int 0x80 производит сам вызов. Подробнее это описано здесь.

А здесь я нашёл нужный список. Syscall 1 — это exit, а нужный нам — это syscall 29:pause. Получилась такая программа:

global _start
_start:
 mov eax, 29
 int 0x80

Мы сэкономили ещё 8 байтов: компиляция выдала результат в 344 байта, и теперь это подходящий нам бинарник, который ничего не делает и ожидает сигнала.

Копаясь в hex'ах


Настало время достать бензопилу и разобраться с бинарником… Для этого я использовал hexer, который по сути vim для бинарных файлов с возможностью прямого редактирования hex'ов. После продолжительных экспериментов я получил из такого:



… вот это:



Данный код делает то же самое, но обратите внимание, сколько строк и пробелов ушло. В процессе своей работы я руководствовался таким документом, но по большому счёту это был путь проб и ошибок.

Итак, размер уменьшился до 136 байт.

Меньше 100 байт?


Хотелось узнать, можно ли пойти дальше. Прочитав это, я предположил, что получится дойти до 45 байт, однако — увы! — нет. Описанные там фокусы рассчитаны только на 32-битные бинарники, а для 64-битных не проходили.

Лучшее же, что мне удалось, — взять эту 64-битную версию программы и встроить в свой системный вызов:

BITS 64
 org 0x400000
 
ehdr: ; Elf64_Ehdr
 db 0x7f, "ELF", 2, 1, 1, 0 ; e_ident
 times 8 db 0
 dw 2 ; e_type
 dw 0x3e ; e_machine
 dd 1 ; e_version
 dq _start ; e_entry
 dq phdr - $$ ; e_phoff
 dq 0 ; e_shoff
 dd 0 ; e_flags
 dw ehdrsize ; e_ehsize
 dw phdrsize ; e_phentsize
 dw 1 ; e_phnum
 dw 0 ; e_shentsize
 dw 0 ; e_shnum
 dw 0 ; e_shstrndx
 ehdrsize equ $ - ehdr
 
phdr: ; Elf64_Phdr
 dd 1 ; p_type
 dd 5 ; p_flags
 dq 0 ; p_offset
 dq $$ ; p_vaddr
 dq $$ ; p_paddr
 dq filesize ; p_filesz
 dq filesize ; p_memsz
 dq 0x1000 ; p_align
 phdrsize equ $ - phdr
 
_start:
 mov eax, 29
 int 0x80
 
filesize equ $ - $$

Результирующий образ — 127 байт. На этом я прекратил попытки уменьшать размер, но принимаю предложения.

Крохотный Docker-образ


Теперь, когда есть бинарник, реализующий бесконечное ожидание, остаётся положить его в Docker-образ.

Чтобы сэкономить каждый возможный байт, я создал бинарник с файловым именем из одного байта — t — и поместил его в Dockerfile, создавая практически пустой образ:

FROM scratch
ADD t /t

Обратите внимание, что в Dockerfile нет CMD, поскольку это увеличило бы размер образа. Для запуска понадобится передавать команду через аргументы к docker run.

Далее командой docker save был создан tar-файл, а затем — сжат с максимальной компрессией gzip. Получился портируемый файл Docker-образа размером менее 1000 байт:

$ docker build -t t .
$ docker save t | gzip -9 - | wc -c
976

Ещё я попытался уменьшить размер tar-файла, экспериментируя с manifest-файлом Docker, но тщетно: из-за специфики формата tar и алгоритма сжатия gzip такие изменения приводили только к росту финального gzip'а. Пробовал и другие алгоритмы компрессии, но gzip оказался лучшим для этого маленького файла.

P.S. от переводчика


Читайте также в нашем блоге:

Tags:
Hubs:
+55
Comments11

Articles

Information

Website
flant.ru
Registered
Founded
Employees
201–500 employees
Location
Россия
Representative
Тимур Тукаев