QEMU固件模拟技术-stm32仿真分析及IRQ仿真实践
文章首發于
https://forum.butian.net/share/124
概述
上一篇文件介紹了luaqemu的實現,也提到luaqemu并沒有對中斷相關api進行封裝,本節主要基于stm32f205-soc的實現來介紹中斷的仿真,并提供一個用于測試qemu設備模擬的裸板程序來測試中斷的仿真。
本文相關代碼地址
https://github.com/hac425xxx/qemu-fuzzing/commit/609538e1407de884f6c9e4d222431c9032abc25b
https://github.com/hac425xxx/qemu-fuzzing/commit/7bc0e0aa35363c18fcf5b89dacab73a0a9bef147
stm32f205-soc實現
為了仿真某個設備,我們需要通過閱讀硬件文檔或者通過逆向程序邏輯來獲取外設的行為,然后再在qemu中進行模擬,stm32f205的手冊可以直接在網上下載
https://www.st.com/resource/en/reference_manual/cd00225773-stm32f205xx-stm32f207xx-stm32f215xx-and-stm32f217xx-advanced-arm-b ased-32-bit-mcus-stmicroelectronics.pdf
qemu中名字為netduino2的Machine使用到了stm32f205-soc這個設備,可以使用 -M 指定使用該設備
qemu-system-arm -M netduino2
netduino2的初始化函數為netduino2_init
static void netduino2_init(MachineState *machine)
{
DeviceState *dev;
dev = qdev_create(NULL, TYPE_STM32F205_SOC);
qdev_prop_set_string(dev, "cpu-type", ARM_CPU_TYPE_NAME("cortex-m3"));
o bject_property_set_bool(o bject(dev), true, "realized", &error_fatal);
armv7m_load_kernel(ARM_CPU(first_cpu), machine->kernel_filename,
FLASH_SIZE);
}
函數邏輯如下:
首先創建stm32f205-soc設備,然后設置cpu-type為 cortex-m3
然后通過設置 realized 觸發stm32f205_soc_realize函數的調用
最后armv7m_load_kernel把命令行-kernel指定的文件加載到虛擬機內存。
static void stm32f205_soc_class_init(o bjectClass *klass, void *data)
{
DeviceClass *dc = DEVICE_CLASS(klass);
dc->realize = stm32f205_soc_realize;
dc->props = stm32f205_soc_properties;
}
static const TypeInfo stm32f205_soc_info = {
.name = TYPE_STM32F205_SOC,
.parent = TYPE_SYS_BUS_DEVICE,
.instance_size = sizeof(STM32F205State),
.instance_init = stm32f205_soc_initfn,
.class_init = stm32f205_soc_class_init,
};
下面分析stm32f205_soc_realize的實現
初始化flash和sram
stm32f205的內存映射如下
stm32f205_soc_realize主要實現了紅框標注的三個內存區域
位于0x8000000處的flash區域
位于0x0處的區域,是flash的alias區域
位于0x20000000處的sram區域
函數入口首先設置了flash和sram.
MemoryRegion *system_memory = get_system_memory();
MemoryRegion *sram = g_new(MemoryRegion, 1);
MemoryRegion *flash = g_new(MemoryRegion, 1);
MemoryRegion *flash_alias = g_new(MemoryRegion, 1);
MemoryRegion *demo_mem = g_new(MemoryRegion, 1);
memory_region_init_ram(flash, NULL, "STM32F205.flash", FLASH_SIZE,
&error_fatal);
memory_region_init_alias(flash_alias, NULL, "STM32F205.flash.alias",
flash, 0, FLASH_SIZE);
memory_region_set_readonly(flash, true);
memory_region_set_readonly(flash_alias, true);
memory_region_add_subregion(system_memory, FLASH_b ase_ADDRESS, flash);
memory_region_add_subregion(system_memory, 0, flash_alias);
memory_region_init_ram(sram, NULL, "STM32F205.sram", SRAM_SIZE,
&error_fatal);
memory_region_add_subregion(system_memory, SRAM_b ase_ADDRESS, sram);
主要就是新建flash區域和flash_alias,然后通過memory_region_add_subregion把這兩個區域放到對應的地址,這樣0x0和0x8000000實際指向的是同一塊RAM。
然后新建sram區域,并把sram放到0x20000000處。
初始化外設
在初始化flash和sram后,會逐步初始化用到的外設,這里以UART外設為例進行介紹
UART外設
初始化
uart使用sysbus_mmio_map把外設的寄存器區域映射為mmio內存,然后使用sysbus_connect_irq初始化外設需要的irq。
/* Attach UART (uses USART registers) and USART controllers */
for (i = 0; i < STM_NUM_USARTS; i++) {
dev = DEVICE(&(s->usart[i]));
qdev_prop_set_chr(dev, "chardev", serial_hd(i));
o bject_property_set_bool(o bject(&s->usart[i]), true, "realized", &err);
if (err != NULL) {
error_propagate(errp, err);
return;
}
busdev = SYS_BUS_DEVICE(dev);
sysbus_mmio_map(busdev, 0, usart_addr[i]);
sysbus_connect_irq(busdev, 0, qdev_get_gpio_in(armv7m, usart_irq[i]));
}
s->usart在stm32f205_soc_initfn中創建
static void stm32f205_soc_initfn(o bject *obj)
{
for (i = 0; i < STM_NUM_USARTS; i++) {
sysbus_init_child_obj(obj, "usart[*]", &s->usart[i],
sizeof(s->usart[i]), TYPE_STM32F2XX_USART);
}
實際就是創建了TYPE_STM32F2XX_USART設備
static const TypeInfo stm32f2xx_usart_info = {
.name = TYPE_STM32F2XX_USART,
.parent = TYPE_SYS_BUS_DEVICE,
.instance_size = sizeof(STM32F2XXUsartState),
.instance_init = stm32f2xx_usart_init,
.class_init = stm32f2xx_usart_class_init,
};
調用sysbus_init_child_obj函數初始化設備時會調用stm32f2xx_usart_init
static const MemoryRegionOps stm32f2xx_usart_ops = {
.read = stm32f2xx_usart_read,
.write = stm32f2xx_usart_write,
.endianness = DEVICE_NATIVE_ENDIAN,
};
static void stm32f2xx_usart_init(o bject *obj)
{
STM32F2XXUsartState *s = STM32F2XX_USART(obj);
sysbus_init_irq(SYS_BUS_DEVICE(obj), &s->irq);
memory_region_init_io(&s->mmio, obj, &stm32f2xx_usart_ops, s,
TYPE_STM32F2XX_USART, 0x400);
sysbus_init_mmio(SYS_BUS_DEVICE(obj), &s->mmio);
}
函數做的工作如下
初始化設備的irq,保存到s->irq
初始化s->mmio,設置memory_region的大小為0x400,mmio內存訪問的回調函數由stm32f2xx_usart_ops指定
sysbus_init_mmio主要是把s->mmio的指針保存到設備mmio數組中,以便后續使用sysbus_mmio_map把memory_region掛載到對應的地址。
mmio映射
stm32f205-soc實現了6個uart設備,設備mmio的起始地址分別為
static const uint32_t usart_addr[STM_NUM_USARTS] = { 0x40011000, 0x40004400,
0x40004800, 0x40004C00, 0x40005000, 0x40011400 };
其中4個uart設備在手冊memory map中的截圖如下
然后在stm32f205_soc_realize函數里面會調用sysbus_mmio_map把設備的memory_region掛載到指定的位置
sysbus_mmio_map(busdev, 0, usart_addr[i]);
中斷初始化
qemu中斷模型
概念
qemu使用GPIO來實現中斷系統,其簡單的原理如下
Device.[GPIO_OUT] ->[GPIO_IN].GIC.[GPIO_OUT]->[GPIO_IN].core
首先CPU有GPIO_IN接口
然后中斷控制器(GIC)有GPIO_IN和GPIO_OUT, GPIO_OUT和CPU的GPIO_IN接口關聯
設備的GPIO_OUT和GIC的GPIO_IN關聯
當有中斷發生時,設備通過GPIO_OUT通知GIC,GIC通過GPIO_OUT通知GPIO_IN。
中斷依賴qemu_irq結構體
struct IRQState {
o bject parent_obj;
qemu_irq_handler handler; // irq處理函數
void *opaque;
int n; // irq的編號
};
typedef struct IRQState *qemu_irq;
要觸發一個irq,可以使用qemu_set_irq函數
void qemu_set_irq(qemu_irq irq, int level)
{
if (!irq)
return;
irq->handler(irq->opaque, irq->n, level); // 調用irq的回調函數,傳入中斷號n
}
GPIO_IN通過qdev_init_gpio_in初始化
void qdev_init_gpio_in(DeviceState *dev, qemu_irq_handler handler, int n)
初始化n個GPIO_IN接口,每個GPIO_IN接口的回調函數為handler,實際就是新建n個qemu_irq對象,qemu_irq的回調函數為handler。
GPIO_OUT初始化函數為sysbus_init_irq
/* Request an IRQ source. The actual IRQ o bject may be populated later. */
void sysbus_init_irq(SysBusDevice *dev, qemu_irq *p)
qemu使用sysbus_connect_irq將GPIO_OUT和GPIO_IN關聯
void sysbus_connect_irq(SysBusDevice *dev, int n, qemu_irq irq)
把dev中的第n個gpio_out和irq關聯
實際就是把irq保存為第n個gpio_out的值
實例分析
比如在armv7m_nvic_realize調用qdev_init_gpio_in初始化num_irq個GPIO_IN
static void armv7m_nvic_realize(DeviceState *dev, Error **errp)
{
qdev_init_gpio_in(dev, set_irq_level, s->num_irq);
uart設備在stm32f2xx_usart_init函數中通過sysbus_init_irq初始化一個GPIO_OUT
sysbus_init_irq(SYS_BUS_DEVICE(obj), &s->irq);
這樣第0個GPIO就指向了s->irq。
stm32f205_soc_realize會使用sysbus_connect_irq把設備的第0個GPIO和 nvic 的特定GPIO_IN進行關聯。
實質上就是把 s->irq = qdev_get_gpio_in(armv7m, timer_irq[i])。
sysbus_connect_irq(busdev, 0, qdev_get_gpio_in(armv7m, usart_irq[i]));
timer_irq 保存了每個uart設備需要使用的IRQ號
static const int usart_irq[STM_NUM_USARTS] = {37, 38, 39, 52, 53, 71};
此外還有一個需要注意的點,這里的irq號和其在異常向量表中的位置存在以下關系
IRQ 號 = IRQ處理函數在異常向量表中的序號 - CPU內置異常數目
以stm32f205-soc為例,其使用的CPU為cortex-m3,CPU的內部中斷數目為16個
比如異常向量表的第17號中斷的irq編號為 17 - 16 = 1,下圖是設備手冊異常向量表中IRQ開頭部分:
uart設備在stm32f2xx_usart_write中需要觸發特定中斷時會調用
if (s->usart_cr1 & USART_CR1_RXNEIE &&
s->usart_sr & USART_SR_RXNE) {
qemu_set_irq(s->irq, 1);
}
s->irq 在之前使用sysbus_connect_irq時就被設置成nvic中對應irq的qemu_irq結構
這里實際會調用set_irq_level通知nvic指定的中斷到來
/* callback when external interrupt line is changed */
static void set_irq_level(void *opaque, int n, int level)
{
n += NVIC_FIRST_IRQ; // irq 號 + CPU內置異常樹(16)
vec = &s->vectors[n];
if (level != vec->level) {
vec->level = level;
if (level) {
armv7m_nvic_set_pending(s, n, false);
}
}
}
主要就是根據IRQ號n,找到對應的異常信息 vec, 然后判斷vec的狀態(高定平(level=1),還是低電平(level=0))
如果是高電平,則會進入armv7m_nvic_set_pending通知CPU中斷到來,實際也是調用CPU之前注冊的GPIO_IN的回調函數通知。
因此qemu的中斷實現其實是依賴于qemu_irq來實現,比如NVIC要通知CPU中斷到來,實際就是調用CPU的qemu_irq中的回調函數實現。
固件加載
netduino2_init在初始化stm32f205-soc后,調用armv7m_load_kernel加載二進制到內存
armv7m_load_kernel(ARM_CPU(first_cpu), machine->kernel_filename, FLASH_SIZE);
void armv7m_load_kernel(ARMCPU *cpu, const char *kernel_filename, int mem_size)
{
..................
if (kernel_filename) {
image_size = load_elf_as(kernel_filename, NULL, NULL, NULL,
&entry, &lowaddr,
NULL, big_endian, EM_ARM, 1, 0, as);
if (image_size < 0) {
image_size = load_image_targphys_as(kernel_filename, 0,
mem_size, as);
lowaddr = 0;
}
}
qemu_register_reset(armv7m_reset, cpu);
}
machine->kernel_filename通過命令的 -kernel 選項指定
armv7m_load_kernel首先嘗試調用load_elf_as以elf格式加載
如果加載失敗,就調用 load_image_targphys_as 直接把文件加載到0地址處
裸板程序和IRQ請求調試
本節基于stm32f205-soc的進行修改實現QEMU對中斷的模擬,然后開發裸板程序對模擬的中斷進行驗證。
stm32f205-soc修改
qemu_irq stm32f2xx_irq_demo_handler = NULL;
#define IRQ_DEMO_b ase 0x88990000
static void stm32f2xx_irq_demo_write(void *opaque, hwaddr addr,
uint64_t val64, unsigned int size)
{
qemu_set_irq(stm32f2xx_irq_demo_handler, 1);
return;
}
static const MemoryRegionOps stm32f2xx_irq_demo_ops = {
.write = stm32f2xx_irq_demo_write,
.endianness = DEVICE_NATIVE_ENDIAN,
};
static void stm32f205_soc_realize(DeviceState *dev_soc, Error **errp)
{
memory_region_init_io(demo_mem, NULL, &stm32f2xx_irq_demo_ops, s,
"irq-demo-mmio", 0x1000);
memory_region_add_subregion(system_memory, IRQ_DEMO_b ase, demo_mem);
stm32f2xx_irq_demo_handler = qdev_get_gpio_in(armv7m, 20); // 拿到nvic的irq 20 的 irq
首先在stm32f205_soc_realize中獲取IRQ為20的NVIC.GPIO_IN,即其對應的qemu_irq結構,然后保存到stm32f2xx_irq_demo_handler中
注冊0x88990000處內存寫回調函數為stm32f2xx_irq_demo_write
當往0x88990000寫數據時會進入stm32f2xx_irq_demo_write
在stm32f2xx_irq_demo_write函數中會調用qemu_set_irq觸發 IRQ 20 中斷
裸板程序
根據手冊定義異常向量表,當系統啟動時會調用Reset_Handler,當IRQ-20中斷觸發時會進入demo_irq_handler
// ISR vecotor data
.section .isr_vector, "a"
g_pfnVectors:
.word stack_top
.word Reset_Handler
.word Default_Handler // NMI
.word Default_Handler // HardFault
.word Default_Handler // MemManage
.word Default_Handler // BusFault
.word Default_Handler // UsageFault
.word 0
.word 0
.word 0
.word 0
.word Default_Handler // SVC
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word demo_irq_handler // CAN1_RX0 for demo_irq
中斷處理程序定義
.thumb_func
Reset_Handler:
@ MOV R0, #0
@ MSR PRIMASK, R0
bl main_func
b .
.thumb_func
demo_irq_handler:
bl demo_irq_func
b .
相關函數實現
#define USART1_b ase_ADDR 0x40011000
#define USART_DR 0x04
#define IRQ_DEMO_b ase 0x88990000
#define NVIC_MEM_b ase 0xe000e000
void print_func(unsigned char* s)
{
while (*s != 0)
{
*(volatile unsigned char*)(USART1_b ase_ADDR + USART_DR) = *s;
s++;
}
}
void enable_demo_irq()
{
unsigned int irq = 20 + 16;
unsigned int offset = (irq - 16) / 8;
offset += 0x180;
offset -= 0x80;
*(volatile unsigned char*)(NVIC_MEM_b ase + offset) = 1 << 4;
}
void main_func()
{
print_func("main_func!
");
enable_demo_irq(); // 配置 nvic 的mmio,讓 20號 irq 的 enabled=1
*(volatile unsigned int*)(IRQ_DEMO_b ase + 4) = 33; // 觸發 demo_irq, 下面進入 demo_irq_func
print_func("end main_func!
");
return;
}
void demo_irq_func()
{
print_func("demo_irq_func!
");
return;
}
print_func函數
通過寫UART的內存實現輸出
main函數
首先打印一個日志,然后調用enable_demo_irq設置 nvic 控制器,讓IRQ-20啟用
然后觸發對IRQ_DEMO_b ase內存的寫,讓qemu端觸發IRQ-20中斷
demo_irq_func函數
打印日志
使用qemu加載固件執行的輸出如下
$ qemu-system-arm -M netduino2 -kernel startup.bin -nographic
main_func!
demo_irq_func!
可以看到首先進入了main函數,觸發IRQ-20中斷后進入了demo_irq_func。
注意
由于系統啟動時NVIC中每個異常向量的enable狀態為0,從而導致即使使用qemu_set_irq通知中斷到來,實際也不會被CPU處理.
static MemTxResult nvic_sysreg_write(void *opaque, hwaddr addr,
uint64_t value, unsigned size,
MemTxAttrs attrs)
{
switch (offset) {
case 0x100 ... 0x13f: /* NVIC Set enable */
offset += 0x80;
setval = 1;
/* fall through */
case 0x180 ... 0x1bf: /* NVIC Clear enable */
startvec = 8 * (offset - 0x180) + NVIC_FIRST_IRQ;
for (i = 0, end = size * 8; i < end && startvec + i < s->num_irq; i++) {
if (value & (1 << i) &&
(attrs.secure || s->itns[startvec + i])) {
s->vectors[startvec + i].enabled = setval;
}
}
nvic_irq_update(s);
所以在觸發中斷前要通過寫NVIC的MMIO內存來設置NVIC中異常向量的enable為1
void enable_demo_irq()
{
unsigned int irq = 20 + 16;
unsigned int offset = (irq - 16) / 8;
offset += 0x180;
offset -= 0x80;
*(volatile unsigned char*)(NVIC_MEM_b ase + offset) = 1 << 4;
}
總結
本文以stm32f205-soc為例子介紹了針對真實硬件仿真的實現,并分析了qemu的中斷模型,最后給出仿真中斷的例子。
參考鏈接
https://blog.csdn.net/alex_mianmian/article/d etails/98174812
https://www.cnblogs.com/utank/p/11304226.html
總結
以上是生活随笔為你收集整理的QEMU固件模拟技术-stm32仿真分析及IRQ仿真实践的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [机器学习] XGB/LGB---自定义
- 下一篇: [深度学习]知识蒸馏技术