Придя в embedded linux из мира микроконтроллеров, такого привычного инструмента отладки кода, как пошаговая отладка кода на целевой железке с помощью аппаратного программатора, - очень не хватало. В предыдущих статьях описано, как мы учились дебажить загрузчик u-boot: 1, 2. С ядром все оказалось сложнее. Например, выяснилось, что ядро Linux в принципе невозможно скомпилировать с отключенной оптимизацией (-O0). В статье описывается как нам все таки удалось запустить ядро на микропроцессоре ARM в режиме пошаговой отладки.

Подготовка исходников

$ git clone https://github.com/wireless-road/imx6ull-openwrt.git
$ cd imx6ull-openwrt
$ ./compile.sh flexcan_ethernet

Исходники ядра после завершения сборки можно найти тут:

./build_dir/target-arm_cortex-a7+neon-vfpv4_musl_eabi/linux-imx6ull_cortexa7/linux-4.14.199/

Сборка ядра с флагом -Og

Первым делом мы попытались скомпилировать ядро с отключенной оптимизацией и наткнулись на неприятный сюрприз:

$ cd ./build_dir/target-arm_cortex-a7+neon-vfpv4_musl_eabi/linux-imx6ull_cortexa7/linux-4.14.199/
$ make menuconfig

Такой возможности в принципе не предусмотрено! Предлагается только два варианта оптимизации: -O2 и -Os. Отладка в GDB в обоих случаях не даст ничего хорошего, - вместо пошагового прохождения кода Program Counter будет хаотично перепрыгивать целые куски кода и вызовы функций. При попытке вычитать значения переменных вы будете то и дело натыкаться на сообщение: Optimized Out. Если ручками залезть в .config и Makefile и попытаться собрать ядро с флагом -O0, то сборка упадет с большим количеством сообщений об ошибке. Как выяснилось, это в принципе невозможно, поскольку оптимизация при сборке ядра используется для совершенно других целей, для которых флаги оптимизации не должны использоваться, а именно, для отключения не использующегося кода. Грязный хак, от которого, видимо, уже не избавиться. На наше счастье, кое кто до нас все таки озадачивался проблемой отладки ядра и даже написал патч, который позволяет собрать ядро с флагом -Og, но, почему-то, отклоненный сообществом. В итоге, пришлось его адаптировать вручную под наши исходники.

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

+ifdef CONFIG_CC_OPTIMIZE_FOR_DEBUGGING
+KBUILD_CFLAGS	+= -Og
+KBUILD_CFLAGS	+= $(call cc-disable-warning,maybe-uninitialized,)
+else
 ifdef CONFIG_CC_OPTIMIZE_FOR_SIZE
 KBUILD_CFLAGS   += -Os $(EXTRA_OPTIMIZATION)
 else
 KBUILD_CFLAGS   += -O2 -fno-reorder-blocks -fno-tree-ch $(EXTRA_OPTIMIZATION)
 endif
+endif

и отключить дефайнами возникающие на этапе компиляции ошибки:

+#if !defined(__CHECKER__) && !defined(CONFIG_CC_OPTIMIZE_FOR_DEBUGGING)
 # define __compiletime_warning(message) __attribute__((warning(message)))
 # define __compiletime_error(message) __attribute__((error(message)))
 #endif /* __CHECKER__ */

Полный код патча и связанных с ним изменений можно глянуть тут: 1, 2.

Device Tree и JTAG

Далее нужно убедиться, что пины микропроцессора, на которые выведен JTAG интерфейс не переиспользуется для каких-либо других целей. Для этого открываем, используемый нами dts-файл и ищем упоминания jtag-пинов. Если вы находите что-либо похожее:

pinctrl_sai2: sai2grp {
	fsl,pins = <
		MX6UL_PAD_JTAG_TDI__SAI2_TX_BCLK 0x17088
		MX6UL_PAD_JTAG_TDO__SAI2_TX_SYNC 0x17088
		MX6UL_PAD_JTAG_TRST_B__SAI2_TX_DATA 0x11088
		MX6UL_PAD_JTAG_TCK__SAI2_RX_DATA 0x11088
		MX6UL_PAD_JTAG_TMS__SAI2_MCLK 0x17088
	>;
};

то вам не повезло. Возможность отладки ядра вы сможете получить только отключив интерфейс, которые переиспользует JTAG-пины. Их придется отключить. Т.е. в худшем случае, если вам нужно отладить функционал, задействующий JTAG-пины микропроцессора, у вас не получится этого сделать. Т.е. необходимость использования JTAG-интерфейса нужно заложить еще на этапе разработки печатной платы устройства. Либо уточнить в документации на приобретаемый модуль.

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

CONFIG_KERNEL_CC_OPTIMIZE_FOR_DEBUGGING=y

После этого можно выполнить повторную сборку образа:

./compile.sh flexcan_ethernet

либо только ядра отдельно:

make target/linux/compile

Графическая IDE Eclipse

Ее настройки под исходники ядра практически ничем не отличается от настройки для отладки загрузчика U-boot: раздел "Установка IDE и создание проекта" предыдущей статьи с тем лишь отличием, что при создании проекта нужно выбрать каталог с исходниками ядра вместо исходников загрузчика.

На данный момент нам пока не удалось корректно дебажить ядро из Eclipse, а потому оно пока используется лишь для навигации по коду. В будущем, планируем прикрутить полноценную графическую отладку кода из IDE.

Отладка ядра в консольном режиме

Итак, приступаем непосредственно к отладке.

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

Запускаем GDB сервер:

$ JLinkGDBServer -device MCIMX6Y2 -if JTAG -speed 1000

Запускаем GDB сессию:

$ cd ./build_dir/target-arm_cortex-a7+neon-vfpv4_musl_eabi/linux-imx6ull_cortexa7/
$ gdb-multiarch vmlinux.debug --nx

в консоли gdb сесии:

(gdb) target remote localhost:2331
(gdb) restore flexcan_ethernet-uImage binary 0x82000000
(gdb) restore image-flexcan_ethernet.dtb binary 0x83000000
(gdb) b __hyp_stub_install
Breakpoint 1 at 0x80110b20: file arch/arm/kernel/hyp-stub.S, line 89.
(gdb) c
Continuing.

после этого должна ожить консоль загрузчика. Выполните в ней загрузку ядра:

=> bootm 82000000 - 83000000

После этого переключитесь обратно в консоль gdb сессии, вы должны увидеть что-то подобное:

Breakpoint 1, __hyp_stub_install () at arch/arm/kernel/hyp-stub.S:89
89 store_primary_cpu_mode r4, r5, r6
(gdb)

Вы находитесь в той точке, с которой начинается работа ядра! Попробуйте погулять по коду командами s и n, и вывести значения переменных/регистров командой p:

Если продолжите шагать по коду командами s и n, то можете утомиться. Чтобы быстрее попасть в интересующую вас функцию, задайте точку остановки, например, функцию start_kernel:

(gdb) b start_kernel
(gdb) c

попробуйте пошагать и в ней. Не должно быть никаких хаотичных перемещений по коду, т.е. если вы задаете команду n (перепрыгнуть функцию), то вы недолжны неожиданно оказаться в теле какой-либо другой функции. Значения большинства встречающихся в коде переменных должно также быть доступно для считывания без каких-либо сообщений "optimized out". Для сверки можете использовать Eclipse, чтобы убедиться, что все действительно идет по плану:

На этом все.

В дальнейших статьях мы:

  • запустим в отладчике работу u-boot и ядра последовательно;

  • разберемся, как загрузчик передает управление ядру;

  • построим карту загрузки ядра по аналогии с картой U-boot;

  • упакуем инструменты разработки в docker-контейнер, что сведет к минимуму ошибки при развертывании среды разработки новыми разработчиками (в том числе новичками), включая и запуск графической IDE, и подключение к USB программатору из докер-контейнера;

  • научимся дебажить модули ядра и многое другое.

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


  1. dmiprok
    01.12.2021 01:18
    -3

    Время за полночь, наткнулся на Вашу статью, ничего не понимаю, но прочитал с интересом.


  1. Kitsok
    01.12.2021 07:23
    +2

    Класс! Я всегда обходился отладочными принтами, даже в загрузчике, но про JTAG как предпоследний способ помню. Спасибо за статьи!


    1. lorc
      01.12.2021 10:51
      +1

      Я бы рекомендовал еще посмотреть на OpenOCD. Он неожиданно восстал из мертвых и начал активно развиваться.

      А по поводу дебага принтами - это хорошо когда они есть. Мы когда-то чинили раннюю загрузку то ли U-Boot, то ли Xen, уже не помню точно. Но факт в том, что на тех этапах загрузки консоль еще не работала. Кто-то придумал гениальную идею - дергать SMC из разных мест, и смотреть как Secure Monitor (это еще на armv7 было) ругается на неизвестный ID запроса.


      1. Kitsok
        01.12.2021 17:08

        Хм, с этой точки зрения я на OpenOCD не смотрел очень давно, а вот программироваться всякие CPLD - только в путь. Спасибо за наводку!


        1. lorc
          01.12.2021 22:53
          +1

          Ну там например активно развивается поддержка ARMv8, что меня безмерно радует. Плюс он умеет одну очень крутую штуку, которую не может дорогущий Lauterbach - одновременный дебаг нескольких ядер с разной архитектурой. На том SoC, с которым мы в основном работаем есть как и здоровые Cortex A5x на ARMv8, так и пару Cortex R на ARMv7. С OpenOCD я могу отлаживать их одновременно. В Trace32 - нет.


  1. realimba
    01.12.2021 15:23
    -1

    наступал 2022 - мы дебажили принтами и роскошным gdb, куда уж тут "убогому" windbg..


    1. almaz1c Автор
      01.12.2021 15:39

      wingdb от слова windows?)


    1. Kitsok
      01.12.2021 17:06
      +1

      Даже в 2122 будут этапы загрузки компьютера, на которых без подобной околожелезной отладки никуда не деться.


    1. lorc
      01.12.2021 22:57

      gdb сервера есть практически везде: от софтовых в Android и заканчивая типа "аппаратными" в каком-нибудь Black Magic Probe. Ну и само собой они есть в qemu и openocd.

      А вот удаленный протокол WinDbg поддерживает... кто? Только Windows?


  1. ignat99
    01.12.2021 17:24

    Для отладки в реальном режиме времени надо делать инструментализацию ядра и подключать профилировщик.

    Отладка ядра дебагом практически ничего не даёт. Так как многие функции запускают процессы (физические) на железе параллельно. Поэтому либо надо знать порядок запуска, либо отлавливать события в реальном режиме времени. IMHO


    1. almaz1c Автор
      01.12.2021 17:26

      вы имеете ввиду нечто подобное ?


    1. lorc
      01.12.2021 22:49

      Зависит от того что нужно отлаживать. Если какой-нибудь высокопараллельный процесс вроде скедулинга или там отрисовки буферов в DRM - то тут конечно трейсинг (особенно аппартный) рулит. А вот классические профилировщики типа oprofile помогают в очень редких случаях.

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