Skip to content

Booting with gdb

Edgar K edited this page Mar 6, 2019 · 31 revisions

Целью данной лабораторной работы является детальное изучение процесса загрузки ядра Cortex-M0 с использованием отладчика GDB. Данная работа не является обязательной и рекомендована для людей, желающих разобраться в языке ассемблер данного ядра.

Стоит напомнить, что на отладочной плате STM32F0-Discovery помимо микроконтроллера STM32F051R8T6 установлен также программатор ST-LINK/V2 для прошивки. Данный программатор представляет собой обычный микроконтроллер STM32, который отличается от нашего лишь серией (STM32F103C8T6) и который использует для прошивки протокол SWD (Serial Wire Debug). Программатор по специальной системной шине имеет прямой доступ к ядру, а следовательно как к FLASH памяти, куда записывается программа, так и ко всей периферии.

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

  1. Подготовить стек;
  2. Скопировать секцию Data из FLASH в RAM;
  3. Скопировать секцию BSS из FLASH в RAM;
  4. Вызвать некоторые вспомогательные функции.

Если с подготовкой стека все более-менее понятно, то зачем копировать куда-то какие-то секции не очень ясно. На лекции было сказано, что секция Data располагается в оперативной памяти и используется микроконтроллером для хранения глобальных или статических переменных, начальное значение которых было задано.

При прошивке программы полученный код (вместе с функцияи и всеми переменными) попадает только во FLASH память и никуда более. И чтобы как-то сохранить все информацию о переменных и их значениях, линкер помещает секцию Data в бинарный файл, а после запуска кода в файле startup_stm32f051x8.s перемещает секцию Data из FLASH в RAM. Таким образом полученный бинарный файл хранит не только код, но и все переменные. Выходит у секции Data есть формально два адреса:

  1. Адрес в оперативной памяти после копирования (который будет использоваться для работы с переменными);
  2. Адрес загрузки во 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

После должна получиться следующая картина:

gdb-init.png Рис. 1. Начальное состояние GDB

Это значит, что микроконтроллер успешно запустился, сохранил адрес для стека и перешел на самый первый обработчик прерывания Reset. Проверьте заодно правильно ли инициализировался стек, распечатав регистр sp ядра cortex-m0:

(gdb) p/x $sp
$1 = 0x20002000

Откройте слайд лекции и убедитесь, что это действительно конец адрес конца оперативной памяти.

Вернемся к Reset_Handler и увидим, что в отладчике код представлен в следующем формате:

| Адрес инструкции в памяти | Имя функции + сдвиг | Инструкция + адрес константы |

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

  • Адрес текущей инструкции: 0x80002d4;
  • Имя функции: Reset_Handler (так как мы в обработчике)
  • Инструкция:
ldr    r0, [pc, #52]   ; (0x800030c <LoopForever+2>)

Инструкцию можно разбить на две составляющие:

  1. pc, #52 - взять следующий адрес инструкции (на след. инструкции pc будет равен 0x80002d8) и прибавить к нему 52 (0x800030c);
  2. 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 и запустите выполнение.

После должна быть следующая картина:

gdb-post.png Рис. 2. После инициализации памяти

Итак, микроконтроллер полностью инициализировал память и теперь переходит на вызов функции SystemInit, в которой происходит сброс регистров системы тактирования. Эта функция присваивает определенным регистрам какие-то значения и завершается. Для нас она интереса не представляет.

Поставьте еще брейкпоинт на адреса 0x8000302 и 0x8000306. Запустите выполнение и теперь отладчик остановится на вызове функции __libc_init_array. Эта функция из стандартной библиотеки libc и необходима, чтобы вызвать конструкторы для всех классов в коде. В C классов быть не может, поэтому функция просто ничего не делает.

Еще раз запустите выполнение кода и теперь отладчик будет готов к вызову функции main.

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

(gdb) layout src
(gdb) c

Микроконтроллер попал в код функции main:

gdb-main.png Рис. 3. Функция main

На этом рассмотрение всего пути от прерывания Reset до main подошло к концу. Дальше будет исполняться код, который вы напишите.

Clone this wiki locally