Поиск Reset Handler и трассировка вызова main
Когда Pico 2 перезагружается, процессор начинает выполнение с Reset Handler. Чтобы понять, как запускается наша программа, найдём Reset Handler, дизассемблируем его и пройдём по цепочке вызовов до нашей настоящей функции main на Rust.
Когда Pico 2 запускается, процессор не переходит сразу к нашей функции main на Rust. Вместо этого он проходит короткую цепочку функций, которую предоставляет среда выполнения Cortex-M.
В этом разделе мы:
- Найдём адрес, с которого чип начинает выполнение после сброса
- Посмотрим, какую функцию вызывает Reset Handler
- Пройдём по цепочке до нашей настоящей функции
mainна Rust
Чтение записи вектора сброса
Процессор Cortex-M начинает выполнение с чтения таблицы в начале флеш-памяти, которая называется таблицей векторов.
Первые две записи:
- Слово 0 (смещение 0x00): начальное значение указателя стека
- Слово 1 (смещение 0x04): адрес обработчика сброса
На Pico 2 флеш-память начинается с адреса 0x10000000, поэтому:
- Начальное значение указателя стека хранится по адресу 0x10000000
- Адрес обработчика сброса находится по адресу 0x10000004
Что такое Reset Handler?
Reset Handler — это первая функция, которая выполняется при включении или сбросе процессора. Она выполняет инициализацию и в конечном итоге вызывает нашу функцию main.
Прочитаем её в GDB:
(gdb) x/wx 0x10000004
Пример вывода — реальные адреса в ваших сборках могут отличаться, используйте свои, следуя этой главе:
0x10000004 <__RESET_VECTOR>: 0x1000010d
Это значение — адрес, на который процессор переходит после сброса. Младший бит (так называемый «бит Thumb») всегда равен 1, поэтому фактический адрес — 0x1000010c.
Но можно использовать любое из двух значений (0x1000010d или 0x1000010c), GDB с обоими справится.
Также можно использовать программу readelf, чтобы найти адрес точки входа:
arm-none-eabi-readelf -h ./target/thumbv8m.main-none-eabihf/debug/pico-debug
Дизассемблирование Reset Handler
Теперь попросим GDB показать инструкции по этому адресу:
(gdb) disas 0x1000010d
# или
(gdb) disas 0x1000010c
Вы увидите ассемблерные инструкции обработчика сброса. Найдите инструкцию bl (Branch with Link), которая вызывает другую функцию:
...
0x10000140 <+52>: isb sy
0x10000144 <+56>: bl 0x1000031c <main>
0x10000148 <+60>: udf #0
Reset Handler вызывает функцию по адресу 0x1000031c, которую GDB показывает как main, — но это ещё не наша функция main на Rust.
Что это за «main»?
main по адресу 0x1000031c — не функция main нашей программы. Это небольшая обёртка, созданная крейтом cortex-m-rt. Такую обёртку часто называют трамплином (trampoline), потому что позже она передаёт управление настоящей точке входа.
Её демангленное имя обычно такое:
#![allow(unused)]
fn main() {
// NOTE: здесь префикс pico_debug — это имя нашего проекта
pico_debug::__cortex_m_rt_main_trampoline
}
Давайте дизассемблируем её.
Дизассемблирование трамплина
(gdb) disas 0x1000031c
Вывод:
Dump of assembler code for function main:
0x1000031c <+0>: push {r7, lr}
0x1000031e <+2>: mov r7, sp
0x10000320 <+4>: bl 0x10000164 <_ZN10pico_debug18__cortex_m_rt_main17he0b4d19700c84ad2E>
End of assembler dump.
Это очень маленький кусок кода. Всё, что он делает, — вызывает настоящую точку входа на Rust, которая называется:
#![allow(unused)]
fn main() {
pico_debug::__cortex_m_rt_main
}
Включение демангленных имён
Имена функций Rust по умолчанию искажены (mangled) и выглядят нечитаемо.
Включим деманглинг (demangling):
set print asm-demangle on
Теперь попробуйте:
(gdb) disas 0x1000031c
# или
(gdb) disas pico_debug::__cortex_m_rt_main_trampoline
Теперь должны отображаться читаемые имена Rust.
Dump of assembler code for function pico_debug::__cortex_m_rt_main_trampoline:
0x1000031c <+0>: push {r7, lr}
0x1000031e <+2>: mov r7, sp
0x10000320 <+4>: bl 0x10000164 <pico_debug::__cortex_m_rt_main>
End of assembler dump.
Дизассемблирование настоящей функции main на Rust
Теперь посмотрим на нашу функцию main:
disas pico_debug::__cortex_m_rt_main
Вы увидите логику программы: сначала код начальной настройки, затем цикл, который переключает вывод светодиода.
#![allow(unused)]
fn main() {
...
0x100002dc <+376>: bl 0x100079a4 <rp235x_hal::timer::Timer<rp235x_hal::timer::CopyableTimer0>::new_timer0>
0x100002e0 <+380>: bl 0x10000b30 <rp235x_hal::gpio::Pin<rp235x_hal::gpio::pin::bank0::Gpio25, rp235x_hal::gpio::func::FunctionNull, rp235x_hal::gpio::pull::PullDown>::into_push_pull_output<rp235x_hal::gpio::pin::bank0::Gpio25, rp235x_hal::gpio::func::FunctionNull, rp235x_hal::gpio::pull::PullDown>>
...
0x100002f8 <+404>: bl 0x10000c48 <rp235x_hal::gpio::eh1::{impl#1}::set_high<rp235x_hal::gpio::pin::bank0::Gpio25, rp235x_hal::gpio::pull::PullDown>>
...
0x10000306 <+418>: bl 0x100006b8 <rp235x_hal::timer::{impl#7}::delay_ms<rp235x_hal::timer::CopyableTimer0>>
...
0x1000030c <+424>: bl 0x10000c38 <rp235x_hal::gpio::eh1::{impl#1}::set_low<rp235x_hal::gpio::pin::bank0::Gpio25, rp235x_hal::gpio::pull::PullDown>>
...
}