-
Notifications
You must be signed in to change notification settings - Fork 62
Booting with gdb
Целью данной лабораторной работы является детальное изучение процесса загрузки ядра Cortex-M0 с использованием отладчика GDB. Данная работа не является обязательной и рекомендована для людей, желающих разобраться в языке ассемблер данного ядра.
Стоит напомнить, что на отладочной плате STM32F0-Discovery помимо микроконтроллера STM32F051R8T6 установлен также программатор ST-LINK/V2 для прошивки. Данный программатор представляет собой обычный микроконтроллер STM32, который отличается от нашего лишь серией (STM32F103C8T6) и который использует для прошивки протокол SWD (Serial Wire Debug). Программатор по специальной системной шине имеет прямой доступ к ядру, а следовательно как к FLASH памяти, куда записывается программа, так и ко всей периферии.
При сборке пустого проекта вы уже запустили отладчик и увидели, что программа начинает свое исполнение с функции main и попадает на бесконечный цикл. На самом деле перед вызовом main процессору необходимо выполнить следующие задачи:
- Подготовить стек;
- Скопировать секцию Data из FLASH в RAM;
- Скопировать секцию BSS из FLASH в RAM;
- Вызвать некоторые вспомогательные функции.
Если с подготовкой стека все более-менее понятно, то зачем копировать куда-то какие-то секции не очень ясно. На лекции было сказано, что секция Data располагается в оперативной памяти и используется микроконтроллером для хранения глобальных или статических переменных, начальное значение которых было задано.
При прошивке программы полученный код (вместе с функцияи и всеми переменными) попадает только во FLASH память и никуда более. И чтобы как-то сохранить все информацию о переменных и их значениях, линкер помещает секцию Data в бинарный файл, а после запуска кода в файле startup_stm32f051x8.s перемещает секцию Data из FLASH в RAM. Таким образом полученный бинарный файл хранит не только код, но и все переменные. Выходит у секции Data есть формально два адреса:
- Адрес в оперативной памяти после копирования (который будет использоваться для работы с переменными);
- Адрес загрузки во FLASH памяти (LMA - Load Memory Address), откуда данные будут скопированы в RAM.
Этот же подход работает и для случая с секцией BSS, только в ней хранятся глобальные и статические переменные, значение которых не было задано.
Для того чтобы убедиться, что все происходит именно так, пройдемся по коду файла startup_stm32f051x8.s, попутно объясняя все, что происходит.
Перейдите в labs/01_blank и выполните:
make gdb-server-st
Откройте второй терминал и выполните:
make gdb-st-util
Переключитесь в графический интерфейс (Ctrl-X -> Ctrl-A) и выполните теперь уже в GDB:
(gdb) layout asm
После должна получиться следующая картина:
Рис. 1. Начальное состояние GDB
Это значит, что микроконтроллер успешно запустился, сохранил адрес для стека и перешел на самый первый обработчик прерывания Reset. Проверьте заодно правильно ли инициализировался стек, распечатав регистр sp ядра cortex-m0:
(gdb) p/x $sp
$1 = 0x20002000
Откройте слайд лекции и убедитесь, что это действительно конец адрес конца оперативной памяти.
Вернемся к Reset_Handler и увидим, что в отладчике код представлен в следующем формате:
| Адрес инструкции в памяти | Имя функции + сдвиг | Инструкция + адрес константы |
Для примера, если взять строчку, на которой вы сейчас находитесь, то ее содержимое будет следующее:
- Адрес текущей инструкции: 0x80002d4;
- Имя функции: Reset_Handler (так как мы в обработчике)
- Инструкция:
ldr r0, [pc, #52] ; (0x800030c <LoopForever+2>)
Инструкцию можно разбить на две составляющие:
- pc, #52 - взять следующий адрес инструкции (на след. инструкции pc будет равен 0x80002d8) и прибавить к нему 52 (0x800030c);
- ldr r0, [0x800030c] - извлечь 4 байтное число из памяти по адресу 0x800030c и положить его в регистр r0
Далее выполните (команда si значит выполнить след. инструкцию):
(gdb) si
(gdb) p/x $r0
И видим, что в регистре r0 оказалось число 0x20002000.
Интересно, что если мы посмотрим на эту строчку в исходном файле startup_stm32f051x8.s, то на этом месте будет:
ldr r0, =_estack
То есть число 0x20002000 загружается напрямую в регистр r0 без указателя в памяти. Дело в том, что в cortex-m0 нет поддержки инструкций с загрузкой 32-битных констант в регистр напрямую, необходимо разместить это число где-то в памяти и потом обратиться к нему по адресу. Собственно за нас это сделала программа ассемблер, упростив нам жизнь. Поэтому чтобы не путаться и видеть названия констант, будем подсматривать попутно в файл startup_stm32f051x8.s.
Выполните следующую команду 4 раза:
(gdb) si
Теперь в регистре r0 окажется адрес начала сегмента данных, в регистре r1 адрес конца сегмента данных а в регистре r2 адрес, откуда секцию Data необходимо скопировать. Таким образом программе необходимо перенести данные из памяти, начиная с адреса r2, в память со диапазоном адресов r0-r1. Весь алгоритм представлен в следующей части кода:
CopyDataInit:
ldr r4, [r2, r3] ; r4 <= *(r2 + r3)
str r4, [r0, r3] ; *(r0 + r3) <= r4
adds r3, r3, #4 ; r3 <= r3 + 4
LoopCopyDataInit:
adds r4, r0, r3 ; r4 <= r0 + r3
cmp r4, r1 ; Сравить r4 с r1
bcc CopyDataInit ; если r4 меньше r1 то перейти на CopyDataInit
На языке C алгоритм выглядел бы следующим образом:
uint32_t r0 = real_data_start;
uint32_t r1 = real_data_end;
uint32_t r2 = data_start;
uint32_t r3 = 0;
uint32_t r4 = 0;
for (r3 = 0; r4 < r1; r3 += 4)
*(r0 +r3) = *(r2 + r3)
r4 = r3 + r0
Собственно это и происходит в коде начиная с адреса, где вы находитесь, до адреса 0x80002ec (можно использовать стрелочки, чтобы листать код). Убедитесь в этом, исполняйте инструкции пока не увидете, что попали в цикл.
Чтобы не исполнять весь код вручную и перейти к следующему участку, установите брейкпоинт на адрес 0x80002ee:
(gdb) b *0x80002ee
И пустите выполнение программы:
(gdb) c
Рассмотрим следующую часть кода:
/* Zero fill the bss segment. */
ldr r2, =_sbss ; Загрузить начальный адрес BSS в r2
ldr r4, =_ebss ; Загрузить конечный адрес секции BSS в r4
movs r3, #0 ; r3=0
b LoopFillZerobss ; Перейти на LoopFillZerobss
FillZerobss:
str r3, [r2] ; Загрузить значение r3 по адресу из r2 (т.е пишем 0)
adds r2, r2, #4 ; r2 <= r2 + 4
LoopFillZerobss:
cmp r2, r4 ; Cравнить r2 и r4
bcc FillZerobss ; Если r2 < r4, то иди в FillZerobss
Единственное отличие кода от предудущего в том, что здесь данные не копируются, а зануляются.
Теперь таким же образом выполните несколько инструкций в gdb, чтобы убедиться, что это цикл. Чтобы не ждать, поставьте брейкпоинт на адрес 0x80002fe и запустите выполнение.
После должна быть следующая картина:
Рис. 2. После инициализации памяти
Итак, микроконтроллер полностью инициализировал память и теперь переходит на вызов функции SystemInit, в которой происходит сброс регистров системы тактирования. Эта функция присваивает определенным регистрам какие-то значения и завершается. Для нас она интереса не представляет.
Поставьте еще брейкпоинт на адреса 0x8000302 и 0x8000306. Запустите выполнение и теперь отладчик остановится на вызове функции __libc_init_array. Эта функция из стандартной библиотеки libc и необходима, чтобы вызвать конструкторы для всех классов в коде. В C классов быть не может, поэтому функция просто ничего не делает.
Еще раз запустите выполнение кода и теперь отладчик будет готов к вызову функции main.
Переключитесь в режим отладки C кода и выполните одну инструкцию:
(gdb) layout src
(gdb) c
Микроконтроллер попал в код функции main:
Рис. 3. Функция main
На этом рассмотрение всего пути от прерывания Reset до main подошло к концу. Дальше будет исполняться код, который вы напишите.