Embedded Linux. Отладка ядра
Придя в 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 программатору из докер-контейнера;
научимся дебажить модули ядра и многое другое.