[Перевод] U-boot. Процедура инициализации по шагам

Здесь мы переходим в каталог, где лежат исходники 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:

image-loader.svg

Казалось бы все, — мы получили долгожданную возможность полноценной отладки кода, получили способ проследить весь путь процедуры инициализации, заглянуть в каждый закоулок исходного кода. Перезапускаем 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
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 (0×8FF2B000).

(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

:

Запускаем работу процессора:

image-loader.svg

И перед тем как выполнить релокацию кода, в отладочной консоли (Debugger console) вводим те же самые команды:

image-loader.svg

Вот теперь можно смело сказать, что появилась полноценная возможность дебажить U-boot. И в качестве вознаграждения можем пробежаться отладчиком от самой первой исполняемой команды и до самой последней, построив попутно подробную диаграмму процедуры инициализации загрузчика:

image-loader.svg

Сама диаграмма в виде png, pdf и libreOffice.

Процедура инициализации загрузчика U-boot

В целом, всю процедуру можно разделить на два этапа:

  1. Инициализация до релокации (фиолетовая ветвь/путь, шаги 1–43)

  2. Инициализация после релокации (зеленая ветвь/путь, шаги 45–80)

разделенных между собой упоминавшейся выше процедурой релокации (красный узел — узел 44)

На каждом из этапов происходит вызов одной и той же функции — initcall_run_list, принимающейна вход аргумент в виде массива указателей на функции инициализации, которые она вызывает в цикле). Естественно, на каждом этапе передаются разные массивы функций. На первом этапе это init_sequence_f, который можно найти в файле common/board_f.c:

image-loader.svg

На втором этапе — init_sequnce_r (файл common/board_r.c):

image-loader.svg

Где все начинается

  • Шаг 1 на графе.

    Исполнение программного кода загрузчика расположено в файле /arch/arm/cpu//startup.S. В нашем случае это — /arch/arm/cpu/armv7/startup.S:

image-loader.svg

Да, все начинается с ассемблера. Если вы пришли в 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. Но это уже в других статьях.

© Habrahabr.ru