Исследуем OpenWRT: чем отличаются образы uImage и sysupgrade
В комментариях к статье «Прошиваем роутер Upvel UR-313N4G на OpenWRT» между вашим покорным слугой и уважаемым Maysoft завязался спор насчет различий в структуре образов uImage и sysupgrade прошивки OpenWRT. Я обещал Maysoft разобраться в проблеме, и вот перед вами эта статья.
Как известно, в каталоге загрузок OpenWRT доступны, по большей части, прошивки двух типов — uImage и sysupgrade, например:
openwrt-15.05-rc3-ramips-rt305x-dir-320-b1-initramfs-uImage.bin
openwrt-15.05-rc3-ramips-rt305x-dir-320-b1-squashfs-sysupgrade.bin
Официальный FAQ пишет об их различиях весьма скупо:
What is the difference between the different image formats?
a factory image is one built for the bootloader flasher or stock software flasher
a sysupgrade image (previously named trx image) is designed to be flashed from within openwrt itself
The two have the same content, but a factory image would have extra header information or whatever the platform needs. Generally speaking, the factory image is to be used with the OEM GUI or OEM flashing utilities to convert the device to OpenWrt. After that, use the sysupgrade images.
Согласно документации, содержание образов идентично, за исключением того, что в образе factory присутствуют дополнительные заголовки, чтобы этот образ можно было прошить через Web-интерфейс оригинальной прошивки.
Отлично, сравним размер прошивок:
openwrt-15.05-rc3-ramips-rt305x-dir-320-b1-initramfs-uImage.bin — 3253035 байт.
openwrt-15.05-rc3-ramips-rt305x-dir-320-b1-squashfs-sysupgrade.bin — 3407876 байт.
Ого, прошивка sysupgrade почти на 140 Кб больше uImage, а по документации они должны быть примерно одного размера, причем uImage за счет этой самой «extra header information» — немного больше.
Конечно, достаточно посмотреть в сборочные скрипты, чтобы понять, чем различаются uImage и sysupgrade-образы, но это, согласитесь, неспортивно. Сегодня мы будем анализировать прошивки «в лоб», как будто у нас нет исходников, а уже в конце заглянем в сборочные скрипты, чтобы подтвердить наши догадки.
Основным средством анализа прошивок на данный момент является утилита binwalk, доступная под Linux. Переименуем файлы прошивок покороче, чтобы нам было удобнее, и начнем анализ.
> binwalk ./uImage.bin
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 uImage header, header size: 64 bytes, header CRC: 0x19DE1499, created: Fri Jul 3 22:16:00 2015, image size: 3252971 bytes, Data Address: 0x80000000, Entry Point: 0x80000000, data CRC: 0x886ADE01, OS: Linux, CPU: IPS, image type: OS Kernel Image, compression type: lzma, image name: "MIPS OpenWrt Linux-3.18.17"
64 0x40 LZMA compressed data, properties: 0x6D, dictionary size: 8388608 bytes, uncompressed size: 5479932 bytes
Кажется, вся прошивка представляет собой образ uImage — в начале следует заголовок длиной 64 (0×40) байт, а вслед за нам — поток данных, сжатый алгоритмом LZMA, размером 3252971 байт. Сложим 64 и 3252971, получим 3253035 байт, то есть размер скачанного образа. Следовательно, кроме образа uImage в файле больше ничего нет. Binwalk умеет распаковывать найденные LZMA-потоки. В принципе, можно вручную отрезать от файла первые 64 байта и распаковать остаток командой lzma -d, но зачем, когда есть такой удобный инструмент?
> binwalk -e ./uImage.bin
Посмотрим, что у нас получилось
> ls -l ./_uImage.bin.extracted/
итого 8532
-rw-r--r-- 1 user user 5479932 авг 8 23:10 40
-rw-r--r-- 1 user user 3252971 авг 8 23:10 40.7z
Файл с именем 40 (смещение в исходном файле) — и есть распакованный поток. Натравим на него binwalk:
binwalk ./_uImage.bin.extracted/40
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
2812692 0x2AEB14 Linux kernel version "3.18.17 (buildbot@builder1) (gcc version 4.8.3 (OpenWrt/Linaro c version 4.8.3 (OpenWrt/Linaro GCC 4.8-2014.04 r46018) ) #2 Fr"
2932132 0x2CBDA4 LZMA compressed data, properties: 0x5D, dictionary size: 16777216 bytes, missing uncompressed size
2936592 0x2CCF10 xz compressed data
3400392 0x33E2C8 LZMA compressed data, properties: 0x6D, dictionary size: 1048576 bytes, uncompressed size: -1 bytes
А здесь у нас что-то, на первый взгляд, непонятное — binwalk обнаружил ядро Linux по смещению 0×2AEB14 и три сжатых потока данных, следующих за ядром. Дело в том, что binwalk для анализа использует эвристики и то, что получается у него на выходе — не истина в последней инстанции, а информация к размышлению.
Здравый смысл подсказывает, что ядро должно начинаться со смещения 0, а сжатый поток — быть один и содержать initramfs — начальную файловую систему, загружаемую в RAM. О том же говорит и документация на ядро:
What is initramfs?
— All 2.6 Linux kernels contain a gzipped «cpio» format archive, which is extracted into rootfs when the kernel boots up. After extracting, the kernel checks to see if rootfs contains a file «init», and if so it executes it as PID 1.
и далее
Populating initramfs:
— The 2.6 kernel build process always creates a gzipped cpio format initramfs archive and links it into the resulting kernel binary. By default, this archive is empty (consuming 134 bytes on x86).
Здесь же упоминается и формат потока — CPIO.
Посмотрим, что binwalk сможет извлечь из нашего образа:
> binwalk -e ./_uImage.bin.extracted/40
ls -l ./_uImage.bin.extracted/_40.extracted/
итого 14028
-rw-r--r-- 1 user user 2547808 авг 8 23:25 2CBDA4.7z
-rw-r--r-- 1 user user 2543340 авг 8 23:25 2CCF10.tar
-rw-r--r-- 1 user user 7186944 авг 8 23:25 33E2C8
-rw-r--r-- 1 user user 2079540 авг 8 23:25 33E2C8.7z
Итак, успешно распаковался только поток по смещению 33E2C8. Если мы все правильно делаем, то это должен быть CPIO-контейнер с файловой системой:
> binwalk _uImage.bin.extracted/_40.extracted/33E2C8
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 ASCII cpio archive (SVR4 with no CRC), file name: "dev", file name length: "0x00000004", file size: "0x00000000"
116 0x74 ASCII cpio archive (SVR4 with no CRC), file name: "dev/console", file name length: "0x0000000C", file size: "0x00000000"
240 0xF0 ASCII cpio archive (SVR4 with no CRC), file name: "lib", file name length: "0x00000004", file size: "0x00000000"
356 0x164 ASCII cpio archive (SVR4 with no CRC), file name: "lib/netifd", file name length: "0x0000000B", file size: "0x00000000"
480 0x1E0 ASCII cpio archive (SVR4 with no CRC), file name: "lib/netifd/netifd-wireless.sh", file name length: "0x0000001E", file size: "0x00001638"
***********Куча файлов***********
7186416 0x6DA7F0 ASCII cpio archive (SVR4 with no CRC), file name: "dev/urandom", file name length: "0x0000000C", file size: "0x00000000"
7186540 0x6DA86C ASCII cpio archive (SVR4 with no CRC), file name: "dev/pts", file name length: "0x00000008", file size: "0x00000000"
7186660 0x6DA8E4 ASCII cpio archive (SVR4 with no CRC), file name: "TRAILER!!!", file name length: "0x0000000B", file size: "0x00000000"
В конце архива мы видим файл с именем TRAILER!!!… который. согласно документации, является меткой конца архива.
Значит, структура прошивки initramfs-uImage такова:
Теперь возьмемся за образ squashfs-sysupgrade. Из названия следует, что в образе содержится (кроме ядра) файловая система squashfs. Посмотрим, так ли это:
> binwalk -e ./sysupgrade.bin
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 uImage header, header size: 64 bytes, header CRC: 0x66CC90D2, created: Mon Jul 6 21:54:35 2015, image size: 1142606 bytes, Data Address: 0x80000000, Entry Point: 0x80000000, data CRC: 0x91B77696, OS: Linux, CPU: MIPS, image type: OS Kernel Image, compression type: lzma, image name: "MIPS OpenWrt Linux-3.18.17"
64 0x40 LZMA compressed data, properties: 0x6D, dictionary size: 8388608 bytes, uncompressed size: 3396940 bytes
1142670 0x116F8E Squashfs filesystem, little endian, version 4.0, compression:lzma (non-standard type definition), size: 2221946 bytes, 1132 inodes, blocksize: 262144 bytes, created: Mon Jul 6 21:54:02 2015
Возьмемся за арифметику: 64 + 1142606 (image size) = 1142670, как раз по этому смещению начинается образ squashfs, а заканчивается он по смещению 1142670 + 2221946 = 3364616. Размер образа, между тем, 3407876 байт, значит, у нас есть еще 3407876 — 3364616 = 43260 байт неидентифицированной информации. Посмотрим, что там, hexdump«ом
> hexdump -s 3364616 ./sysupgrade.bin
0335708 ffff ffff ffff ffff ffff ffff ffff ffff
*
0335ff8 ffff ffff ffff ffff adde dec0 ffff ffff
0336008 ffff ffff ffff ffff ffff ffff ffff ffff
*
0337ff8 ffff ffff ffff ffff adde dec0 ffff ffff
0338008 ffff ffff ffff ffff ffff ffff ffff ffff
*
033fff8 ffff ffff ffff ffff adde dec0
0340004
Тут явно какая-то заглушка. Вернемся к ней позднее.
Посмотрим, что у нас в каталоге с распакованным образом:
> ls -l _sysupgrade.bin.extracted/
итого 8820
-rw-r--r-- 1 user user 2221946 авг 8 23:40 116F8E.squashfs
-rw-r--r-- 1 user user 3396940 авг 8 23:40 40
-rw-r--r-- 1 user user 3407812 авг 8 23:40 40.7z
Распакуем LZMA-поток по смещению 40:
binwalk -e _sysupgrade.bin.extracted/40
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
2812644 0x2AEAE4 Linux kernel version "3.18.17 (buildbot@builder1) (gcc version 4.8.3 (OpenWrt/Linaro c version 4.8.3 (OpenWrt/Linaro GCC 4.8-2014.04 r46018) ) #1 Fr"
2932068 0x2CBD64 LZMA compressed data, properties: 0x5D, dictionary size: 16777216 bytes, missing uncompressed size
2936444 0x2CCE7C xz compressed data
3396424 0x33D348 ASCII cpio archive (SVR4 with no CRC), file name: "dev", file name length: "0x00000004", file size: "0x00000000"
3396540 0x33D3BC ASCII cpio archive (SVR4 with no CRC), file name: "dev/console", file name length: "0x0000000C", file size: "0x00000000"
3396664 0x33D438 ASCII cpio archive (SVR4 with no CRC), file name: "root", file name length: "0x00000005", file size: "0x00000000"
3396780 0x33D4AC ASCII cpio archive (SVR4 with no CRC), file name: "TRAILER!!!", file name length: "0x0000000B", file size: "0x00000000"
Здесь у нас ядро Linux и небольшой initramfs-образ с четырьмя файлами. Остальное, видимо, переместилось в образ squashfs:
> unsquashfs -i ./_sysupgrade.bin.extracted/116F8E.squashfs
Parallel unsquashfs: Using 4 processors
1033 inodes (1034 blocks) to write
squashfs-root
squashfs-root/bin
squashfs-root/bin/ash
squashfs-root/bin/board_detect
squashfs-root/bin/busybox
***********Куча файлов***********
squashfs-root/www/luci-static/resources/load.svg
squashfs-root/www/luci-static/resources/wifirate.svg
squashfs-root/www/luci-static/resources/wireless.svg
squashfs-root/www/luci-static/resources/xhr.js
created 848 files
created 99 directories
created 184 symlinks
created 0 devices
created 0 fifos
Действительно, основная файловая система содержится в образе squashfs.
Теперь разберемся с заглушкой в конце образа. Есть подозрение, что она как-то относится к ФС JFFS2, потому что при прошивке образа sysupgrade и последующей загрузке в dmesg появляются строки:
[ 29.550000] jffs2_scan_eraseblock(): End of filesystem marker found at 0x0
[ 29.580000] jffs2_build_filesystem(): unlocking the mtd device... done.
[ 29.590000] jffs2_build_filesystem(): erasing all blocks after the end marker...
а при прошивке и загрузке образа uImage этих строк нет. Поиск в «ванильном» ядре по этим строкам ничего не дает, а вот в дереве исходных кодов OpenWRT такие строки есть:
--- a/fs/jffs2/build.c
+++ b/fs/jffs2/build.c
@@ -114,6 +114,16 @@ static int jffs2_build_filesystem(struct
dbg_fsbuild("scanned flash completely\n");
jffs2_dbg_dump_block_lists_nolock(c);
+ if (c->flags & (1 << 7)) {
+ printk("%s(): unlocking the mtd device... ", __func__);
+ mtd_unlock(c->mtd, 0, c->mtd->size);
+ printk("done.\n");
+
+ printk("%s(): erasing all blocks after the end marker... ", __func__);
+ jffs2_erase_pending_blocks(c, -1);
+ printk("done.\n");
+ }
+
dbg_fsbuild("pass 1 starting\n");
c->flags |= JFFS2_SB_FLAG_BUILDING;
/* Now scan the directory tree, increasing nlink according to every dirent found. */
--- a/fs/jffs2/scan.c
+++ b/fs/jffs2/scan.c
@@ -148,8 +148,14 @@ int jffs2_scan_medium(struct jffs2_sb_in
/* reset summary info for next eraseblock scan */
jffs2_sum_reset_collected(s);
- ret = jffs2_scan_eraseblock(c, jeb, buf_size?flashbuf:(flashbuf+jeb->offset),
- buf_size, s);
+ if (c->flags & (1 << 7)) {
+ if (mtd_block_isbad(c->mtd, jeb->offset))
+ ret = BLK_STATE_BADBLOCK;
+ else
+ ret = BLK_STATE_ALLFF;
+ } else
+ ret = jffs2_scan_eraseblock(c, jeb, buf_size?flashbuf:(flashbuf+jeb->offset),
+ buf_size, s);
if (ret < 0)
goto out;
@@ -561,6 +567,17 @@ full_scan:
return err;
}
+ if ((buf[0] == 0xde) &&
+ (buf[1] == 0xad) &&
+ (buf[2] == 0xc0) &&
+ (buf[3] == 0xde)) {
+ /* end of filesystem. erase everything after this point */
+ printk("%s(): End of filesystem marker found at 0x%x\n", __func__, jeb->offset);
+ c->flags |= (1 << 7);
+
+ return BLK_STATE_ALLFF;````
+ }
+
/* We temporarily use 'ofs' as a pointer into the buffer/jeb */
ofs = 0;
max_ofs = EMPTY_SCAN_SIZE(c->sector_size);
Ага, вот и наша заглушка 0xDEADCODE. Если драйвер JFFS2 находит эту метку, то он считает ее концом предыдущей файловой системы и стирает все, начиная с нулевого байта метки и заканчивая концом накопителя. Таким образом сама метка также затирается.
После этого драйвер создает на этом месте новый экземпляр JFFS2.
Итак, получается следующая структура образа:
Таким образом:
- uImage содержит минимально необходимый функционал для запуска OpenWRT и за счет этого его структуру можно легко изменить так, чтобы она проходила проверки на корректность в Web-интерфейсах оригинальных прошивок.
- Sysupgrade устроен сложнее и использует Linux-специфичные инструменты — SquashFS и JFFS2.
- Оба типа образов не содержат начального загрузчика (и не должны)
- При прошивке через начальный загрузчик (через UART или аварийное восстановление) можно сразу шить sysuprade.
- При прошивке через mtd_write, если эта утилита доступна через telnet из официальной прошивки, также можно сразу шить sysupgrade.
В заключении заглянем сборочные скрипты OpenWRT, чтобы убедиться в своих выводах. Начнем с файла /target/linux/ramips/Makefile. Но если вы думаете, что это обычный GNU Makefile, то это не так. Вот как описывают улучшенный Makefile сами разработчики:
Looking at one of the package makefiles, you’d hardly recognize it as a makefile. Through what can only be described as blatant disregard and abuse of the traditional make format, the makefile has been transformed into an object oriented template which simplifies the entire ordeal.
Кажется, сборка образа запускается в строке 564:
Image/Build/Profile/DIR-320-B1=$(call BuildFirmware/Default8M/$(1),$(1),dir-320-b1,DIR-320-B1)
Цель BuildFirmware/Default8M описана в строках 195 и 196:
BuildFirmware/Default8M/squashfs=$(call BuildFirmware/OF,$(1),$(2),$(3),$(ralink_default_fw_size_8M),$(4))
BuildFirmware/Default8M/initramfs=$(call BuildFirmware/OF/initramfs,$(1),$(2),$(3),$(4))
Она состоит из двух позиций: squashfs и initramfs. Цели BuildFirmware/OF и BuildFirmware/OF/initramfs описаны в том же файле чуть выше
149 # $(1), Rootfs type, e.g. squashfs
150 # $(2), lowercase board name
151 # $(3), DTS filename without .dts extension
152 # $(4), maximum size of sysupgrade image
153 # $(5), uImage header's ih_name field
154 define BuildFirmware/OF
155 $(call MkImageLzmaDtb,$(2),$(3),$(5))
156 $(call MkImageSysupgrade/$(1),$(1),$(2),$(4),$(6))
157 endef
и
169 # $(1), squashfs/initramfs
170 # $(2), lowercase board name
171 # $(3), DTS filename without .dts extension
172 # $(4), ih_name field of uImage header
173 define BuildFirmware/OF/initramfs
174 $(call MkImageLzmaDtb,$(2),$(3),$(4),-initramfs)
175 $(CP) $(KDIR)/vmlinux-$(2)-initramfs.uImage $(call imgname,$(1),$(2))-uImage.bin
176 endef
По имени цели MkImageLzmaDtb можно догадаться, что она создает образ uImage по описанию из DTS-файла. Для цели BuildFirmware/OF/initramfs к этому образу добавляется раздел initramfs с основной файловой системой, после чего образ копируется в выходной каталог. Для цели BuildFirmware/OF исходный образ обрабатывается функцией MkImageSysupgrade, которая с помощью команды «cat» прицепляет к нему раздел squashfs, а затем, взывает функцию prepare_generic_squashfs, определенную в файле image.mk.
# pad to 4k, 8k, 16k, 64k, 128k, 256k and add jffs2 end-of-filesystem mark
110 define prepare_generic_squashfs
111 $(STAGING_DIR_HOST)/bin/padjffs2 $(1) 4 8 16 64 128 256
112 endef
А утилита padjffs2, написанная на C, записывает отметку 0xDEADCODE в конец файла образа:
static unsigned char eof_mark[4] = {0xde, 0xad, 0xc0, 0xde};
***
static unsigned char *pad = eof_mark;
***
/* write out the JFFS end-of-filesystem marker */
t = write(fd, pad, pad_len);
if (t != pad_len) {
ERRS("Unable to write to %s", name);
goto close;
}
В общем, мир OpenWRT прекрасен и удивителен, особенно доставляют комментарии типа:
# The real magic happens inside these templates
или
/* vodoo from original driver */
В заключение хочется сказать, что при сборке OpenWRT из исходных кодов нужно обращать внимание на сборочную среду, например, в этом репозитории на моем Debian 8+Sid не собирается даже набор инструментов для кросс-компиляции. А вот на Debian 7 все это богатство собирается просто изумительно, если не править начальную конфигурацию. Если же ее поправить, может оказаться, что некоторые Web-источники, из которых сборочный скрипт скачивает исходные коды, уже «протухли», надо искать новые источники, исправлять скрипт и так далее.