В предыдущей статье был описан процесс настройки IDE Eclipse для кросс-платформенной отладки загрузчика U-boot. В данной статье описывается устранение последних ограничений, препятствовавших полноценной его отладки. После чего, получив возможность полноценной отладки кода, пройдемся по всей процедуре инициализации загрузчика от первой инструкции и до конца в режиме отладки.
Устранение проблемы с fdtcontroladdr
Если помните, в прошлый раз остановились на том, что в коде пришлось захардкодить магическое число, позволявшее запустить процессор под отладчиком. Этот костыль не давал покоя. В итоге, продолжив бороздить просторы интернета и исходных кодов загрузчика удалось разобраться что к чему. Как оказалось, fdtcontroladdr - есть адрес памяти, по которому располагается бинарный файл скомпилированного дерева устройств. Подобного тому, что используется ядром Linux. Если мы хотим загрузить отладочную прошивку загрузчика, то должны также позаботиться и о том, чтобы прошивка смогла найти также и бинарь дерева устройств. Как правило, это файл с расширением .dtb. Но если есть сомнения, то открываем файл любым hex ридером и смотрим на первые четыре байта - они должны содержать магическое значение D0 0D FE ED, являющееся признаком начала скомпилированного дерева устройств:
Файл найден. По какому адресу в памяти его нужно расположить и как это сделать?
Ответ на первый вопрос может быть найден в той строчке кода, в которой мы хардкодили адрес:
_end - адрес конца прошивки, который можно узнать, заглянув в файл u-boot.map, - карту памяти, полученную по завершении процедуры линковки:
Ответ на второй вопрос - команда gdb:
restore u-boot.dtb binary 0x87882d68
Настало время проверить гипотезу.
Запускаем GDB сервер:
-
$ JLinkGDBServer -device MCIMX6Y2 -if JTAG -speed 1000
Запускаем отладочную сессию:
$ cd /opt/eclipse/imx6ull-openwrt/build_dir/target-arm_cortex-a7+neon-vfpv4_musl_eabi/u-boot-wirelessroad_ecspi3/u-boot-2017.07 $ gdb-multiarch u-boot --nx
-
Здесь мы переходим в каталог, где лежат исходники u-boot и скомпилированный образ прошивки (файл u-boot).
Далее подключаемся к железке и загружаем прошивку:(gdb) target remote localhost:2331 Remote debugging using localhost:2331 (gdb) monitor reset Resetting target (gdb) monitor halt (gdb) monitor sleep 200 Sleep 200ms (gdb) load Loading section .text, size 0x5c610 lma 0x87800000 ... Loading section .gnu.hash, size 0x18 lma 0x87882e2c Start address 0x87800000, load size 536128 Transfer rate: 69 KB/sec, 12468 bytes/write.
Подгружаем device tree blob и запускаем работу процессора:
(gdb) restore u-boot.dtb binary 0x87882d68 Restoring binary file u-boot.dtb into memory (0x87882d68 to 0x8788953b) (gdb) c
В консоли загрузчика видим логи загрузки, значит процессор стартанул.
Теперь можно добавить эту команду в Debug конфигурацию Eclipse:
Казалось бы все, - мы получили долгожданную возможность полноценной отладки кода, получили способ проследить весь путь процедуры инициализации, заглянуть в каждый закоулок исходного кода. Перезапускаем GDB сессию, ставим точку остановки в одной из самыъ последних вызываемых функций (main_loop) и запускаем работу процессора:
(gdb) b main_loop c
и с удивлением обнаруживаем, что U-boot стартанул, прогрузился сам и запустил процедуру инициализации ядра Linux, но точка остановки так и не сработала! Магия!
Снова лезем в исходники и интернет. Несколько часов поисков и находим то, что лежало под носом (да-да, rtfm).
Как выясняется, в процессе инициализации загрузчик получает возможность узнать полный размер располагаемой оперативной памяти, вычисляет точку в самом ее конце, копирует самого себя и после этого продолжает работу как ни в чем не бывало. Там же, в документации описывается, как данную проблему обойти в случае, если возникла необходимость отладки кода.
В случае нашей железки это выглядит следующим образом. Снова запускаем gdb сервер:
$ JLinkGDBServer -device MCIMX6Y2 -if JTAG -speed 1000
$ gdb-multiarch u-boot --nx (gdb) target remote localhost:2331 (gdb) monitor reset (gdb) monitor halt (gdb) monitor sleep 200 (gdb) load (gdb) restore u-boot.dtb binary 0x87882d68
Смотрим адрес, с которого начнется исполнение кода:
(gdb) display /x $pc 1: /x $pc = 0x87800000
Ставим точку остановки в функции relocate_code, как это описывается в документации и запускаем работу процессора:
(gdb) b relocate_code Breakpoint 1 at 0x87802db4: file arch/arm/lib/relocate.S, line 81. (gdb) c Continuing. Breakpoint 1, relocate_code () at arch/arm/lib/relocate.S:81
Теперь, нам нужно загрузить ту же самую отладочную прошивку (с отладочными символами), но уже по другому адресу, в который намеревается перепрыгнуть процессор. Если мы этого не сделаем, то ничего страшного не случится, - u-boot продолжит работу штатно, но мы уже не сможем ловить процессор точками остановки, поскольку он будет работать по новым адресам. Подгрузим по этому адресу отладочные символы, - и сохраним возможность сопровождать процессор дебаггером. Определить адрес, по которому планируется прыжок можно двумя способами:
в консоли загрузчика ввести команду bdinfo:
=> bdinfo arch_number = 0x00000000 boot_params = 0x80000100 DRAM bank = 0x00000000 -> start = 0x80000000 -> size = 0x10000000 eth0name = FEC ethaddr = 86:72:04:c5:7e:83 current eth = FEC ip_addr = 192.168.31.99 baudrate = 115200 bps TLB addr = 0x8FFF0000 relocaddr = 0x8FF2B000 reloc off = 0x0872B000 irq_sp = 0x8EF216C0 sp start = 0x8EF216B0 Early malloc usage: 188 / 400 fdt_blob = 8ef216d8
и глянуть значение переменной relocaddr (0x8FF2B000).
дебаггером вывести значение переменной gd->relocaddr:
(gdb) p /x gd->relocaddr $1 = 0x8ff2b000
Итак, располагаем отладочные символы по этому адресу, проверяем расположение счетчика программы:
(gdb) add-symbol-file u-boot 0x8FF2B000 add symbol table from file "u-boot" at .text_addr = 0x8ff2b000 (y or n) y Reading symbols from u-boot... (gdb) display /x $pc 2: /x $pc = 0x87802db4
И пробегаем функцию relocation_code:
(gdb) fin Run till exit from #0 relocate_code () at arch/arm/lib/relocate.S:81 _main () at arch/arm/lib/crt0.S:116 116 bl relocate_vectors
Снова смотрим расположение счетчика программ:
(gdb) display /x $pc 3: /x $pc = 0x8ff2d8cc
И вздыхаем с облегчением, - адрес больше того значения, по которому мы только что разместили прошивку. Значит перепрыгнули)
Теперь ставим точку остановки в функции board_init_r и видим, что установилось две точки (в прошивке по первоначальному адресу и в прошивке по адресу релокации):
(gdb) b board_init_r Note: breakpoint 1 also set at pc 0x87815b4c Note: breakpoint 1 also set at pc 0x8ff40b4c Breakpoint 5 at 0x87815b4c: board_init_r. (2 locations)
запускаем исполнение процессора и убеждаемся, что точка остановки по новому адресу сработала:
gdb) c Continuing. Breakpoint 2, board_init_r (new_gd=0x8ef28eb8, dest_addr=2415046656) at common/board_r.c:904 904 { 1: /x $pc = 0x8ff40b4c
В Eclipse то же самое будет выглядеть следующим образом. Ставим точку остановки на функцию relocate_code
:
Запускаем работу процессора:
И перед тем как выполнить релокацию кода, в отладочной консоли (Debugger console) вводим те же самые команды:
Вот теперь можно смело сказать, что появилась полноценная возможность дебажить U-boot. И в качестве вознаграждения можем пробежаться отладчиком от самой первой исполняемой команды и до самой последней, построив попутно подробную диаграмму процедуры инициализации загрузчика:
Сама диаграмма в виде png, pdf и libreOffice.
Процедура инициализации загрузчика U-boot
В целом, всю процедуру можно разделить на два этапа:
Инициализация до релокации (фиолетовая ветвь/путь, шаги 1-43)
Инициализация после релокации (зеленая ветвь/путь, шаги 45-80)
разделенных между собой упоминавшейся выше процедурой релокации (красный узел - узел 44)
На каждом из этапов происходит вызов одной и той же функции - initcall_run_list, принимающей на вход аргумент в виде массива указателей на функции инициализации, которые она вызывает в цикле). Естественно, на каждом этапе передаются разные массивы функций. На первом этапе это init_sequence_f, который можно найти в файле common/board_f.c:
На втором этапе - init_sequnce_r (файл common/board_r.c):
Где все начинается
-
Шаг 1 на графе.
Исполнение программного кода загрузчика расположено в файле /arch/arm/cpu/<arch>/startup.S. В нашем случае это - /arch/arm/cpu/armv7/startup.S:
Да, все начинается с ассемблера. Если вы пришли в embedded linux, из микроконтроллеров, то, возможно, вы уже сталкивались с ним.
Шаги 2, 3 и 4.
Последовательно производится: отключение предываний, инициализация таблицы векторов прерываний, отключение MMU и TLBs, инициализация временного стека и минимально необходимой периферии.
Шаги 5, 6 и 7
Функция _main выделяет память под глобальный дескриптор (global_data, gd), - структуру данных, описывающей текущее состояние загрузчика.
Шаги 8 и 9
После чего происходит вызов функции board_init_f, в свою очередь вызывающую уже упоминавшуюся функцию initcall_run_list в первый раз.
Шаги 10-43
которая вызывает в цикле все переденные ей входным аргументом функции инициализации. Тут происходит и инициализация переменных окружения u-boot, и инициализация консоли, вывод первых логов, парсинг дерева устройств, бинд драйверов, соответствующих узлам дерева. Под конец производится вычисления адреса, по которому будет произведена релокация загрузчика.
Шаги 44-45
Собственно, релокация, в процессе которой загрузчик копирует сам себя в конец оперативной памяти, чтобы выделить мах количество свободной оперативной памяти для ядра linux в виде единого цельного участка памяти.
Шаги 48-49
Происходит вызов функции board_init_r, вызывающей, в свою очередь, функцию initcall_run_list во второй раз.
Шаги 50-75
Последовательно вызываются все функции, переданные board_init_r. Самой последней вызывается функция run_main_loop, которая вызывает main_loop, в которой запускается таймер обратного отсчета, по прошествии которого управление передается ядру Linux, либо нет, если пользователь ввел что-либо в консоль.
Естественно, это лишь верхушка айсберга, и исходники еще вычитывать и вычитывать. Определенно, стоит разобраться с тем, как парсится дерево устройств и биндятся соответствующие драйвера; с тем как передается управление ядру Linux. Но это уже в других статьях.
-
BiosUefi
Прекрасный материал. А сожалению подобное железо, не всегда под рукой, чтобы по горячим следам повторить и вопросы которые могут возникнуть своевременно прояснить.
А есть ли у армов, что-то подобное PostCode, чтобы отслеживать проходимую u-boot фазу?
almaz1c Автор
Спасибо! К своему стыду не знаю, что такое PostCode. И нагуглить не удалось.
VBKesha
На x86 в биосах реализована такая штука как при прохождении определенного меса в определнный порт IO(обычно 0x80) пишется число которое говорит какая фаза загрузки идёт. И при помощи POST карты можно эти числа смотреть и примерно понимать что пошло не так.
BiosUefi
Начинали с IO, перешли на LPC шину, сейчас eSPI. Интел имеет свои сотни кодов, амд - свои , ами - свои.
Бывают очень полезно понатыкать и собственных, если под дебагом поведение отличается от баздебажного, а железного отладчика нет.
Кстати, а софтверный отладчик достаточно умный , если проходит по смене модели памяти, или прыжки из CAR в RAM?