Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
Инструменты/GitHubGitHub/sjgallagher2/am335xbootrom
Безопасность встроенных системОбратная инженерияОтладчикиАппаратный ХакингАнализ Бинарных ФайловОбучение и ОбразованиеАнализ Прошивок
GitHubsjgallagher2/am335xbootrom

am335xbootrom

Обратная разработка загрузочного ПЗУ TI AM3358

Репозиторий
61562 лет назадПроверено Kitploit

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться

Обратная разработка загрузочного ПЗУ AM335x

Прошло, наверное, восемнадцать месяцев с тех пор, как я впервые заполучил несколько плат Beaglebone Black, спасённых из мусорного бака. К сожалению, платы не заработали сразу. Это был мой первый опыт работы с такими платами, да и с любым одноплатным компьютером, поэтому я не был уверен, проблема в том, что я делаю что-то не так, или в самих платах (возможно, именно поэтому они и отправились в мусорный бак). У меня ушло довольно много времени и огромное количество усилий, чтобы заставить эти платы действительно начать загружаться, но, чёрт возьми, у меня получилось. И вот что я узнал на этом пути.

ПРИМЕЧАНИЕ: Как использовать XML-файл Ghidra

Я включил в этот репозиторий несколько утилит, а также xml-файл, экспортированный из Ghidra, со всеми символами, которые мне удалось получить в ходе реверса. Я использовал этот пост, чтобы выполнить экспорт без самой прошивки и избежать возможных проблем с авторскими правами. Если вы хотите отлаживать загрузочное ПЗУ самостоятельно, у вас уже будет подключён JTAG, так что вы сможете сами снять дамп загрузочного ПЗУ (с 0x20000 по 0x2BFFF).

Чтобы загрузить символы:

  1. Создайте новый проект Ghidra. Импортируйте двоичный файл (не XML) в Ghidra: используйте ARMv7 Little Endian и убедитесь, что в Options вы установили базовый адрес 0x20000; имя блока можно задать как bootrom.
  2. Откройте этот двоичный файл в CodeBrowser. НЕ ЗАПУСКАЙТЕ АНАЛИЗ.
  3. Перейдите в File > Add program и выберите XML-файл. Настройки по умолчанию должны подойти. Теперь вы можете пройти по обработчику сброса, перейти к main() или к обработчику загрузки с MMC/SD-карты.

Проблема

Для начала я знал, что это кастомные версии стандартной Beaglebone Black, поэтому довольно рано решил, что проблема может быть в отсутствии чего-то на самой плате, например идентификатора платы. При загрузке стандартной SD-карты, отформатированной с помощью balenaEtcher, я не видел ничего. Я ожидал, что светодиоды на плате начнут мигать, и что подключение кабеля UART-USB позволит мне увидеть процесс U-Boot. Однако UART молчал. Если я вынимал SD-карту, он снова и снова выводил букву C, что является ожидаемым поведением для загрузки через UART/последовательный порт. Плата определённо пыталась загрузиться, и SD-карта меняла это поведение, но у меня не было никакой дополнительной видимости. Большинство инструкций по устранению неполадок в интернете использовали вывод U-Boot как отправную точку для диагностики. Похоже, такой роскоши у меня не будет.

В этот момент я решил, что стоит подключить отладочный зонд. К сожалению, у меня не было подходящего разъёма под существующее посадочное место, поэтому я сделал свой.

Плата Beaglebone имеет разъём с обозначением P2, на который выведены соединения JTAG. Я подсоединил к нему провода, ведущие к маточному разъёму, чтобы можно было общаться с ней через мой J-Link.

Запустив Ozone (отладчик Segger), я настроил J-Link и начал с попытки найти точку входа. Я думал, что reset-halt поместит меня туда, куда нужно, и из-за этого пришёл к (неверному) предположению, что точка входа — 0x2148a, хотя, конечно, замечал, что это непостоянно. Позже я понял, что платы AM335x плохо ладят с reset-halt'ом J-Link, поэтому на самом деле была задержка в несколько сотен тактов, из-за которой я недетерминированно попадал куда-то внутрь загрузочного обработчика. (В конце концов я обошёл это, написав GEL-файл для Code Composer Studio от TI, который поддерживает отладку через J-Link — при сбросе регистр PC устанавливается на обработчик сброса, регистры очищаются, а режим инструкций принудительно переключается на ARM.)

Из ветки на форумах TI (AM335x: сотрудники TI, где я могу получить исходный код/символы загрузчика ПЗУ?) я раздобыл пару отладочных символов: SPI Initialize по адресу 0x231e0, SPI ReadSectors по адресу 0x23230, а 0x24bfa — это процедура чтения через UART. Полезная помощь, пожалуй. Я заметил, что загрузка завершалась неудачей, уходя в бесконечный цикл по адресу 0x402f0440 — мёртвый цикл. Хм, довольно далеко от остального загрузочного ПЗУ, наверное, это в RAM или что-то вроде того. Пожалуй, пора заглянуть в техническое справочное руководство (TRM)!

Глава 26 TRM содержит массу информации о загрузке. Мы получаем следующее представление загрузочного ПЗУ:

Описание:

Архитектура Public ROM Code показана на рисунке 26-1. Она разделена на три основных уровня по принципу «сверху вниз»: высокоуровневый, драйверы и уровень аппаратных абстракций (HAL). Каждый уровень взаимодействует с нижележащим уровнем через унифицированный интерфейс. Высокоуровневый уровень отвечает за основные задачи Public ROM Code: настройку сторожевого таймера и тактовых частот, а также основную процедуру загрузки. Уровень драйверов реализует логические и коммуникационные протоколы для любого загрузочного устройства в соответствии со спецификацией интерфейса. Наконец, HAL реализует низкоуровневый код для взаимодействия с аппаратными IP-блоками. Конечные загрузочные устройства подключаются к контактным площадкам ввода-вывода.

На рисунке 26-2 показан высокоуровневый процесс загрузки Public ROM Code. На этом устройстве Public ROM Code запускается после завершения безопасного запуска (выполняемого Secure ROM Code). Затем ROM Code выполняет конфигурацию и инициализацию платформы в рамках публичной процедуры запуска. Список загрузочных устройств создаётся на основе выводов SYSBOOT. Загрузочным устройством может быть устройство загрузки из памяти (распаянная flash-память или временное загрузочное устройство вроде карты памяти) или периферийный интерфейс, подключённый к хосту. Основной цикл процедуры загрузки проходит по списку загрузочных устройств и пытается найти образ на текущем выбранном загрузочном устройстве. Цикл завершается, если найден и успешно выполнен валидный загрузочный образ, или по истечении времени сторожевого таймера. На HS-устройстве процедура проверки подлинности образа выполняется до его запуска. Сбой процедуры проверки подлинности приводит к переходу в «мёртвый цикл» в Secure ROM (в ожидании сброса по сторожевому таймеру).

Карта памяти! Векторы исключений! Блок-схемы! В этом разделе масса информации. Моя задача стала намного проще.

В этот момент я использовал JTAG-зонд, чтобы выгрузить прошивку в несколько разных файлов, и начал загружать их в Ghidra. Похоже, не существовало SVD-файлов или других описаний регистров в удобном формате, что очень обидно, потому что это означало, что мне нужно вручную определять области памяти, регистры и всё остальное. Это был утомительный процесс, но через некоторое время у меня появился python-скрипт, с помощью которого я мог загружать символы в Ghidra для AM3358. Одной проблемой меньше!

Реверс-инжиниринг

Похоже, файлы, которые у меня есть, можно сопоставить следующим образом:

Любопытно, что бесконечный цикл по адресу 0x402f_0440 находится в начале «загруженного образа» во внутренней SRAM, в то время как векторы исключений хранятся в другом месте. Возможно, позже это окажется важной подсказкой...

При сбросе приватное загрузочное ПЗУ занимается вопросами безопасности и переходит к 0x2 0000, где находятся векторы сброса. Первая инструкция — это переход к 0x2 08d0, который, должно быть, является точкой входа. Это не инструкция BX, так что, предположительно, в этот момент мы всё ещё в режиме ARM.

Обработчик сброса

Это первый выполняемый код, а значит, это не совсем «функция» с параметрами, а скорее сгенерированный компилятором стартовый скрипт. Первый базовый блок:```arm ldr r4,[->Peripherals::CM_PER] mov r0,#0x2c ldr r6,[r4,r0]=>CM_PER.CM_PER_OCMCRAM_CLKCTRL mov r6,#0x2 str r6,[r4,r0]=>CM_PER.CM_PER_OCMCRAM_CLKCTRL mov r0,#0x2c poll: ldr r6,[r4,r0]=>CM_PER.CM_PER_OCMCRAM_CLKCTRL cmp r6,#0x2 bne poll

root@kitploit:~
Этот блок включает тактовый сигнал OCMC RAM:
1. Установите `CM_PER_OCMCRAM_CLKCTRL=0x2`
2. Проверьте, установлен ли регистр; если нет, продолжайте опрос
Регистр `CM_PER_OCMCRAM_CLKCTRL` использует биты 0 и 1 для поля `MODULEMODE`; установка значения `=0x2` включает тактовый сигнал для OCMC RAM.

Следующий базовый блок:```arm
	    ldr        r0,[PTR_control_status] 
	    ldr        r0,[r0,#0x0]=>control_status 
	    and        r0,r0,#0x700
	    mov        r0,r0, lsr #0x8
	    cmp        r0,#0x3
	    bne        skip
		ldr        r0,[PTR_control_status] 
	    ldr        r0,[r0,#0x0]=>control_status
	    cpy        r6,r0
	    and        r0,r0,#0x1f
	    cmp        r0,#0x1f
	    bleq       GPMIC_init 
skip:   ...

Этот блок делает следующее:

  1. Проверяет, равно ли (control_status & 0x700) >> 8 == 0x3, пропускает, если не равно
  2. Проверяет, равно ли control_status & 0x1f == 0x1f, и если да, вызывает функцию GPMC_init после загрузки control_status в r6

Следующий блок настраивает сопроцессор:```arm msr cpsr_c,#0xd3 ldr r4,[->Exceptions::ROM_RESET_VECTOR] mcr p15,0x0,r4,cr12,cr0,0x0 bl LAB_00020934 bl LAB_00020938 bl LAB_0002093c bl LAB_00020940 bl LAB_00020944 bl LAB_00020948 bl LAB_0002094c bl LAB_00020950 mrc p15,0x0,r0,cr1,cr0,0x0 orr r0,r0,#0x800 mcr p15,0x0,r0,cr1,cr0,0x0 b LAB_000207f0

root@kitploit:~
Операции в этом блоке:
1. Переместить `11010011b` в поле управления CPSR (`I=1`,`F=1`,`T=0`,`MODE=10011`)
	1. `I` — это отключение прерываний, `F` — отключение быстрых прерываний (поэтому `I=F=1` означает, что прерывания отключены)
	2. `T` — это Thumb-режим, установлен в `0`
	3. `MODE=10011` устанавливает режим процессора Supervisor ([ссылка](https://developer.arm.com/documentation/ddi0406/b/System-Level-Architecture/The-System-Level-Programmers--Model/ARM-processor-modes-and-core-registers/ARM-processor-modes?lang=en#CIHGHDGI))
	4. Подробнее см. [здесь](https://developer.arm.com/documentation/ddi0406/b/System-Level-Architecture/The-System-Level-Programmers--Model/ARM-processor-modes-and-core-registers/Program-Status-Registers--PSRs-)
2. Загрузить адрес вектора сброса ROM
3. Получить доступ к регистру Security Extensions `c12` [сопроцессора 15](https://developer.arm.com/documentation/den0013/d/ARM-Processor-Modes-and-Registers/Registers/Coprocessor-15) (сопроцессора управления системой) и загрузить вектор сброса ROM в `VBAR` (регистр базового адреса векторов)
![](https://assets.kitploit.com/production/public/readmes/48883/6c4ef51fde89883fcdb3a9497004830d928d6f6c716668acd86dce3b9a68c93b.png)
4. Похоже на `nop`-переходы? Почему `bl`, а не `b`?
5. Включить предсказание переходов (установить бит 11 системного управляющего регистра `SCTLR`)
![](https://assets.kitploit.com/production/public/readmes/48883/7a99ff8bd5285136c05b9d91bdf5057a6512c614cacc2726ce740254e4c240fa.png)
*Регистры CP15 `c1` (системные управляющие регистры) в реализации VMSA*

Описание регистра `SCTLR`:
> SCTLR обеспечивает верхний уровень управления системой, включая её подсистему памяти.
> Этот регистр входит в функциональную группу регистров управления виртуальной памятью.

См. страницу B4-1687 в TRM. Бит 11 — это бит *разрешения предсказания переходов*; его установка включает [предсказание переходов](https://developer.arm.com/documentation/ddi0406/b/System-Level-Architecture/Common-Memory-System-Architecture-Features/Caches/Branch-predictors).

6. Вызвать функцию (через переход к инструкции вызова)

### Функция по адресу `0x20894` (`__main`)

К этой функции выполняется переход из другой ранней подпрограммы. Полагаю, она инициализирует стек и, возможно, таймеры или сторожевой таймер, прежде чем вызвать `FUN_0002889c` (как позже выясняется, это `main()`!).```arm
		 ldr    sp,[->RESERVED_EXCEPTION_BRANCH]   ; 0x4030ce00
		 blx    load_stack_1
		 ldr    r12,[DWORD_1]
		 add    r12,r12,pc
		 tst    r12,#0x1
		 adrne  lr,0x208bd
		 cpyeq  lr,pc
		 bx     r12 ;=>init_timers_maybe
		 adr    r12,0x208bd
		 bx     r12 ;=>LAB_000208bc
000208bc bl     FUN_0002889c
000208c0 ddw    0x109
000208c4 addr   RESERVED_EXCEPTION_BRANCH
...
RESERVED_EXCEPTION_BRANCH:
         ldr    pc=>LAB_00020090,[PTR_LAB_4030ce20]
         ; 20090 is a dead loop

Выполняемые здесь операции:

  1. Загрузить начало таблицы исключений ОЗУ в указатель стека
  2. Перейти ветвлением к функции, которая помещает r0,r1,r2,r3,r4,lr в стек (публичный стек в карте памяти)
    1. Эта функция load_stack_1 передаёт управление пустой функции bx lr, затем извлекает r0,r1,r2,r3,r4,pc из стека (по сути, возвращает эти данные в регистры и помещает то, что находится в lr, в pc, чтобы вернуться)
  3. Проверить, является ли pc + 0x109 нечётным; если чётное — загрузить 0x208bd в lr, в противном случае скопировать pc в lr
  4. Вызвать функцию main
  5. Вызвать FUN_0002889c

Это должна быть функция __main(), упомянутая на блок-схеме загрузки:

Это делает следующую функцию функцией main.

Как показано в верхней части рисунка 26-8, после завершения инициализации безопасной загрузки CPU переходит к вектору сброса Public ROM Code. В публичном режиме при запуске системы CPU выполняет инициализацию публичной стороны и настройку стека (автоматически сгенерированную компилятором C-инициализацию или «scatter loading»). Затем он настраивает сторожевой таймер 1 (установленный на три минуты), выполняет конфигурацию системных тактовых сигналов. Наконец, он переходит к процедуре загрузки.

Главная функция (0x209b0)

Когда вызывается main, регистр SP указывает на 0x4030ce00. Это место, где начинается стек, и он растёт вниз по направлению к 0x4030 b800; а поскольку после помещения 4 регистров (разница в 16 байт или 4 слова) мы указываем на адрес 0x4030 cdf0, мы используем полный нисходящий стек, как в AAPCS. То есть SP указывает на самое последнее слово в стеке и растёт вниз.

Вот декомпилированная функция main():```c int main() { uint local_10; uint local_c;

local_c = 0; local_10 = 0; check_stack_prm(&local_10); update_coldreset_tracing_vector(local_10); update_current_tracing_vector(1); main_clock_init(6,0); watchdog_softreset(); watchdog_write_disable_seq_data2(); set_watchdog(300000); if ((local_10 & 1) != 0) { update_current_tracing_vector(2); local_c = local_c & 0xffff | 1; } timer_func_1(); clock_init_func_4(&local_c); run_booting_loop(&local_c,local_10 & 0xff); return 0; }

root@kitploit:~
Наиболее интересной для моих целей является функция `run_booting_loop` по адресу `0x20a10`.

### Заметки о X-Loader

Пройдя через процедуру запуска и добравшись до этого места со строками вроде «ISSW», «CHSETTINGS» и «X-LOADER», я начал искать другие места, где эти строки могут встречаться в контекстах, связанных с U-Boot. Я наткнулся на [эту ветку обсуждения](https://forum.xda-developers.com/t/discussion-on-the-boot-loader-cracked.1378886/) людей, занимающихся реверсом или взломом прошивки Nook, а в [исходниках x-loader](https://github.com/joelagnel/x-loader/blob/f3c74bc9b01dac58e553393d6ec1041353f2f1f7/scripts/signGP.c) есть ссылки на такие вещи, как `CHSETTINGS`. Поискав вокруг, я понял, что «ISSW», [судя по всему](https://github.com/u-boot/u-boot/blob/master/doc/README.ti-secure), относится к загрузке с устройств, не являющихся памятью.

Вспомним высокоуровневый код из документации по инициализации:
![](https://assets.kitploit.com/production/public/readmes/48883/3b7ecd05acbf1f0aa5497009b4df65d17a7b0e133b9b711bebaf9f5bebf546cf.png)

Заслуживают внимания:
- RNDIS
- FAR
- XMODEM
- BOOTP
- TFTP
- DFT

Возможно, пора снова попробовать живую отладку. Пытаться реверсить все эти структуры было бы, наверное, мучительно, учитывая большой объём данных, который я не могу понять...

Ура! Живая отладка работает, если вручную задать PC и SP, используя найденные мной векторы:
![](https://assets.kitploit.com/production/public/readmes/48883/3886242947cd1cef1093f3cb02dbbcc02faf27f1298708dd849e69eae7b75f97.png)

По исходникам и векторам трассировки мне удалось сопоставить различные варианты загрузки с номерами устройств, которые им присваиваются. Позже это оказалось очень полезным, поскольку мне приходилось различать MMC0 (8) и MMCSD1 (9) при установке точек останова в обработчике загрузки SD/MMC.

| Тип        | Устройство        | ID устройства |
| ---------- | ----------------- | ----------- |
| Память     | XIP (MUX2)        | 1           |
| Память     | XIP w/WAIT (MUX2) | 2           |
| Память     | XIP (MUX1)        | 3           |
| Память     | XIP w/WAIT (MUX1) | 4           |
| Память     | NAND              | 5           |
| Память     | MMCSD1            | 7, 9 (eMMC) |
| Память     | NAND_I2C          | 10          |
| Память     | MMC0              | 8, 12 (SD)  |
| Периферия  | UART0             | 16          |
| Периферия  | USB               | 20          |
| Периферия  | GPGMAC0           | 22          |

Информацию о том, как загружается процессор, см. в TRM и [этом ответе на Stack Exchange](https://stackoverflow.com/a/31252989/8565545). Если кратко:
1. Загрузочное ПЗУ (boot ROM) обнаружило файл MLO (Mmc LOader) на SD-карте и скопировало его в SRAM
2. Это вторичный загрузчик программ — меньший загрузчик, который инициализирует полный объём RAM и копирует туда полный бинарный файл U-Boot для выполнения
3. После запуска бинарного файла U-Boot мы (точнее, U-Boot) наконец загружаем ядро

### `run_booting_loop()`

Это основной цикл загрузки. Он выполняется бесконечно или до тех пор, пока выполнение не перейдёт на другой загрузчик, который должен быть загружен в RAM.

Начало процедуры без обновлений векторов трассировки:
- Определить тип устройства
	- Если тип устройства равен 5 (защищённое устройство), выполнить некоторую другую инициализацию
- Выполнить `build_boot_list(int,buffer[],data[],int)`  
	- `buffer[]` инициализируется значением `0xff`, а `data[]` содержит тип устройства (вероятно)```c
void run_booting_loop(uint32_t *r0_config,undefined4 param_2,undefined4 param_3,
                     undefined4 default_list)

{
  int iVar1;
  uint j;
  uint i;
  int device_type;
  byte alt_list [12];
  undefined4 boot_status;
  byte boot_list [8];
  uint8_t local_buffer [8];
  
  update_current_tracing_vector(3);
                    /* Device type is 3 */
  lookup_device_type(&device_type);
  if ((device_type == AM335X_HIGH_SECURITY) && (iVar1 = return_zero_4(), iVar1 != 0)) {
    init_something_1_small(&STATIC_DATA_1);
  }
                    /* param1 = 1
                       param2 = 4030 ebc4
                       param3 = 4030 ebb4 */
  build_boot_list(*(ushort *)r0_config,boot_list,alt_list,default_list);
  do {
    i = 0;
    local_buffer[0] = 0xff;
    local_buffer[1] = 0xff;
    local_buffer[2] = 0xff;
    local_buffer[3] = 0xff;
    do {
      if (boot_list[i] - 1 < 12) {
        update_current_tracing_vector(4);
                    /* No return unless there is an error */
        boot_device_1(r0_config,boot_list[i],local_buffer);
      }
      else if (boot_list[i] - 65 < 8) {
        update_current_tracing_vector(5);
        watchdog_write_disable_seq_data2();
        boot_status = 0xffffffff;
        boot_device_2((uint32_t)r0_config,boot_list[i],&boot_status,local_buffer);
        watchdog_write_enable_seq_data2();
        if (boot_status != 0xffffffff) {
          local_buffer[0] = (undefined)boot_status;
          local_buffer[1] = boot_status._1_1_;
          local_buffer[2] = boot_status._2_1_;
          local_buffer[3] = boot_status._3_1_;
          if ((boot_status & 0xffff00ff) == 0xf0030006) {
            update_current_tracing_vector(9);
            boot_list[i + 1] = (byte)(boot_status >> 8);
          }
          else if (boot_status != 0xf0030002) {
            update_current_tracing_vector(8);
            j = 0;
            do {
              if (63 < boot_list[j]) {
                boot_list[j] = 0;
              }
              j = j + 1 & 0xff;
            } while (j < 8);
          }
        }
      }
      i = i + 1 & 0xff;
    } while (i < 8);
    update_current_tracing_vector(6);
  } while( true );
}

Booting into SRAM

Я обнаружил, что по адресу 0x23d7a находится функция, которую я назвал boot_into_SRAM() — это была последняя функция, вызываемая перед переходом в SRAM, а уже оттуда — в обработчик исключений. Ранее у меня было сохранено другое состояние RAM, но когда я ещё раз провёл живую отладку (теперь уже более чем через год, в июле 2024), я понял, что происходит. Плата успешно читала данные с SD-карты и выполняла код, загруженный с карты! Чтобы проверить это, мне нужно было найти в SRAM байты, совпадающие с содержимым SD-карты. Оказалось, что в каталоге am335x-evm-linux-sdk-bin-.../board-support/prebuilt-images/ есть бинарный файл u-boot-spl.bin-am335x-evm, и код из этого файла совпадает с тем, что появляется в SRAM. Мы добрались до uboot SPL!

Терминал UART

Мы успешно загрузились в SRAM, теперь меня интересует терминал UART, который должен показывать информацию о uboot. Схема подключения приведена ниже.

При подключении к устройству через CuteCom, 115200 @ 8-N-1, без вставленной SD-карты оно просто выводит C повторно.

Но при вставленной SD-карте UART ничего не выводит. Ни сообщений, ни символов. Похоже, сбой происходит слишком рано в процессе загрузки? Но теперь, когда я знаю, какой код выполняется (и у меня есть его исходники), я смогу собрать для него отладочные символы и запустить нормальную отладочную сессию. Это может быть непросто: нужно убедиться, что я компилирую код так же, возможно, потребуется время, чтобы точно выяснить, что именно SDK загрузил на мою SD-карту и как это собрать.

Поскольку U-Boot должен выводить текст в UART, а я ничего не вижу, предполагаю, что мы ловим исключение где-то в SPL.

На этом этапе я потратил много-много часов на реверс-инжиниринг и приведение в порядок декомпилированного исходного кода загрузочного ПЗУ в Ghidra, изучая структуры и то, как каждый член данных использовался в функциях, иногда во вложенных структурах, что создавало для меня массу проблем. Пока это было на втором плане, я решил, что пора начинать отлаживать и компилировать собственный код. Мы ведь всё-таки в RAM, почему бы не загрузить символы SPL и не посмотреть, что происходит?

SDK Development

Отладка

Отлаживать можно с помощью Ozone — отладчика Segger для J-Link. Также можно использовать собственный Code Composer Studio (CCS) от TI или, возможно, его «облегчённую» версию в стиле VSCode — CCS Theia. Мне удалось собрать всё в SDK, следуя этому видео: Sitara Linux Board Porting Series: Module 6. Нужно собрать три компонента:

  • Конфигурация процессора
  • Бинарный файл U-Boot
  • Вторичный загрузчик U-Boot (SPL)

Я последовал видео Module 7 из указанной серии, и мне удалось заставить всё работать, с несколькими замечаниями:

  1. s_init() больше не существует
  2. Аппаратные точки останова с J-Link нужно устанавливать через панель управления J-Link (см. значок в трее). Не уверен, как с помощью этого загрузить код. Возможно, через Ozone.

Есть символы? Прекрасно. Теперь я вижу, что происходит при выполнении, начиная с обработчика сброса reset(), и могу видеть, где мы приходим к нашему исключению. Чтобы вычислить его, я установил точку останова в обработчике исключений по адресу 0x402f 0440 и проверил регистр связи (link register), в котором всё ещё хранился адрес последней вызванной функции. Им оказался адрес 0x402f 76ce, хотя он, похоже, непостоянен. Что вызывает ошибку?

Примечание: при отладке, следуя видео Module 7, выполняйте код до 0x402f 0400, затем делайте шаг Load Memory(). Это нужно делать после каждого перезапуска.

Мы заходим в функцию device_probe() (0x402f 74c4), затем ещё в пару функций? Потом из do_setup_dpll() (ветвление по адресу 0x402f 07fc) мы не выходим, так что давайте продолжать заходить в неё. Шагая дальше, мы возвращаемся в _main() из crt0.S, расположенный по адресу 0x402f 14e0. Похоже, мы выходим из board_init_f() и переходим к spl_relocate_stack_gd(). Этот вызов не завершается. Мы доходим до dm_fixup_for_gd_move(). В нём содержится инструкция, которая сбоит, по адресу 0x402f 76c2. Думаю, вот оно: код пытается обратиться к 0x81ff ff20. Судя по всему, безуспешно. У меня есть подозрение, что проблема в конфигурации SDRAM. Я перерыл все темы на форумах TI, связанные с похожими проблемами, и нашёл полдюжины с подсказками, которые помогли мне. Я пришёл к выводу, что это, вероятно, связано либо с (a) настройкой EMIF, либо с (b) программным выравниванием.

Конфигурация DDR3 RAM

Моя плата показана ниже.

Память производства Micron, тогда как в схеме BeagleBone Black, которая у меня есть (ревизия C3), используется память DDR3 от Kingston, а именно D2516EC4BXGGB. Микросхема DDR3 — это U12, мы можем воспользоваться страницей декодера маркировки Micron, чтобы найти деталь:

  • MT41K256M16TW-107 XIT:P
    • 256 Meg x 16
    • 96-шариковый FBGA 8mm x 14mm, ревизия P
    • $t_{CK}$=1.07ns, CL = 13

Давайте убедимся, что устройство живо. Начал я с простой проверки подачи питания. В спецификации указано, что напряжение должно быть 1,5 В ± 0,075 В. Я измеряю 1,506 В на R6 с нижней стороны платы. У нас есть две контрольные точки, TP1 и TP2.

На случай, если когда-нибудь пригодится, вот несколько контрольных точек.

Схема памяти подробно описана на странице аппаратного дизайна.

  • Устройство памяти

Проверим линию разрешения тактового сигнала. Можно проверить обе стороны R96: одна сторона должна быть заземлена, другая должна быть подтянута к высокому уровню. Подтверждено: на CKE 1,5 В.

Следующий шаг — проверить тактовый сигнал. Здесь я сделал всё, что мог: использовал tinySA с подключённой антенной, направленной примерно в сторону микросхемы RAM. Занимаясь таким «сниффингом», я довольно уверен, что тактовый сигнал присутствует, по крайней мере, на текущий момент этого достаточно.

Двигаемся дальше: пора разобраться с интерфейсом внешней памяти. Часто встречается понятие GEL-файла. Это интерпретируемый язык, разработанный Texas Instruments для Code Composer Studio, расшифровывается как General Extension Language.

  • Создание GEL-файлов инициализации устройств

GEL-файл включён в инструмент конфигурации DDR-памяти.

Отлично! Я выполнил процедуру настройки (насколько смог) и нашёл оптимальные значения для GEL-файла.```


root@kitploit:~
The Slave Ratio Search Program Values are... 

PARAMETER MAX | MIN | OPTIMUM | RANGE


DATA_PHY_RD_DQS_SLAVE_RATIO 0x071 | 0x005 | 0x03b | 0x06c DATA_PHY_FIFO_WE_SLAVE_RATIO 0x1b3 | 0x046 | 0x0fc | 0x16d DATA_PHY_WR_DQS_SLAVE_RATIO 0x0f7 | 0x01a | 0x088 | 0x0dd DATA_PHY_WR_DATA_SLAVE_RATIO 0x137 | 0x05a | 0x0c8 | 0x0dd


root@kitploit:~
Попытка подкрутить параметры RAM в Memory Browser... Ого! Работает!

Итак, память, похоже, точно работает, но SPL всё ещё падает, так что, возможно, здесь что-то связано с тем, как SPL пытается инициализировать SDRAM? Ах, точно, в процедуре настройки есть ещё кое-что, конечно! Нужно же ещё и обновить сам SPL...

Мы уже ближе. Файл `board.c` инициализирует DDR, проверяя, какая у нас плата. Но для этой платы все функции (`board_is_evm_sk()`, `board_is_icev2()`, `board_is_bone_lt()` и т.д.) возвращают false, поэтому используется значение по умолчанию `config_ddr(266, ...)`, где 266 — это тактовая частота в МГц, а *должно* быть 400 МГц. Это, безусловно, проблема.

`board_is_bone_lt` нужно обойти так, чтобы она всегда возвращала true. Я это сделал, и продвинулся чуть дальше, но меня кое-что беспокоит. Загрузка нового файла MLO на SD-карту не работает, хотя прямая загрузка программы работает нормально. Что здесь происходит? Я вижу, что код, загружаемый в SRAM, — это не тот код, который я скомпилировал. Более того, я даже отформатировал SD-карту, и в SRAM всё равно загружается какой-то SPL по умолчанию! Я проверил, что при использовании другой SD-карты процесс загрузки не пытается продолжаться. Значит, загрузчик точно ищет загрузочный раздел на SD-карте, затем передаёт управление в SRAM, но данные ещё не скопированы? Уф. Откуда же он там берётся??

Эта проблема доставила мне немало хлопот. Я удалил все разделы, занулил MBR, занулил загрузочный раздел, перепробовал разные SD-карты, и только моя карта всё ещё могла загружаться, значит, на ней где-то должны были остаться *какие-то* загрузочные данные. В конце концов мне удалось положить конец этому безумию, занулив карту *целиком*.

Также в этот момент я узнал о **векторах трассировки**, к которым можно получить доступ при отладке загрузочного ПЗУ. Это впоследствии оказалось чрезвычайно полезным и для реверса, поскольку я знал, откуда исходят все вызовы трассировки, и мог присваивать имена функциям и т.д. на основе этих вызовов. Я сделал таблицу для интерпретации этих векторов трассировки и использовал её, чтобы быстро понимать, как меняется процедура загрузки при изменении параметров карты. И действительно, она снова и снова сообщала о находке CHSETTINGS, пока я пытался форматировать и переформатировать SD-карту, прежде чем в отчаянии не занулил её целиком.

Чтобы попытаться прочитать карту так, как это делал бы процессор TI, можно использовать `dd`. Используйте размер блока 512 и укажите первый сектор (попробуйте проверить, какой именно, с помощью GParted), пропустив первые `n`. Пример: первый сектор — 2048, устройство — `sda`, читаем только первый сектор:```
sudo dd if=/dev/sda1 of=/home/sam/sector2048 bs=512 skip=2048 count=1

Я использовал это, чтобы скачать образы MBR и начала загрузочного раздела прямо с SD-карты — оба они позже пригодились.

Копаем в SD-карте

Мои усилия по реверс-инжинирингу в Ghidra привели меня к функциям обработчика загрузки SD-карты, и теперь я мог видеть функции, отправляющие SD-команды карте, и проходить по шагам, наблюдая, что карта отвечает. Я думал, что смотрю в правильном месте и что карта возвращает все нули. Позже я узнал, что, возможно, я проходил по обработчику eMMC (тот же обработчик, но с другим идентификатором устройства), или что-то ещё было не так, потому что с SD-картой всё было в порядке. Тем не менее я решил, что пора разобраться, как работают эти карты.

Мне было любопытно, почему карта упорно возвращает все нули при каждом запросе блока. Очевидно, функции карты и программное обеспечение могут читать с неё, потому что раньше это уже делалось. Но всё равно пора было подключить всё и посмотреть с помощью логического анализатора. Немного микропайки, фиксация проводов 30awg УФ-отверждаемой эпоксидной смолой, подключение щупов моего Saleae — и у нас есть рабочее устройство.

Я использовал этот анализатор для анализа данных. Сначала я попробовал без вставленной карты.

Для первых нескольких команд тактовая частота составляет 120 кГц. Очевидно, карта не отвечает (её нет).``` CMD0, arg=\0 GO_IDLE_STATE CMD8, arg=\x01\xAA SEND_EXT_CSD CMD55, arg=\0 APP_CMD CMD1, arg=\0 SEND_OP_COND ... CMD0, arg=\0 GO_IDLE_STATE CMD8, arg=\x01\xAA SEND_EXT_CSD CMD55, arg=\0 APP_CMD CMD1, arg=\0 SEND_OP_COND

root@kitploit:~
Когда карта фактически вставлена, после конфигурации частота подскакивает примерно до 6 МГц.

![](https://assets.kitploit.com/production/public/readmes/48883/80a4aca50543bd6766805e8491ba504f07a660b61804ec7a673f738dac0b03cc.png)

После того как я пережил несколько падений из-за использования режима SD (подсказка: даже для этой SD-карты следует использовать режим MMC), я смог убедиться, что карта выдаёт разумные данные. На этом я остановился, потому что удвоил усилия по пониманию загрузочного обработчика SD-карты и осознал, что правильные данные *действительно* считывались, и это были те же данные, что я получил, вручную выполнив `dd` карты! Что ж, это был забавный крюк, который помог мне убедиться, что карта работает.

### Поиск проблемы

Данные приходят из адреса `0x4030c928` (стековая переменная, массив 512 байт) после выполнения ветки по адресу `0x25c2e` для адреса `0x0000` и устройства `8` (см. статические данные по адресу `0x4030d00c`). Войдя в метод, который я назвал `MBR_detection`, программа проверяет магические байты `0xaa55`. Сначала она загружает вторые два байта `0xaa`, затем первый `0xaa`.```
r0 = data[0x1ff]
r1 = data[0x1fe]
orr r0,r1,r0,lsl #8
sub r1,r0,#0xaa00
subs r1,#0x55
bne <return FAIL>

Это удаётся. Однако следующая проверка завершается неудачей:``` r0 = data[0xc] => 0 r1 = data[0xb] => 0 orr r0,r1,r0, lsl #8 cmp r0,#0x200 bne

root@kitploit:~
Псевдокод декомпилятора:```c
if (
	 data[0x1fe] != 0x55aa || 
	 data[0xb]   != 0x200  ||
	(data[0xd] != 1 &&     // bit 0
	 data[0xd] != 2 &&     // bit 1
	 data[0xd] != 4 &&     // bit 2
	 data[0xd] != 8 &&     // bit 3
	 data[0xd] != 0x10 &&  // bit 4
	 data[0xd] != 0x20 &&  // bit 5
	 data[0xd] != 0x40 &&  // bit 6
	 data[0xd] != 0x80)    // bit 7
	) 
{
	return 1;
}

Это проверяет, равен ли байт по адресу 0xb = 11 значению 0x200, и проверяет, равен ли байт по адресу 0xd однобитовому значению. Оба условия должны выполняться, иначе возвращается сбой.

После того как функция обнаружения возвращает 1, обработчик загрузки далее пытается прочитать его как MBR и загружает смещение первого раздела, чтобы попытаться выяснить, является ли он загрузочным разделом. Вот процедура:```C // Call block read function // mmc_block_read_something(boot_device *dev,blk_read_struct blk) ret = ((code *)blockread_struct->block_read_func)(blockread_struct->device_ptr,&block_read_info); if (ret != 0) { return 1; } // Check if device doesn't use MBR ret = MBR_check_bootable_partition((partition_struct *)block_data,blockread_struct); if (ret != 0) { // It uses MBR, try each partition in the partition entries for a bootable // partition ret = MBR_check_entries(block_data,blockread_struct); if (ret != 0) { return 1; } ret = MBR_parse_entries(block_data,&blockread_struct->part_entry); if (ret != 0) { return 1; } // Get bootable partition offset block_read_info = (blk_read_struct *)(blockread_struct->part_entry).first_sect_pos; uStack_220 = 1; pbStack_21c = block_data; // Call block read function // mmc_block_read_something(boot_device *dev,blk_read_struct blk) ret = ((code *)blockread_struct->block_read_func) (blockread_struct->device_ptr,&block_read_info); if (ret != 0) { return 1; } // Try and verify bootable partition again ret = MBR_check_bootable_partition((partition_struct *)block_data,blockread_struct); if (ret != 0) { return 1; } }

root@kitploit:~
Итак, теперь я перехожу к моменту, где считываются данные по адресу `0x800`, и я получаю корректный дамп. Но метод обнаружения MBR по-прежнему возвращает 1, даже с правильными данными (и там есть несколько уровней косвенности, которые мне пришлось проследить, grrr), так что проблема должна быть именно в этом.

Теперь финишная прямая. Первое чтение памяти с SD-карты — это MBR, который содержит до четырёх записей таблицы разделов, см. таблицы 26-20, 26-21 в TRM. Запись таблицы разделов для загрузочного раздела говорит, что раздел содержит `0x40000` секторов. Но файловая система раздела (см. таблицу 26-23 в TRM) сообщает, что их только `0x3fff8`, по какой-то причине, и загрузочный ROM обнаруживает это и даёт сбой.

В качестве эксперимента я перескочил через проблемный переход (это может плохо кончиться... скрестим пальцы...), и программа, безусловно, продолжила работу, хотя я не уверен, куда я попал, похоже на мусор. Но если не обращать на это внимания, устройство на самом деле загружается! Установив точку останова на `0x402f0400` (начало загруженного образа), вижу, что всё идёт правильно. Может, пора подключить UART? UART — это здорово!

Что касается проблемы с SD-картой? Я задал [вопрос на Unix SE](https://unix.stackexchange.com/questions/781715/why-do-the-mbr-partition-entry-and-partition-filesystem-disagree-on-the-number-o/781755), но не получил особой помощи по существу проблемы (хотя кое-какую полезную информацию я всё же получил). После этого: я наконец это исправил! Просматривая команды `mkfs` для создания файловой системы FAT16, я заметил, что [другой источник](https://blog.billvanleeuwen.ca/porting-u-boot-onto-the-beaglebone) использует флаг -a, который отключает выравнивание. В этом и был ключ. Добавив этот флаг и пересобрав, количество секторов совпало (`0x40000`), и система загружается. Полагаю, загрузочный ROM не поддерживает такое выравнивание.

Теперь при подключённой карте я получаю сообщения ниже в цикле загрузки. Ура! Осталось только выяснить, почему ядро не запускается, и тогда мы в дамках! Вся эта работа может наконец окупиться несколькими рабочими платами beaglebone. Стоило ли оно того? Кто знает.```
U-Boot SPL 2021.01-00001-gc59bf25a382-dirty (Jul 24 2024 - 20:38:49 -0400)
Trying to boot from MMC1


U-Boot 2021.01-00001-gc59bf25a382-dirty (Jul 28 2024 - 20:36:46 -0400)

CPU  : AM335X-GP rev 2.1
Model: TI AM335x BeagleBone Black
DRAM:  512 MiB
WDT:   Started with servicing (60s timeout)
NAND:  0 MiB
MMC:   OMAP SD/MMC: 0, OMAP SD/MMC: 1
Loading Environment from FAT... *** Warning - bad CRC, using default environment

<ethaddr> not set. Validating first E-fuse MAC
Net:   eth2: ethernet@4a100000, eth3: usb_ether
Hit any key to stop autoboot:  2 <0x08><0x08><0x08> 1 <0x08><0x08><0x08> 0 
WARNING: Could not determine device tree to use
switch to partitions #0, OK
mmc0 is current device
SD/MMC found on device 0
Failed to load 'boot.scr'
Failed to load 'uEnv.txt'
switch to partitions #0, OK
mmc0 is current device
Scanning mmc 0:1...
libfdt fdt_check_header(): FDT_ERR_BADMAGIC
<0x1b>7<0x1b>[r<0x1b>[999;999H<0x1b>[6n<0x1b>8Scanning disk [email protected]...
Scanning disk [email protected]...
** Unrecognized filesystem type **
Found 4 disks
No EFI system partition
BootOrder not defined
EFI boot manager: Cannot load any image
switch to partitions #0, OK
mmc0 is current device
SD/MMC found on device 0
4997632 bytes read in 353 ms (13.5 MiB/s)
Failed to load '/boot/undefined'

Starting kernel ...

Итак, есть проблема с деревом устройств ("WARNING: Could not determine device tree to use"). Это моя первая встреча с ядром и загрузкой ядра, так что я понятия не имею, что это значит.

«Загрузка ядра» включает следующее, как я это понимаю:

  1. U-Boot загружается и ищет образ ядра (zImage); образ ядра — это сжатый бинарник ядра, zImage самораспаковывается
  2. Образ ядра загружается в память, а затем распаковывается либо сам (zImage), либо U-Boot (uImage)
  3. Ядро выполняет свои обычные низкоуровневые действия, а затем запускает init-программы/демоны

Важным аспектом процесса загрузки ядра является дерево устройств. Оно хранится в файле .dtb (device tree binary; сравните с .dts исходными файлами дерева устройств) для платы.

Моя проблема сейчас в том, что U-Boot не загружает дерево устройств платы, так как в логе нет сообщения reading /am335x-boneblack.dtb. Вместо этого мы получаем WARNING: Could not determine device tree to use. Так что это хорошее доказательство! Предполагаю, что это связано с отсутствующим идентификатором платы в EEPROM.

Некоторые подробности о том, как он получает плату, можно найти в этой теме на форумах TI.

Итак, как U-Boot узнаёт, как настроить себя и правильно загрузиться? В исходниках U-Boot, которые мы собрали, есть папка configs/, в которой хранятся файлы defconfig для различных плат. Эти файлы определяют различные параметры конфигурации U-Boot, включая команду загрузки, которая может выглядеть так:``` if test ${boot_fit} -eq 1; then run update_to_fit; fi; run findfdt; run init_console; run envboot; run finduuid; run distro_bootcmd

root@kitploit:~
Мы определяем, какую конфигурацию использовать, когда запускаем цель `make <boardname>_config`. Функция `findfdt` используется для определения платы, на которой мы работаем, и корректной настройки дерева устройств. Выглядит это так (определена в `am335x_evm.h`):```
"findfdt="\
		"if test $board_name = A335BONE; then " \
			"setenv fdtfile am335x-bone.dtb; fi; " \
		"if test $board_name = A335BNLT; then " \
			"setenv fdtfile am335x-boneblack.dtb; fi; " \
		"if test $board_name = A335PBGL; then " \
			"setenv fdtfile am335x-pocketbeagle.dtb; fi; " \
		"if test $board_name = BBBW; then " \
			"setenv fdtfile am335x-boneblack-wireless.dtb; fi; " \
		"if test $board_name = BBG1; then " \
			"setenv fdtfile am335x-bonegreen.dtb; fi; " \
		"if test $board_name = BBGW; then " \
			"setenv fdtfile am335x-bonegreen-wireless.dtb; fi; " \
		"if test $board_name = BBBL; then " \
			"setenv fdtfile am335x-boneblue.dtb; fi; " \
		"if test $board_name = BBEN; then " \
			"setenv fdtfile am335x-sancloud-bbe.dtb; fi; " \
		"if test $board_name = A33515BB; then " \
			"setenv fdtfile am335x-evm.dtb; fi; " \
		"if test $board_name = A335X_SK; then " \
			"setenv fdtfile am335x-evmsk.dtb; fi; " \
		"if test $board_name = A335_ICE && test $ice_mii = rmii; then " \
			"setenv fdtfile am335x-icev2.dtb; fi; " \
		"if test $board_name = A335_ICE && test $ice_mii = mii; then " \
			"setenv fdtfile am335x-icev2-prueth.dtb; fi; " \
		"if test $fdtfile = undefined; then " \
			"echo WARNING: Could not determine device tree to use; fi; \0" \

Если мы хотим поведение по умолчанию, мы можем просто изменить переменную board_name, верно? Ну, возможно, нет, или по крайней мере я не знаю, где лучше всего её изменить. Но хотя установка board_name сама по себе не сработала, я фактически также обновил файл .dtb по умолчанию, и это сработало!``` _____                    _____           _         _    
|  _  |___ ___ ___ ___   |  _  |___ ___  ||__ | |  
|     |  _| .'| . | . |  |   |  | . | | | -|  _|  _|
|
|
|| |__,|  ||  ||  || ||| ||||   
             |
|                    |___|             

Arago Project http://arago-project.org am335x-evm ttyS0

Arago 2021.09 am335x-evm ttyS0

am335x-evm login: root
root@am335x-evm:~#

root@kitploit:~
Наконец-то мы на терминале. Мои хламовые платы ожили!

### Исправление отсутствующего идентификатора EEPROM

В качестве последнего шага я запишу правильный идентификатор платы в EEPROM — это легко сделать из пользовательского пространства Linux. В исходном коде SPL указаны следующие идентификаторы плат:
- `A335BONE` — плата Beaglebone
- `A335BNLT` — плата Beaglebone Black
- `A335PBGL`
- `A335X_SK`
- `A33515BB`
- `A335_ICE`

Существует также необязательная ревизия платы. Согласно схеме, EEPROM находится на шине I2C0, а сама микросхема (в моей схеме это 24LC32A, хотя она маркирована как '256Kx8) обеспечивает I2C-адрес устройства `0x50` (двоичное `b1010` с последующим `000` адресом микросхемы, поскольку корпус с 5 выводами не имеет дополнительных адресных выводов). И ещё одно важное замечание: вывод WP подтянут к HIGH через резистор 10 кОм, поэтому защита от записи включена по умолчанию; его нужно притянуть к низкому уровню перед любой записью, иначе микросхема будет подтверждать приём, но просто ничего не запишет.

К EEPROM можно обратиться через ядро по адресу `/sys/bus/i2c/devices/0-0050`, внутри которого есть файл `eeprom`. Поэтому, при вытянутом в LOW выводе WP (привяжите TP4 сверху, рядом с разъёмом питания, к земле), достаточно нескольких вызовов `echo`. Формат описан в Beaglebone Black System Reference Manual. Я адаптировал это [отсюда](https://groups.google.com/g/beagleboard/c/di5O5JCl4yw).```sh
root@am335x-evm:~# cat fix_eeprom.sh 
#!/bin/bash
# Fix board ID EEPROM

EEPROM_FILE=/tmp/eeprom.tmp
EEPROM=/sys/bus/i2c/devices/0-0050/eeprom

# header bytes
echo -ne "\xaa\x55\x33\xee" > ${EEPROM_FILE}
# Board ID
echo -n "A335BNLT" >> ${EEPROM_FILE}
# serial number (I left this basically as the template)
echo -n "000C24wwBBoxxxx" >> ${EEPROM_FILE}

dd if=${EEPROM_FILE} of=${EEPROM}

Используя less для проверки, впредь у нас не должно возникнуть проблем с запуском стандартной SD-карты. Изменения в SPL и конфигурации U-Boot можно откатить, как только на всех платах будут записаны EEPROM.

Скачать инструмент
РегионНачальный адресДлина
Boot ROM (Public)0x4002_00000xBFFF
Boot ROM (Public, alias)0x0002_00000xBFFF
SRAM Internal0x402F_04000xFC00
L3 OCM00x4030_00000x10000
Контрольная точкаПодключениеЛист схемыСторона платы
TP1DGND2 (D1)Верх
TP2VDD_MPUON (VDD_MPU_MON)5 (C4)Верх
TP3TESTOUT5 (B2)Верх
TP4Board ID WP11 (B1)Верх