Исследуем OpenWRT: чем отличаются образы uImage и sysupgrade

d909e526b8034f0bb821806160cc0c32.jpg
В комментариях к статье «Прошиваем роутер 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 такова:

4a11dccface14747906267e1cddd283b.png

Теперь возьмемся за образ 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.
Итак, получается следующая структура образа:

3d810d4959f94827b32755ac1cb90686.png

Таким образом:

  1. uImage содержит минимально необходимый функционал для запуска OpenWRT и за счет этого его структуру можно легко изменить так, чтобы она проходила проверки на корректность в Web-интерфейсах оригинальных прошивок.
  2. Sysupgrade устроен сложнее и использует Linux-специфичные инструменты — SquashFS и JFFS2.
  3. Оба типа образов не содержат начального загрузчика (и не должны)
  4. При прошивке через начальный загрузчик (через UART или аварийное восстановление) можно сразу шить sysuprade.
  5. При прошивке через 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-источники, из которых сборочный скрипт скачивает исходные коды, уже «протухли», надо искать новые источники, исправлять скрипт и так далее.

© Habrahabr.ru