Using GPT on eMMC with ODROID-C2
I wanted to use UEFI with ODROID-C2 and while that is possible with U-Boot on
MBR, bootctl install
doesn’t work and I lose out on
systemd-gpt-auto-generator
figuring out the partitions for the specific
install. Unfortunately, the bootloader on ODROID-C2 is written to sector
1 which conflicts with the GPT header.
Some other boards include SPI flash or the SoC supports eMMC boot0. But testing showed that boot0 didn’t work on the Amlogic S905/GXBB (and this is confirmed by U-Boot’s Amlogic docs). (It’s also not fun/interesting to mix two of eMMC, SD, or USB for booting.)
Introduction
A normal GPT disk has an MBR on sector 0, GPT header on sector 1, and the GPT table entries on sectors 2-33. The MBR and GPT header can’t be adjusted, but the GPT table entries can be moved or changed in size. The specification says “A minimum of 16,384 bytes of space must be reserved for the GPT Partition Entry Array,” which is why 32 sectors are used when the sector size is 512. In practice, smaller sizes work fine. Each entry is 128 bytes, so each sector can hold 4 entries.
For ODROID-C2, a bootloader header is actually written to both sector 0 and sector 1. This is because the Amlogic S905 boot ROM reads from sector 0 for eMMC and sector 1 for SD. eMMC had been a problem for other Amlogic S905 boards, but @chewitt mentions that apparently HardKernel got a different bootloader that only stores a small amount on sector 0 to leave space for the MBR. An old commit by @Kwiboo describes how a single disk image can boot both SD and eMMC. It also points out that the bootloader was previously open source. And it created the potentially-convenient aml_chksum (available now also in a LibreELEC repo).
The commit description is pretty helpful to understanding the Amlogic boot and our options, so let’s look at it:
Create u-boot.bin.hardkernel that can be booted from both SD and eMMC
SD: bl1 uses a 512 offset and expects header+checksum at 528-623 and bl2 at 4608+
eMMC: bl1 expects header+checksum at 16-111 and bl2 at 4096+
u-boot.bin.hardkernel layout:
16-111: eMMC header+checksum (start addr set to 1024)
528-623: SD header+checksum
1024-1315: code that copies bl2 from 4608 to 4096
4608-49151: bl2+bl21
49152+: bl30+bl301, bl31 and bl33(u-boot)
bl2.bin is modified to remove +512 image offset used when sd-booting by replacing
934: 91080273 add x19, x19, #0x200 // src += 512;
with
934: d503201f nop
aml_chksum is a small tool that writes new headers with updated offsets and checksums
The boot configuration uses the same payload for both eMCC and SD, but SD is missing sector 0 when read into memory, so the data in memory is shifted forward 512 bytes (eMMC sees 4608 while SD sees 4096). There’s some code that moves the bootloader in memory so that afterward it is in the same memory location for both SD and eMMC. Then there’s the part about rewriting the assembly. That implies the code is not signed; only a checksum is used. If we are willing to write or modify boot code, then we should have a lot of freedom.
Based on that commit message, SD has no hope of GPT, because the SD header is at the same location as the GPT header. But it would seem eMCC doesn’t need the SD header to boot. However, testing showed that eMMC wouldn’t boot without the SD header. To clear out the SD header:
dd conv=fsync if=/dev/zero of=$DEV seek=1 count=1
Investigation
Let’s investigate bl1.bin.hardkernel
as needed by U-Boot (as mentioned by
chewitt, the file name is misleading; it is the second-stage bootloader). The
U-Boot ODROID-C2 documentation (and Arch Linux
ARM) shows that BL2 should still be gotten from
HardKernel’s U-Boot fork:
git clone --depth 1 https://github.com/hardkernel/u-boot.git -b odroidc2-v2015.01 $DIR
...
BL1=$DIR/sd_fuse/bl1.bin.hardkernel
dd if=$BL1 of=$DEV conv=fsync bs=1 count=442
dd if=$BL1 of=$DEV conv=fsync bs=512 skip=1 seek=1
Looking at the contents of bl1.bin.hardkernel
(sha256:
15cf3aa6c6bd1f296de6f1e08de68c200f95f80197765a587743d6b4edf06435):
00000000: 24c8 161a a92c f048 24d3 5124 4fff 2776 $....,.H$.Q$O.'v
00000010: 4041 4d4c f0bf 0000 4000 0100 0000 0000 @AML....@.......
00000020: 0000 0000 4000 0000 0002 0000 0002 0000 ....@...........
00000030: 0000 0000 4002 0000 b00f 0000 f0bd 0000 ....@...........
00000040: 0000 0000 0004 0000 00b0 0000 0000 0000 ................
00000050: fac5 de05 6593 8ad9 43aa 478c 26f5 08ca ....e...C.G.&...
00000060: 05fa 2208 1353 fc59 41be c276 c814 f690 .."..S.YA..v....
... zeros
00000200: 24c8 161a a92c f048 24d3 5124 4fff 2776 $....,.H$.Q$O.'v
00000210: 4041 4d4c f0bf 0000 4000 0100 0000 0000 @AML....@.......
00000220: 0000 0000 4000 0000 0002 0000 0002 0000 ....@...........
00000230: 0000 0000 4002 0000 b00f 0000 f0bb 0000 ....@...........
00000240: 0000 0000 0010 0000 00b0 0000 0000 0000 ................
00000250: 7255 e49c a599 09dc 45e8 51e3 a35c ce59 rU......E.Q..\.Y
00000260: e0e9 7275 b287 cddf 8b7c f9fb f9c7 50ca ..ru.....|....P.
... zeros
00000400: f403 00aa f503 01aa a000 38d5 3500 0094 ..........8.5...
00000410: 4001 00b4 0010 38d5 0000 74b2 0010 18d5 @.....8...t.....
00000420: df3f 03d5 a000 38d5 2200 0094 a000 38d5 .?....8.".....8.
00000430: 2600 0094 1200 0094 0000 0014 fd7b bfa9 &............{..
00000440: 0140 82d2 0000 82d2 fd03 0091 0200 96d2 .@..............
00000450: 0120 bbf2 0020 bbf2 0d00 0094 0000 82d2 . ... ..........
00000460: 0100 96d2 0020 bbf2 2200 0094 0000 82d2 ..... ..".......
00000470: fd7b c1a8 0020 bbf2 0000 1fd6 fd7b bfa9 .{... .......{..
00000480: fd03 0091 eeff ff97 0000 0014 0300 80d2 ................
00000490: 7f00 02eb a000 0054 2468 6338 0468 2338 .......T$hc8.h#8
000004a0: 6304 0091 fbff ff17 c003 5fd6 0000 0000 c........._.....
000004b0: 8000 0058 1f00 0091 c003 5fd6 0000 0000 ...X......_.....
000004c0: 400c 00d9 0000 0000 8000 0058 1f00 0091 @..........X....
000004d0: c003 5fd6 0000 0000 4009 00d9 0000 0000 .._.....@.......
000004e0: 003c 4092 1f00 00f1 e017 9f9a c003 5fd6 .<@..........._.
000004f0: 2300 3bd5 634c 50d3 8200 80d2 4220 c39a #.;.cLP.....B ..
00000500: 0100 018b 4304 00d1 0000 238a 207e 0bd5 ....C.....#. ~..
00000510: 0000 028b 1f00 01eb a3ff ff54 9f3f 03d5 ...........T.?..
00000520: c003 5fd6 0000 0000 0000 0000 0000 0000 .._.............
... zeros
00001200: 0200 0014 10aa 00d9 f403 00aa f503 01aa ................
00001210: a000 38d5 3d21 0094 a002 00b4 207f 0410 ..8.=!...... ...
00001220: 00c0 18d5 0010 38d5 0000 74b2 0010 18d5 ......8...t.....
... lots more
We see the same layout:
- 0x0000 (sector 0): eMMC header
- 0x0200 (sector 1): SD header
- 0x0400 (sector 2): eMCC adjustment code
- 0x1200 (sector 6+): the actual bootloader code
Comparing the two headers show they are very similar. Byte 0x3d is 0xbd vs 0xbb, and byte 0x45 is 0x04 vs 0x10. Why is the SD header necessary for eMMC boot?
Looking at the HardKernel U-Boot before the bl2 deletion, secureboot.c is the most interesting:
typedef enum{
AML_SIG_TYPE_NONE=0,
AML_SIG_TYPE_RSA_PKCS_V15,
AML_SIG_TYPE_RSA_PKCS_V21, //??
AML_SIG_TYPE_RSA_PKCS_V15_AES,
}e_aml_sig_type;
typedef enum{
AML_DATA_TYPE_NONE=0,
AML_DATA_TYPE_RSA_KEY,
AML_DATA_TYPE_AES_KEY,
AML_DATA_TYPE_PROGRAM_GO, //need this?
AML_DATA_TYPE_PROGRAM_CALL, //need this?
}e_aml_data_type;
#define AML_BLK_ID (0x4C4D4140)
#define AML_BLK_VER_MJR (1)
#define AML_BLK_VER_MIN (0)
typedef struct __st_aml_block_header{
//16
unsigned int dwMagic; //"@AML"
unsigned int nTotalSize; //total size: sizeof(hdr)+
// nSigLen + nDataLen
unsigned char bySizeHdr; //sizeof(st_aml_block)
unsigned char byRootKeyIndex;//root key index; only romcode
// will use it, others just skip
unsigned char byVerMajor; //major version
unsigned char byVerMinor; //minor version
unsigned char szPadding1[4]; //padding???
//16+16
unsigned int nSigType; //e_aml_sig_type : AML_SIG_TYPE_NONE...
unsigned int nSigOffset; //sig data offset, include header
unsigned int nSigLen; //sig data length
//unsigned char szPadding2[4]; //padding???
unsigned int nCHKStart; //begin to be protected with SHA2
//32+16
unsigned int nPUKType; //e_aml_data_type : AML_DATA_TYPE_PROGRAM
unsigned int nPUKOffset; //raw data offset, include header
unsigned int nPUKDataLen; //raw data length
//unsigned char szPadding4[4]; //padding???
unsigned int nCHKSize; //size to be protected with SHA2
//48+16
unsigned int nDataType; //e_aml_data_type : AML_DATA_TYPE_PROGRAM
unsigned int nDataOffset; //raw data offset, include header
unsigned int nDataLen; //raw data length
unsigned char szPadding3[4]; //padding???
//64
} st_aml_block_header;
// ...
/**
* pBuffer : st_aml_block_header + signature data + st_aml_block_header(mirror) + [PUK] + raw data
* 1. st_aml_block_header will have offset, length, type for signature data, PUK and raw data
* 2. signature data is a PKCS #1 v1.5 RSA signature of SHA256 (st_aml_block_header(mirror) + [PUK] + raw data)
* signed with the PUK's corresponding private key.
* 3. st_aml_block_header(mirror) is a mirror copy of st_aml_block_header for data consistency
* 4. PUK is a RSA Public Key to be used for signature check (PKCSv15 format)
* 5. raw data: has two types for romcode
* 5.1 1st stage: two keymax : the first is for 4 root key SHA256 and the second is for 4 user key SHA256
* 5.2 2nd stage: BL2 raw data
* ...
*/
st_aml_block_header
has some general values, then three regions (SIG, PUK,
DATA) along with a CHK region. The later comment shows PUK as being optional.
Let’s decode sector 0 (for eMMC). Note that bl1.bin.hardkernel
is 0xc200
bytes long. The @AML
magic starts at 0x10 as mentioned earlier (I assume
bytes 0-15 are magic), so decoding from there:
// SECTOR 0
dwMagic = "@AML"
nTotalSize = 0xbff0;
bySizeHdr = 0x40
byRootKeyIndex = 0;
byVerMajor = 1;
byVerMinor = 0;
szPadding1[4] = {0};
//16+16 (0x10 struct offset, 0x20 file offset)
nSigType = 0;
nSigOffset = 0x40;
nSigLen = 0x200;
nCHKStart = 0x200;
//32+16 (0x20 struct offset, 0x30 file offset)
nPUKType = 0;
nPUKOffset = 0x240;
nPUKDataLen = 0xfb0;
nCHKSize = 0xbdf0; // SECTOR 1: 0xbbf0
//48+16 (0x30 struct offset, 0x40 file offset)
nDataType = 0;
nDataOffset = 0x400; // SECTOR 1: 0x1000
nDataLen = 0xb000;
szPadding3[4] = {0};
Offsets appear to be relative to the start of the header (so are offset by 0x10). SIG starts at 0x40, immediately after the 0x40 byte header. PUK is next, after the 0x200 byte SIG. PUK ends at 0x11F0 and DATA overlaps it, but the SD header looks better. Decoding sector 1 (for SD) shows DATA starts at 0x1200 (since the SD header is offset by 0x200 from eMCC header). The assembly on-disk starts at 0x400 and 0x1200, so nDataOffset must ignore the 0x10 offset, which makes DATA immediately after PUK.
And we also see CHK starts at 0x200, which includes the SD header! So zeroing out the SD header causes the eMMC checksum to fail, even though it isn’t used.
I modified aml_chksum.c to only update the SHA256 checksum. I then used a hex editor to set nCHKSize=0 for eMMC, recalculated the checksum, and eMMC totally worked with GPT!
Testing out GPT
I have my (further) modified bl1.bin.hardkernel-fixedheaders. It just contains the two headers, and the SD header changes don’t really matter. To write only the eMMC header:
dd conv=fsync if=bl1.bin.hardkernel-fixedheaders of=$DEV bs=1 count=442
Since we only freed up space for the GPT header, we need to move the main GPT
table away from the bootloader. There’s unused space between U-Boot and the
first partition. Using gdisk
, after creating the partitions, type x
to
enter Expert mode (m
to go back), then j
to “move the main partition
table”. For the starting location, choose the last available starting sector
(generally 2016) before the first partition.
Command (? for help): x
Expert command (? for help): j
Currently, main partition table begins at sector 2 and ends at sector 33
Enter new starting location (2 to 2016; default is 2; 1 to abort): 2016
Expert command (? for help): m
Command (? for help):
p
shows “Main partition table begins at sector” to confirm where it is
positioned.
Command (? for help): p
Disk /dev/mmcblk0: 16777216 sectors, 8.0 GiB
Sector size (logical): 512 bytes
Disk identifier (GUID): D43FD65D-717C-43D5-B0F2-A821C3946118
Partition table holds up to 128 entries
Main partition table begins at sector 2016 and ends at sector 2047
First usable sector is 2048, last usable sector is 16777182
Partitions will be aligned on 2048-sector boundaries
Total free space is 2015 sectors (1007.5 KiB)
Number Start (sector) End (sector) Size Code Name
1 2048 1050623 512.0 MiB EF00 EFI system partition
2 1050624 16775167 7.5 GiB 8305 Linux ARM64 root (/)
Further changes
Other modifications that looked interesting:
- Set nCHKStart/nCHKSize to just cover functional bytes
- Set nSigLen=0x20. SHA256 is not 0x200 in size. Has no real effect, but it would make the values more obvious
- Set nPUKOffset=0/nPUKDataLen=0. It isn’t used; don’t act like it is there
- Set eMMC nDataOffset=0xE00 and move the relocation code. This would free up 5 sectors for the main GPT table
- Set eMMC nDataOffset=0x70 if we are fine with the relocation code not being part of CHK. This would free up 2 more sectors relative to using 0xE00
- (Failed) Set eMCC nDataOffset=0x40 and put the relocation code before SIG and still checksumed. Unfortunately, based on testing we can’t increase the header size. Moving SIG to start later didn’t work either
- Use modified relocation code for both SD and eMCC to free up another 9 sectors. 0xae00-0xc000 is empty, so we can shift the main BL2 code backward 9 sectors on-disk
Pushing much beyond 0xc000 would be annoying as that’s getting to where
BL3/U-Boot is stored. It also doesn’t sound feasible as aml_chksum.c,
HardKernel’s arch-gxb/cpu_config.h’s BL2_SIZE, and
firmware/plat/gxb/include/platform_def.h’s BL2_LIMIT mention 48 KiB as the BL2
size limit. Since we can’t get the 32 sectors necessary for a spec-compliant
GPT table, options 4-7 don’t seem too valuable. Even though gdisk
has the
expert command s
to resize the partition table, moving it is just as easy and
remains spec-compliant.
I made modifications 1, 2, and 3 in bl1.bin.hardkernel-fixedheaders.
Tying up loose ends
Why did Kwiboo strip out the +512 offset but we no longer need to? Because in our version of bl1.bin.hardkernel both SD and eMMC have the +512 disk offset (see assembly at 0x1890 for eMCC and 0x18b8 for SD). So our BL3 is on-disk at 0xc200 instead of 0xc000.
HardKernel U-Boot’s bl2_main.c has the comment: “Since arm standard c libraries are not PIC, printf et al are not available. We rely on assertions to signal error conditions”. Don’t let that confuse you; the BL2 binary is not fully PIC. The code assumes it runs at 0xd9001000 (bl1 loads bl2 starting at 0xd9000000, and the code is at the offset 0x1000 for SD), and there are absolute addresses. Although the relocation code is position-independent, it is hard-coded to memcpy 0xb000 bytes from 0xd9001200 to 0xd9001000.