В предыдущей статье был описан процесс настройки 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 сервер:

  1. $ 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
    1. Здесь мы переходим в каталог, где лежат исходники 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. Инициализация до релокации (фиолетовая ветвь/путь, шаги 1-43)

      2. Инициализация после релокации (зеленая ветвь/путь, шаги 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. Но это уже в других статьях.

Комментарии (8)


  1. BiosUefi
    25.10.2021 20:46

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

    А есть ли у армов, что-то подобное PostCode, чтобы отслеживать проходимую u-boot фазу?


    1. almaz1c Автор
      25.10.2021 20:48

      Спасибо! К своему стыду не знаю, что такое PostCode. И нагуглить не удалось.


      1. VBKesha
        25.10.2021 21:46
        +2

        На x86 в биосах реализована такая штука как при прохождении определенного меса в определнный порт IO(обычно 0x80) пишется число которое говорит какая фаза загрузки идёт. И при помощи POST карты можно эти числа смотреть и примерно понимать что пошло не так.


        1. BiosUefi
          25.10.2021 22:04

          Начинали с IO, перешли на LPC шину, сейчас eSPI. Интел имеет свои сотни кодов, амд - свои , ами - свои.

          Бывают очень полезно понатыкать и собственных, если под дебагом поведение отличается от баздебажного, а железного отладчика нет.

          Кстати, а софтверный отладчик достаточно умный , если проходит по смене модели памяти, или прыжки из CAR в RAM?


  1. BiosUefi
    26.10.2021 14:08

    Вы проводили опыты на одноядерном железе, а в принципе gdb поддерживает много ядерную отладку, может знаете ?


    1. almaz1c Автор
      26.10.2021 14:32

      Да, припоминаю, дебажил двух ядерный zynq-7000 с помощью gdb. Правда программатор был другой, - от Xilinx.


  1. dlinyj
    26.10.2021 15:24
    +1

    Статья отличная, больше бы подобного материала на просторах хабра!


    1. almaz1c Автор
      26.10.2021 15:28

      Спасибо!