[Перевод] 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

image-loader.svgimage-loader.svgimage-loader.svg

Такой возможности в принице не предусмотрено! Предлагается только два варианта оптимизации: -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:

image-loader.svg

Запускаем 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:

image-loader.svg

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

(gdb) b start_kernel
(gdb) c

image-loader.svg

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

image-loader.svg

На этом все.

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

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

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

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

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

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

© Habrahabr.ru