Tools

These “perl” scripts are pretty much bastardized shell scripts based on binwalk and inspired by Neubsi’s SSA->JFFS->SSA utility scripts. They could use a whole lot of touching up, but they do the job.

Here’s what they look like in practice:

rcw@initiative:~/Projects/E4200v2/firmware$ ls
assemble.pl  Binwalk.pm  extract.pl  images
rcw@initiative:~/Projects/E4200v2/firmware$ ./extract.pl images/FW_E4200_2.1.39.145204.SSA 
Image images/FW_E4200_2.1.39.145204.SSA is 19925875 bytes ...
Binwalking images/FW_E4200_2.1.39.145204.SSA ...
JFFS2 offset is 2752512 ...
Reading images/FW_E4200_2.1.39.145204.SSA ...
Writing 2752512 bytes to FW_E4200_2.1.39.145204.kernel ...
Writing 17173363 bytes to FW_E4200_2.1.39.145204.jffs2 ...
Mounting FW_E4200_2.1.39.145204.jffs2 ...
Copying JFFS2 filesystem so we can unmount it ...
Unmounting FW_E4200_2.1.39.145204.jffs2 ...
Cleaning up ...
rcw@initiative:~/Projects/E4200v2/firmware$ du -hs *
4.0K    assemble.pl
4.0K    Binwalk.pm
4.0K    extract.pl
38M     FW_E4200_2.1.39.145204.jffs2
2.7M    FW_E4200_2.1.39.145204.kernel
117M    images
rcw@initiative:~/Projects/E4200v2/firmware$ ./assemble.pl
Using JFFS2: FW_E4200_2.1.39.145204.jffs2
Using kernel: ../build/publication/src/linux/extracted/linux-2.6.35.9/arch/arm/boot/uImage
Copying ../build/publication/src/linux/extracted/linux-2.6.35.9/arch/arm/boot/uImage to ./FW_E4200_2.1.39.145204.kernel ...
FW_E4200_2.1.39.145204.kernel is 2769772 bytes ...
Next boundry at 2883584 bytes ...
Padding FW_E4200_2.1.39.145204.kernel by 113812 bytes ...
Adding build date (20130623222909) to FW_E4200_2.1.39.145204.jffs2/etc/builddate ...
Adding build tag (wolfteck-20130623222909) to FW_E4200_2.1.39.145204.jffs2/etc/version ...
Squashing JFFS2 filesystem to FW_E4200_2.1.39.145204.jffs2.tmp ...
Concatinating FW_E4200_2.1.39.145204.kernel and FW_E4200_2.1.39.145204.jffs2.tmp to FW_E4200_2.1.39.145204-wolfteck-20130623222909.ssa ...
Removing FW_E4200_2.1.39.145204.jffs2.tmp ...
rcw@initiative:~/Projects/E4200v2/firmware$ du -hs *
4.0K    assemble.pl
4.0K    Binwalk.pm
4.0K    extract.pl
43M     FW_E4200_2.1.39.145204.jffs2
2.8M    FW_E4200_2.1.39.145204.kernel
22M     FW_E4200_2.1.39.145204-wolfteck-20130623222909.ssa
117M    images
rcw@initiative:~/Projects/E4200v2/firmware$

Theory

Hat tip to Hugh Dachbach for generously explaining what he remembered of the stock burn and boot processes – thank you for getting me over several hurdles.

MTD Partitions

The E4200v2 has no less than seven NAND flash partitions:

~ # cat /proc/mtd 
dev:    size   erasesize  name
mtd0: 00080000 00020000 "uboot"
mtd1: 00020000 00020000 "u_env"
mtd2: 00020000 00020000 "s_env"
mtd3: 01a00000 00020000 "kernel"
mtd4: 01720000 00020000 "rootfs"
mtd5: 01a00000 00020000 "alt_kernel"
mtd6: 01760000 00020000 "alt_rootfs"
mtd7: 04a00000 00020000 "syscfg"
~ #

They range from 128K for u_env to a whopping 74M for syscfg:

~ # for x in 0 1 2 3 4 5 6 7 ; do echo /dev/mtd$x ; echo ; mtd_debug info /dev/mtd$x ; 
done
/dev/mtd0

mtd.type = MTD_NANDFLASH
mtd.flags = MTD_CAP_ROM
mtd.size = 524288 (512K)
mtd.erasesize = 131072 (128K)
mtd.writesize = 2048 (2K)
mtd.oobsize = 64 
regions = 0

/dev/mtd1

mtd.type = MTD_NANDFLASH
mtd.flags = MTD_CAP_NANDFLASH
mtd.size = 131072 (128K)
mtd.erasesize = 131072 (128K)
mtd.writesize = 2048 (2K)
mtd.oobsize = 64 
regions = 0

/dev/mtd2

mtd.type = MTD_NANDFLASH
mtd.flags = MTD_CAP_NANDFLASH
mtd.size = 131072 (128K)
mtd.erasesize = 131072 (128K)
mtd.writesize = 2048 (2K)
mtd.oobsize = 64 
regions = 0

/dev/mtd3

mtd.type = MTD_NANDFLASH
mtd.flags = MTD_CAP_NANDFLASH
mtd.size = 27262976 (26M)
mtd.erasesize = 131072 (128K)
mtd.writesize = 2048 (2K)
mtd.oobsize = 64 
regions = 0

/dev/mtd4

mtd.type = MTD_NANDFLASH
mtd.flags = MTD_CAP_NANDFLASH
mtd.size = 24248320 (23M)
mtd.erasesize = 131072 (128K)
mtd.writesize = 2048 (2K)
mtd.oobsize = 64 
regions = 0

/dev/mtd5

mtd.type = MTD_NANDFLASH
mtd.flags = MTD_CAP_NANDFLASH
mtd.size = 27262976 (26M)
mtd.erasesize = 131072 (128K)
mtd.writesize = 2048 (2K)
mtd.oobsize = 64 
regions = 0

/dev/mtd6

mtd.type = MTD_NANDFLASH
mtd.flags = MTD_CAP_NANDFLASH
mtd.size = 24510464 (23M)
mtd.erasesize = 131072 (128K)
mtd.writesize = 2048 (2K)
mtd.oobsize = 64 
regions = 0
/dev/mtd7

mtd.type = MTD_NANDFLASH
mtd.flags = MTD_CAP_NANDFLASH
mtd.size = 77594624 (74M)
mtd.erasesize = 131072 (128K)
mtd.writesize = 2048 (2K)
mtd.oobsize = 64 
regions = 0

~ #  

Boot Process

The E4200v2 can boot from either kernel using rootfs for the file system or from alt_kernel using alt_rootfs for the file system. This was designed to make it easy to rollback if a firmware update fails. The early boot process keeps track of failed boots and after 4 or 5, it’ll switch which partitions it tries to load. Yes, that means you can unbrick the device by just power cycling it a handful of times.

Burning to Flash

Unlike many devices which split the firmware file into kernel and rootfs images to burn individually to their respective partitions, the E4200v2 burns the entire image to the kernel partition, which happens to overlap with the rootfs partition. The big takeaways are that the entire images is burned to the kernel portion of the partition pair and the JFFS2 filesystem embedded in the image must start on a boundary that is a multiple of the MTD erase block size.

From linux-2.6.35.8-lgmrvl_041-support-for-single-image-updates.patch in the build tree:

From: Hugh Daschbach 
Date: Mon, 9 May 2011 17:22:46 -0700
Subject: [PATCH] Kernel support for single image updates.

This patch allows MTD partitioning routine to dynamically find the start of the root filesystem.

The underlying implementations still requires two partitions: one for the kernel and one for the root filesystem.  But the expectation is that during boot both filesystems will share the same offset and size.  The root filesystem partitions is expected to be marked with a new attribute: "fs".

During boot, any partition marked "fs" will be scanned looking for the start of a JFFS2 filesystem.  The starting location of the partition will be adjusted to point to the beginning of the filesystem.  

So, for example, the following mtdparts definition:

mtdparts=nand_mtd:640k(uboot)ro,128k@640k(u_env),128k@768k(s_env),\
23m@1m(kernel),23m@1m(rootfs)fs,\
23m@24m(alt_kernel),23m@24m(alt_rootfs)fs,\
23m@47m(downloads),42m@70m(syscfg)

defines two pairs of overlapping partitions: the primary and alternate boot partitions.  Both pairs are 23 MB is size.  Since the rootfs entries in the pairs terminate with the string "fs", the starting offset of these partitions will be adjusted, skipping past the kernel.  It is expected that firmware updates will write a single image of both a kernel and root filesystem image.  This image be written to the "kernel" partition of the pair.  And it is assumed that the JFFS2 filesystem embedded in the image will start a boundary that is an multiple of the MTD erase block size.

Once we know that, it’s pretty easy to calculate the padding needed to make everything work: (from E4200v2 Firmware Assembler)

my $size = -s $kernel;
print "$k is $size bytes ...\n";

my $nextBlock = (int($size/$eraseBlockSize)+1)*$eraseBlockSize;
print "Next boundry at $nextBlock bytes ...\n";

my $pad = $nextBlock-$size;
print "Padding $kernel by $pad bytes ...\n";

Yeah, this implementation will waste $eraseBlockSize bytes on a useless pad if $kernel happens to land perfectly on a boundary instead of just leaving the boundary as-was, but I’m lazy and it will likely never happen anyway.