Прим. перев.: Автор этого материала — архитектор в Barclays и Open Source-энтузиаст из Великобритании Ian Miell. Он задаётся целью сделать удобный образ Docker (со «спящим» бинарником), который не нужно скачивать, а достаточно просто копировать через copy & paste. Методом проб, ошибок и экспериментов с Assembler-кодом он достигает цели, подготовив образ размером менее килобайта.
Однажды коллега показал Docker-образ, который он использовал для тестирования кластеров Kubernetes. Он ничего не делал: просто запускал под и ждал, пока вы его убьёте.
И тут мне стало любопытно, какой же минимальный образ Docker я смогу создать. Хотелось получить такой, что можно было бы закодировать в base64 и отправлять буквально куда угодно простым copy & paste. Поскольку Docker-образ — это просто tar-файл, а tar-файл — это всего лишь файл, всё должно получиться.
В первую очередь мне был нужен очень маленький Linux-бинарник, которые ничего не делает. Потребуется немного волшебства — и вот две замечательные, содержательные и достойные прочтения статьи о создании маленьких исполняемых файлов:
Мне нужен был не «Hello World», а программа, которая просто спит и работает на x86_64. Я начал с примера из первой статьи:
Запустим:
Получается бинарник в 504 байта.
Но всё-таки нужен не «Hello World»… Во-первых, я выяснил, что излишни секции
И он скомпилировался уже в 352 байта.
Но это ещё не искомый результат, потому что программа просто завершает свою работу, а нам нужно, чтобы она спала. В результате дополнительных исследований выяснилось, что команда
А здесь я нашёл нужный список. Syscall 1 — это
Мы сэкономили ещё 8 байтов: компиляция выдала результат в 344 байта, и теперь это подходящий нам бинарник, который ничего не делает и ожидает сигнала.
Настало время достать бензопилу и разобраться с бинарником… Для этого я использовал hexer, который по сути vim для бинарных файлов с возможностью прямого редактирования hex'ов. После продолжительных экспериментов я получил из такого:
… вот это:
Данный код делает то же самое, но обратите внимание, сколько строк и пробелов ушло. В процессе своей работы я руководствовался таким документом, но по большому счёту это был путь проб и ошибок.
Итак, размер уменьшился до 136 байт.
Хотелось узнать, можно ли пойти дальше. Прочитав это, я предположил, что получится дойти до 45 байт, однако — увы! — нет. Описанные там фокусы рассчитаны только на 32-битные бинарники, а для 64-битных не проходили.
Лучшее же, что мне удалось, — взять эту 64-битную версию программы и встроить в свой системный вызов:
Результирующий образ — 127 байт. На этом я прекратил попытки уменьшать размер, но принимаю предложения.
Теперь, когда есть бинарник, реализующий бесконечное ожидание, остаётся положить его в Docker-образ.
Чтобы сэкономить каждый возможный байт, я создал бинарник с файловым именем из одного байта —
Обратите внимание, что в
Далее командой
Ещё я попытался уменьшить размер tar-файла, экспериментируя с manifest-файлом Docker, но тщетно: из-за специфики формата tar и алгоритма сжатия gzip такие изменения приводили только к росту финального gzip'а. Пробовал и другие алгоритмы компрессии, но gzip оказался лучшим для этого маленького файла.
Читайте также в нашем блоге:
Вот он (закодирован в 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-бинарник, которые ничего не делает. Потребуется немного волшебства — и вот две замечательные, содержательные и достойные прочтения статьи о создании маленьких исполняемых файлов:
- «Smallest x86 ELF Hello World»;
- «A Whirlwind Tutorial on Creating Really Teensy ELF Executables for 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. от переводчика
Читайте также в нашем блоге:
- «Linux-дистрибутив from scratch для сборки Docker-образов — наш опыт с dappdeps»;
- « Статистика по базовым операционным системам в образах на Docker Hub»;
- «Собираем Docker-образы для CI/CD быстро и удобно вместе с dapp (обзор доклада и видео)»;
- «Play with Docker — онлайн-сервис для практического знакомства с Docker»;
- «Шпаргалка с командами Docker»;
- «Обзор GUI-интерфейсов для управления Docker-контейнерами».