Changchun Master Li

[RPi bring up] 从树莓派linux源码中窥探bcm2835和arm1176jzfs的中断管理

2023-10-29

阅读本文您不需要掌握的知识有

  • 高深的操作系统理论
  • 高深的计算机体系结构理论

阅读本文您需要具备

  • 全日制小学生学历及其同等学历 ★★★★★
  • 熟读arm1176jzfs datasheet ★★☆☆☆
  • ARM汇编语言 ★☆☆☆☆
  • C语言 ★★★★☆

当中断发生时,程序执行流程将暂停并且运行中断处理程序。在中断处理程序运行结束后,恢复之前的执行流程。同步中断,通过执行指令生成,也叫异常。异步中断,由外部事件生成
bcm2835 datasheet中有关中断控制的描述过于简略,但没关系,我们可以从linux源码中扒有用的东西

用最新的buildroot构建linux系统

获取最新版本buildroot

1
2
3
4
5
git clone https://github.com/buildroot/buildroot.git
cd buildroot
make raspberrypi_defconfig
make all
# output/images/sdcard.img

打开jtag

要改一下boot/config.txt,这里有两种改法

第一种,board/raspberrypi/post-image.sh脚本会先检测有没有文件board/raspberrypi/genimage-${BOARD_NAME}.cfg,如果没有就用模板board/raspberrypi/post-image.sh生成一个临时的配置文件output/images/genimage.cfg。在post-image.sh中改config.txt,然后重新跑一次make即可。

第二种,buidlroot有一个配置项BR2_PACKAGE_RPI_FIRMWARE_CONFIG_FILE(package/rpi-firmware/Config.in),打开配置菜单可以看到,make menuconfig

1
Target packages -> Hardware handling -> Firmware -> Path to a file stored as boot/config.txt

默认下config.txt对应config_default.txt,在board/raspberrypi/config_default.txt加上下面三行

1
2
3
# Enable bcm2835 jtag, set GPIO4 instead GPIO 26
enable_jtag_gpio=1
gpio=4=a5

这需要我们重新编译一下rpi-firmware

1
make rpi-firmware-rebuild && make

调试信息

在编译的时候保留调试信息,有几个地方也需要定制
make menuconfig

1
Build options -> build packages with debugging symbols

选level3,包含宏定义,打开

1
Build options -> build packages with runtime debugging info

关闭

1
Build options -> strip target binaries

gcc优化选debugging

1
Build options -> gcc optimization level

linux kernel config也要改,make linux-menuconfig

1
Kernel hacking -> Compile-time checks and compiler options -> Compile the kernel with debug info

最后,在start_kernel里加一个死循环,可以在启动时停住,用gdb命令(gdb) set pc+0x4跳过

1
2
3
4
5
6
__asm (
"lo: b lo\n\t"
:
:
:
);

重新编译内核

1
make linux-dirclean && make linux-rebuild && make

把output/images/sdcard.img写入sd卡,就可以用jlink调试了

linux中断代码浅析

注意,默认配置中 https://github.com/buildroot/buildroot/blob/master/configs/raspberrypi_defconfig buildroot使用的是raspberry pi kernel而非mainline kernel https://github.com/raspberrypi/linux.git 当前版本为rpi-5.10.y

bcm2835的驱动在irq-bcm2835.c,说白了就是用IRQCHIP_DECLARE宏定义了一个struct结构体__of_table_bcm2835_armctrl_ic

1
IRQCHIP_DECLARE(bcm2835_armctrl_ic, "brcm,bcm2835-armctrl-ic", bcm2835_armctrl_of_init);

可以打出来看一下

1
2
3
4
(gdb) p __of_table_bcm2835_armctrl_ic 
$10 = {name = '\000' <repeats 31 times>, type = '\000' <repeats 31 times>,
compatible = "brcm,bcm2835-armctrl-ic", '\000' <repeats 104 times>,
data = 0xc0b8c990 <bcm2835_armctrl_of_init>}

设备树中bcm2835-armctrl-ic,通过bcm2835_armctrl_of_init进行初始化。

armctrl_of_init

跟其他中断控制器驱动的套路一样,驱动drivers/irqchip/irq-bcm2835.c,of_init初始化中断控制器

1
2
3
4
5
6
7
armctrl_ic
intc: interrupt-controller@7e00b200 {
compatible = "brcm,bcm2835-armctrl-ic";
reg = <0x7e00b200 0x200>;
interrupt-controller;
#interrupt-cells = <2>;
};

初始化阶段

of_iomap映射node设备地址0x7e00b200到base,irq_domain_add_linear返回一个线性映射的domain

中断控制器的MMIO地址记录在intc.pending,intc.enable,intc.disable数组上,和bcm2835 datasheet的描述是对应的

| address offset | register name | intc |
| —- | —- | —- |
| 0x200 | IRQ basic pending | intc.pending[0] |
| 0x204 | IRQ pending 1 | intc.pending[1] |
| 0x208 | IRQ pending 2 | intc.pending[2] |
| 0x20C | FIQ control | |
| 0x210 | Enable IRQs 1 | intc.enable[1] |
| 0x214 | Enable IRQs 2 | intc.enable[2] |
| 0x218 | Enable Basic IRQs | intc.enable[0] |
| 0x21C | Disable IRQs 1 | intc.disable[1] |
| 0x220 | Disable IRQs 2 | intc.disable[2] |
| 0x224 | Disable Basic IRQs | intc.disable[0] |

irq_create_mapping分配一个linux系统irq,建立硬件hwirq到irq的map
irq_set_chip_and_handler设置handler为handle_level_irq
set_handle_irq设置bcm2835_handle_irq为中断处理函数
bcm2835_handle_irq每次被触发都会通过get_next_armctrl_hwirq获得hwirq,从bcm2835的datasheet找到IRQ basic pending的定义
IRQ pend base Address: 0x200 Reset: 0x000

bits function
20:15 selected interrupts from GPU IRQ 63:32
14:10 selected interrupts from GPU IRQ 31:0
9 GPU IRQ 63:32 One or more bits set in pending register 2
8 GPU IRQ 31:0 One or more bits set in pending register 1
7 Illegal access type 0 IRQ pending
6 Illegal access type 1 IRQ pending
5 GPU1 halted IRQ pending
4 GPU0 halted IRQ pending
3 ARM Doorbell 1 IRQ pending
2 ARM Doorbell 0 IRQ pending
1 ARM Mailbox IRQ pending
0 ARM Timer IRQ pending
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static u32 get_next_armctrl_hwirq(void)
{
u32 stat = readl_relaxed(intc.pending[0]) & BANK0_VALID_MASK;

if (stat == 0)
return ~0;
else if (stat & BANK0_HWIRQ_MASK)
return MAKE_HWIRQ(0, ffs(stat & BANK0_HWIRQ_MASK) - 1);
else if (stat & SHORTCUT1_MASK)
return armctrl_translate_shortcut(1, stat & SHORTCUT1_MASK);
else if (stat & SHORTCUT2_MASK)
return armctrl_translate_shortcut(2, stat & SHORTCUT2_MASK);
else if (stat & BANK1_HWIRQ)
return armctrl_translate_bank(1);
else if (stat & BANK2_HWIRQ)
return armctrl_translate_bank(2);
else
BUG();
}

原理很简单,用不同的mask检查pending0的bits
0xff arm中断
0x7c00 gpu1中断
0x1f8000 gpu2中断
0x100 gpu1中断
0x200 gpu2中断

armctrl_of_init建立了硬件hwirq 到 linux irq 到 desc->handle_irq 的映射

linux中断处理

让我们跟踪一下uart中断处理程序,pl011_int位于drivers/tty/serial/amba-pl011.c
打断点,然后在uart里敲一个字符就会触发中断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(gdb) b pl011_int
...
(gdb) bt
#0 pl011_int (irq=81, dev_id=0xc1912020) at drivers/tty/serial/amba-pl011.c:1504
#1 0xc0071484 in __handle_irq_event_percpu (desc=desc@entry=0xc10d8400, flags=flags@entry=0xc0bd7e70)
at kernel/irq/handle.c:156
#2 0xc00716f0 in handle_irq_event_percpu (desc=0xc10d8400) at kernel/irq/handle.c:196
#3 handle_irq_event (desc=desc@entry=0xc10d8400) at kernel/irq/handle.c:213
#4 0xc007578c in handle_level_irq (desc=0xc10d8400) at kernel/irq/chip.c:653
#5 0xc0070c80 in generic_handle_irq_desc (desc=<optimized out>) at ./include/linux/irqdesc.h:152
#6 generic_handle_irq (irq=81) at kernel/irq/irqdesc.c:650
#7 __handle_domain_irq (domain=0xc10d2000, hwirq=<optimized out>, lookup=lookup@entry=true,
regs=regs@entry=0xc0bd7ee8) at kernel/irq/irqdesc.c:687
#8 0xc000932c in handle_domain_irq (regs=0xc0bd7ee8, hwirq=<optimized out>, domain=<optimized out>)
at ./include/linux/irqdesc.h:170
#9 bcm2835_handle_irq (regs=0xc0bd7ee8) at drivers/irqchip/irq-bcm2835.c:339
#10 0xc0008c7c in __irq_svc () at arch/arm/kernel/entry-armv.S:205

hwirq=89 (0b1011001) bank2,source25
查阅Documentation/devicetree/bindings/interrupt-controller/brcm,bcm2835-armctrl-ic.txt可知,中断源是VC_UART
irq=81
desc=0xc10d8400
generic_handle_irq调用irq_to_desc(irq)找到对应的中断描述,irq_desc在内核中的数据结构实现是基数树Radix Tree,
generic_handle_irq_desc调用desc->handle_irq(desc)
这里desc->handle_irq指向了handle_level_irq
handle_level_irq最终会遍历action列表desc->action,调用每一个action->handler,pl011_int已由pl011_allocate_irq注册到action->handler上了
一张图胜过千言万语

linux interrupt calling stack

emperorOS中断框架设计

https://github.com/996refuse/emperorOS/tree/interrupt

中断过程中硬件和软件发生了什么?

操作系统, arm核心, 中断控制器, 硬件在中断中的工作流程

  • 中断控制器 位于offset 0xb200,linux内核intc.enable变量,在对应bit写1打卡对应的irq
  • cpu开中断 位于CPSR寄存器的I bit,清零,cpu可响应中断
  • 中断源 外部或者内部中断,比如timer compare引起的时钟中断
  • 中断队列 soc内部的队列,当发生中断时,队列不为空,触发中断,cpu会挂起当前状态进入中断模式
  • 触发中断 cpu进入中断模式,此时CPSR I bit置1,屏蔽cpu响应中断,以免进入中断嵌套
  • 中断向量 cpu根据中断向量运行中断处理函数
  • 中断返回 cpu从中断模式中恢复

框架实现

main函数最终进入用户态执行”init”进程来测试中断处理程序,目前init只有loop: b loop死循环,其binary为

1
unsigned char inifiniteloop[] = {0xfe, 0xff, 0xff, 0xea};

实现为一个单内核栈多用户栈

1
user -> irq mode(irq stack 0xC0006000) -> supervisor(main/proc_schd stack)

不可重入,内核态应尽快返回,以避免堵塞其他进程调度(sleep操作需要由用户态函数实现)
所有进程时间平均分配,不考虑优先级,优先级反转,优先级继承等问题

timer中断的实现

armv6 使用p15协处理器指令设置中断向量

1
"mcr p15, 0, %[v], c12, c0, 0\n\t"

打开中断,这里模仿intc.enable

1
arm_intr_reg->gpu_enable[0] |= 1 << bit

最后由systimer_set设置compare定时器

中断上下文保存和恢复

上下文指r0-r15,cpsr寄存器的当前状态
trap_return保存当前内核态上下文到r1,恢复r0的用户态上下文
schd进入内核态上下文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
.globl trap_return
trap_return:
add r1, #64

stmfd r1!, {r0-r14, lr}
mrs lr, cpsr
stmfd r1!, {lr}

mov sp, r0
/* restore cpsr */
ldmfd sp!, {lr}
msr spsr, lr

/* restore r0-r14 */
ldmfd sp!, {r0-r14}^

/* restore pc */
ldmfd sp!, {lr}
movs pc,lr

.global schd
schd:
ldmfd r0!, {lr}
msr cpsr, lr

ldmfd r0!, {r0-r14}
bx lr

设备MMIO不要打开cache

DDI0301H_arm1176jzfs_r0p7_trm/P332

1
((uint32_t*)PDE)[PDX(0)] = 0|PDX_AP(AP_U_NA)|PDX_TYPE(TYPE_SECTION);

在设备映射的内存区域,页表中TEX/Cache/Buffer这三个标志位一定是0,Strongly Ordered模式。否则会面临严重的一致性的问题,内核代码读的数据是cache中的脏数据。

爆发于2017年的漏洞,熔断Meltdown,就是一个利用d-cache漏洞的旁路攻击,尝试解释一下

内存地址a仅可在内核态访问。在用户态构造代码

1
2
3
invalid_cache;
raise_exception;
array[*a];

由于现代处理器是超标量乱序多发射处理器,处理器在执行raise_exception时,也在同时访问array[*a],只是array[a]没有最终进入rob提交。但是位于地址a处的cache仍被更新了。因此只要遍历一遍array的数据,看哪一个地址访问速度快,进而得知是从cache中读取的数据,就可以知道a地址存的是什么了
所以修复的办法是不要让内核数据在内存中有固定的位置,在中断处理切换特权模式时要同时修改内核空间的映射

使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

扫描二维码,分享此文章